editorbrain 0.28.2 → 0.29.1
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 +2 -2
- package/bin/editorbrain.mjs +1769 -958
- package/package.json +1 -1
- package/skill/editorbrain/AGENTS.md +2 -0
- package/skill/editorbrain/SKILL.md +5 -1
- package/skill/editorbrain/references/audio-production.md +5 -2
- package/skill/editorbrain/references/cli-mcp.md +9 -2
- package/skill/editorbrain/references/local-production.md +2 -0
- package/skill/editorbrain/references/onboarding.md +23 -4
- package/skill/editorbrain/references/production-and-review.md +2 -0
- package/skill/editorbrain/references/production-continuity.md +91 -0
- package/skill/editorbrain/references/sfx-library.md +32 -0
- package/skill/editorbrain/references/storage-and-previews.md +34 -0
- package/skill/editorbrain/references/studio.md +52 -0
package/bin/editorbrain.mjs
CHANGED
|
@@ -1,5 +1,246 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// packages/cli/src/sfx.ts
|
|
4
|
+
import { readdir as readdir2, lstat as lstat3 } from "node:fs/promises";
|
|
5
|
+
import { join as join2, extname as extname2, basename } from "node:path";
|
|
6
|
+
|
|
7
|
+
// packages/core/src/onboarding-guide.ts
|
|
8
|
+
var onboardingStudioSteps = [
|
|
9
|
+
["Empecemos", "Un editor que entiende tu mundo.", "Tu marca, tu gusto y tu forma de trabajar. Sin empezar de cero en cada video."],
|
|
10
|
+
["Empecemos", "Tus ideas. Tu agente. Tu criterio.", "EditorBrain aporta contexto y referencias. Claude o Codex adapta, edita y revisa."],
|
|
11
|
+
["Empecemos", "Trae un pedacito de tu mundo.", "Puedes usar un clip para probar c\xF3mo se ven los textos. Tu web y tus redes se guardan en el siguiente paso."],
|
|
12
|
+
["Empecemos", "Que se vea. Que se sienta tuyo.", "Define tu identidad, tus colores y tus tipograf\xEDas. O recupera una marca que ya tengas."],
|
|
13
|
+
["Empecemos", "\xBFQu\xE9 quieres contar?", "A c\xE1mara, sin aparecer, con im\xE1genes o con animaciones: tu contenido determina la edici\xF3n."],
|
|
14
|
+
["Tu gusto", "Elige lo que te hace mirar.", "Hasta tres direcciones. Nos interesa c\xF3mo cuentan la historia, no copiar el tema ni la paleta."],
|
|
15
|
+
["Tu gusto", "Movimiento con intenci\xF3n.", "Explora c\xF3mo explicar una idea. Tu agente adaptar\xE1 las animaciones a cada escena."],
|
|
16
|
+
["Tu gusto", "Tu universo visual.", "Fotograf\xEDa, ilustraci\xF3n, collage\u2026 Combina lo que te guste o deja que tu agente decida."],
|
|
17
|
+
["Tu gusto", "Encuentra tu ritmo.", "Una misma idea puede respirar o ir a toda velocidad. Prueba la diferencia."],
|
|
18
|
+
["Tus detalles", "Los primeros segundos importan.", "Elige c\xF3mo te gusta abrir. El guion y el material mandan en cada video."],
|
|
19
|
+
["Tus detalles", "Palabras que se ven.", "Prueba subt\xEDtulos, \xE9nfasis y posiciones antes de elegir."],
|
|
20
|
+
["Tus detalles", "Tu voz. Tu cierre.", "Define el sonido y qu\xE9 quieres que haga quien te ve al terminar."],
|
|
21
|
+
["Tus detalles", "Tus l\xEDmites tambi\xE9n cuentan.", "Lo que nunca deber\xEDa aparecer, y lo que prefieres decidir despu\xE9s."],
|
|
22
|
+
["Listo para editar", "Antes del primer video.", "Tres cosas claras para empezar con las expectativas correctas."],
|
|
23
|
+
["Listo para editar", "Tu caja de herramientas.", "Declara los servicios que tienes. Tu agente comprobar\xE1 el acceso antes de usarlos."],
|
|
24
|
+
["Listo para editar", "As\xED empieza tu cerebro.", "Tus elecciones ya son documentos. Puedes revisar una propuesta de IA antes de incorporarla."],
|
|
25
|
+
["Listo para editar", "P\xEDdelo como lo imaginas.", "Describe tu objetivo y entrega tu material. Elige cu\xE1ndo quieres revisar."],
|
|
26
|
+
["Listo para editar", "Ll\xE9valo a tu agente.", "Instala y autoriza EditorBrain con una indicaci\xF3n. Una conexi\xF3n real requiere tu consentimiento."],
|
|
27
|
+
["Listo para editar", "Tu siguiente idea empieza aqu\xED.", "Tu marca est\xE1 preparada. Crea un proyecto y entrega el material a tu agente."]
|
|
28
|
+
];
|
|
29
|
+
var localOnboardingSteps = [
|
|
30
|
+
["Tu contenido", ...onboardingStudioSteps[4].slice(1)],
|
|
31
|
+
["Tu marca", ...onboardingStudioSteps[3].slice(1)],
|
|
32
|
+
["Tus gustos", "Elige lo que te hace mirar.", "Explora im\xE1genes y movimiento. Tu agente adapta estas referencias a cada escena."],
|
|
33
|
+
["Tu edici\xF3n", "Encuentra tu ritmo.", "Decide cu\xE1nto apoyo visual, texto y subt\xEDtulos quieres. Puedes dejarlo a criterio de tu agente."],
|
|
34
|
+
["Tu sonido", "Tu voz. Tu cierre.", "Voz, m\xFAsica y efectos se eligen por separado. La cantidad de efectos no es su volumen."],
|
|
35
|
+
["Tu cerebro", "As\xED empieza tu cerebro.", "Revisa tus decisiones antes de guardarlas. Puedes modificarlas despu\xE9s."]
|
|
36
|
+
];
|
|
37
|
+
function getOnboardingGuide() {
|
|
38
|
+
return {
|
|
39
|
+
schemaVersion: 1,
|
|
40
|
+
web: { url: "https://editorbrain.com/app/onboarding", description: "Recorrido visual completo: preferencias, integraciones personales y conexi\xF3n con tu agente. Requiere tu sesi\xF3n web y elegir el espacio correcto." },
|
|
41
|
+
local: {
|
|
42
|
+
description: "Edici\xF3n local de preferencias en seis grupos, con el dise\xF1o del estudio. Funciona sin cuenta; no simula conexiones, integraciones ni an\xE1lisis de IA.",
|
|
43
|
+
steps: localOnboardingSteps.map(([label]) => label),
|
|
44
|
+
commands: ["editorbrain onboarding create --brand <brand-id> --project <new-project-id> --dir <folder>", "editorbrain onboarding serve --session <returned-session> --dir <folder>"],
|
|
45
|
+
completion: "Lee las respuestas; valida y aplica con onboarding apply. Sincroniza expl\xEDcitamente con onboarding sync despu\xE9s de autenticarte. Conserva versiones y resuelve conflictos; nunca sobrescribas una marca existente para completar el onboarding."
|
|
46
|
+
},
|
|
47
|
+
agent: [
|
|
48
|
+
"Ofrece la web o el artefacto local seg\xFAn el entorno del usuario; no le pidas rellenar ambos.",
|
|
49
|
+
"Descubre los comandos disponibles con editorbrain help antes de ejecutarlos. Lee references/onboarding.md solo al configurar o modificar preferencias.",
|
|
50
|
+
"Si la marca ya existe, recupera sus archivos actuales antes de crear el artefacto. Prefiere actualizaciones parciales y conserva lo que no cambi\xF3.",
|
|
51
|
+
"MCP no puede abrir un servidor en la computadora del usuario. Sin terminal local, enlaza el recorrido web; no inventes una URL localhost.",
|
|
52
|
+
"Al terminar, consulta check_workflow con el proyecto seleccionado y carga sus restricciones actuales. No supongas que terminar una p\xE1gina conect\xF3 las herramientas.",
|
|
53
|
+
"El analista de marca es opcional: requiere petici\xF3n expl\xEDcita, documentos sincronizados y revisi\xF3n de su propuesta. No se ejecuta al abrir esta gu\xEDa."
|
|
54
|
+
]
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// packages/core/src/audio-production.ts
|
|
59
|
+
function record(value, name, keys2) {
|
|
60
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${name} must be an object.`);
|
|
61
|
+
const result2 = value;
|
|
62
|
+
if (Object.keys(result2).some((key) => !keys2.includes(key))) throw new Error(`${name} contains an unsupported field.`);
|
|
63
|
+
return result2;
|
|
64
|
+
}
|
|
65
|
+
function text(value, name, max = 200) {
|
|
66
|
+
if (typeof value !== "string" || !value.trim() || value.length > max || /[\x00-\x1f\x7f]/.test(value)) throw new Error(`${name} must be nonempty text of at most ${max} characters.`);
|
|
67
|
+
return value.trim();
|
|
68
|
+
}
|
|
69
|
+
function validateAudioLoudnessTarget(value) {
|
|
70
|
+
const input = record(value, "loudness", ["targetIntegratedLufs", "toleranceLu", "maxTruePeakDbtp"]);
|
|
71
|
+
for (const field of ["targetIntegratedLufs", "toleranceLu", "maxTruePeakDbtp"]) {
|
|
72
|
+
const number2 = input[field];
|
|
73
|
+
if (typeof number2 !== "number" || !Number.isFinite(number2) || (field === "toleranceLu" ? number2 < 0 || number2 > 150 : number2 < -150 || number2 > 24)) throw new Error(`loudness.${field} must be a finite measurement target within the supported range.`);
|
|
74
|
+
}
|
|
75
|
+
return { targetIntegratedLufs: input.targetIntegratedLufs, toleranceLu: input.toleranceLu, maxTruePeakDbtp: input.maxTruePeakDbtp };
|
|
76
|
+
}
|
|
77
|
+
function validateAudioProductionPreferences(value) {
|
|
78
|
+
const input = record(value, "audioProduction", ["schemaVersion", "voice", "music", "sfx", "loudness"]);
|
|
79
|
+
if (input.schemaVersion !== 1) throw new Error("audioProduction.schemaVersion must be 1.");
|
|
80
|
+
const result2 = { schemaVersion: 1 };
|
|
81
|
+
if (input.voice === null) result2.voice = null;
|
|
82
|
+
else if (input.voice !== void 0) {
|
|
83
|
+
const voice = record(input.voice, "voice", ["mode", "provider", "voiceId"]);
|
|
84
|
+
if (voice.mode === "original" || voice.mode === "none") {
|
|
85
|
+
if (voice.provider !== void 0 || voice.voiceId !== void 0) throw new Error("Original/no narration does not select a generated voice.");
|
|
86
|
+
result2.voice = { mode: voice.mode };
|
|
87
|
+
} else if (voice.mode === "catalog" || voice.mode === "cloned") {
|
|
88
|
+
result2.voice = { mode: voice.mode, provider: text(voice.provider, "voice.provider"), voiceId: text(voice.voiceId, "voice.voiceId") };
|
|
89
|
+
} else throw new Error("voice.mode must explicitly select original, none, catalog or cloned.");
|
|
90
|
+
}
|
|
91
|
+
if (input.music === null) result2.music = null;
|
|
92
|
+
else if (input.music !== void 0) {
|
|
93
|
+
const music = record(input.music, "music", ["intensity"]);
|
|
94
|
+
result2.music = {};
|
|
95
|
+
if (music.intensity !== void 0) {
|
|
96
|
+
if (music.intensity !== null && (typeof music.intensity !== "string" || !["none", "subtle", "present"].includes(music.intensity))) throw new Error("music.intensity must be none, subtle, present or null.");
|
|
97
|
+
result2.music.intensity = music.intensity;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (input.sfx === null) result2.sfx = null;
|
|
101
|
+
else if (input.sfx !== void 0) {
|
|
102
|
+
const sfx = record(input.sfx, "sfx", ["library", "generation", "density", "hookWithoutEffects", "character"]);
|
|
103
|
+
result2.sfx = {};
|
|
104
|
+
if (sfx.library === null) result2.sfx.library = null;
|
|
105
|
+
else if (sfx.library !== void 0) {
|
|
106
|
+
const library = record(sfx.library, "sfx.library", ["bankId"]);
|
|
107
|
+
const bankId = text(library.bankId, "sfx.library.bankId");
|
|
108
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(bankId)) throw new Error("sfx.library.bankId must be a portable identifier, not a path.");
|
|
109
|
+
result2.sfx.library = { bankId };
|
|
110
|
+
}
|
|
111
|
+
if (sfx.generation !== void 0) {
|
|
112
|
+
if (sfx.generation !== null && (typeof sfx.generation !== "string" || !["allowed", "forbidden"].includes(sfx.generation))) throw new Error("sfx.generation must be allowed, forbidden or null.");
|
|
113
|
+
result2.sfx.generation = sfx.generation;
|
|
114
|
+
}
|
|
115
|
+
if (sfx.density !== void 0) {
|
|
116
|
+
if (sfx.density !== null && (typeof sfx.density !== "string" || !["none", "sparse", "moderate", "dense"].includes(sfx.density))) throw new Error("sfx.density must be none, sparse, moderate, dense or null.");
|
|
117
|
+
result2.sfx.density = sfx.density;
|
|
118
|
+
}
|
|
119
|
+
if (sfx.character !== void 0) {
|
|
120
|
+
if (sfx.character !== null && !["organic", "digital", "cinematic", "mixed"].includes(sfx.character)) throw new Error("Invalid SFX character.");
|
|
121
|
+
result2.sfx.character = sfx.character;
|
|
122
|
+
}
|
|
123
|
+
if (sfx.hookWithoutEffects !== void 0) {
|
|
124
|
+
if (sfx.hookWithoutEffects !== null && typeof sfx.hookWithoutEffects !== "boolean") throw new Error("sfx.hookWithoutEffects must be a boolean or null.");
|
|
125
|
+
result2.sfx.hookWithoutEffects = sfx.hookWithoutEffects;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (input.loudness !== void 0) result2.loudness = input.loudness === null ? null : validateAudioLoudnessTarget(input.loudness);
|
|
129
|
+
return result2;
|
|
130
|
+
}
|
|
131
|
+
function resolveAudioProductionPreferences(workspace, project) {
|
|
132
|
+
const base = workspace === void 0 ? { schemaVersion: 1 } : validateAudioProductionPreferences(workspace);
|
|
133
|
+
const override = project === void 0 ? { schemaVersion: 1 } : validateAudioProductionPreferences(project);
|
|
134
|
+
return { ...base, ...override, ...override.music ? { music: { ...base.music, ...override.music } } : {}, ...override.sfx ? { sfx: { ...base.sfx, ...override.sfx } } : {} };
|
|
135
|
+
}
|
|
136
|
+
function applyEditorialAudioConstraints(preferences, profile) {
|
|
137
|
+
let effective = preferences === void 0 ? void 0 : validateAudioProductionPreferences(preferences);
|
|
138
|
+
const warnings = [];
|
|
139
|
+
const withoutMusic = profile.sound === "Sin m\xFAsica ni efectos";
|
|
140
|
+
const withoutSfx = withoutMusic || profile.avoid?.includes("Sin efectos de sonido");
|
|
141
|
+
if (withoutMusic) {
|
|
142
|
+
if (effective?.music?.intensity && effective.music.intensity !== "none") warnings.push("Music is disabled by the saved editorial restriction; the original audio preferences remain unchanged.");
|
|
143
|
+
effective = { ...effective, schemaVersion: 1, music: { ...effective?.music, intensity: "none" } };
|
|
144
|
+
}
|
|
145
|
+
if (withoutSfx) {
|
|
146
|
+
if (effective?.sfx?.density && effective.sfx.density !== "none") warnings.push("SFX are disabled by the saved editorial restriction; the original audio preferences remain unchanged.");
|
|
147
|
+
effective = { ...effective, schemaVersion: 1, sfx: { ...effective?.sfx, density: "none", generation: "forbidden" } };
|
|
148
|
+
}
|
|
149
|
+
const narration = profile.editorialDelivery?.narration;
|
|
150
|
+
const voice = effective?.voice;
|
|
151
|
+
if (narration === "original" || narration === "none") {
|
|
152
|
+
if (voice && voice.mode !== narration) warnings.push(`Narration preference ${narration} conflicts with audio voice ${voice.mode}. Do not generate narration; confirm any deliberate project exception with the current request.`);
|
|
153
|
+
effective = { ...effective, schemaVersion: 1, voice: { mode: narration } };
|
|
154
|
+
} else if (narration === "voiceover" && voice && (voice.mode === "original" || voice.mode === "none")) {
|
|
155
|
+
warnings.push(`Voiceover preference conflicts with audio voice ${voice.mode}. Resolve narration with the current request; do not infer a paid provider or voice.`);
|
|
156
|
+
}
|
|
157
|
+
return { ...effective ? { preferences: effective } : {}, warnings };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// packages/core/src/sfx-library.ts
|
|
161
|
+
var sfxCharacters = ["organic", "digital", "cinematic", "mixed"];
|
|
162
|
+
var hash = /^[a-f0-9]{64}$/;
|
|
163
|
+
function sfxBankPath(bankId) {
|
|
164
|
+
if (!/^[a-z0-9][a-z0-9_-]{0,79}$/.test(bankId)) throw new Error("SFX bank ID must use lowercase letters, numbers, _ or - (max 80).");
|
|
165
|
+
return `.editorbrain/sfx/${bankId}/catalog.json`;
|
|
166
|
+
}
|
|
167
|
+
function object(value, keys2) {
|
|
168
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((k) => !keys2.includes(k))) throw new Error("Unsupported SFX catalog fields. Local paths stay in .editorbrain-local/.");
|
|
169
|
+
return value;
|
|
170
|
+
}
|
|
171
|
+
function text2(value, max) {
|
|
172
|
+
if (typeof value !== "string" || !value.trim() || value.length > max || /[\x00-\x1f\x7f]/.test(value)) throw new Error("Invalid SFX text.");
|
|
173
|
+
return value.trim();
|
|
174
|
+
}
|
|
175
|
+
function validateSfxBank(value) {
|
|
176
|
+
const v = object(value, ["schemaVersion", "bankId", "entries"]);
|
|
177
|
+
if (v.schemaVersion !== 1 || typeof v.bankId !== "string") throw new Error("Invalid SFX bank.");
|
|
178
|
+
sfxBankPath(v.bankId);
|
|
179
|
+
if (!Array.isArray(v.entries) || v.entries.length > 100) throw new Error("A SFX bank supports at most 100 entries. Split larger collections by purpose.");
|
|
180
|
+
const ids = /* @__PURE__ */ new Set();
|
|
181
|
+
const entries = v.entries.map((raw) => {
|
|
182
|
+
const e = object(raw, ["id", "name", "tags", "character", "durationSeconds", "sha256", "assetId", "license", "audition"]);
|
|
183
|
+
if (typeof e.id !== "string" || !/^[a-z0-9][a-z0-9_-]{0,79}$/.test(e.id) || ids.has(e.id)) throw new Error("Invalid or duplicate SFX ID.");
|
|
184
|
+
ids.add(e.id);
|
|
185
|
+
if (typeof e.sha256 !== "string" || !hash.test(e.sha256) || e.assetId !== void 0 && (typeof e.assetId !== "string" || !hash.test(e.assetId))) throw new Error("SFX must pin a SHA-256 and optional owned asset ID.");
|
|
186
|
+
if (!Number.isFinite(e.durationSeconds) || e.durationSeconds <= 0 || e.durationSeconds > 600) throw new Error("SFX duration must be measured, between 0 and 600 seconds.");
|
|
187
|
+
if (!Array.isArray(e.tags) || e.tags.length > 24) throw new Error("Use up to 24 SFX tags.");
|
|
188
|
+
if (e.character !== void 0 && !sfxCharacters.includes(e.character)) throw new Error("Unknown SFX character.");
|
|
189
|
+
const license = object(e.license, ["kind", "note"]);
|
|
190
|
+
if (!["owned", "cc0", "licensed", "unverified"].includes(license.kind) || !["unreviewed", "reviewed"].includes(e.audition)) throw new Error("Declare rights and listening status separately.");
|
|
191
|
+
return { id: e.id, name: text2(e.name, 180), tags: e.tags.map((t) => text2(t, 60)), ...e.character ? { character: e.character } : {}, durationSeconds: e.durationSeconds, sha256: e.sha256, ...e.assetId ? { assetId: e.assetId } : {}, license: { kind: license.kind, note: text2(license.note, 500) }, audition: e.audition };
|
|
192
|
+
});
|
|
193
|
+
return { schemaVersion: 1, bankId: v.bankId, entries };
|
|
194
|
+
}
|
|
195
|
+
function sfxPolicy(audio, sceneRole = "body") {
|
|
196
|
+
if (!["hook", "body", "cta"].includes(sceneRole)) throw new Error("Choose sceneRole hook, body or cta.");
|
|
197
|
+
const sfx = validateAudioProductionPreferences(audio ?? { schemaVersion: 1 }).sfx;
|
|
198
|
+
const blocked = sfx?.density === "none" || sceneRole === "hook" && sfx?.hookWithoutEffects === true;
|
|
199
|
+
return {
|
|
200
|
+
enabled: !blocked,
|
|
201
|
+
density: sfx?.density ?? "adaptive",
|
|
202
|
+
character: sfx?.character ?? "adaptive",
|
|
203
|
+
hookWithoutEffects: sfx?.hookWithoutEffects ?? false,
|
|
204
|
+
bankId: sfx?.library?.bankId ?? null,
|
|
205
|
+
reason: blocked ? sfx?.density === "none" ? "SFX disabled by effective preferences." : "Keep this hook free of added SFX." : null,
|
|
206
|
+
guidance: "Density is frequency, not volume or a quota. Choose by scene purpose, audition first, synchronize the accent, protect speech, vary repeated sounds and inspect the final mix. No automatic generation or spending."
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
var groups = [["click", "clic", "boton", "button", "interfaz", "interface"], ["whoosh", "swish", "transicion", "transition", "barrido"], ["paper", "papel", "pagina", "page"], ["tick", "reloj", "clock", "tictac"], ["impact", "impacto", "golpe", "hit"], ["pop", "popping", "burbuja"], ["bell", "campana", "notificacion", "notification"]];
|
|
210
|
+
var normalize = (v) => v.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
|
|
211
|
+
function words(v) {
|
|
212
|
+
return normalize(v).match(/[a-z0-9]+/g) ?? [];
|
|
213
|
+
}
|
|
214
|
+
function searchSfx(bank, options) {
|
|
215
|
+
bank = validateSfxBank(bank);
|
|
216
|
+
if (typeof options.query !== "string" || options.query.length > 200 || /[\x00-\x1f\x7f]/.test(options.query) || !Number.isInteger(options.limit ?? 6) || (options.limit ?? 6) < 1 || (options.limit ?? 6) > 20 || options.sceneRole !== void 0 && !["hook", "body", "cta"].includes(options.sceneRole)) throw new Error("Use query up to 200 characters, limit 1\u201320 and sceneRole hook/body/cta.");
|
|
217
|
+
const policy = sfxPolicy(options.audio, options.sceneRole);
|
|
218
|
+
if (policy.bankId && policy.bankId !== bank.bankId) throw new Error("The requested bank differs from the effective project preference. Record a deliberate project exception first.");
|
|
219
|
+
const terms = words(options.query).filter((w) => !["un", "una", "de", "para", "con", "el", "la", "a", "the", "for", "and", "sound", "sonido", "efecto"].includes(w));
|
|
220
|
+
const expanded = new Set(terms.flatMap((t) => groups.find((g) => g.includes(t)) ?? [t]));
|
|
221
|
+
const candidates = policy.enabled ? bank.entries.filter((e) => options.includeUnverified || e.license.kind !== "unverified").map((e) => {
|
|
222
|
+
const haystack = new Set(words([e.name, ...e.tags].join(" ")));
|
|
223
|
+
const matches = [...expanded].filter((t) => haystack.has(t));
|
|
224
|
+
return { entry: e, matches, score: matches.length * 4 + (policy.character !== "adaptive" && e.character === policy.character ? 2 : 0) };
|
|
225
|
+
}).filter((e) => !terms.length || e.matches.length > 0).sort((a, b) => b.score - a.score || a.entry.id.localeCompare(b.entry.id)) : [];
|
|
226
|
+
return { bankId: bank.bankId, policy, total: candidates.length, items: candidates.slice(0, options.limit ?? 6).map(({ entry, matches }) => ({ ...entry, matchedTerms: matches })), notice: "Lexical candidates with bilingual tags, not audio understanding. Rights are declared, not independently verified. Audition before use; metadata or a preview is not proof of an audible final mix." };
|
|
227
|
+
}
|
|
228
|
+
function checkSfxCues(audio, input) {
|
|
229
|
+
const p = object(input, ["durationSeconds", "hookEndSeconds", "cues"]);
|
|
230
|
+
if (!Number.isFinite(p.durationSeconds) || p.durationSeconds <= 0 || !Number.isFinite(p.hookEndSeconds) || p.hookEndSeconds < 0 || p.hookEndSeconds > p.durationSeconds || !Array.isArray(p.cues) || p.cues.length > 1e3) throw new Error("Declare video duration, hookEndSeconds and up to 1000 cues.");
|
|
231
|
+
const issues = [], warnings = [];
|
|
232
|
+
const cues = p.cues.map((raw) => {
|
|
233
|
+
const c = object(raw, ["at", "durationSeconds", "soundId", "purpose"]);
|
|
234
|
+
if (!Number.isFinite(c.at) || c.at < 0 || !Number.isFinite(c.durationSeconds) || c.durationSeconds <= 0 || c.at + c.durationSeconds > p.durationSeconds + 1e-3) throw new Error("SFX cue lies outside the video.");
|
|
235
|
+
return { at: c.at, durationSeconds: c.durationSeconds, soundId: text2(c.soundId, 80), purpose: text2(c.purpose, 200) };
|
|
236
|
+
}).sort((a, b) => a.at - b.at);
|
|
237
|
+
if (!sfxPolicy(audio).enabled && cues.length) issues.push("SFX are disabled, but the plan contains added effects.");
|
|
238
|
+
if (audio?.sfx?.hookWithoutEffects && cues.some((c) => c.at < p.hookEndSeconds)) issues.push("Added SFX overlap the protected hook.");
|
|
239
|
+
if (cues.some((c, i) => i > 0 && c.at - cues[i - 1].at < 0.3)) warnings.push("Closely spaced accents: listen for saturation.");
|
|
240
|
+
if (cues.some((c, i) => i > 1 && c.soundId === cues[i - 1].soundId && c.soundId === cues[i - 2].soundId)) warnings.push("Same sound repeated three times: check whether repetition is intentional.");
|
|
241
|
+
return { passed: issues.length === 0, issues, warnings, policy: sfxPolicy(audio), cueCount: cues.length, scope: "Declared cue plan only; does not inspect or certify audio. Run production audio-check and listen to the final render." };
|
|
242
|
+
}
|
|
243
|
+
|
|
3
244
|
// packages/core/src/memory.ts
|
|
4
245
|
function memoryPath(scope, id2) {
|
|
5
246
|
if (!["brand", "style", "project"].includes(scope) || !/^[a-z0-9][a-z0-9_-]{0,79}$/.test(id2)) throw new Error("Invalid memory scope");
|
|
@@ -32,7 +273,7 @@ function resolveMemory(files, p) {
|
|
|
32
273
|
|
|
33
274
|
// packages/core/src/style-authoring.ts
|
|
34
275
|
var own = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
|
|
35
|
-
var
|
|
276
|
+
var object2 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
36
277
|
var nonempty = (value) => typeof value === "string" && !!value.trim();
|
|
37
278
|
var identifier = /^[a-z0-9][a-z0-9_-]{0,79}$/;
|
|
38
279
|
var digest = /^[a-f0-9]{64}$/;
|
|
@@ -52,11 +293,11 @@ function validateStyleResources(value, expectedId) {
|
|
|
52
293
|
const fields = (entry, allowed, label) => {
|
|
53
294
|
if (Object.keys(entry).some((key) => !allowed.includes(key))) add(`${label} has unsupported fields; do not include URLs, credentials or hidden resource sources`);
|
|
54
295
|
};
|
|
55
|
-
const
|
|
296
|
+
const text9 = (value2, label) => {
|
|
56
297
|
if (!nonempty(value2)) add(`${label} must be nonempty text`);
|
|
57
298
|
else if (forbiddenText.test(value2) || /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(value2)) add(`${label} cannot contain URLs, credentials or control characters`);
|
|
58
299
|
};
|
|
59
|
-
if (!
|
|
300
|
+
if (!object2(value)) return { valid: false, issues: [{ path, message: "Style resources must be a JSON object" }] };
|
|
60
301
|
fields(value, ["schemaVersion", "id", "resources"], "Style resources");
|
|
61
302
|
if (value.schemaVersion !== 1) add("schemaVersion must be 1");
|
|
62
303
|
if (!identifier.test(expectedId) || value.id !== expectedId) add(`Style resource id must match path: ${expectedId}`);
|
|
@@ -65,7 +306,7 @@ function validateStyleResources(value, expectedId) {
|
|
|
65
306
|
const ids = /* @__PURE__ */ new Set();
|
|
66
307
|
for (const [index, resource] of value.resources.entries()) {
|
|
67
308
|
const label = `Resource ${index + 1}`;
|
|
68
|
-
if (!
|
|
309
|
+
if (!object2(resource)) {
|
|
69
310
|
add(`${label} must be an object`);
|
|
70
311
|
continue;
|
|
71
312
|
}
|
|
@@ -74,10 +315,10 @@ function validateStyleResources(value, expectedId) {
|
|
|
74
315
|
else if (ids.has(resource.id)) add(`Duplicate resource id: ${resource.id}`);
|
|
75
316
|
else ids.add(resource.id);
|
|
76
317
|
if (!kinds.includes(resource.kind)) add(`${label} has an invalid kind`);
|
|
77
|
-
|
|
318
|
+
text9(resource.purpose, `${label} purpose`);
|
|
78
319
|
if (typeof resource.required !== "boolean") add(`${label} required must be boolean`);
|
|
79
320
|
const source = resource.source;
|
|
80
|
-
if (!
|
|
321
|
+
if (!object2(source)) {
|
|
81
322
|
add(`${label} source must be local, asset or requirement`);
|
|
82
323
|
continue;
|
|
83
324
|
}
|
|
@@ -94,7 +335,7 @@ function validateStyleResources(value, expectedId) {
|
|
|
94
335
|
if (typeof source.assetId !== "string" || !digest.test(source.assetId)) add(`${label} assetId must be a 64-character lowercase hexadecimal digest`);
|
|
95
336
|
} else if (source.type === "requirement") {
|
|
96
337
|
fields(source, ["type", "description"], `${label} requirement source`);
|
|
97
|
-
|
|
338
|
+
text9(source.description, `${label} requirement description`);
|
|
98
339
|
} else add(`${label} source must be local, asset or requirement`);
|
|
99
340
|
}
|
|
100
341
|
}
|
|
@@ -743,10 +984,10 @@ var MEDIA_STYLE_CATALOG = {
|
|
|
743
984
|
function getMediaStyle(id2, version) {
|
|
744
985
|
return MEDIA_STYLE_CATALOG.styles.find((style) => style.id === id2 && (version === void 0 || style.version === version));
|
|
745
986
|
}
|
|
746
|
-
var
|
|
987
|
+
var record2 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
747
988
|
var only = (value, fields) => Object.keys(value).every((key) => fields.includes(key));
|
|
748
989
|
function visualTasteIssues(value) {
|
|
749
|
-
if (!
|
|
990
|
+
if (!record2(value) || !only(value, ["density", "energy", "contrast"])) return ["visualTaste supports density, energy and contrast only."];
|
|
750
991
|
const options = { density: ["minimal", "layered"], energy: ["calm", "lively"], contrast: ["soft", "strong"] };
|
|
751
992
|
return Object.entries(options).flatMap(([key, allowed]) => value[key] === void 0 || typeof value[key] === "string" && allowed.includes(value[key]) ? [] : [`Invalid visualTaste ${key}. Omit undecided choices.`]);
|
|
752
993
|
}
|
|
@@ -763,7 +1004,7 @@ function mediaStyleSelectionIssues(value, category) {
|
|
|
763
1004
|
if (!Array.isArray(value) || value.length > 24) return ["mediaStyles must be an array of at most 24 selections."];
|
|
764
1005
|
const issues = [], ids = /* @__PURE__ */ new Set(), defaults = /* @__PURE__ */ new Set();
|
|
765
1006
|
for (const selection of value) {
|
|
766
|
-
if (!
|
|
1007
|
+
if (!record2(selection) || !only(selection, ["styleId", "version", "state"]) || typeof selection.styleId !== "string" || typeof selection.version !== "string" || !["favorite", "default", "avoid"].includes(String(selection.state))) {
|
|
767
1008
|
issues.push("Media style selections require styleId, version and favorite/default/avoid state.");
|
|
768
1009
|
continue;
|
|
769
1010
|
}
|
|
@@ -783,12 +1024,12 @@ function mediaStyleSelectionIssues(value, category) {
|
|
|
783
1024
|
return issues;
|
|
784
1025
|
}
|
|
785
1026
|
function mediaStyleOverrideIssues(value) {
|
|
786
|
-
if (!
|
|
1027
|
+
if (!record2(value) || !only(value, ["image", "motion"])) return ["mediaStyleOverrides supports image and motion categories only."];
|
|
787
1028
|
const issues = [];
|
|
788
1029
|
for (const category of ["image", "motion"]) {
|
|
789
1030
|
const override = value[category];
|
|
790
1031
|
if (override === void 0) continue;
|
|
791
|
-
if (!
|
|
1032
|
+
if (!record2(override) || !["inherit", "replace", "disabled"].includes(String(override.mode))) {
|
|
792
1033
|
issues.push(`Invalid ${category} media style override.`);
|
|
793
1034
|
continue;
|
|
794
1035
|
}
|
|
@@ -867,11 +1108,11 @@ var profileOptions = {
|
|
|
867
1108
|
aspects: ["Tipograf\xEDa", "Composici\xF3n", "Colores", "Im\xE1genes", "Ritmo", "Animaci\xF3n", "Sonido"]
|
|
868
1109
|
};
|
|
869
1110
|
var emptyProfile = () => ({ schemaVersion: 1, experience: "", role: "", niche: "", platforms: [], formats: [], goal: "", pace: "", captions: "", sound: "", freedom: "", assets: "", avoid: [], notes: "", references: [] });
|
|
870
|
-
var
|
|
1111
|
+
var record3 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
871
1112
|
var string = (value, min, max) => typeof value === "string" && value.length >= min && value.length <= max;
|
|
872
1113
|
var choices = (value, options) => Array.isArray(value) && value.length <= options.length && value.every((item) => typeof item === "string" && options.includes(item));
|
|
873
1114
|
function editorialProfileIssues(value) {
|
|
874
|
-
if (!
|
|
1115
|
+
if (!record3(value)) return ["El perfil editorial debe ser un objeto."];
|
|
875
1116
|
const issues = [];
|
|
876
1117
|
if (value.schemaVersion !== 1) issues.push("schemaVersion del perfil editorial debe ser 1.");
|
|
877
1118
|
const fields = [...Object.keys(emptyProfile()), "mediaStyles", "visualTaste", "motionSelection", "motionProduction", "editorialDelivery", "mediaSources"];
|
|
@@ -886,7 +1127,7 @@ function editorialProfileIssues(value) {
|
|
|
886
1127
|
if (!string(value.notes, 0, 1500)) issues.push("notes debe ser texto de hasta 1500 caracteres.");
|
|
887
1128
|
if (!Array.isArray(value.references) || value.references.length > 12) issues.push("references admite hasta 12 referencias.");
|
|
888
1129
|
else for (const ref of value.references) {
|
|
889
|
-
if (!
|
|
1130
|
+
if (!record3(ref) || !string(ref.videoId, 1, 150) || !string(ref.exampleId, 1, 150) || typeof ref.timestampMs !== "number" || !Number.isFinite(ref.timestampMs) || ref.timestampMs < 0 || !["like", "avoid"].includes(String(ref.sentiment)) || !choices(ref.aspects, profileOptions.aspects)) issues.push("Referencia editorial inv\xE1lida.");
|
|
890
1131
|
}
|
|
891
1132
|
if (value.mediaStyles !== void 0) issues.push(...mediaStyleSelectionIssues(value.mediaStyles));
|
|
892
1133
|
if (value.visualTaste !== void 0) issues.push(...visualTasteIssues(value.visualTaste));
|
|
@@ -941,115 +1182,17 @@ ${block}
|
|
|
941
1182
|
`;
|
|
942
1183
|
}
|
|
943
1184
|
|
|
944
|
-
// packages/core/src/audio-production.ts
|
|
945
|
-
function record3(value, name, keys2) {
|
|
946
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${name} must be an object.`);
|
|
947
|
-
const result2 = value;
|
|
948
|
-
if (Object.keys(result2).some((key) => !keys2.includes(key))) throw new Error(`${name} contains an unsupported field.`);
|
|
949
|
-
return result2;
|
|
950
|
-
}
|
|
951
|
-
function text(value, name, max = 200) {
|
|
952
|
-
if (typeof value !== "string" || !value.trim() || value.length > max || /[\x00-\x1f\x7f]/.test(value)) throw new Error(`${name} must be nonempty text of at most ${max} characters.`);
|
|
953
|
-
return value.trim();
|
|
954
|
-
}
|
|
955
|
-
function validateAudioLoudnessTarget(value) {
|
|
956
|
-
const input = record3(value, "loudness", ["targetIntegratedLufs", "toleranceLu", "maxTruePeakDbtp"]);
|
|
957
|
-
for (const field of ["targetIntegratedLufs", "toleranceLu", "maxTruePeakDbtp"]) {
|
|
958
|
-
const number2 = input[field];
|
|
959
|
-
if (typeof number2 !== "number" || !Number.isFinite(number2) || (field === "toleranceLu" ? number2 < 0 || number2 > 150 : number2 < -150 || number2 > 24)) throw new Error(`loudness.${field} must be a finite measurement target within the supported range.`);
|
|
960
|
-
}
|
|
961
|
-
return { targetIntegratedLufs: input.targetIntegratedLufs, toleranceLu: input.toleranceLu, maxTruePeakDbtp: input.maxTruePeakDbtp };
|
|
962
|
-
}
|
|
963
|
-
function validateAudioProductionPreferences(value) {
|
|
964
|
-
const input = record3(value, "audioProduction", ["schemaVersion", "voice", "music", "sfx", "loudness"]);
|
|
965
|
-
if (input.schemaVersion !== 1) throw new Error("audioProduction.schemaVersion must be 1.");
|
|
966
|
-
const result2 = { schemaVersion: 1 };
|
|
967
|
-
if (input.voice === null) result2.voice = null;
|
|
968
|
-
else if (input.voice !== void 0) {
|
|
969
|
-
const voice = record3(input.voice, "voice", ["mode", "provider", "voiceId"]);
|
|
970
|
-
if (voice.mode === "original" || voice.mode === "none") {
|
|
971
|
-
if (voice.provider !== void 0 || voice.voiceId !== void 0) throw new Error("Original/no narration does not select a generated voice.");
|
|
972
|
-
result2.voice = { mode: voice.mode };
|
|
973
|
-
} else if (voice.mode === "catalog" || voice.mode === "cloned") {
|
|
974
|
-
result2.voice = { mode: voice.mode, provider: text(voice.provider, "voice.provider"), voiceId: text(voice.voiceId, "voice.voiceId") };
|
|
975
|
-
} else throw new Error("voice.mode must explicitly select original, none, catalog or cloned.");
|
|
976
|
-
}
|
|
977
|
-
if (input.music === null) result2.music = null;
|
|
978
|
-
else if (input.music !== void 0) {
|
|
979
|
-
const music = record3(input.music, "music", ["intensity"]);
|
|
980
|
-
result2.music = {};
|
|
981
|
-
if (music.intensity !== void 0) {
|
|
982
|
-
if (music.intensity !== null && (typeof music.intensity !== "string" || !["none", "subtle", "present"].includes(music.intensity))) throw new Error("music.intensity must be none, subtle, present or null.");
|
|
983
|
-
result2.music.intensity = music.intensity;
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
if (input.sfx === null) result2.sfx = null;
|
|
987
|
-
else if (input.sfx !== void 0) {
|
|
988
|
-
const sfx = record3(input.sfx, "sfx", ["library", "generation", "density", "hookWithoutEffects"]);
|
|
989
|
-
result2.sfx = {};
|
|
990
|
-
if (sfx.library === null) result2.sfx.library = null;
|
|
991
|
-
else if (sfx.library !== void 0) {
|
|
992
|
-
const library = record3(sfx.library, "sfx.library", ["bankId"]);
|
|
993
|
-
const bankId = text(library.bankId, "sfx.library.bankId");
|
|
994
|
-
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(bankId)) throw new Error("sfx.library.bankId must be a portable identifier, not a path.");
|
|
995
|
-
result2.sfx.library = { bankId };
|
|
996
|
-
}
|
|
997
|
-
if (sfx.generation !== void 0) {
|
|
998
|
-
if (sfx.generation !== null && (typeof sfx.generation !== "string" || !["allowed", "forbidden"].includes(sfx.generation))) throw new Error("sfx.generation must be allowed, forbidden or null.");
|
|
999
|
-
result2.sfx.generation = sfx.generation;
|
|
1000
|
-
}
|
|
1001
|
-
if (sfx.density !== void 0) {
|
|
1002
|
-
if (sfx.density !== null && (typeof sfx.density !== "string" || !["none", "sparse", "moderate", "dense"].includes(sfx.density))) throw new Error("sfx.density must be none, sparse, moderate, dense or null.");
|
|
1003
|
-
result2.sfx.density = sfx.density;
|
|
1004
|
-
}
|
|
1005
|
-
if (sfx.hookWithoutEffects !== void 0) {
|
|
1006
|
-
if (sfx.hookWithoutEffects !== null && typeof sfx.hookWithoutEffects !== "boolean") throw new Error("sfx.hookWithoutEffects must be a boolean or null.");
|
|
1007
|
-
result2.sfx.hookWithoutEffects = sfx.hookWithoutEffects;
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
if (input.loudness !== void 0) result2.loudness = input.loudness === null ? null : validateAudioLoudnessTarget(input.loudness);
|
|
1011
|
-
return result2;
|
|
1012
|
-
}
|
|
1013
|
-
function resolveAudioProductionPreferences(workspace, project) {
|
|
1014
|
-
const base = workspace === void 0 ? { schemaVersion: 1 } : validateAudioProductionPreferences(workspace);
|
|
1015
|
-
const override = project === void 0 ? { schemaVersion: 1 } : validateAudioProductionPreferences(project);
|
|
1016
|
-
return { ...base, ...override, ...override.music ? { music: { ...base.music, ...override.music } } : {}, ...override.sfx ? { sfx: { ...base.sfx, ...override.sfx } } : {} };
|
|
1017
|
-
}
|
|
1018
|
-
function applyEditorialAudioConstraints(preferences, profile) {
|
|
1019
|
-
let effective = preferences === void 0 ? void 0 : validateAudioProductionPreferences(preferences);
|
|
1020
|
-
const warnings = [];
|
|
1021
|
-
const withoutMusic = profile.sound === "Sin m\xFAsica ni efectos";
|
|
1022
|
-
const withoutSfx = withoutMusic || profile.avoid?.includes("Sin efectos de sonido");
|
|
1023
|
-
if (withoutMusic) {
|
|
1024
|
-
if (effective?.music?.intensity && effective.music.intensity !== "none") warnings.push("Music is disabled by the saved editorial restriction; the original audio preferences remain unchanged.");
|
|
1025
|
-
effective = { ...effective, schemaVersion: 1, music: { ...effective?.music, intensity: "none" } };
|
|
1026
|
-
}
|
|
1027
|
-
if (withoutSfx) {
|
|
1028
|
-
if (effective?.sfx?.density && effective.sfx.density !== "none") warnings.push("SFX are disabled by the saved editorial restriction; the original audio preferences remain unchanged.");
|
|
1029
|
-
effective = { ...effective, schemaVersion: 1, sfx: { ...effective?.sfx, density: "none", generation: "forbidden" } };
|
|
1030
|
-
}
|
|
1031
|
-
const narration = profile.editorialDelivery?.narration;
|
|
1032
|
-
const voice = effective?.voice;
|
|
1033
|
-
if (narration === "original" || narration === "none") {
|
|
1034
|
-
if (voice && voice.mode !== narration) warnings.push(`Narration preference ${narration} conflicts with audio voice ${voice.mode}. Do not generate narration; confirm any deliberate project exception with the current request.`);
|
|
1035
|
-
effective = { ...effective, schemaVersion: 1, voice: { mode: narration } };
|
|
1036
|
-
} else if (narration === "voiceover" && voice && (voice.mode === "original" || voice.mode === "none")) {
|
|
1037
|
-
warnings.push(`Voiceover preference conflicts with audio voice ${voice.mode}. Resolve narration with the current request; do not infer a paid provider or voice.`);
|
|
1038
|
-
}
|
|
1039
|
-
return { ...effective ? { preferences: effective } : {}, warnings };
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
1185
|
// packages/core/src/onboarding.ts
|
|
1043
1186
|
var pretty = (value) => JSON.stringify(value, null, 2) + "\n";
|
|
1044
1187
|
var designText = "# Dise\xF1o\n\nAdapta composici\xF3n, ritmo y jerarqu\xEDa al contenido. Consulta tokens.json para los colores de la marca. Las tipograf\xEDas y recursos pendientes se deciden con el usuario antes de editar.\n";
|
|
1045
1188
|
var onboardingDocumentNames = ["brand.md", "design.md", "tokens.json", "language.json", "assets.json", "audio.json"];
|
|
1046
|
-
var
|
|
1189
|
+
var object3 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
1047
1190
|
var own2 = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
|
|
1048
1191
|
function record4(value, label, allowed) {
|
|
1049
|
-
if (!
|
|
1192
|
+
if (!object3(value) || Object.keys(value).some((key) => !allowed.includes(key))) throw new Error(`${label} contiene campos desconocidos o no es un objeto.`);
|
|
1050
1193
|
return value;
|
|
1051
1194
|
}
|
|
1052
|
-
function
|
|
1195
|
+
function text3(value, label, max, nonempty3 = false) {
|
|
1053
1196
|
if (typeof value !== "string" || value.length > max || /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(value) || nonempty3 && !value.trim()) throw new Error(`${label} debe ser texto${nonempty3 ? " no vac\xEDo" : ""} de hasta ${max} caracteres.`);
|
|
1054
1197
|
}
|
|
1055
1198
|
function validDocument(path, content) {
|
|
@@ -1069,7 +1212,7 @@ function mergeProfile(base, patch) {
|
|
|
1069
1212
|
}
|
|
1070
1213
|
if (key === "editorialDelivery" || key === "visualTaste") {
|
|
1071
1214
|
const fields = record4(value, key, key === "editorialDelivery" ? Object.keys(editorialDeliveryOptions) : ["density", "energy", "contrast"]);
|
|
1072
|
-
const previous =
|
|
1215
|
+
const previous = object3(result2[key]) ? result2[key] : {};
|
|
1073
1216
|
const next = { ...previous };
|
|
1074
1217
|
for (const [field, selection] of Object.entries(fields)) {
|
|
1075
1218
|
if (selection === null) delete next[field];
|
|
@@ -1082,7 +1225,7 @@ function mergeProfile(base, patch) {
|
|
|
1082
1225
|
}
|
|
1083
1226
|
function validateIdentity(value) {
|
|
1084
1227
|
const input = record4(value, "identity", ["name", "description", "audience", "tone", "links"]);
|
|
1085
|
-
for (const [key, max] of [["name", 100], ["description", 1500], ["audience", 500], ["tone", 500], ["links", 1500]]) if (input[key] !== void 0)
|
|
1228
|
+
for (const [key, max] of [["name", 100], ["description", 1500], ["audience", 500], ["tone", 500], ["links", 1500]]) if (input[key] !== void 0) text3(input[key], `identity.${key}`, max, key === "name");
|
|
1086
1229
|
return clone(input);
|
|
1087
1230
|
}
|
|
1088
1231
|
function validateOnboardingIntake(value) {
|
|
@@ -1092,17 +1235,17 @@ function validateOnboardingIntake(value) {
|
|
|
1092
1235
|
if (new TextEncoder().encode(JSON.stringify(input)).length > 96 * 1024) throw new Error("El formulario de onboarding supera 96 KiB.");
|
|
1093
1236
|
if (input.identity !== void 0) validateIdentity(input.identity);
|
|
1094
1237
|
if (input.tokens !== void 0) {
|
|
1095
|
-
if (!
|
|
1238
|
+
if (!object3(input.tokens) || Object.keys(input.tokens).length > 100) throw new Error("tokens debe contener hasta 100 reglas.");
|
|
1096
1239
|
for (const [key, rule] of Object.entries(input.tokens)) {
|
|
1097
1240
|
const check = rule === null ? { value: "", strength: "preferred" } : rule;
|
|
1098
1241
|
record4(check, `tokens.${key}`, ["value", "strength", "rationale"]);
|
|
1099
|
-
if (
|
|
1242
|
+
if (object3(check) && check.rationale !== void 0) text3(check.rationale, "rationale", 1500);
|
|
1100
1243
|
validDocument(`.editorbrain/brands/${input.brandId}/tokens.json`, pretty({ schemaVersion: 1, id: input.brandId, tokens: { [key]: check } }));
|
|
1101
1244
|
}
|
|
1102
1245
|
}
|
|
1103
1246
|
if (input.language !== void 0) {
|
|
1104
1247
|
const language = record4(input.language, "language", ["defaultLanguage", "glossary", "pronunciations", "doNotTranslate"]);
|
|
1105
|
-
if (language.defaultLanguage !== void 0 && language.defaultLanguage !== null)
|
|
1248
|
+
if (language.defaultLanguage !== void 0 && language.defaultLanguage !== null) text3(language.defaultLanguage, "defaultLanguage", 120, true);
|
|
1106
1249
|
for (const key of ["glossary", "pronunciations", "doNotTranslate"]) if (language[key] !== void 0 && (!Array.isArray(language[key]) || language[key].length > 1e3)) throw new Error(`${key} admite hasta 1000 entradas.`);
|
|
1107
1250
|
const next = { ...language };
|
|
1108
1251
|
if (next.defaultLanguage === null) delete next.defaultLanguage;
|
|
@@ -1397,14 +1540,14 @@ function record5(value, name, keys2) {
|
|
|
1397
1540
|
if (!value || typeof value !== "object" || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) throw new Error(`${name} must be an object.`);
|
|
1398
1541
|
if (Object.keys(value).some((key) => !keys2.includes(key))) throw new Error(`${name} has unsupported fields.`);
|
|
1399
1542
|
}
|
|
1400
|
-
function
|
|
1543
|
+
function text4(value, name, max) {
|
|
1401
1544
|
if (typeof value !== "string" || !value.trim() || value.length > max * 2 || Array.from(value).length > max || /[\x00-\x1f\x7f-\x9f\u202a-\u202e\u2066-\u2069]/.test(value) || /[\ud800-\udfff]/u.test(value)) throw new Error(`${name} requires 1\u2013${max} Unicode code points without control characters.`);
|
|
1402
1545
|
}
|
|
1403
1546
|
function numeric(value, name, min, max, integer2 = false) {
|
|
1404
1547
|
if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max || integer2 && !Number.isInteger(value)) throw new Error(`${name} must be ${integer2 ? "an integer" : "a number"} from ${min} to ${max}.`);
|
|
1405
1548
|
}
|
|
1406
1549
|
function rasterDimensions(binary, mime, name) {
|
|
1407
|
-
const
|
|
1550
|
+
const invalid5 = () => new Error(`${name} has malformed or missing raster dimensions.`);
|
|
1408
1551
|
const byte = (offset2) => binary.charCodeAt(offset2);
|
|
1409
1552
|
const u16 = (offset2) => byte(offset2) * 256 + byte(offset2 + 1);
|
|
1410
1553
|
const u32 = (offset2) => u16(offset2) * 65536 + u16(offset2 + 2);
|
|
@@ -1412,50 +1555,50 @@ function rasterDimensions(binary, mime, name) {
|
|
|
1412
1555
|
const le24 = (offset2) => le16(offset2) + byte(offset2 + 2) * 65536;
|
|
1413
1556
|
const le32 = (offset2) => le24(offset2) + byte(offset2 + 3) * 16777216;
|
|
1414
1557
|
const check = (width, height) => {
|
|
1415
|
-
if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) throw
|
|
1558
|
+
if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) throw invalid5();
|
|
1416
1559
|
if (width > MOTION_INPUT_LIMITS.maxImageDimension || height > MOTION_INPUT_LIMITS.maxImageDimension || width * height > MOTION_INPUT_LIMITS.maxImagePixels) throw new Error(`${name} raster dimensions exceed ${MOTION_INPUT_LIMITS.maxImageDimension} pixels per side or ${MOTION_INPUT_LIMITS.maxImagePixels} total pixels.`);
|
|
1417
1560
|
};
|
|
1418
1561
|
if (mime === "image/png") {
|
|
1419
|
-
if (u32(8) !== 13) throw
|
|
1562
|
+
if (u32(8) !== 13) throw invalid5();
|
|
1420
1563
|
check(u32(16), u32(20));
|
|
1421
1564
|
return;
|
|
1422
1565
|
}
|
|
1423
1566
|
if (mime === "image/jpeg") {
|
|
1424
1567
|
let offset2 = 2, dimensions;
|
|
1425
1568
|
while (offset2 < binary.length) {
|
|
1426
|
-
if (byte(offset2++) !== 255) throw
|
|
1569
|
+
if (byte(offset2++) !== 255) throw invalid5();
|
|
1427
1570
|
while (offset2 < binary.length && byte(offset2) === 255) offset2++;
|
|
1428
|
-
if (offset2 >= binary.length) throw
|
|
1571
|
+
if (offset2 >= binary.length) throw invalid5();
|
|
1429
1572
|
const marker = byte(offset2++);
|
|
1430
1573
|
if (marker === 217 || marker === 218) break;
|
|
1431
|
-
if (marker === 216 || marker === 0) throw
|
|
1574
|
+
if (marker === 216 || marker === 0) throw invalid5();
|
|
1432
1575
|
if (marker === 1 || marker >= 208 && marker <= 215) continue;
|
|
1433
|
-
if (offset2 + 2 > binary.length) throw
|
|
1576
|
+
if (offset2 + 2 > binary.length) throw invalid5();
|
|
1434
1577
|
const size = u16(offset2);
|
|
1435
|
-
if (size < 2 || offset2 + size > binary.length) throw
|
|
1578
|
+
if (size < 2 || offset2 + size > binary.length) throw invalid5();
|
|
1436
1579
|
if ([192, 193, 194, 195, 197, 198, 199, 201, 202, 203, 205, 206, 207].includes(marker)) {
|
|
1437
|
-
if (size < 11 || size !== 8 + byte(offset2 + 7) * 3) throw
|
|
1580
|
+
if (size < 11 || size !== 8 + byte(offset2 + 7) * 3) throw invalid5();
|
|
1438
1581
|
const width = u16(offset2 + 5), height = u16(offset2 + 3);
|
|
1439
1582
|
check(width, height);
|
|
1440
|
-
if (dimensions && (dimensions[0] !== width || dimensions[1] !== height)) throw
|
|
1583
|
+
if (dimensions && (dimensions[0] !== width || dimensions[1] !== height)) throw invalid5();
|
|
1441
1584
|
dimensions = [width, height];
|
|
1442
1585
|
}
|
|
1443
1586
|
offset2 += size;
|
|
1444
1587
|
}
|
|
1445
|
-
if (!dimensions) throw
|
|
1588
|
+
if (!dimensions) throw invalid5();
|
|
1446
1589
|
return;
|
|
1447
1590
|
}
|
|
1448
|
-
if (le32(4) !== binary.length - 8) throw
|
|
1591
|
+
if (le32(4) !== binary.length - 8) throw invalid5();
|
|
1449
1592
|
const frameDimensions = (kind, size, data) => {
|
|
1450
1593
|
if (kind === "VP8L") {
|
|
1451
|
-
if (size < 5 || byte(data) !== 47 || byte(data + 4) >> 5 !== 0) throw
|
|
1594
|
+
if (size < 5 || byte(data) !== 47 || byte(data + 4) >> 5 !== 0) throw invalid5();
|
|
1452
1595
|
const width = 1 + (byte(data + 1) | (byte(data + 2) & 63) << 8);
|
|
1453
1596
|
const height = 1 + (byte(data + 2) >> 6 | byte(data + 3) << 2 | (byte(data + 4) & 15) << 10);
|
|
1454
1597
|
check(width, height);
|
|
1455
1598
|
return true;
|
|
1456
1599
|
}
|
|
1457
1600
|
if (kind === "VP8 ") {
|
|
1458
|
-
if (size < 10 || byte(data) & 1 || binary.slice(data + 3, data + 6) !== "\x9D*") throw
|
|
1601
|
+
if (size < 10 || byte(data) & 1 || binary.slice(data + 3, data + 6) !== "\x9D*") throw invalid5();
|
|
1459
1602
|
check(le16(data + 6) & 16383, le16(data + 8) & 16383);
|
|
1460
1603
|
return true;
|
|
1461
1604
|
}
|
|
@@ -1464,26 +1607,26 @@ function rasterDimensions(binary, mime, name) {
|
|
|
1464
1607
|
let offset = 12, found = false;
|
|
1465
1608
|
while (offset + 8 <= binary.length) {
|
|
1466
1609
|
const kind = binary.slice(offset, offset + 4), size = le32(offset + 4), data = offset + 8;
|
|
1467
|
-
if (size > binary.length - data) throw
|
|
1610
|
+
if (size > binary.length - data) throw invalid5();
|
|
1468
1611
|
if (kind === "VP8X") {
|
|
1469
|
-
if (size !== 10) throw
|
|
1612
|
+
if (size !== 10) throw invalid5();
|
|
1470
1613
|
check(le24(data + 4) + 1, le24(data + 7) + 1);
|
|
1471
1614
|
found = true;
|
|
1472
1615
|
} else if (kind === "ANMF") {
|
|
1473
|
-
if (size < 16) throw
|
|
1616
|
+
if (size < 16) throw invalid5();
|
|
1474
1617
|
check(le24(data + 6) + 1, le24(data + 9) + 1);
|
|
1475
1618
|
let nested = data + 16, frame = false;
|
|
1476
1619
|
while (nested + 8 <= data + size) {
|
|
1477
1620
|
const frameKind = binary.slice(nested, nested + 4), frameSize = le32(nested + 4), frameData = nested + 8;
|
|
1478
|
-
if (frameSize > data + size - frameData) throw
|
|
1621
|
+
if (frameSize > data + size - frameData) throw invalid5();
|
|
1479
1622
|
frame = frameDimensions(frameKind, frameSize, frameData) || frame;
|
|
1480
1623
|
nested = frameData + frameSize + frameSize % 2;
|
|
1481
1624
|
}
|
|
1482
|
-
if (!frame || nested !== data + size) throw
|
|
1625
|
+
if (!frame || nested !== data + size) throw invalid5();
|
|
1483
1626
|
} else found = frameDimensions(kind, size, data) || found;
|
|
1484
1627
|
offset = data + size + size % 2;
|
|
1485
1628
|
}
|
|
1486
|
-
if (!found || offset !== binary.length) throw
|
|
1629
|
+
if (!found || offset !== binary.length) throw invalid5();
|
|
1487
1630
|
}
|
|
1488
1631
|
function dataUrl(value, name, kind) {
|
|
1489
1632
|
const max = kind === "font" ? MOTION_INPUT_LIMITS.fontBytes : MOTION_INPUT_LIMITS.imageBytes;
|
|
@@ -1519,16 +1662,16 @@ function validateMotionSceneConfig(value) {
|
|
|
1519
1662
|
const bg = luminance(value.palette.background), fg = luminance(value.palette.foreground);
|
|
1520
1663
|
if ((Math.max(bg, fg) + 0.05) / (Math.min(bg, fg) + 0.05) < 4.5) throw new Error("Foreground/background contrast must be at least 4.5:1; brand colors were not changed.");
|
|
1521
1664
|
record5(value.font, "font", ["family", "dataUrl"]);
|
|
1522
|
-
|
|
1665
|
+
text4(value.font.family, "font.family", 100);
|
|
1523
1666
|
if (!/^[\p{L}\p{N} _-]+$/u.test(value.font.family)) throw new Error("font.family must be one font family using letters, numbers, spaces, underscores or hyphens.");
|
|
1524
1667
|
dataUrl(value.font.dataUrl, "font.dataUrl", "font");
|
|
1525
|
-
|
|
1526
|
-
if (value.subtitle !== void 0)
|
|
1668
|
+
text4(value.title, "title", MOTION_INPUT_LIMITS.titleCodePoints);
|
|
1669
|
+
if (value.subtitle !== void 0) text4(value.subtitle, "subtitle", MOTION_INPUT_LIMITS.subtitleCodePoints);
|
|
1527
1670
|
if (!Array.isArray(value.items) || value.items.length < 2 || value.items.length > 4) throw new Error("Motion scene requires 2\u20134 items.");
|
|
1528
1671
|
value.items.forEach((item, index) => {
|
|
1529
1672
|
record5(item, `items[${index}]`, ["label", "detail", "image", "shape"]);
|
|
1530
|
-
|
|
1531
|
-
if (item.detail !== void 0)
|
|
1673
|
+
text4(item.label, `items[${index}].label`, MOTION_INPUT_LIMITS.labelCodePoints);
|
|
1674
|
+
if (item.detail !== void 0) text4(item.detail, `items[${index}].detail`, MOTION_INPUT_LIMITS.detailCodePoints);
|
|
1532
1675
|
if (item.image !== void 0 && codeNative(String(value.componentId))) throw new Error(`items[${index}].image is unsupported by this code-native component.`);
|
|
1533
1676
|
if (item.image !== void 0) dataUrl(item.image, `items[${index}].image`, "image");
|
|
1534
1677
|
if (item.shape !== void 0 && !["circle", "capsule", "diamond", "heart", "shield", "chat", "star"].includes(String(item.shape))) throw new Error(`items[${index}].shape is unsupported.`);
|
|
@@ -1541,8 +1684,8 @@ function validateMotionSceneConfig(value) {
|
|
|
1541
1684
|
var record6 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
1542
1685
|
var positive = (v) => typeof v === "number" && Number.isFinite(v) && v > 0;
|
|
1543
1686
|
var nonnegative = (v) => v === 0 || positive(v);
|
|
1544
|
-
var
|
|
1545
|
-
var
|
|
1687
|
+
var hash2 = (v) => typeof v === "string" && /^[a-f0-9]{64}$/.test(v);
|
|
1688
|
+
var text5 = (v) => typeof v === "string" && v.trim().length > 0 && v.length <= 2e3;
|
|
1546
1689
|
var range = (v) => record6(v) && nonnegative(v.startSeconds) && positive(v.endSeconds) && v.endSeconds > v.startSeconds;
|
|
1547
1690
|
var MAX_ENTRIES = 2e3;
|
|
1548
1691
|
function validateProductionPlan(value) {
|
|
@@ -1552,12 +1695,12 @@ function validateProductionPlan(value) {
|
|
|
1552
1695
|
if (!Array.isArray(value.inspections) || value.inspections.length > MAX_ENTRIES) issues.push("inspections must be an array of at most 2000 observations.");
|
|
1553
1696
|
else value.inspections.forEach((item, index) => {
|
|
1554
1697
|
const label = `inspections[${index}]`;
|
|
1555
|
-
if (!record6(item) || !
|
|
1698
|
+
if (!record6(item) || !hash2(item.sourceSha256) || !["visual", "motion", "audio"].includes(String(item.modality)) || !["sampled_frames", "playback", "listening"].includes(String(item.method))) {
|
|
1556
1699
|
issues.push(`${label}: invalid source hash, modality or method.`);
|
|
1557
1700
|
return;
|
|
1558
1701
|
}
|
|
1559
1702
|
if (Object.keys(item).some((key) => !["sourceSha256", "modality", "method", "intervals", "sampledSeconds", "note"].includes(key))) issues.push(`${label}: unsupported field.`);
|
|
1560
|
-
if (item.note !== void 0 && !
|
|
1703
|
+
if (item.note !== void 0 && !text5(item.note)) issues.push(`${label}: note must be nonempty text of at most 2000 characters.`);
|
|
1561
1704
|
if (item.method === "sampled_frames") {
|
|
1562
1705
|
if (item.modality !== "visual" || item.intervals !== void 0 || !Array.isArray(item.sampledSeconds) || !item.sampledSeconds.length || item.sampledSeconds.length > MAX_ENTRIES || !item.sampledSeconds.every(nonnegative)) issues.push(`${label}: sampled_frames requires visual modality and sampledSeconds; snapshots never establish continuous interval coverage.`);
|
|
1563
1706
|
} else {
|
|
@@ -1570,14 +1713,14 @@ function validateProductionPlan(value) {
|
|
|
1570
1713
|
const ids = /* @__PURE__ */ new Set();
|
|
1571
1714
|
value.uses.forEach((item, index) => {
|
|
1572
1715
|
const label = `uses[${index}]`;
|
|
1573
|
-
if (!record6(item) || typeof item.id !== "string" || !/^[a-zA-Z0-9_-]{1,80}$/.test(item.id) || !
|
|
1716
|
+
if (!record6(item) || typeof item.id !== "string" || !/^[a-zA-Z0-9_-]{1,80}$/.test(item.id) || !hash2(item.sourceSha256) || !range(item.source) || !range(item.timeline)) {
|
|
1574
1717
|
issues.push(`${label}: requires ID, source hash, source and timeline intervals.`);
|
|
1575
1718
|
return;
|
|
1576
1719
|
}
|
|
1577
1720
|
if (ids.has(item.id)) issues.push(`${label}: duplicate use ID ${item.id}.`);
|
|
1578
1721
|
ids.add(item.id);
|
|
1579
1722
|
if (Object.keys(item).some((key) => !["id", "sourceSha256", "source", "timeline", "purpose", "reuseReason"].includes(key))) issues.push(`${label}: unsupported field.`);
|
|
1580
|
-
for (const key of ["purpose", "reuseReason"]) if (item[key] !== void 0 && !
|
|
1723
|
+
for (const key of ["purpose", "reuseReason"]) if (item[key] !== void 0 && !text5(item[key])) issues.push(`${label}: ${key} must be nonempty text of at most 2000 characters.`);
|
|
1581
1724
|
});
|
|
1582
1725
|
}
|
|
1583
1726
|
return { valid: !issues.length, issues };
|
|
@@ -1605,7 +1748,7 @@ function checkProductionPlan(inventory, value) {
|
|
|
1605
1748
|
findings.push({ code: "INVENTORY_INCOMPLETE", severity: "warning", message: "Inventory traversal or probing was incomplete. This plan check covers only recorded assets." });
|
|
1606
1749
|
}
|
|
1607
1750
|
for (const asset of inventory.assets) {
|
|
1608
|
-
if (!record6(asset) || !
|
|
1751
|
+
if (!record6(asset) || !hash2(asset.sha256) || !(asset.durationSeconds === null || nonnegative(asset.durationSeconds)) || asset.streams !== void 0 && (!Array.isArray(asset.streams) || asset.streams.some((s) => !record6(s) || typeof s.type !== "string"))) {
|
|
1609
1752
|
findings.push({ code: "INVALID_INVENTORY", severity: "error", message: "Inventory assets need a source hash, nonnegative duration or null, and valid optional streams." });
|
|
1610
1753
|
continue;
|
|
1611
1754
|
}
|
|
@@ -1670,11 +1813,11 @@ function checkProductionPlan(inventory, value) {
|
|
|
1670
1813
|
|
|
1671
1814
|
// packages/core/src/production-evidence.ts
|
|
1672
1815
|
var record7 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
1673
|
-
var
|
|
1674
|
-
var
|
|
1816
|
+
var text6 = (v) => typeof v === "string" && v.trim().length > 0 && v.length <= 2e3;
|
|
1817
|
+
var hash3 = (v) => typeof v === "string" && /^[a-f0-9]{64}$/.test(v);
|
|
1675
1818
|
var id = (v) => typeof v === "string" && /^[a-zA-Z0-9_-]{1,80}$/.test(v);
|
|
1676
1819
|
var url = (v) => {
|
|
1677
|
-
if (!
|
|
1820
|
+
if (!text6(v)) return false;
|
|
1678
1821
|
try {
|
|
1679
1822
|
const u = new URL(v);
|
|
1680
1823
|
return u.protocol === "https:" && !u.username && !u.password;
|
|
@@ -1695,7 +1838,7 @@ function validateProductionEvidence(value) {
|
|
|
1695
1838
|
const seen = { assets: /* @__PURE__ */ new Set(), uses: /* @__PURE__ */ new Set(), claims: /* @__PURE__ */ new Set() };
|
|
1696
1839
|
for (const [i, item] of value.assets.entries()) {
|
|
1697
1840
|
const label = `assets[${i}]`;
|
|
1698
|
-
if (!record7(item) || !
|
|
1841
|
+
if (!record7(item) || !hash3(item.sha256) || !text6(item.description) || !oneOf(item.origin, ["owned", "licensed", "generated"]) || !keys(item, ["sha256", "origin", "description", "source", "review", "anchors"])) {
|
|
1699
1842
|
issues.push(`${label}: invalid asset.`);
|
|
1700
1843
|
continue;
|
|
1701
1844
|
}
|
|
@@ -1704,13 +1847,13 @@ function validateProductionEvidence(value) {
|
|
|
1704
1847
|
if (item.origin === "licensed" && !item.source) issues.push(`${label}: licensed material requires source metadata.`);
|
|
1705
1848
|
if (item.source !== void 0) {
|
|
1706
1849
|
const s = item.source;
|
|
1707
|
-
if (!record7(s) || !keys(s, ["url", "author", "license", "retrievedAt"]) || !url(s.url) || !
|
|
1850
|
+
if (!record7(s) || !keys(s, ["url", "author", "license", "retrievedAt"]) || !url(s.url) || !text6(s.author) || !text6(s.license) || typeof s.retrievedAt !== "string" || !/^\d{4}-\d{2}-\d{2}T/.test(s.retrievedAt) || !Number.isFinite(Date.parse(s.retrievedAt))) issues.push(`${label}: source needs HTTPS page URL, author, license and ISO retrieval time.`);
|
|
1708
1851
|
}
|
|
1709
|
-
if (item.review !== void 0 && (!record7(item.review) || !keys(item.review, ["method", "reviewedSha256"]) || !oneOf(item.review.method, ["view_image", "playback", "listening"]) || !
|
|
1710
|
-
if (item.anchors !== void 0 && (!Array.isArray(item.anchors) || item.anchors.length > 50 || item.anchors.some((a) => !record7(a) || !keys(a, ["label", "x", "y", "evidenceUrl"]) || !
|
|
1852
|
+
if (item.review !== void 0 && (!record7(item.review) || !keys(item.review, ["method", "reviewedSha256"]) || !oneOf(item.review.method, ["view_image", "playback", "listening"]) || !hash3(item.review.reviewedSha256))) issues.push(`${label}: invalid review declaration.`);
|
|
1853
|
+
if (item.anchors !== void 0 && (!Array.isArray(item.anchors) || item.anchors.length > 50 || item.anchors.some((a) => !record7(a) || !keys(a, ["label", "x", "y", "evidenceUrl"]) || !text6(a.label) || !fraction(a.x) || !fraction(a.y) || a.evidenceUrl !== void 0 && !url(a.evidenceUrl)))) issues.push(`${label}: anchors need label and normalized source-image x/y; optional evidenceUrl must be HTTPS.`);
|
|
1711
1854
|
}
|
|
1712
1855
|
for (const [i, item] of value.uses.entries()) {
|
|
1713
|
-
if (!record7(item) || !keys(item, ["id", "sha256", "representation", "claimIds", "disclosure"]) || !id(item.id) || !
|
|
1856
|
+
if (!record7(item) || !keys(item, ["id", "sha256", "representation", "claimIds", "disclosure"]) || !id(item.id) || !hash3(item.sha256) || !oneOf(item.representation, ["documentary", "illustrative", "original"]) || item.disclosure !== void 0 && !text6(item.disclosure) || item.claimIds !== void 0 && (!Array.isArray(item.claimIds) || item.claimIds.length > 100 || !item.claimIds.every(id))) {
|
|
1714
1857
|
issues.push(`uses[${i}]: invalid use.`);
|
|
1715
1858
|
continue;
|
|
1716
1859
|
}
|
|
@@ -1718,7 +1861,7 @@ function validateProductionEvidence(value) {
|
|
|
1718
1861
|
seen.uses.add(item.id);
|
|
1719
1862
|
}
|
|
1720
1863
|
for (const [i, item] of value.claims.entries()) {
|
|
1721
|
-
if (!record7(item) || !keys(item, ["id", "text", "status", "sources"]) || !id(item.id) || !
|
|
1864
|
+
if (!record7(item) || !keys(item, ["id", "text", "status", "sources"]) || !id(item.id) || !text6(item.text) || !oneOf(item.status, ["supported", "disputed", "unverified"]) || !Array.isArray(item.sources) || item.sources.length > 20 || item.sources.some((s) => !record7(s) || !keys(s, ["url", "kind", "evidence"]) || !url(s.url) || !oneOf(s.kind, ["primary", "secondary"]) || !text6(s.evidence))) {
|
|
1722
1865
|
issues.push(`claims[${i}]: invalid claim or source.`);
|
|
1723
1866
|
continue;
|
|
1724
1867
|
}
|
|
@@ -1738,7 +1881,7 @@ function checkProductionEvidence(inventory, value) {
|
|
|
1738
1881
|
else {
|
|
1739
1882
|
inventoryComplete = inventory.complete && !(Array.isArray(inventory.failures) && inventory.failures.length);
|
|
1740
1883
|
for (const asset of inventory.assets) {
|
|
1741
|
-
if (!record7(asset) || !
|
|
1884
|
+
if (!record7(asset) || !hash3(asset.sha256) || !Array.isArray(asset.streams) || asset.streams.some((s) => !record7(s) || typeof s.type !== "string")) {
|
|
1742
1885
|
add("INVALID_INVENTORY", "error", "Inventory assets require hashes and stream types.");
|
|
1743
1886
|
continue;
|
|
1744
1887
|
}
|
|
@@ -1963,7 +2106,7 @@ var integrationCatalog = [
|
|
|
1963
2106
|
{ id: "pexels", name: "Pexels", description: "Fotos y videos de stock; revisa la licencia y el contexto de uso.", capabilities: ["stock-image", "stock-video"], accessMethods: ["agent", "api", "browser", "files"], docsUrl: "https://www.pexels.com/api/documentation/" },
|
|
1964
2107
|
{ id: "assemblyai", name: "AssemblyAI", description: "Transcripci\xF3n con marcas de tiempo para planificar cortes y subt\xEDtulos.", capabilities: ["transcription"], accessMethods: ["agent", "api", "files"], docsUrl: "https://www.assemblyai.com/docs" }
|
|
1965
2108
|
];
|
|
1966
|
-
function
|
|
2109
|
+
function object4(value) {
|
|
1967
2110
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1968
2111
|
}
|
|
1969
2112
|
function knownFields(value, allowed) {
|
|
@@ -1975,12 +2118,12 @@ function containsCredential(value) {
|
|
|
1975
2118
|
function validateIntegrationUpdate(providerId, input) {
|
|
1976
2119
|
const provider = integrationCatalog.find((entry) => entry.id === providerId);
|
|
1977
2120
|
if (!provider) throw new Error("Unknown integration provider.");
|
|
1978
|
-
if (!
|
|
2121
|
+
if (!object4(input)) throw new Error("Integration settings must be an object.");
|
|
1979
2122
|
knownFields(input, ["enabled", "accessMethod", "preferences", "expectedRevision"]);
|
|
1980
2123
|
if (typeof input.enabled !== "boolean") throw new Error("enabled must be a boolean.");
|
|
1981
2124
|
if (!provider.accessMethods.includes(input.accessMethod)) throw new Error("Unsupported access method for this provider.");
|
|
1982
2125
|
if (!Number.isSafeInteger(input.expectedRevision) || input.expectedRevision < 0) throw new Error("The current expectedRevision is required.");
|
|
1983
|
-
if (!
|
|
2126
|
+
if (!object4(input.preferences)) throw new Error("preferences must be an object.");
|
|
1984
2127
|
knownFields(input.preferences, ["instructions", "model", "voiceId", "language", "musicMode", "spending"]);
|
|
1985
2128
|
const preferences = {};
|
|
1986
2129
|
for (const [field, max] of [["instructions", 1e3], ["model", 120], ["voiceId", 120], ["language", 60]]) {
|
|
@@ -16036,8 +16179,8 @@ var aliases = {
|
|
|
16036
16179
|
"spotlight-roll": "testimonio archivo persona foto foco montaje"
|
|
16037
16180
|
};
|
|
16038
16181
|
var stopwords = new Set("a al algo como con cual de del donde el ella en entre es esa ese esta este hay la las lo los mas mi muy no o para pero por que se si sin sobre su sus te tu un una uno unos unas y ya mostrar explicar video escena animacion animar".split(" "));
|
|
16039
|
-
function stems(
|
|
16040
|
-
return new Set((normalized(
|
|
16182
|
+
function stems(text9) {
|
|
16183
|
+
return new Set((normalized(text9).match(/[a-z0-9]+/g) ?? []).filter((word) => word.length > 2 && !stopwords.has(word)).map((word) => word.slice(0, 5)));
|
|
16041
16184
|
}
|
|
16042
16185
|
var searchIndex = scenes.map((scene) => ({ scene, fields: [
|
|
16043
16186
|
[scene.title, 3],
|
|
@@ -16047,7 +16190,7 @@ var searchIndex = scenes.map((scene) => ({ scene, fields: [
|
|
|
16047
16190
|
[(scene.crossNicheUses ?? []).join(" "), 2.5],
|
|
16048
16191
|
[(scene.presets ?? []).map((preset) => `${preset.name.replace(/-/g, " ")} ${preset.summary}`).join(" "), 2],
|
|
16049
16192
|
[scene.useFor ?? "", 1.5]
|
|
16050
|
-
].map(([
|
|
16193
|
+
].map(([text9, weight]) => ({ terms: stems(text9), weight })) }));
|
|
16051
16194
|
function rankedCandidates(search) {
|
|
16052
16195
|
if (!search) return scenes;
|
|
16053
16196
|
const exact = scenes.find((scene) => scene.id === search);
|
|
@@ -16062,8 +16205,8 @@ function rankedCandidates(search) {
|
|
|
16062
16205
|
function legacyCandidates(search) {
|
|
16063
16206
|
const terms = search.split(" ").filter(Boolean);
|
|
16064
16207
|
return legacyScenes.filter((scene) => {
|
|
16065
|
-
const
|
|
16066
|
-
return terms.every((term) =>
|
|
16208
|
+
const text9 = normalized([scene.id, scene.title, scene.mechanism, scene.intent, scene.niche, scene.style, aliases[scene.id] || ""].join(" "));
|
|
16209
|
+
return terms.every((term) => text9.includes(term));
|
|
16067
16210
|
});
|
|
16068
16211
|
}
|
|
16069
16212
|
function summary(scene) {
|
|
@@ -16133,7 +16276,7 @@ var identifier3 = /^[a-z0-9][a-z0-9_-]{0,79}$/;
|
|
|
16133
16276
|
var tokenName = /^[a-z][a-z0-9_.-]{0,79}$/;
|
|
16134
16277
|
var tokenNameHelp = "Use 1\u201380 characters: start with a lowercase letter, then lowercase letters, digits, _, . or - (for example font_family). __proto__, constructor and prototype are reserved. Keys are flat semantic names, not CSS property names.";
|
|
16135
16278
|
var own3 = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
|
|
16136
|
-
var
|
|
16279
|
+
var object5 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
16137
16280
|
var nonempty2 = (value) => typeof value === "string" && value.trim().length > 0;
|
|
16138
16281
|
var finite = (value) => typeof value === "number" && Number.isFinite(value);
|
|
16139
16282
|
var tokenValue = (value) => typeof value === "string" || typeof value === "boolean" || finite(value) || Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
@@ -16150,13 +16293,13 @@ function safeDocumentPath(path) {
|
|
|
16150
16293
|
return path;
|
|
16151
16294
|
}
|
|
16152
16295
|
function checkRules(value, add) {
|
|
16153
|
-
if (!
|
|
16296
|
+
if (!object5(value)) {
|
|
16154
16297
|
add("tokens must be an object");
|
|
16155
16298
|
return;
|
|
16156
16299
|
}
|
|
16157
16300
|
for (const [key, rule] of Object.entries(value)) {
|
|
16158
16301
|
if (!tokenName.test(key) || ["__proto__", "constructor", "prototype"].includes(key)) add(`Invalid semantic token ${key}. ${tokenNameHelp}`);
|
|
16159
|
-
if (!
|
|
16302
|
+
if (!object5(rule) || !tokenValue(rule.value) || !["required", "preferred", "example"].includes(String(rule.strength))) add(`Token ${key} requires value and strength required/preferred/example`);
|
|
16160
16303
|
}
|
|
16161
16304
|
}
|
|
16162
16305
|
function checkEvidence(value, add) {
|
|
@@ -16165,7 +16308,7 @@ function checkEvidence(value, add) {
|
|
|
16165
16308
|
return;
|
|
16166
16309
|
}
|
|
16167
16310
|
for (const ref of value) {
|
|
16168
|
-
if (!
|
|
16311
|
+
if (!object5(ref) || !nonempty2(ref.videoId) || !["reference_only", "licensed_asset", "owned_asset"].includes(String(ref.relationship))) add("Evidence must include videoId and explicit relationship");
|
|
16169
16312
|
else {
|
|
16170
16313
|
if (ref.timestampSeconds !== void 0 && (!finite(ref.timestampSeconds) || ref.timestampSeconds < 0)) add("Evidence timestamp must be nonnegative seconds");
|
|
16171
16314
|
if (ref.reviewStatus !== void 0 && !["unreviewed", "agent_reviewed", "human_verified"].includes(String(ref.reviewStatus))) add("Invalid evidence reviewStatus");
|
|
@@ -16210,7 +16353,7 @@ function validateDocument(path, content) {
|
|
|
16210
16353
|
add("Invalid JSON");
|
|
16211
16354
|
return { valid: false, issues };
|
|
16212
16355
|
}
|
|
16213
|
-
if (!
|
|
16356
|
+
if (!object5(doc)) {
|
|
16214
16357
|
add("JSON document must be an object");
|
|
16215
16358
|
return { valid: false, issues };
|
|
16216
16359
|
}
|
|
@@ -16227,6 +16370,14 @@ function validateDocument(path, content) {
|
|
|
16227
16370
|
if (!identifier3.test(expected)) add("Invalid resource identifier in path");
|
|
16228
16371
|
if (doc.id !== void 0 && doc.id !== expected) add(`Document id must match path: ${expected}`);
|
|
16229
16372
|
}
|
|
16373
|
+
if (parts[0] === ".editorbrain" && parts[1] === "sfx") {
|
|
16374
|
+
try {
|
|
16375
|
+
const bank = validateSfxBank(doc);
|
|
16376
|
+
if (parts.length !== 4 || parts[3] !== "catalog.json" || bank.bankId !== parts[2]) add("SFX bank must match .editorbrain/sfx/<bankId>/catalog.json");
|
|
16377
|
+
} catch (error) {
|
|
16378
|
+
add(error.message);
|
|
16379
|
+
}
|
|
16380
|
+
}
|
|
16230
16381
|
if (isBrand && parts[3] === "audio.json" || parts[0] === "videos" && parts.length === 3 && parts[2] === "audio.json") {
|
|
16231
16382
|
try {
|
|
16232
16383
|
validateAudioProductionPreferences(doc);
|
|
@@ -16246,20 +16397,20 @@ function validateDocument(path, content) {
|
|
|
16246
16397
|
if (isStyle) {
|
|
16247
16398
|
if (!nonempty2(doc.name)) add("Style name is required");
|
|
16248
16399
|
if (doc.references !== void 0) checkEvidence(doc.references, add);
|
|
16249
|
-
if (doc.components !== void 0 && (!Array.isArray(doc.components) || doc.components.some((c) => !
|
|
16400
|
+
if (doc.components !== void 0 && (!Array.isArray(doc.components) || doc.components.some((c) => !object5(c) || !nonempty2(c.id) || !["concept", "implemented", "transfer_tested"].includes(String(c.status))))) add("Components need id and explicit implementation status");
|
|
16250
16401
|
}
|
|
16251
16402
|
}
|
|
16252
16403
|
if (isProject) {
|
|
16253
16404
|
for (const key of ["id", "brand", "style", "outputProfile"]) if (typeof doc[key] !== "string" || !identifier3.test(doc[key])) add(`${key} must be a valid identifier`);
|
|
16254
|
-
if (!
|
|
16405
|
+
if (!object5(doc.intent) || !nonempty2(doc.intent.topic) || !nonempty2(doc.intent.goal)) add("intent requires topic and goal");
|
|
16255
16406
|
if (doc.reviewMode !== void 0 && !["direct", "directions", "sample"].includes(doc.reviewMode)) add("reviewMode must be direct, directions or sample");
|
|
16256
16407
|
if (doc.mediaStyleOverrides !== void 0) for (const issue of mediaStyleOverrideIssues(doc.mediaStyleOverrides)) add(issue);
|
|
16257
16408
|
if (doc.script !== void 0 && typeof doc.script !== "string") add("script must be text");
|
|
16258
16409
|
if (doc.overrides !== void 0) {
|
|
16259
|
-
if (!
|
|
16410
|
+
if (!object5(doc.overrides)) add("overrides must be an object");
|
|
16260
16411
|
else for (const [key, rule] of Object.entries(doc.overrides)) {
|
|
16261
16412
|
if (!tokenName.test(key) || ["__proto__", "constructor", "prototype"].includes(key)) add(`Invalid override token ${key}. ${tokenNameHelp}`);
|
|
16262
|
-
if (!
|
|
16413
|
+
if (!object5(rule) || !tokenValue(rule.value) || !nonempty2(rule.reason) || rule.overrideRequired !== void 0 && typeof rule.overrideRequired !== "boolean") add(`Override ${key} requires value and a reason; overrideRequired must be boolean`);
|
|
16263
16414
|
}
|
|
16264
16415
|
}
|
|
16265
16416
|
}
|
|
@@ -16269,7 +16420,7 @@ function validateDocument(path, content) {
|
|
|
16269
16420
|
else {
|
|
16270
16421
|
const ids = /* @__PURE__ */ new Set();
|
|
16271
16422
|
for (const asset of doc.assets) {
|
|
16272
|
-
if (!
|
|
16423
|
+
if (!object5(asset) || typeof asset.id !== "string" || !/^[a-f0-9]{64}$/.test(asset.id) || !nonempty2(asset.name) || typeof asset.mimeType !== "string" || !/^(video|image|audio)\/[a-zA-Z0-9][a-zA-Z0-9.+-]*$/.test(asset.mimeType) || !Number.isSafeInteger(asset.bytes) || Number(asset.bytes) < 1) {
|
|
16273
16424
|
add("Production assets require a SHA-256 id, name, media mimeType and positive integer bytes");
|
|
16274
16425
|
continue;
|
|
16275
16426
|
}
|
|
@@ -16283,10 +16434,10 @@ function validateDocument(path, content) {
|
|
|
16283
16434
|
for (const key of ["width", "height"]) if (!Number.isInteger(doc[key]) || Number(doc[key]) < 1 || Number(doc[key]) > 16384) add(`${key} must be an integer between 1 and 16384`);
|
|
16284
16435
|
if (!finite(doc.fps) || doc.fps <= 0 || doc.fps > 240) add("fps must be between 0 and 240");
|
|
16285
16436
|
if (doc.safeAreas !== void 0) {
|
|
16286
|
-
if (!
|
|
16437
|
+
if (!object5(doc.safeAreas) || ["top", "right", "bottom", "left"].some((k) => !finite(doc.safeAreas[k]) || Number(doc.safeAreas[k]) < 0 || Number(doc.safeAreas[k]) >= 0.5)) add("safeAreas must have normalized fractions from 0 up to (not including) 0.5 for all four sides");
|
|
16287
16438
|
}
|
|
16288
16439
|
if (doc.captions !== void 0) {
|
|
16289
|
-
if (!
|
|
16440
|
+
if (!object5(doc.captions)) add("captions must be an object");
|
|
16290
16441
|
else {
|
|
16291
16442
|
if (typeof doc.captions.enabled !== "boolean") add("captions.enabled must be a boolean");
|
|
16292
16443
|
if (doc.captions.language !== void 0 && !nonempty2(doc.captions.language)) add("captions.language must be nonempty text");
|
|
@@ -16325,13 +16476,13 @@ function validateWorkspace(files) {
|
|
|
16325
16476
|
return { valid: issues.length === 0, issues };
|
|
16326
16477
|
}
|
|
16327
16478
|
function stableJson(value) {
|
|
16328
|
-
const sort = (item) => Array.isArray(item) ? item.map(sort) :
|
|
16479
|
+
const sort = (item) => Array.isArray(item) ? item.map(sort) : object5(item) ? Object.fromEntries(Object.keys(item).sort().map((key) => [key, sort(item[key])])) : item;
|
|
16329
16480
|
return JSON.stringify(sort(value), null, 2) + "\n";
|
|
16330
16481
|
}
|
|
16331
16482
|
async function sha256(content) {
|
|
16332
16483
|
const bytes = typeof content === "string" ? new TextEncoder().encode(content) : content;
|
|
16333
|
-
const
|
|
16334
|
-
return Array.from(new Uint8Array(
|
|
16484
|
+
const digest3 = await crypto.subtle.digest("SHA-256", new Uint8Array(bytes));
|
|
16485
|
+
return Array.from(new Uint8Array(digest3), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
16335
16486
|
}
|
|
16336
16487
|
async function resolveProject(files, projectId) {
|
|
16337
16488
|
files = canonicalFiles(files);
|
|
@@ -16528,42 +16679,263 @@ function canonicalFiles(files) {
|
|
|
16528
16679
|
return result2;
|
|
16529
16680
|
}
|
|
16530
16681
|
|
|
16531
|
-
// packages/cli/src/
|
|
16682
|
+
// packages/cli/src/media-runtime.ts
|
|
16683
|
+
import { spawn } from "node:child_process";
|
|
16684
|
+
import { createHash } from "node:crypto";
|
|
16532
16685
|
import { constants } from "node:fs";
|
|
16533
|
-
import { lstat,
|
|
16534
|
-
import { dirname,
|
|
16535
|
-
|
|
16536
|
-
|
|
16537
|
-
|
|
16538
|
-
|
|
16539
|
-
|
|
16540
|
-
|
|
16541
|
-
|
|
16542
|
-
|
|
16543
|
-
|
|
16544
|
-
if (document2) safeDocumentPath(path);
|
|
16545
|
-
const target = resolve(root, path);
|
|
16546
|
-
const rel = relative(root, target);
|
|
16547
|
-
if (!rel || rel.startsWith(`..${sep}`) || rel === ".." || resolve(path) === path) throw new Error("Path escapes workspace");
|
|
16548
|
-
let cursor = root;
|
|
16549
|
-
for (const segment of rel.split(sep)) {
|
|
16550
|
-
cursor = join(cursor, segment);
|
|
16551
|
-
try {
|
|
16552
|
-
const info = await lstat(cursor);
|
|
16553
|
-
if (info.isSymbolicLink()) throw new Error(`Symlinks are not allowed: ${path}`);
|
|
16554
|
-
} catch (error) {
|
|
16555
|
-
if (error.code !== "ENOENT") throw error;
|
|
16556
|
-
}
|
|
16686
|
+
import { lstat, open, realpath } from "node:fs/promises";
|
|
16687
|
+
import { dirname, extname, parse, resolve } from "node:path";
|
|
16688
|
+
|
|
16689
|
+
// packages/cli/src/errors.ts
|
|
16690
|
+
var CliError = class extends Error {
|
|
16691
|
+
constructor(code, message, hint, retryable = false, details = {}) {
|
|
16692
|
+
super(message);
|
|
16693
|
+
this.code = code;
|
|
16694
|
+
this.hint = hint;
|
|
16695
|
+
this.retryable = retryable;
|
|
16696
|
+
this.details = details;
|
|
16557
16697
|
}
|
|
16558
|
-
|
|
16559
|
-
|
|
16560
|
-
|
|
16561
|
-
|
|
16562
|
-
|
|
16563
|
-
|
|
16564
|
-
|
|
16565
|
-
|
|
16566
|
-
|
|
16698
|
+
code;
|
|
16699
|
+
hint;
|
|
16700
|
+
retryable;
|
|
16701
|
+
details;
|
|
16702
|
+
};
|
|
16703
|
+
function errorOutput(error) {
|
|
16704
|
+
if (error instanceof CliError) return { error: error.message, code: error.code, hint: error.hint, retryable: error.retryable, ...error.details };
|
|
16705
|
+
if (error instanceof ValidationError) return { error: "Workspace validation failed.", code: "VALIDATION_FAILED", retryable: false, hint: "Correct the reported documents before retrying.", issues: error.issues };
|
|
16706
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
16707
|
+
const safe = message.replace(/\b(?:eh_[a-f0-9]{64}|npm_[A-Za-z0-9]+)\b/g, "[REDACTED]").replace(/Bearer\s+\S+/gi, "Bearer [REDACTED]").replace(/(https?:\/\/[^\s?]+)\?[^\s]*/g, "$1?[REDACTED]");
|
|
16708
|
+
return { error: safe, code: "COMMAND_FAILED", retryable: false, hint: "Read the command help and run editorbrain doctor. Do not retry writes blindly." };
|
|
16709
|
+
}
|
|
16710
|
+
|
|
16711
|
+
// packages/cli/src/media-runtime.ts
|
|
16712
|
+
var MediaToolError = class extends CliError {
|
|
16713
|
+
constructor(code, message) {
|
|
16714
|
+
super(code, message, code === "MEDIA_TOOL_NOT_FOUND" ? "Install FFmpeg/FFprobe separately and make them available on PATH; no account or model is needed." : "Review the local input and reported limit. An incomplete check does not validate the media.");
|
|
16715
|
+
this.name = "MediaToolError";
|
|
16716
|
+
}
|
|
16717
|
+
};
|
|
16718
|
+
var SUPPORTED_MEDIA_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
16719
|
+
".mp4",
|
|
16720
|
+
".mov",
|
|
16721
|
+
".m4v",
|
|
16722
|
+
".mkv",
|
|
16723
|
+
".webm",
|
|
16724
|
+
".avi",
|
|
16725
|
+
".mpg",
|
|
16726
|
+
".mpeg",
|
|
16727
|
+
".mts",
|
|
16728
|
+
".m2ts",
|
|
16729
|
+
".ts",
|
|
16730
|
+
".ogv",
|
|
16731
|
+
".wmv",
|
|
16732
|
+
".wav",
|
|
16733
|
+
".mp3",
|
|
16734
|
+
".m4a",
|
|
16735
|
+
".aac",
|
|
16736
|
+
".flac",
|
|
16737
|
+
".ogg",
|
|
16738
|
+
".opus",
|
|
16739
|
+
".aif",
|
|
16740
|
+
".aiff",
|
|
16741
|
+
".ac3",
|
|
16742
|
+
".eac3",
|
|
16743
|
+
".jpg",
|
|
16744
|
+
".jpeg",
|
|
16745
|
+
".png",
|
|
16746
|
+
".webp",
|
|
16747
|
+
".bmp",
|
|
16748
|
+
".tif",
|
|
16749
|
+
".tiff",
|
|
16750
|
+
".gif",
|
|
16751
|
+
".avif",
|
|
16752
|
+
".heic",
|
|
16753
|
+
".heif"
|
|
16754
|
+
]);
|
|
16755
|
+
var PROTOCOLS = "file,pipe";
|
|
16756
|
+
var FORMATS = "mov,matroska,webm,avi,mpeg,mpegts,ogg,wav,flac,mp3,aac,aiff,ac3,eac3,asf,jpeg_pipe,png_pipe,webp_pipe,bmp_pipe,tiff_pipe,gif,heif,avif";
|
|
16757
|
+
var DEFAULT_TIMEOUT = 3e4;
|
|
16758
|
+
var DEFAULT_OUTPUT_LIMIT = 2 * 1024 * 1024;
|
|
16759
|
+
function resolveLocalMediaPath(value) {
|
|
16760
|
+
if (!value || value.includes("\0") || /[\r\n]/.test(value) || /^[a-z][a-z\d+.-]*:/i.test(value) && !(process.platform === "win32" && /^[a-z]:[\\/]/i.test(value)) || value.startsWith("\\\\")) {
|
|
16761
|
+
throw new MediaToolError("MEDIA_LOCAL_PATH_REQUIRED", "Provide a local filesystem path, not a URL, protocol or network share.");
|
|
16762
|
+
}
|
|
16763
|
+
return resolve(value);
|
|
16764
|
+
}
|
|
16765
|
+
async function assertNoMediaSymlinks(value) {
|
|
16766
|
+
const path = resolveLocalMediaPath(value);
|
|
16767
|
+
let cursor = path;
|
|
16768
|
+
while (cursor !== parse(cursor).root) {
|
|
16769
|
+
const info = await lstat(cursor);
|
|
16770
|
+
if (info.isSymbolicLink()) {
|
|
16771
|
+
const systemAlias = process.platform === "darwin" && (cursor === "/tmp" || cursor === "/var") && await realpath(cursor) === `/private${cursor}`;
|
|
16772
|
+
if (!systemAlias) throw new MediaToolError("MEDIA_SYMLINK", "Symlink media paths are not followed. Use the original local path.");
|
|
16773
|
+
}
|
|
16774
|
+
cursor = dirname(cursor);
|
|
16775
|
+
}
|
|
16776
|
+
return path;
|
|
16777
|
+
}
|
|
16778
|
+
async function assertLocalMediaFile(value) {
|
|
16779
|
+
const path = await assertNoMediaSymlinks(value);
|
|
16780
|
+
const info = await lstat(path);
|
|
16781
|
+
if (!info.isFile()) throw new MediaToolError("MEDIA_NOT_REGULAR", "The media input must be a regular local file.");
|
|
16782
|
+
if (!SUPPORTED_MEDIA_EXTENSIONS.has(extname(path).toLowerCase())) throw new MediaToolError("MEDIA_FORMAT_UNSUPPORTED", "This file extension is not supported for local media inspection.");
|
|
16783
|
+
return path;
|
|
16784
|
+
}
|
|
16785
|
+
function localInputArgs(path) {
|
|
16786
|
+
return ["-protocol_whitelist", PROTOCOLS, "-format_whitelist", FORMATS, "-i", resolveLocalMediaPath(path)];
|
|
16787
|
+
}
|
|
16788
|
+
async function runMediaTool(tool, args, options = {}) {
|
|
16789
|
+
if (tool !== "ffmpeg" && tool !== "ffprobe") throw new MediaToolError("MEDIA_TOOL_INVALID", "Unsupported media executable.");
|
|
16790
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT;
|
|
16791
|
+
const maxOutputBytes = options.maxOutputBytes ?? DEFAULT_OUTPUT_LIMIT;
|
|
16792
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 10 * 6e4 || !Number.isInteger(maxOutputBytes) || maxOutputBytes < 1 || maxOutputBytes > 16 * 1024 * 1024) {
|
|
16793
|
+
throw new MediaToolError("MEDIA_TOOL_LIMITS_INVALID", "Media runtime limits are outside the supported range.");
|
|
16794
|
+
}
|
|
16795
|
+
if (!Array.isArray(args) || args.length > 200 || args.some((arg) => typeof arg !== "string" || arg.length > 8192 || arg.includes("\0"))) {
|
|
16796
|
+
throw new MediaToolError("MEDIA_TOOL_ARGUMENTS_INVALID", "Invalid media tool arguments.");
|
|
16797
|
+
}
|
|
16798
|
+
for (let i = 0; i < args.length; i++) {
|
|
16799
|
+
const arg = args[i];
|
|
16800
|
+
if (/(?:^|[=,;\s])(?:https?|ftp|sftp|tcp|udp|rtsp|rtmp|smb|data|concat|subfile|crypto):/i.test(arg) || arg === "-safe" && args[i + 1] === "0" || arg === "-enable_drefs" && args[i + 1] !== "0") {
|
|
16801
|
+
throw new MediaToolError("MEDIA_REMOTE_INPUT_FORBIDDEN", "Only self-contained local media inputs are allowed.");
|
|
16802
|
+
}
|
|
16803
|
+
if (arg === "-protocol_whitelist" && args[i + 1] !== PROTOCOLS || arg === "-format_whitelist" && args[i + 1] !== FORMATS) {
|
|
16804
|
+
throw new MediaToolError("MEDIA_TOOL_ARGUMENTS_INVALID", "The local media allowlists cannot be widened.");
|
|
16805
|
+
}
|
|
16806
|
+
if (arg === "-i") {
|
|
16807
|
+
await assertLocalMediaFile(args[i + 1] ?? "");
|
|
16808
|
+
if (args[i - 4] !== "-protocol_whitelist" || args[i - 3] !== PROTOCOLS || args[i - 2] !== "-format_whitelist" || args[i - 1] !== FORMATS) {
|
|
16809
|
+
throw new MediaToolError("MEDIA_TOOL_ARGUMENTS_INVALID", "Every media input must use localInputArgs.");
|
|
16810
|
+
}
|
|
16811
|
+
}
|
|
16812
|
+
}
|
|
16813
|
+
return new Promise((resolveResult, reject) => {
|
|
16814
|
+
const stdout = [], stderr = [];
|
|
16815
|
+
let size = 0, done = false;
|
|
16816
|
+
const child = spawn(tool, ["-hide_banner", ...tool === "ffmpeg" ? ["-nostdin"] : [], ...args], { shell: false, stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
16817
|
+
const fail2 = (error) => {
|
|
16818
|
+
if (done) return;
|
|
16819
|
+
done = true;
|
|
16820
|
+
clearTimeout(timer);
|
|
16821
|
+
child.kill("SIGKILL");
|
|
16822
|
+
reject(error);
|
|
16823
|
+
};
|
|
16824
|
+
const timer = setTimeout(() => fail2(new MediaToolError("MEDIA_TOOL_TIMEOUT", "The local media check exceeded its time limit. No complete result is available.")), timeoutMs);
|
|
16825
|
+
const collect = (target) => (chunk) => {
|
|
16826
|
+
if (done) return;
|
|
16827
|
+
size += chunk.length;
|
|
16828
|
+
if (size > maxOutputBytes) return fail2(new MediaToolError("MEDIA_TOOL_OUTPUT_LIMIT", "The local media check exceeded its output limit. No complete result is available."));
|
|
16829
|
+
target.push(chunk);
|
|
16830
|
+
};
|
|
16831
|
+
child.stdout.on("data", collect(stdout));
|
|
16832
|
+
child.stderr.on("data", collect(stderr));
|
|
16833
|
+
child.on("error", (error) => fail2(new MediaToolError(error.code === "ENOENT" ? "MEDIA_TOOL_NOT_FOUND" : "MEDIA_TOOL_FAILED", error.code === "ENOENT" ? `${tool} is not installed or available on PATH. Install it separately to use local media checks.` : "The local media executable could not run.")));
|
|
16834
|
+
child.on("close", (code) => {
|
|
16835
|
+
if (done) return;
|
|
16836
|
+
done = true;
|
|
16837
|
+
clearTimeout(timer);
|
|
16838
|
+
if (code !== 0) return reject(new MediaToolError("MEDIA_TOOL_FAILED", "The local media tool could not inspect or decode this input. No complete result is available."));
|
|
16839
|
+
resolveResult({ stdout: Buffer.concat(stdout).toString("utf8"), stderr: Buffer.concat(stderr).toString("utf8") });
|
|
16840
|
+
});
|
|
16841
|
+
});
|
|
16842
|
+
}
|
|
16843
|
+
var finite2 = (value) => {
|
|
16844
|
+
if (typeof value !== "number" && typeof value !== "string" || value === "") return null;
|
|
16845
|
+
const n = Number(value);
|
|
16846
|
+
return Number.isFinite(n) ? n : null;
|
|
16847
|
+
};
|
|
16848
|
+
var nonnegative2 = (value) => {
|
|
16849
|
+
const n = finite2(value);
|
|
16850
|
+
return n !== null && n >= 0 ? n : null;
|
|
16851
|
+
};
|
|
16852
|
+
var positiveInteger = (value) => {
|
|
16853
|
+
const n = finite2(value);
|
|
16854
|
+
return n !== null && Number.isInteger(n) && n > 0 ? n : void 0;
|
|
16855
|
+
};
|
|
16856
|
+
var rate2 = (value) => {
|
|
16857
|
+
if (typeof value !== "string" || !/^\d+(?:\/\d+)?$/.test(value)) return null;
|
|
16858
|
+
const [n, d = "1"] = value.split("/");
|
|
16859
|
+
const result2 = Number(n) / Number(d);
|
|
16860
|
+
return Number.isFinite(result2) && result2 > 0 ? result2 : null;
|
|
16861
|
+
};
|
|
16862
|
+
var sameFile = (a, b) => a.dev === b.dev && a.ino === b.ino && a.size === b.size && a.mtimeMs === b.mtimeMs && a.ctimeMs === b.ctimeMs;
|
|
16863
|
+
async function probeLocalMedia(value) {
|
|
16864
|
+
const path = await assertLocalMediaFile(value);
|
|
16865
|
+
const handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
16866
|
+
try {
|
|
16867
|
+
const before = await handle.stat();
|
|
16868
|
+
if (!before.isFile()) throw new MediaToolError("MEDIA_NOT_REGULAR", "The media input must be a regular local file.");
|
|
16869
|
+
const entries = "format=duration,start_time:stream=index,codec_type,codec_name,width,height,avg_frame_rate,sample_rate,channels,duration,color_transfer,sample_aspect_ratio,pix_fmt:stream_disposition=attached_pic";
|
|
16870
|
+
const { stdout } = await runMediaTool("ffprobe", ["-v", "error", "-show_entries", entries, "-of", "json", ...localInputArgs(path)]);
|
|
16871
|
+
const hash6 = createHash("sha256");
|
|
16872
|
+
for await (const chunk of handle.createReadStream({ autoClose: false })) hash6.update(chunk);
|
|
16873
|
+
const after = await handle.stat();
|
|
16874
|
+
await assertNoMediaSymlinks(path);
|
|
16875
|
+
const current2 = await lstat(path);
|
|
16876
|
+
if (!sameFile(before, after) || !sameFile(after, current2)) throw new MediaToolError("MEDIA_FILE_CHANGED", "The media file changed while it was being inspected. Retry with a stable source.");
|
|
16877
|
+
let raw;
|
|
16878
|
+
try {
|
|
16879
|
+
raw = JSON.parse(stdout);
|
|
16880
|
+
} catch {
|
|
16881
|
+
throw new MediaToolError("MEDIA_PROBE_INVALID", "The media probe returned invalid metadata.");
|
|
16882
|
+
}
|
|
16883
|
+
if (!raw || !Array.isArray(raw.streams) || raw.streams.length > 256 || raw.streams.some((stream) => !stream || typeof stream !== "object" || Array.isArray(stream))) throw new MediaToolError("MEDIA_PROBE_INVALID", "The media probe returned invalid or excessive stream metadata.");
|
|
16884
|
+
const streams = raw.streams.map((stream, offset) => {
|
|
16885
|
+
const type = typeof stream.codec_type === "string" && /^(video|audio|subtitle|data|attachment|unknown)$/.test(stream.codec_type) ? stream.codec_type : "unknown";
|
|
16886
|
+
const codec = typeof stream.codec_name === "string" && /^[a-zA-Z0-9_]{1,64}$/.test(stream.codec_name) ? stream.codec_name : null;
|
|
16887
|
+
const index = finite2(stream.index);
|
|
16888
|
+
return {
|
|
16889
|
+
index: index !== null && Number.isInteger(index) && index >= 0 ? index : offset,
|
|
16890
|
+
type,
|
|
16891
|
+
codec,
|
|
16892
|
+
durationSeconds: nonnegative2(stream.duration),
|
|
16893
|
+
...type === "video" ? { width: positiveInteger(stream.width), height: positiveInteger(stream.height), fps: rate2(stream.avg_frame_rate), ...typeof stream.sample_aspect_ratio === "string" ? { sampleAspectRatio: stream.sample_aspect_ratio } : {}, ...typeof stream.pix_fmt === "string" ? { pixelFormat: stream.pix_fmt } : {}, ...typeof stream.color_transfer === "string" ? { colorTransfer: stream.color_transfer } : {}, attachedPicture: stream.disposition?.attached_pic === 1 } : {},
|
|
16894
|
+
...type === "audio" ? { sampleRate: nonnegative2(stream.sample_rate), channels: positiveInteger(stream.channels) ?? null } : {}
|
|
16895
|
+
};
|
|
16896
|
+
});
|
|
16897
|
+
return { path, sha256: hash6.digest("hex"), bytes: after.size, durationSeconds: nonnegative2(raw.format?.duration), startTimeSeconds: finite2(raw.format?.start_time), streams };
|
|
16898
|
+
} finally {
|
|
16899
|
+
await handle.close();
|
|
16900
|
+
}
|
|
16901
|
+
}
|
|
16902
|
+
|
|
16903
|
+
// packages/cli/src/io.ts
|
|
16904
|
+
import { constants as constants2 } from "node:fs";
|
|
16905
|
+
import { lstat as lstat2, mkdir, open as open2, readdir, realpath as realpath2, rename, unlink, link } from "node:fs/promises";
|
|
16906
|
+
import { dirname as dirname2, join, relative, resolve as resolve2, sep } from "node:path";
|
|
16907
|
+
import { randomUUID } from "node:crypto";
|
|
16908
|
+
async function ensureRoot(directory, create = false) {
|
|
16909
|
+
const root = resolve2(directory);
|
|
16910
|
+
if (create) await mkdir(root, { recursive: true });
|
|
16911
|
+
const info = await lstat2(root);
|
|
16912
|
+
if (info.isSymbolicLink() || !info.isDirectory()) throw new Error("Workspace root must be a real directory, not a symlink");
|
|
16913
|
+
return realpath2(root);
|
|
16914
|
+
}
|
|
16915
|
+
async function checkedPath(root, path, document2 = true) {
|
|
16916
|
+
if (document2) safeDocumentPath(path);
|
|
16917
|
+
const target = resolve2(root, path);
|
|
16918
|
+
const rel = relative(root, target);
|
|
16919
|
+
if (!rel || rel.startsWith(`..${sep}`) || rel === ".." || resolve2(path) === path) throw new Error("Path escapes workspace");
|
|
16920
|
+
let cursor = root;
|
|
16921
|
+
for (const segment of rel.split(sep)) {
|
|
16922
|
+
cursor = join(cursor, segment);
|
|
16923
|
+
try {
|
|
16924
|
+
const info = await lstat2(cursor);
|
|
16925
|
+
if (info.isSymbolicLink()) throw new Error(`Symlinks are not allowed: ${path}`);
|
|
16926
|
+
} catch (error) {
|
|
16927
|
+
if (error.code !== "ENOENT") throw error;
|
|
16928
|
+
}
|
|
16929
|
+
}
|
|
16930
|
+
return target;
|
|
16931
|
+
}
|
|
16932
|
+
async function readText(root, path, document2 = true) {
|
|
16933
|
+
const target = await checkedPath(root, path, document2);
|
|
16934
|
+
const handle = await open2(target, constants2.O_RDONLY | constants2.O_NOFOLLOW);
|
|
16935
|
+
try {
|
|
16936
|
+
const info = await handle.stat();
|
|
16937
|
+
if (!info.isFile()) throw new Error(`Not a regular file: ${path}`);
|
|
16938
|
+
if (document2 && info.size > MAX_DOCUMENT_BYTES) throw new Error(`Document exceeds 128 KiB: ${path}`);
|
|
16567
16939
|
return new TextDecoder("utf-8", { fatal: true }).decode(await handle.readFile());
|
|
16568
16940
|
} finally {
|
|
16569
16941
|
await handle.close();
|
|
@@ -16571,10 +16943,10 @@ async function readText(root, path, document2 = true) {
|
|
|
16571
16943
|
}
|
|
16572
16944
|
async function atomicWrite(root, path, content, options = {}) {
|
|
16573
16945
|
const target = await checkedPath(root, path, options.document ?? true);
|
|
16574
|
-
await mkdir(
|
|
16946
|
+
await mkdir(dirname2(target), { recursive: true });
|
|
16575
16947
|
await checkedPath(root, path, options.document ?? true);
|
|
16576
|
-
const temporary = join(
|
|
16577
|
-
const handle = await
|
|
16948
|
+
const temporary = join(dirname2(target), `.eh-${randomUUID()}.tmp`);
|
|
16949
|
+
const handle = await open2(temporary, "wx", options.mode ?? 420);
|
|
16578
16950
|
try {
|
|
16579
16951
|
await handle.writeFile(content, "utf8");
|
|
16580
16952
|
await handle.close();
|
|
@@ -16643,32 +17015,280 @@ async function readProjectWorkspace(root, projectId) {
|
|
|
16643
17015
|
return canonicalFiles(files);
|
|
16644
17016
|
}
|
|
16645
17017
|
|
|
16646
|
-
// packages/cli/src/
|
|
16647
|
-
|
|
17018
|
+
// packages/cli/src/production-context.ts
|
|
17019
|
+
async function prepareProductionContext(root, projectId, client) {
|
|
17020
|
+
const resolved = await resolveProject(await readProjectWorkspace(root, projectId), projectId);
|
|
17021
|
+
const sourceKey = stableJson(resolved.lock.sourceHashes);
|
|
17022
|
+
let contextVersion;
|
|
17023
|
+
const remote = async () => {
|
|
17024
|
+
const result2 = await client.request("/v1/workflow/check", { method: "POST", body: { projectId } });
|
|
17025
|
+
const snapshot = result2.editorialSnapshot;
|
|
17026
|
+
if (!snapshot || snapshot.projectId !== projectId || typeof snapshot.contextVersion !== "string" || !/^[a-f0-9]{64}$/.test(snapshot.contextVersion) || !snapshot.sourceHashes || typeof snapshot.sourceHashes !== "object" || Array.isArray(snapshot.sourceHashes) || Object.values(snapshot.sourceHashes).some((hash6) => typeof hash6 !== "string" || !/^[a-f0-9]{64}$/.test(hash6))) throw new CliError("EDITORIAL_CONTEXT_UNAVAILABLE", "The service did not return a versioned editorial context.", "Update the service before connected verification, or explicitly use --offline to check local documents only.");
|
|
17027
|
+
if (stableJson(snapshot.sourceHashes) !== sourceKey) throw new CliError("PRODUCTION_CONTEXT_CONFLICT", "Local and web project preferences or material differ.", "Use status and pull --project to inspect/reconcile changes. Do not overwrite either side automatically.");
|
|
17028
|
+
return snapshot.contextVersion;
|
|
17029
|
+
};
|
|
17030
|
+
if (client) contextVersion = await remote();
|
|
17031
|
+
else contextVersion = await sha256(sourceKey);
|
|
17032
|
+
const validateContext = async () => {
|
|
17033
|
+
const localUnchanged = async () => stableJson((await resolveProject(await readProjectWorkspace(root, projectId), projectId)).lock.sourceHashes) === sourceKey;
|
|
17034
|
+
if (!await localUnchanged()) return false;
|
|
17035
|
+
if (!client) return true;
|
|
17036
|
+
try {
|
|
17037
|
+
if (await remote() !== contextVersion) return false;
|
|
17038
|
+
} catch (error) {
|
|
17039
|
+
if (error instanceof CliError && error.code === "PRODUCTION_CONTEXT_CONFLICT") return false;
|
|
17040
|
+
throw error;
|
|
17041
|
+
}
|
|
17042
|
+
return localUnchanged();
|
|
17043
|
+
};
|
|
17044
|
+
return { resolved, contextVersion, validateContext, contextScope: client ? "local_and_remote" : "local_only" };
|
|
17045
|
+
}
|
|
17046
|
+
|
|
17047
|
+
// packages/cli/src/sfx.ts
|
|
17048
|
+
var localPath = (id2) => {
|
|
17049
|
+
sfxBankPath(id2);
|
|
17050
|
+
return `.editorbrain-local/sfx/${id2}.json`;
|
|
17051
|
+
};
|
|
17052
|
+
async function indexSfx(directory, input, bankId) {
|
|
17053
|
+
const root = await ensureRoot(directory), target = localPath(bankId), folder = await assertNoMediaSymlinks(input);
|
|
17054
|
+
if (!(await lstat3(folder)).isDirectory()) throw new Error("SFX input must be a directory.");
|
|
17055
|
+
const entries = [], paths2 = {}, failures = [];
|
|
17056
|
+
const files = await readdir2(folder, { withFileTypes: true });
|
|
17057
|
+
const selected = files.filter((e) => e.isFile() && [".wav", ".mp3", ".m4a", ".ogg", ".flac", ".aif", ".aiff"].includes(extname2(e.name).toLowerCase()));
|
|
17058
|
+
if (selected.length > 100) throw new Error("Choose a folder with up to 100 audio files; split larger banks by purpose. No files were changed.");
|
|
17059
|
+
for (const file of selected.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
17060
|
+
try {
|
|
17061
|
+
const probe = await probeLocalMedia(join2(folder, file.name));
|
|
17062
|
+
if (!probe.durationSeconds || probe.durationSeconds > 600 || !probe.streams.some((s) => s.type === "audio") || probe.streams.some((s) => s.type === "video" && !s.attachedPicture)) throw new Error("Expected a measured audio effect up to 600 seconds.");
|
|
17063
|
+
const id2 = "sfx-" + probe.sha256.slice(0, 24);
|
|
17064
|
+
if (paths2[id2]) continue;
|
|
17065
|
+
entries.push({ id: id2, name: basename(file.name, extname2(file.name)).slice(0, 180), tags: ["filename-indexed"], durationSeconds: probe.durationSeconds, sha256: probe.sha256, license: { kind: "unverified", note: "Local user file. Verify this file\u2019s license for the intended video; not approved for redistribution." }, audition: "unreviewed" });
|
|
17066
|
+
paths2[id2] = probe.path;
|
|
17067
|
+
} catch (e) {
|
|
17068
|
+
failures.push({ name: file.name, error: e.message });
|
|
17069
|
+
}
|
|
17070
|
+
}
|
|
17071
|
+
const bank = validateSfxBank({ schemaVersion: 1, bankId, entries });
|
|
17072
|
+
await atomicWrite(root, target, stableJson({ schemaVersion: 1, bank, paths: paths2 }), { document: false, overwrite: false, mode: 384 });
|
|
17073
|
+
return { bankId, entries: entries.length, failures, catalogPath: target, mediaUploaded: false, contentReviewed: false, notice: "Local metadata index only; filenames are hints, not auditory analysis. No originals copied or modified. Existing banks are never overwritten. Use --include-unverified only for discovery; verify rights before use." };
|
|
17074
|
+
}
|
|
17075
|
+
async function localSfx(options) {
|
|
17076
|
+
const root = await ensureRoot(options.directory), context = await prepareProductionContext(root, options.projectId);
|
|
17077
|
+
const audio = context.resolved.audioProduction, policy = sfxPolicy(audio, options.sceneRole);
|
|
17078
|
+
if (!policy.enabled) {
|
|
17079
|
+
if (options.soundId) throw new Error(policy.reason);
|
|
17080
|
+
return { items: [], total: 0, policy, contextHash: context.contextVersion, scope: "local_only" };
|
|
17081
|
+
}
|
|
17082
|
+
const id2 = options.bankId ?? policy.bankId;
|
|
17083
|
+
if (!id2) throw new Error("Choose --bank or configure audio.json sfx.library.bankId. Local mode never queries the cloud.");
|
|
17084
|
+
if (policy.bankId && policy.bankId !== id2) throw new Error("Bank differs from project preferences. Save a project exception first.");
|
|
17085
|
+
const serialized = await readText(root, localPath(id2), false);
|
|
17086
|
+
if (Buffer.byteLength(serialized) > 256 * 1024) throw new Error("Local SFX index exceeds 256 KiB.");
|
|
17087
|
+
const saved = JSON.parse(serialized), bank = validateSfxBank(saved.bank), catalogSha256 = await sha256(stableJson(bank));
|
|
17088
|
+
if (bank.bankId !== id2) throw new Error("Local SFX bank ID mismatch.");
|
|
17089
|
+
if (options.soundId) {
|
|
17090
|
+
if (options.catalogSha256 !== catalogSha256) throw new Error("Catalog changed or pin missing. Search again and pass --catalog-sha256.");
|
|
17091
|
+
const entry = bank.entries.find((e) => e.id === options.soundId), path = saved.paths?.[options.soundId];
|
|
17092
|
+
if (!entry || typeof path !== "string") throw new Error("Sound not found.");
|
|
17093
|
+
if (entry.license.kind === "unverified" && !options.includeUnverified) throw new Error("Verify rights first. --include-unverified permits local audition only, never publication.");
|
|
17094
|
+
const probe = await probeLocalMedia(path);
|
|
17095
|
+
if (probe.sha256 !== entry.sha256) throw new Error("Original sound changed; index it as a new bank before use.");
|
|
17096
|
+
if (!await context.validateContext()) throw new Error("Audio preferences changed; retry with current context.");
|
|
17097
|
+
return { entry, path, catalogSha256, policy, contextHash: context.contextVersion, scope: "local_only", usage: entry.license.kind === "unverified" ? "audition_only" : "verify_license_for_project" };
|
|
17098
|
+
}
|
|
17099
|
+
const result2 = searchSfx(bank, { query: options.query ?? "", limit: options.limit, audio, sceneRole: options.sceneRole, includeUnverified: options.includeUnverified });
|
|
17100
|
+
if (!await context.validateContext()) throw new Error("Audio preferences changed; retry.");
|
|
17101
|
+
return { ...result2, catalogSha256, contextHash: context.contextVersion, scope: "local_only" };
|
|
17102
|
+
}
|
|
17103
|
+
async function checkLocalSfx(directory, projectId, plan) {
|
|
17104
|
+
const root = await ensureRoot(directory), context = await prepareProductionContext(root, projectId);
|
|
17105
|
+
const content = await readText(root, plan, false);
|
|
17106
|
+
if (Buffer.byteLength(content) > 256 * 1024) throw new Error("SFX plan too large.");
|
|
17107
|
+
const result2 = checkSfxCues(context.resolved.audioProduction, JSON.parse(content));
|
|
17108
|
+
if (!await context.validateContext()) throw new Error("Preferences changed during the check.");
|
|
17109
|
+
return { ...result2, contextHash: context.contextVersion, scope: "local_only" };
|
|
17110
|
+
}
|
|
17111
|
+
async function remoteSfx(client, subcommand, params) {
|
|
17112
|
+
return client.request(`/v1/sfx/${subcommand}?${new URLSearchParams(params)}`);
|
|
17113
|
+
}
|
|
17114
|
+
|
|
17115
|
+
// packages/cli/src/media-preview.ts
|
|
17116
|
+
import { chmod, link as link2, mkdtemp, rm } from "node:fs/promises";
|
|
17117
|
+
import { dirname as dirname4, join as join3 } from "node:path";
|
|
17118
|
+
|
|
17119
|
+
// packages/cli/src/production-output.ts
|
|
17120
|
+
import { constants as constants3 } from "node:fs";
|
|
17121
|
+
import { mkdir as mkdir2, open as open3 } from "node:fs/promises";
|
|
17122
|
+
import { dirname as dirname3 } from "node:path";
|
|
17123
|
+
async function productionOutputPath(root, relativePath) {
|
|
17124
|
+
if (!relativePath.startsWith(".editorbrain-local/") || relativePath.split(/[\\/]/).some((p) => !p || p === ".." || p === ".") || /[\\\x00-\x1f\x7f]/.test(relativePath)) {
|
|
17125
|
+
throw new CliError("LOCAL_OUTPUT_REQUIRED", "Production output must be inside .editorbrain-local/.", "Use --out .editorbrain-local/<project>/inventory.json, or omit --out for JSON stdout. These reports are not synced.");
|
|
17126
|
+
}
|
|
17127
|
+
return checkedPath(root, relativePath, false);
|
|
17128
|
+
}
|
|
17129
|
+
async function prepareSampleDirectory(root, path) {
|
|
17130
|
+
const target = await productionOutputPath(root, path);
|
|
17131
|
+
await mkdir2(dirname3(target), { recursive: true });
|
|
17132
|
+
await productionOutputPath(root, path);
|
|
17133
|
+
return target;
|
|
17134
|
+
}
|
|
17135
|
+
async function readProductionJson(root, path) {
|
|
17136
|
+
const target = await checkedPath(root, path, false);
|
|
17137
|
+
const handle = await open3(target, constants3.O_RDONLY | constants3.O_NOFOLLOW | constants3.O_NONBLOCK);
|
|
17138
|
+
try {
|
|
17139
|
+
const stat = await handle.stat();
|
|
17140
|
+
if (!stat.isFile() || stat.size > 16 * 1024 * 1024) throw new CliError("INVALID_REPORT", "Expected a regular JSON file of at most 16 MiB.", "Select a local inventory, plan or check profile.");
|
|
17141
|
+
return JSON.parse(await handle.readFile("utf8"));
|
|
17142
|
+
} finally {
|
|
17143
|
+
await handle.close();
|
|
17144
|
+
}
|
|
17145
|
+
}
|
|
17146
|
+
async function saveProductionReport(root, path, report) {
|
|
17147
|
+
const target = await productionOutputPath(root, path);
|
|
17148
|
+
if (!path.endsWith(".json")) throw new CliError("INVALID_ARGUMENT", "Report output needs a .json extension.", "Use a new path under .editorbrain-local/.");
|
|
17149
|
+
await atomicWrite(root, path, stableJson(report), { document: false, overwrite: false, mode: 384 });
|
|
17150
|
+
return target;
|
|
17151
|
+
}
|
|
17152
|
+
function summarizeProductionReport(value) {
|
|
17153
|
+
const r = value;
|
|
17154
|
+
const base = { schemaVersion: r.schemaVersion, kind: r.kind, summary: true, mediaUploaded: false, quality: "not_assessed" };
|
|
17155
|
+
if (r.kind === "editorbrain.media-inventory") return {
|
|
17156
|
+
...base,
|
|
17157
|
+
complete: r.complete,
|
|
17158
|
+
contentReviewed: false,
|
|
17159
|
+
counts: { assets: r.assets.length, failures: r.failures.length, skipped: r.skipped.length },
|
|
17160
|
+
assets: r.assets.slice(0, 8),
|
|
17161
|
+
failures: r.failures.slice(0, 5),
|
|
17162
|
+
omitted: { assets: Math.max(0, r.assets.length - 8), failures: Math.max(0, r.failures.length - 5), skipped: r.skipped.length },
|
|
17163
|
+
next: "Inspect selected source content. Use media sample for specific frames. Read the saved report or rerun with --details for omitted metadata; no content was reviewed automatically."
|
|
17164
|
+
};
|
|
17165
|
+
if (r.kind === "editorbrain.media-samples") return { ...base, sourceSha256: r.source.sha256, frames: r.frames, contentReviewed: false, next: "View these frames to inspect composition. They do not prove motion or sound." };
|
|
17166
|
+
if (r.kind === "editorbrain.production-plan-check") return {
|
|
17167
|
+
...base,
|
|
17168
|
+
valid: r.valid,
|
|
17169
|
+
inventoryComplete: r.inventoryComplete,
|
|
17170
|
+
observations: r.observations,
|
|
17171
|
+
sourceFilesReverified: false,
|
|
17172
|
+
counts: { sources: r.coverage.length, findings: r.findings.length, repeatedPairs: r.repeatedPairs ?? 0 },
|
|
17173
|
+
findings: [...r.findings].sort((a, b) => ({ error: 0, warning: 1, info: 2 })[a.severity] - { error: 0, warning: 1, info: 2 }[b.severity]).slice(0, 8),
|
|
17174
|
+
omittedFindings: Math.max(0, r.findings.length - 8),
|
|
17175
|
+
next: "Review source reuse and inspection gaps in the saved report. Observations are agent-declared, not independently certified."
|
|
17176
|
+
};
|
|
17177
|
+
if (r.kind === "editorbrain.render-check") return {
|
|
17178
|
+
...base,
|
|
17179
|
+
complete: r.complete,
|
|
17180
|
+
technicalStatus: r.technicalStatus,
|
|
17181
|
+
file: r.file,
|
|
17182
|
+
checks: r.checks,
|
|
17183
|
+
findings: r.findings.slice(0, 8),
|
|
17184
|
+
findingsSummary: r.findingsSummary,
|
|
17185
|
+
omittedFindings: Math.max(0, r.findings.length - 8),
|
|
17186
|
+
next: "Reconcile technical candidates with the brief; inspect the actual video/audio. Technical checks do not assess narrative, aesthetics or SFX audibility."
|
|
17187
|
+
};
|
|
17188
|
+
if (r.kind === "editorbrain.media-source-search") return {
|
|
17189
|
+
...base,
|
|
17190
|
+
complete: r.complete,
|
|
17191
|
+
results: r.results.slice(0, 5).map((item) => ({ provider: item.provider, id: item.id.slice(0, 180), title: item.title.slice(0, 180), sourceUrl: item.sourceUrl.length <= 1500 ? item.sourceUrl : null, author: item.author?.slice(0, 120) ?? null, license: { id: item.license.id, reason: item.license.reason, url: item.license.url && item.license.url.length <= 500 ? item.license.url : null }, contentReviewed: false, rightsVerified: false })),
|
|
17192
|
+
metadataAbbreviated: true,
|
|
17193
|
+
failures: r.failures.slice(0, 5),
|
|
17194
|
+
counts: r.counts,
|
|
17195
|
+
omitted: { results: Math.max(0, r.results.length - 5), failures: Math.max(0, r.failures.length - 5) },
|
|
17196
|
+
searchExhaustive: false,
|
|
17197
|
+
rightsVerified: false,
|
|
17198
|
+
next: "Inspect relevant source pages and media. These archives supplement web image/video search; empty results do not prove absence on the internet."
|
|
17199
|
+
};
|
|
17200
|
+
if (r.kind === "editorbrain.evidence-check") return {
|
|
17201
|
+
...base,
|
|
17202
|
+
valid: r.valid,
|
|
17203
|
+
inventoryComplete: r.inventoryComplete,
|
|
17204
|
+
observations: r.observations,
|
|
17205
|
+
sourceFilesReverified: false,
|
|
17206
|
+
factualTruth: "not_assessed",
|
|
17207
|
+
reuseRights: "not_assessed",
|
|
17208
|
+
findings: r.findings.slice(0, 8),
|
|
17209
|
+
omitted: { findings: Math.max(0, r.findings.length - 8) }
|
|
17210
|
+
};
|
|
17211
|
+
if (r.kind === "editorbrain.audio-check") return {
|
|
17212
|
+
...base,
|
|
17213
|
+
complete: r.complete,
|
|
17214
|
+
technicalStatus: r.technicalStatus,
|
|
17215
|
+
file: r.file,
|
|
17216
|
+
fileStable: r.fileStable,
|
|
17217
|
+
coverage: r.coverage,
|
|
17218
|
+
measurements: r.measurements.slice(0, 8),
|
|
17219
|
+
errorCode: r.errorCode,
|
|
17220
|
+
creativeQuality: r.creativeQuality,
|
|
17221
|
+
omitted: { measurements: Math.max(0, r.measurements.length - 8) },
|
|
17222
|
+
next: "Measurements describe this exact file. Listen for intelligibility, pronunciation, SFX and sync when the modality is available."
|
|
17223
|
+
};
|
|
17224
|
+
if (r.kind === "editorbrain.motion-review") return {
|
|
17225
|
+
...base,
|
|
17226
|
+
samples: r.samples ? { frameCount: r.samples.frameCount, fps: r.samples.fps, radiusFrames: r.samples.radiusFrames, samples: r.samples.samples.slice(0, 12), omittedSamples: Math.max(0, r.samples.samples.length - 12) } : null,
|
|
17227
|
+
geometryReviews: r.geometryReviews.slice(0, 3).map((g) => ({ scope: g.scope, status: g.status, candidates: g.candidates.slice(0, 8), omittedCandidates: Math.max(0, g.candidates.length - 8), checks: g.checks })),
|
|
17228
|
+
omittedGeometryReviews: Math.max(0, r.geometryReviews.length - 3),
|
|
17229
|
+
next: "This plans samples and checks declared boxes only. Render/view those frames; read saved details for omitted candidates and intentional overlaps."
|
|
17230
|
+
};
|
|
17231
|
+
throw new CliError("INVALID_REPORT", "Unknown production report kind.", "Use media inventory/sample or production check/plan-check.");
|
|
17232
|
+
}
|
|
17233
|
+
|
|
17234
|
+
// packages/cli/src/media-preview.ts
|
|
17235
|
+
async function prepareMediaPreview(root, file, out, quality = 720, audioStream) {
|
|
17236
|
+
if (![720, 1080].includes(quality) || !out.endsWith(".mp4")) throw new CliError("INVALID_ARGUMENT", "Use --quality 720 or 1080 and a new .mp4 output.", "Previews belong under .editorbrain-local/.");
|
|
17237
|
+
const source = await probeLocalMedia(await assertLocalMediaFile(file));
|
|
17238
|
+
const videos = source.streams.filter((s) => s.type === "video" && !s.attachedPicture), audio = source.streams.filter((s) => s.type === "audio");
|
|
17239
|
+
if (videos.length !== 1 || !source.durationSeconds || source.durationSeconds > 1800) throw new CliError("PREVIEW_SOURCE", "A preview needs one video stream and a known duration up to 30 minutes.", "Prepare long or multi-angle material explicitly; never truncate it silently.");
|
|
17240
|
+
if (["smpte2084", "arib-std-b67"].includes(videos[0].colorTransfer ?? "")) throw new CliError("PREVIEW_HDR", "HDR needs an explicit color-managed preview.", "Keep the original; export an SDR review copy in your editor first.");
|
|
17241
|
+
if (audio.length > 1 && audioStream === void 0) throw new CliError("AUDIO_TRACK_REQUIRED", "Choose the audio track for this preview.", "Use --audio-stream with its stream index.");
|
|
17242
|
+
const sar = videos[0].sampleAspectRatio;
|
|
17243
|
+
if (sar && !["1:1", "0:1", "N/A"].includes(sar)) throw new CliError("PREVIEW_PIXEL_ASPECT", "Non-square pixels need an explicit display-aspect conversion.", "Export a square-pixel review copy in your editor first.");
|
|
17244
|
+
if (/^(yuva|gbrap|rgba|bgra|argb|abgr|ya)/.test(videos[0].pixelFormat ?? "")) throw new CliError("PREVIEW_ALPHA", "Transparent media needs an explicit background for review.", "Keep the alpha master and export a composited review copy first.");
|
|
17245
|
+
const selected = audioStream === void 0 ? audio[0] : audio.find((s) => s.index === audioStream);
|
|
17246
|
+
if (audioStream !== void 0 && !selected) throw new CliError("INVALID_ARGUMENT", "The selected audio stream does not exist.", "Inspect the material before selecting a track.");
|
|
17247
|
+
const target = await prepareSampleDirectory(root, out), temp = await mkdtemp(join3(dirname4(target), ".preview-"));
|
|
17248
|
+
try {
|
|
17249
|
+
const prepared = join3(temp, "preview.mp4"), edge = quality === 720 ? 1280 : 1920;
|
|
17250
|
+
await runMediaTool("ffmpeg", ["-v", "error", "-n", "-copyts", "-start_at_zero", ...localInputArgs(source.path), "-map", `0:${videos[0].index}`, ...selected ? ["-map", `0:${selected.index}`, "-c:a", "aac", "-b:a", "128k"] : ["-an"], "-sn", "-dn", "-map_metadata", "-1", "-vf", `scale=w='min(iw,${edge})':h='min(ih,${edge})':force_original_aspect_ratio=decrease:force_divisible_by=2,setsar=1`, "-c:v", "libx264", "-preset", "fast", "-crf", "23", "-pix_fmt", "yuv420p", "-movflags", "+faststart", prepared], { timeoutMs: 6e5 });
|
|
17251
|
+
const result2 = await probeLocalMedia(prepared);
|
|
17252
|
+
if (result2.durationSeconds === null || Math.abs(result2.durationSeconds - source.durationSeconds) > 0.25 || result2.streams.some((s) => s.type === "audio") !== !!selected) throw new CliError("PREVIEW_VERIFICATION", "Preview duration or audio does not match the source.", "No output was accepted. Inspect source timing in your editor.");
|
|
17253
|
+
if ((await probeLocalMedia(source.path)).sha256 !== source.sha256) throw new CliError("SOURCE_CHANGED", "The source changed during compression.", "Retry with a stable original.");
|
|
17254
|
+
await chmod(prepared, 384);
|
|
17255
|
+
await link2(prepared, target);
|
|
17256
|
+
return { kind: "editorbrain.review-preview", path: target, sha256: result2.sha256, sourceSha256: source.sha256, sourceBytes: source.bytes, bytes: result2.bytes, durationSeconds: result2.durationSeconds, quality, originalPreserved: true, uploaded: false, uploadEligible: result2.bytes <= 90 * 1024 * 1024, savedPercent: Math.round((1 - result2.bytes / source.bytes) * 100), purpose: "preview", note: "Review copy, not an export master. Inspect text, color and audio before sharing. Small sources may become larger; originals remain untouched." };
|
|
17257
|
+
} finally {
|
|
17258
|
+
await rm(temp, { recursive: true, force: true });
|
|
17259
|
+
}
|
|
17260
|
+
}
|
|
16648
17261
|
|
|
16649
|
-
// packages/cli/src/
|
|
16650
|
-
|
|
16651
|
-
|
|
16652
|
-
|
|
16653
|
-
|
|
16654
|
-
|
|
16655
|
-
|
|
16656
|
-
|
|
17262
|
+
// packages/cli/src/studio-audio.ts
|
|
17263
|
+
import { link as link3, mkdtemp as mkdtemp2, readFile, rm as rm2 } from "node:fs/promises";
|
|
17264
|
+
import { dirname as dirname5, join as join4 } from "node:path";
|
|
17265
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
17266
|
+
async function prepareStudioAudio(root, file, out, streamIndex) {
|
|
17267
|
+
const source = await probeLocalMedia(await assertLocalMediaFile(file));
|
|
17268
|
+
const audio = source.streams.filter((stream) => stream.type === "audio");
|
|
17269
|
+
if (!audio.length) throw new CliError("AUDIO_REQUIRED", "The source has no audio stream.", "Use visual inspection instead; do not invent a transcript.");
|
|
17270
|
+
if (audio.length > 1 && streamIndex === void 0) throw new CliError("AUDIO_TRACK_REQUIRED", "The file has several audio tracks.", "Inspect them and select the intended stream with --audio-stream.", false, { audioStreams: audio.map((s) => s.index) });
|
|
17271
|
+
const selected = audio.find((s) => s.index === (streamIndex ?? audio[0].index));
|
|
17272
|
+
if (!selected) throw new CliError("INVALID_ARGUMENT", "The selected stream is not an audio track.", "Inspect media inventory before selecting a track.");
|
|
17273
|
+
if (source.durationSeconds === null || source.durationSeconds > 1800) throw new CliError("AUDIO_DURATION_LIMIT", "Scribe preparation requires a known source duration up to 30 minutes.", "Split long recordings explicitly and preserve each segment offset; never silently truncate.");
|
|
17274
|
+
if (!out.endsWith(".wav")) throw new CliError("INVALID_ARGUMENT", "The prepared output must be a .wav file.", "Use --out .editorbrain-local/studio/source.wav.");
|
|
17275
|
+
const target = await prepareSampleDirectory(root, out), temp = await mkdtemp2(join4(dirname5(target), ".scribe-"));
|
|
17276
|
+
try {
|
|
17277
|
+
const prepared = join4(temp, "audio.wav");
|
|
17278
|
+
await runMediaTool("ffmpeg", ["-v", "error", "-n", "-copyts", "-start_at_zero", ...localInputArgs(source.path), "-map", `0:${selected.index}`, "-vn", "-sn", "-dn", "-map_metadata", "-1", "-af", "aresample=16000:async=1:first_pts=0", "-ac", "1", "-ar", "16000", "-c:a", "pcm_s16le", "-t", "1801", prepared], { timeoutMs: 6e5 });
|
|
17279
|
+
const verified = await probeLocalMedia(prepared);
|
|
17280
|
+
if (verified.durationSeconds === null || verified.durationSeconds > 1800 || verified.bytes > 90 * 1024 * 1024) throw new CliError("AUDIO_DURATION_LIMIT", "The decoded audio exceeds the supported duration.", "Inspect source timestamps; no file was uploaded.");
|
|
17281
|
+
if ((await probeLocalMedia(source.path)).sha256 !== source.sha256) throw new CliError("SOURCE_CHANGED", "The source changed during extraction.", "Retry only after the source is stable.");
|
|
17282
|
+
const bytes = await readFile(prepared), hash6 = createHash2("sha256").update(bytes).digest("hex");
|
|
17283
|
+
await link3(prepared, target);
|
|
17284
|
+
return { kind: "editorbrain.scribe-input", path: target, sha256: hash6, bytes: bytes.length, durationMs: Math.round(verified.durationSeconds * 1e3), mimeType: "audio/wav", sourceSha256: source.sha256, sourcePath: source.path, audioStream: selected.index, sourceStartTimeSeconds: source.startTimeSeconds, timestampBasis: "milliseconds_from_container_start", silenceRemoved: false, uploaded: false, next: "Upload this WAV using assets upload, then quote Scribe with its assetId. Keep this source mapping locally; returned word times refer to this prepared audio." };
|
|
17285
|
+
} finally {
|
|
17286
|
+
await rm2(temp, { recursive: true, force: true });
|
|
16657
17287
|
}
|
|
16658
|
-
code;
|
|
16659
|
-
hint;
|
|
16660
|
-
retryable;
|
|
16661
|
-
details;
|
|
16662
|
-
};
|
|
16663
|
-
function errorOutput(error) {
|
|
16664
|
-
if (error instanceof CliError) return { error: error.message, code: error.code, hint: error.hint, retryable: error.retryable, ...error.details };
|
|
16665
|
-
if (error instanceof ValidationError) return { error: "Workspace validation failed.", code: "VALIDATION_FAILED", retryable: false, hint: "Correct the reported documents before retrying.", issues: error.issues };
|
|
16666
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
16667
|
-
const safe = message.replace(/\b(?:eh_[a-f0-9]{64}|npm_[A-Za-z0-9]+)\b/g, "[REDACTED]").replace(/Bearer\s+\S+/gi, "Bearer [REDACTED]").replace(/(https?:\/\/[^\s?]+)\?[^\s]*/g, "$1?[REDACTED]");
|
|
16668
|
-
return { error: safe, code: "COMMAND_FAILED", retryable: false, hint: "Read the command help and run editorbrain doctor. Do not retry writes blindly." };
|
|
16669
17288
|
}
|
|
16670
17289
|
|
|
16671
17290
|
// packages/cli/src/client.ts
|
|
17291
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
16672
17292
|
var searchErrors = {
|
|
16673
17293
|
invalid_search_brief: { status: 400, code: "INVALID_SEARCH_BRIEF", hint: "Check references/search-brief.md and the service contract. Older services may reject eventQueries objects with channels. Do not silently remove constraints or retry a changed query as if filtering succeeded." },
|
|
16674
17294
|
source_inspection_required: { status: 422, code: "SOURCE_INSPECTION_REQUIRED", hint: "Inspect the supplied source and declare only modalities actually reviewed. A script requires script inspection; video requires frames, motion or audio." },
|
|
@@ -16862,11 +17482,11 @@ async function pushWorkspace(client, root, files, options = {}) {
|
|
|
16862
17482
|
for (const [path, content] of Object.entries(files)) {
|
|
16863
17483
|
const validation = validateDocument(path, content);
|
|
16864
17484
|
if (!validation.valid) throw new Error(validation.issues.map((x) => `${x.path}: ${x.message}`).join("\n"));
|
|
16865
|
-
const
|
|
17485
|
+
const digest3 = await sha256(content);
|
|
16866
17486
|
const existing = remote.get(path);
|
|
16867
17487
|
const base = state.documents[path];
|
|
16868
|
-
if (existing?.sha256 ===
|
|
16869
|
-
state.documents[path] = { revision: existing.revision, sha256:
|
|
17488
|
+
if (existing?.sha256 === digest3) {
|
|
17489
|
+
state.documents[path] = { revision: existing.revision, sha256: digest3 };
|
|
16870
17490
|
unchanged2++;
|
|
16871
17491
|
continue;
|
|
16872
17492
|
}
|
|
@@ -16887,8 +17507,8 @@ ${conflicts.join("\n")}`, "Run push --dry-run and reconcile the reported paths."
|
|
|
16887
17507
|
for (const item of pending) {
|
|
16888
17508
|
const result2 = await client.put(item.path, item.content, item.expectedRevision);
|
|
16889
17509
|
checkRemote(result2);
|
|
16890
|
-
const
|
|
16891
|
-
if (result2.path !== item.path || result2.sha256 !==
|
|
17510
|
+
const digest3 = await sha256(item.content);
|
|
17511
|
+
if (result2.path !== item.path || result2.sha256 !== digest3) throw new Error("Server returned inconsistent write metadata");
|
|
16892
17512
|
state.documents[item.path] = { revision: result2.revision, sha256: result2.sha256 };
|
|
16893
17513
|
uploaded.push(item.path);
|
|
16894
17514
|
await saveSyncState(root, state);
|
|
@@ -16906,14 +17526,14 @@ async function pullWorkspace(client, root, local, options = {}) {
|
|
|
16906
17526
|
for (const summary2 of summaries) {
|
|
16907
17527
|
checkRemote(summary2);
|
|
16908
17528
|
const content = local[summary2.path];
|
|
16909
|
-
const
|
|
17529
|
+
const digest3 = content === void 0 ? void 0 : await sha256(content);
|
|
16910
17530
|
const base = state.documents[summary2.path];
|
|
16911
|
-
if (
|
|
17531
|
+
if (digest3 === summary2.sha256) {
|
|
16912
17532
|
state.documents[summary2.path] = { revision: summary2.revision, sha256: summary2.sha256 };
|
|
16913
17533
|
unchanged2++;
|
|
16914
17534
|
continue;
|
|
16915
17535
|
}
|
|
16916
|
-
if (content !== void 0 && (!base ||
|
|
17536
|
+
if (content !== void 0 && (!base || digest3 !== base.sha256)) {
|
|
16917
17537
|
conflicts.push(summary2.path);
|
|
16918
17538
|
continue;
|
|
16919
17539
|
}
|
|
@@ -16949,6 +17569,39 @@ ${conflicts.join("\n")}`, "Run pull --dry-run and reconcile the reported paths."
|
|
|
16949
17569
|
return { downloaded, unchanged: unchanged2 };
|
|
16950
17570
|
}
|
|
16951
17571
|
|
|
17572
|
+
// packages/cli/src/studio.ts
|
|
17573
|
+
async function studioCommand(client, root, command, argument, flags) {
|
|
17574
|
+
const allowed = /* @__PURE__ */ new Set(["dir", "url", ...command === "quote" ? ["file"] : command === "run" ? ["max-credits", "user-confirmed"] : []]);
|
|
17575
|
+
for (const flag of Object.keys(flags)) if (!allowed.has(flag)) throw new CliError("INVALID_ARGUMENT", `--${flag} is not supported for studio ${command}.`, "Read editorbrain help.");
|
|
17576
|
+
try {
|
|
17577
|
+
if (command === "catalog" || command === "jobs") {
|
|
17578
|
+
if (argument) throw new CliError("INVALID_ARGUMENT", "This command has no positional input.", "Read editorbrain help.");
|
|
17579
|
+
return await client.request(`/v1/studio/${command}`);
|
|
17580
|
+
}
|
|
17581
|
+
if (command === "quote") {
|
|
17582
|
+
if (argument || typeof flags.file !== "string") throw new CliError("INVALID_ARGUMENT", "studio quote requires --file request.json.", "Read references/studio.md.");
|
|
17583
|
+
const body = await readProductionJson(root, flags.file);
|
|
17584
|
+
if (Buffer.byteLength(JSON.stringify(body)) > 16e3) throw new CliError("INVALID_ARGUMENT", "Studio input exceeds 16 KB.", "Use a short scene description or script.");
|
|
17585
|
+
return await client.request("/v1/studio/quotes", { method: "POST", body });
|
|
17586
|
+
}
|
|
17587
|
+
if (!argument || !/^[a-f0-9-]{36}$/i.test(argument)) throw new CliError("INVALID_ARGUMENT", "Supply the quote/job ID returned by EditorBrain.", "Read editorbrain studio jobs before repeating a run.");
|
|
17588
|
+
if (command === "get") return await client.request(`/v1/studio/jobs/${argument}`);
|
|
17589
|
+
if (command === "run") {
|
|
17590
|
+
const maxCredits = Number(flags["max-credits"]);
|
|
17591
|
+
if (flags["user-confirmed"] !== true || !Number.isFinite(maxCredits) || maxCredits <= 0 || maxCredits > 1e4) throw new CliError("SPENDING_APPROVAL_REQUIRED", "Supply --user-confirmed and --max-credits only after the user authorized this quote or budget.", "Preferences and direct editing mode do not authorize spending.");
|
|
17592
|
+
return await client.request("/v1/studio/jobs", { method: "POST", body: { quoteId: argument, maxCredits, userConfirmed: true } });
|
|
17593
|
+
}
|
|
17594
|
+
throw new CliError("INVALID_ARGUMENT", "Use studio catalog, quote, run, get or jobs.", "Read editorbrain help.");
|
|
17595
|
+
} catch (error) {
|
|
17596
|
+
if (error instanceof ApiError) {
|
|
17597
|
+
const code = error.body?.error?.code;
|
|
17598
|
+
const hints = { studio_unavailable: "This EditorBrain tool is not activated. No generation was started.", studio_insufficient_credits: "Check studio catalog for your available creative-credit balance.", studio_quote_expired: "Request a new quote and review its price.", studio_budget_exceeded: "The quote exceeds the authorized budget. Do not increase it yourself.", stale_context: "Read the current project preferences and request a new quote.", studio_preference_conflict: "Reconcile the project language, voice or no-music preference with the user before generating.", studio_audio_format: "Prepare PCM 16-bit WAV (mono/stereo, up to 30 minutes) from the original with local FFmpeg, then upload it." };
|
|
17599
|
+
if (code && hints[code]) throw new CliError(code.toUpperCase(), "EditorBrain Studio could not start this request.", hints[code]);
|
|
17600
|
+
}
|
|
17601
|
+
throw error;
|
|
17602
|
+
}
|
|
17603
|
+
}
|
|
17604
|
+
|
|
16952
17605
|
// packages/cli/src/project-selection.ts
|
|
16953
17606
|
async function listProjects(client) {
|
|
16954
17607
|
const documents = await client.list("videos/");
|
|
@@ -16991,13 +17644,13 @@ async function pullProject(client, root, local, options = {}) {
|
|
|
16991
17644
|
}
|
|
16992
17645
|
|
|
16993
17646
|
// packages/cli/src/setup.ts
|
|
16994
|
-
import { spawn } from "node:child_process";
|
|
16995
|
-
import { resolve as
|
|
17647
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
17648
|
+
import { resolve as resolve4 } from "node:path";
|
|
16996
17649
|
|
|
16997
17650
|
// packages/cli/src/agent-guidance.ts
|
|
16998
|
-
import { createHash } from "node:crypto";
|
|
16999
|
-
import { constants as
|
|
17000
|
-
import { lstat as
|
|
17651
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
17652
|
+
import { constants as constants4 } from "node:fs";
|
|
17653
|
+
import { lstat as lstat4, open as open4, readdir as readdir3, rename as rename2 } from "node:fs/promises";
|
|
17001
17654
|
import { fileURLToPath } from "node:url";
|
|
17002
17655
|
|
|
17003
17656
|
// packages/cli/src/agent-guidance-history.ts
|
|
@@ -17131,6 +17784,33 @@ var VERIFIED_GUIDANCE_RELEASES = [
|
|
|
17131
17784
|
"editorbrain/references/style-creator.md": "1b1bc289a5bc308cbbf7f4d699de2933b8934733a2b69b3e98fb1b7420d986f2",
|
|
17132
17785
|
"editorbrain/references/workspace-files.md": "cd3006a7cc4f04ff5dfeb1bd901a21c53455017aa7d19fcde9a6e26f1fbbd5b6"
|
|
17133
17786
|
}
|
|
17787
|
+
},
|
|
17788
|
+
{
|
|
17789
|
+
"version": "0.28.2",
|
|
17790
|
+
"archiveSha256": "1d73319f34603a7a1df834f8ed0884c49ec9c2b16d255fea3b30388f4d91f416",
|
|
17791
|
+
"files": {
|
|
17792
|
+
"editorbrain-motion/SKILL.md": "cd0e0c25c1025a775ab547bab16d6efb43a4f388b8f3ed928cdfed3999938ac7",
|
|
17793
|
+
"editorbrain-motion/references/authored-scenes.md": "3d617226b0aafe0970c635e77d285b752913fca33417d7c04e51d6b6c60af973",
|
|
17794
|
+
"editorbrain-motion/references/code-motion.md": "202240b2ae7c062c3cdd4b488f1032f991b4509af78206ad846faa6a792e1d0a",
|
|
17795
|
+
"editorbrain-motion/references/component-library.md": "fea1fb710d23830c3ed14c3e4bed7bc53540ae2e2e662b74fe6eb6307b010d28",
|
|
17796
|
+
"editorbrain-motion/references/generated-motion.md": "a8d214b53c15f742658abd84f03b768fa2821f8cde94f50aab82a3759a30b36b",
|
|
17797
|
+
"editorbrain/AGENTS.md": "cda14533a7232f1324b8137dfb24698ecbbf60e3c32c3b2216495a7ab7a3a777",
|
|
17798
|
+
"editorbrain/SKILL.md": "58c490d4f3acb9e83a958b7d93eb91815387b0eb6480bfa523ca4d91b3442a03",
|
|
17799
|
+
"editorbrain/references/audio-production.md": "9e10ea3583e859677eb241c24ddb3ebc77b5c7077654346201ede688d2fcc149",
|
|
17800
|
+
"editorbrain/references/cli-mcp.md": "aa17a0d5c366e9c9318aaea978694575be4f4ba0051e1c5e0684886b1bcaccec",
|
|
17801
|
+
"editorbrain/references/first-edit.md": "ad5682046717d2ccb10a310c237c03ead8de294f1b907e550bd50b479bee0ed6",
|
|
17802
|
+
"editorbrain/references/integrations.md": "a3500dd79c71ab3ee80a865bb5bf8c94287efb933e07d577738891e276a819a0",
|
|
17803
|
+
"editorbrain/references/local-production.md": "e981c0a02f8e0d1998e39d64de393b264703d24bacc904600f12885ff3576588",
|
|
17804
|
+
"editorbrain/references/media-preferences.md": "c7904e4a6475b5ff935d7b942bd2c715de6fda9bead93e90d3c4ee99532d5f54",
|
|
17805
|
+
"editorbrain/references/memory-and-review.md": "51a274e522270f1fc295d0c0b9a1c23672ce01abea097c2499c9a8db56c0c289",
|
|
17806
|
+
"editorbrain/references/motion-review.md": "0fb09a2d61ecd13114ced5cca5daa4580a922cf7d7b3f8698077ceb0e96da6f0",
|
|
17807
|
+
"editorbrain/references/onboarding.md": "96bd762314a1f3dbbeb5172dba85a7b1f2fbc26627a09f7b48fa2889a9d09ab2",
|
|
17808
|
+
"editorbrain/references/production-and-review.md": "ed662a151dedbe168fcacc59be1d997a012818cd7abd9b9a8e936138e2e0b1f2",
|
|
17809
|
+
"editorbrain/references/real-media.md": "e80ba70caca8a1253fbf13b56ee2a85674ccf9f09e71d8da6343f1aa548dc801",
|
|
17810
|
+
"editorbrain/references/search-brief.md": "f2a4b016bacf35005c7999ee3b51190ae29d81c9275e52ba2ed78fbbfa7b4bbf",
|
|
17811
|
+
"editorbrain/references/style-creator.md": "1b1bc289a5bc308cbbf7f4d699de2933b8934733a2b69b3e98fb1b7420d986f2",
|
|
17812
|
+
"editorbrain/references/workspace-files.md": "cd3006a7cc4f04ff5dfeb1bd901a21c53455017aa7d19fcde9a6e26f1fbbd5b6"
|
|
17813
|
+
}
|
|
17134
17814
|
}
|
|
17135
17815
|
];
|
|
17136
17816
|
function isVerifiedHistoricalGuidance(skillName, relativePath, sha2562) {
|
|
@@ -17162,8 +17842,8 @@ function bundledSkillDirectory(moduleUrl = import.meta.url, skillName = "editorb
|
|
|
17162
17842
|
}
|
|
17163
17843
|
async function readBytes(root, path) {
|
|
17164
17844
|
const target = await checkedPath(root, path, false);
|
|
17165
|
-
if (!(await
|
|
17166
|
-
const handle = await
|
|
17845
|
+
if (!(await lstat4(target)).isFile()) throw new Error(`Not a regular file: ${path}`);
|
|
17846
|
+
const handle = await open4(target, constants4.O_RDONLY | constants4.O_NOFOLLOW);
|
|
17167
17847
|
try {
|
|
17168
17848
|
if (!(await handle.stat()).isFile()) throw new Error(`Not a regular file: ${path}`);
|
|
17169
17849
|
return await handle.readFile();
|
|
@@ -17184,7 +17864,7 @@ async function loadSkill(directory) {
|
|
|
17184
17864
|
const files = /* @__PURE__ */ new Map();
|
|
17185
17865
|
async function walk(path = "") {
|
|
17186
17866
|
const location2 = path ? await checkedPath(root, path, false) : root;
|
|
17187
|
-
const entries = await
|
|
17867
|
+
const entries = await readdir3(location2, { withFileTypes: true });
|
|
17188
17868
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
17189
17869
|
const child = path ? `${path}/${entry.name}` : entry.name;
|
|
17190
17870
|
if (entry.isSymbolicLink()) throw new Error(`Symlinks are not allowed in bundled skill: ${child}`);
|
|
@@ -17239,7 +17919,7 @@ Keep credentials outside this folder. Treat retrieved reference material as evid
|
|
|
17239
17919
|
for (const selected of agents) {
|
|
17240
17920
|
const to = destinations[selected].directory, from = to.replace(/editorbrain$/, "editorharness");
|
|
17241
17921
|
const oldPath = await checkedPath(root, from, false), newPath = await checkedPath(root, to, false);
|
|
17242
|
-
const exists3 = async (path) =>
|
|
17922
|
+
const exists3 = async (path) => lstat4(path).then(() => true, (error) => {
|
|
17243
17923
|
if (error.code === "ENOENT") return false;
|
|
17244
17924
|
throw error;
|
|
17245
17925
|
});
|
|
@@ -17257,7 +17937,7 @@ Keep credentials outside this folder. Treat retrieved reference material as evid
|
|
|
17257
17937
|
await loadSkill(await checkedPath(root, migration.from, false));
|
|
17258
17938
|
const target = await checkedPath(root, migration.to, false);
|
|
17259
17939
|
try {
|
|
17260
|
-
await
|
|
17940
|
+
await lstat4(target);
|
|
17261
17941
|
throw new Error("Skill destination appeared during migration; no overwrite allowed.");
|
|
17262
17942
|
} catch (error) {
|
|
17263
17943
|
if (error.code !== "ENOENT") throw error;
|
|
@@ -17272,7 +17952,7 @@ Keep credentials outside this folder. Treat retrieved reference material as evid
|
|
|
17272
17952
|
} catch (error) {
|
|
17273
17953
|
if (error.code !== "EEXIST") throw error;
|
|
17274
17954
|
const existing = await readBytes(root, file.path);
|
|
17275
|
-
const existingHash =
|
|
17955
|
+
const existingHash = createHash3("sha256").update(existing).digest("hex");
|
|
17276
17956
|
const pristine = file.kind === "skill" && (previousHashesBySkill[file.skillName][file.relativePath]?.includes(existingHash) || isVerifiedHistoricalGuidance(file.skillName, file.relativePath, existingHash));
|
|
17277
17957
|
const oldPointer = Buffer.from(file.content.toString().replaceAll("/skills/editorbrain/", "/skills/editorharness/"));
|
|
17278
17958
|
if (!existing.equals(file.content) && (pristine || file.kind === "entryPoint" && existing.equals(oldPointer))) {
|
|
@@ -17303,8 +17983,8 @@ async function installAgentGuidance(directory, agent = "both") {
|
|
|
17303
17983
|
}
|
|
17304
17984
|
|
|
17305
17985
|
// packages/cli/src/workspace-lock.ts
|
|
17306
|
-
import { constants as
|
|
17307
|
-
import { lstat as
|
|
17986
|
+
import { constants as constants5 } from "node:fs";
|
|
17987
|
+
import { lstat as lstat5, open as open5, rename as rename3, unlink as unlink2 } from "node:fs/promises";
|
|
17308
17988
|
import { hostname } from "node:os";
|
|
17309
17989
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
17310
17990
|
var LOCK_PATH = ".editorbrain-operation.lock";
|
|
@@ -17312,7 +17992,7 @@ async function withWorkspaceLock(root, operation, options = {}) {
|
|
|
17312
17992
|
const legacyLock = await checkedPath(root, ".editorharness-operation.lock", false);
|
|
17313
17993
|
let legacy;
|
|
17314
17994
|
try {
|
|
17315
|
-
legacy = await
|
|
17995
|
+
legacy = await open5(legacyLock, constants5.O_CREAT | constants5.O_EXCL | constants5.O_WRONLY | constants5.O_NOFOLLOW, 384);
|
|
17316
17996
|
} catch (error) {
|
|
17317
17997
|
if (error.code === "EEXIST") throw new CliError("WORKSPACE_BUSY", "Another CLI operation holds this workspace lock.", "Wait for the other operation to finish.");
|
|
17318
17998
|
throw error;
|
|
@@ -17321,7 +18001,7 @@ async function withWorkspaceLock(root, operation, options = {}) {
|
|
|
17321
18001
|
const path = await checkedPath(root, LOCK_PATH, false);
|
|
17322
18002
|
let handle;
|
|
17323
18003
|
try {
|
|
17324
|
-
handle = await
|
|
18004
|
+
handle = await open5(path, constants5.O_CREAT | constants5.O_EXCL | constants5.O_WRONLY | constants5.O_NOFOLLOW, 384);
|
|
17325
18005
|
} catch (error) {
|
|
17326
18006
|
if (error.code !== "EEXIST") throw error;
|
|
17327
18007
|
throw new CliError("WORKSPACE_BUSY", "Another CLI operation holds this workspace lock.", "Wait for the other operation. If it crashed, verify no process is using the folder before removing .editorbrain-operation.lock. Locks are never removed automatically.");
|
|
@@ -17332,7 +18012,7 @@ async function withWorkspaceLock(root, operation, options = {}) {
|
|
|
17332
18012
|
await handle.close();
|
|
17333
18013
|
const oldRoot = await checkedPath(root, ".editorharness", false);
|
|
17334
18014
|
const newRoot = await checkedPath(root, ".editorbrain", false);
|
|
17335
|
-
const exists3 = async (path2) =>
|
|
18015
|
+
const exists3 = async (path2) => lstat5(path2).then(() => true, (error) => {
|
|
17336
18016
|
if (error.code === "ENOENT") return false;
|
|
17337
18017
|
throw error;
|
|
17338
18018
|
});
|
|
@@ -17353,10 +18033,10 @@ async function withWorkspaceLock(root, operation, options = {}) {
|
|
|
17353
18033
|
}
|
|
17354
18034
|
|
|
17355
18035
|
// packages/cli/src/doctor.ts
|
|
17356
|
-
import { readFile } from "node:fs/promises";
|
|
17357
|
-
import { resolve as
|
|
18036
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
18037
|
+
import { resolve as resolve3 } from "node:path";
|
|
17358
18038
|
async function cliVersion() {
|
|
17359
|
-
const metadata = JSON.parse(await
|
|
18039
|
+
const metadata = JSON.parse(await readFile2(new URL("../package.json", import.meta.url), "utf8"));
|
|
17360
18040
|
return metadata.version;
|
|
17361
18041
|
}
|
|
17362
18042
|
async function diagnose(options, fetcher = fetch) {
|
|
@@ -17390,7 +18070,7 @@ async function diagnose(options, fetcher = fetch) {
|
|
|
17390
18070
|
try {
|
|
17391
18071
|
for (const file of await bundledGuidanceFiles(skillName)) {
|
|
17392
18072
|
const current2 = await readText(root, `${directory}/${file}`, false);
|
|
17393
|
-
const bundled = await
|
|
18073
|
+
const bundled = await readFile2(resolve3(bundledSkillDirectory(void 0, skillName), file), "utf8");
|
|
17394
18074
|
if (current2 !== bundled) different = true;
|
|
17395
18075
|
}
|
|
17396
18076
|
checks.push({ name, status: different ? "warning" : "pass", detail: different ? "Installed guidance differs from this CLI release." : "All bundled guidance files match.", hint: different ? "Review customizations or older guidance; skill install preserves differences." : void 0 });
|
|
@@ -17421,12 +18101,12 @@ async function diagnose(options, fetcher = fetch) {
|
|
|
17421
18101
|
failure3("remote", error);
|
|
17422
18102
|
}
|
|
17423
18103
|
}
|
|
17424
|
-
return { schemaVersion: 1, cliVersion: await cliVersion(), ok: !checks.some((check) => check.status === "fail"), directory: root ??
|
|
18104
|
+
return { schemaVersion: 1, cliVersion: await cliVersion(), ok: !checks.some((check) => check.status === "fail"), directory: root ?? resolve3(options.directory), workspaceId, checks, mcpClient: "not_verified", renderVerified: false };
|
|
17425
18105
|
}
|
|
17426
18106
|
|
|
17427
18107
|
// packages/cli/src/setup.ts
|
|
17428
18108
|
var runHost = (command, args, cwd, interactive = false) => new Promise((done, reject) => {
|
|
17429
|
-
const child =
|
|
18109
|
+
const child = spawn2(command, args, { cwd, shell: false, stdio: ["ignore", interactive ? 2 : "pipe", interactive ? 2 : "pipe"], windowsHide: !interactive });
|
|
17430
18110
|
let stdout = "", stderr = "", size = 0;
|
|
17431
18111
|
const collect = (kind, data) => {
|
|
17432
18112
|
size += data.length;
|
|
@@ -17474,183 +18154,71 @@ async function configureMcp(agent, directory, url2, noBrowser, runner = runHost)
|
|
|
17474
18154
|
exists3 = list.some((item) => item?.name === "editorbrain");
|
|
17475
18155
|
} else {
|
|
17476
18156
|
exists3 = inspect.code === 0;
|
|
17477
|
-
if (!exists3 && !/^No MCP server named "editorbrain"[.\s]/.test(inspect.stderr + inspect.stdout)) throw new CliError("MCP_INSPECTION_FAILED", "Could not inspect Claude MCP configuration.", "Inspect it manually; setup did not overwrite it.");
|
|
17478
|
-
}
|
|
17479
|
-
if (exists3) return { agent, status: "preserved", next: "An existing editorbrain server was preserved. Check its endpoint and authentication, then " + next, hostVerified: false };
|
|
17480
|
-
const added = await runner(agent, args, directory, true);
|
|
17481
|
-
return { agent, status: added.code === 0 ? "registered" : "action_required", next, hostVerified: false };
|
|
17482
|
-
}
|
|
17483
|
-
async function setup(options, runner = runHost) {
|
|
17484
|
-
const steps = {};
|
|
17485
|
-
let selected = await options.getConfig();
|
|
17486
|
-
let client = new Client(selected);
|
|
17487
|
-
let me;
|
|
17488
|
-
try {
|
|
17489
|
-
if (!selected.apiKey) throw new CliError("AUTH_REQUIRED", "Login required.", "Run login.");
|
|
17490
|
-
me = await client.me();
|
|
17491
|
-
} catch (error) {
|
|
17492
|
-
if (!(error instanceof CliError) || error.code !== "AUTH_REQUIRED") throw error;
|
|
17493
|
-
await options.login();
|
|
17494
|
-
selected = await options.getConfig();
|
|
17495
|
-
client = new Client(selected);
|
|
17496
|
-
me = await client.me();
|
|
17497
|
-
}
|
|
17498
|
-
if (options.workspaceId && me.workspaceId !== options.workspaceId) throw new CliError("WORKSPACE_MISMATCH", "The authorized account selected a different workspace.", "Run login for the intended workspace. No project files were changed.");
|
|
17499
|
-
steps.account = { status: "verified", workspaceId: me.workspaceId, endpoint: client.url };
|
|
17500
|
-
const root = await ensureRoot(
|
|
17501
|
-
let folderReady = false, skillReady = false;
|
|
17502
|
-
await withWorkspaceLock(root, async () => {
|
|
17503
|
-
try {
|
|
17504
|
-
steps.documents = { status: "ready", ...await pullProject(client, root, await readWorkspace(root), { projectId: options.projectId }) };
|
|
17505
|
-
folderReady = true;
|
|
17506
|
-
} catch (error) {
|
|
17507
|
-
steps.documents = { status: error instanceof CliError && error.code === "PROJECT_SELECTION_REQUIRED" ? "selection_required" : "failed", ...errorOutput(error) };
|
|
17508
|
-
}
|
|
17509
|
-
try {
|
|
17510
|
-
const result2 = await installAgentGuidance(root, options.agent);
|
|
17511
|
-
steps.skill = { status: result2.conflicts.length ? "review_required" : "ready", ...result2 };
|
|
17512
|
-
skillReady = !result2.conflicts.length;
|
|
17513
|
-
} catch (error) {
|
|
17514
|
-
steps.skill = { status: "failed", ...errorOutput(error) };
|
|
17515
|
-
}
|
|
17516
|
-
});
|
|
17517
|
-
const hosts = [];
|
|
17518
|
-
if (!options.skipMcp && skillReady) {
|
|
17519
|
-
for (const agent of options.agent === "both" ? ["codex", "claude"] : [options.agent]) {
|
|
17520
|
-
try {
|
|
17521
|
-
hosts.push(await configureMcp(agent, root, client.url, !!options.noBrowser, runner));
|
|
17522
|
-
} catch (error) {
|
|
17523
|
-
hosts.push({ agent, status: "action_required", ...errorOutput(error), hostVerified: false });
|
|
17524
|
-
}
|
|
17525
|
-
}
|
|
17526
|
-
}
|
|
17527
|
-
steps.mcp = { status: options.skipMcp ? "skipped" : "verification_required", hosts, expectedWorkspaceId: me.workspaceId, next: "Inside your agent, call workspace_info and confirm this workspace ID. Configuration alone does not verify a connection." };
|
|
17528
|
-
const diagnosis = await diagnose({ directory: root, agent: options.agent, config: selected });
|
|
17529
|
-
steps.diagnostics = diagnosis;
|
|
17530
|
-
return { schemaVersion: 1, directory: root, workspaceId: me.workspaceId, cliReady: folderReady && skillReady && diagnosis.ok, status: folderReady && skillReady && diagnosis.ok && options.skipMcp ? "ready" : "action_required", steps, renderVerified: false };
|
|
17531
|
-
}
|
|
17532
|
-
|
|
17533
|
-
// packages/cli/src/onboarding.ts
|
|
17534
|
-
import { createServer } from "node:http";
|
|
17535
|
-
import { randomBytes, createHash as createHash2 } from "node:crypto";
|
|
17536
|
-
import { join as join2, dirname as dirname3 } from "node:path";
|
|
17537
|
-
import { mkdir as mkdir3 } from "node:fs/promises";
|
|
17538
|
-
|
|
17539
|
-
// packages/cli/src/production-output.ts
|
|
17540
|
-
import { constants as constants4 } from "node:fs";
|
|
17541
|
-
import { mkdir as mkdir2, open as open4 } from "node:fs/promises";
|
|
17542
|
-
import { dirname as dirname2 } from "node:path";
|
|
17543
|
-
async function productionOutputPath(root, relativePath) {
|
|
17544
|
-
if (!relativePath.startsWith(".editorbrain-local/") || relativePath.split(/[\\/]/).some((p) => !p || p === ".." || p === ".") || /[\\\x00-\x1f\x7f]/.test(relativePath)) {
|
|
17545
|
-
throw new CliError("LOCAL_OUTPUT_REQUIRED", "Production output must be inside .editorbrain-local/.", "Use --out .editorbrain-local/<project>/inventory.json, or omit --out for JSON stdout. These reports are not synced.");
|
|
17546
|
-
}
|
|
17547
|
-
return checkedPath(root, relativePath, false);
|
|
17548
|
-
}
|
|
17549
|
-
async function prepareSampleDirectory(root, path) {
|
|
17550
|
-
const target = await productionOutputPath(root, path);
|
|
17551
|
-
await mkdir2(dirname2(target), { recursive: true });
|
|
17552
|
-
await productionOutputPath(root, path);
|
|
17553
|
-
return target;
|
|
17554
|
-
}
|
|
17555
|
-
async function readProductionJson(root, path) {
|
|
17556
|
-
const target = await checkedPath(root, path, false);
|
|
17557
|
-
const handle = await open4(target, constants4.O_RDONLY | constants4.O_NOFOLLOW | constants4.O_NONBLOCK);
|
|
17558
|
-
try {
|
|
17559
|
-
const stat = await handle.stat();
|
|
17560
|
-
if (!stat.isFile() || stat.size > 16 * 1024 * 1024) throw new CliError("INVALID_REPORT", "Expected a regular JSON file of at most 16 MiB.", "Select a local inventory, plan or check profile.");
|
|
17561
|
-
return JSON.parse(await handle.readFile("utf8"));
|
|
17562
|
-
} finally {
|
|
17563
|
-
await handle.close();
|
|
17564
|
-
}
|
|
17565
|
-
}
|
|
17566
|
-
async function saveProductionReport(root, path, report) {
|
|
17567
|
-
const target = await productionOutputPath(root, path);
|
|
17568
|
-
if (!path.endsWith(".json")) throw new CliError("INVALID_ARGUMENT", "Report output needs a .json extension.", "Use a new path under .editorbrain-local/.");
|
|
17569
|
-
await atomicWrite(root, path, stableJson(report), { document: false, overwrite: false, mode: 384 });
|
|
17570
|
-
return target;
|
|
17571
|
-
}
|
|
17572
|
-
function summarizeProductionReport(value) {
|
|
17573
|
-
const r = value;
|
|
17574
|
-
const base = { schemaVersion: r.schemaVersion, kind: r.kind, summary: true, mediaUploaded: false, quality: "not_assessed" };
|
|
17575
|
-
if (r.kind === "editorbrain.media-inventory") return {
|
|
17576
|
-
...base,
|
|
17577
|
-
complete: r.complete,
|
|
17578
|
-
contentReviewed: false,
|
|
17579
|
-
counts: { assets: r.assets.length, failures: r.failures.length, skipped: r.skipped.length },
|
|
17580
|
-
assets: r.assets.slice(0, 8),
|
|
17581
|
-
failures: r.failures.slice(0, 5),
|
|
17582
|
-
omitted: { assets: Math.max(0, r.assets.length - 8), failures: Math.max(0, r.failures.length - 5), skipped: r.skipped.length },
|
|
17583
|
-
next: "Inspect selected source content. Use media sample for specific frames. Read the saved report or rerun with --details for omitted metadata; no content was reviewed automatically."
|
|
17584
|
-
};
|
|
17585
|
-
if (r.kind === "editorbrain.media-samples") return { ...base, sourceSha256: r.source.sha256, frames: r.frames, contentReviewed: false, next: "View these frames to inspect composition. They do not prove motion or sound." };
|
|
17586
|
-
if (r.kind === "editorbrain.production-plan-check") return {
|
|
17587
|
-
...base,
|
|
17588
|
-
valid: r.valid,
|
|
17589
|
-
inventoryComplete: r.inventoryComplete,
|
|
17590
|
-
observations: r.observations,
|
|
17591
|
-
sourceFilesReverified: false,
|
|
17592
|
-
counts: { sources: r.coverage.length, findings: r.findings.length, repeatedPairs: r.repeatedPairs ?? 0 },
|
|
17593
|
-
findings: [...r.findings].sort((a, b) => ({ error: 0, warning: 1, info: 2 })[a.severity] - { error: 0, warning: 1, info: 2 }[b.severity]).slice(0, 8),
|
|
17594
|
-
omittedFindings: Math.max(0, r.findings.length - 8),
|
|
17595
|
-
next: "Review source reuse and inspection gaps in the saved report. Observations are agent-declared, not independently certified."
|
|
17596
|
-
};
|
|
17597
|
-
if (r.kind === "editorbrain.render-check") return {
|
|
17598
|
-
...base,
|
|
17599
|
-
complete: r.complete,
|
|
17600
|
-
technicalStatus: r.technicalStatus,
|
|
17601
|
-
file: r.file,
|
|
17602
|
-
checks: r.checks,
|
|
17603
|
-
findings: r.findings.slice(0, 8),
|
|
17604
|
-
findingsSummary: r.findingsSummary,
|
|
17605
|
-
omittedFindings: Math.max(0, r.findings.length - 8),
|
|
17606
|
-
next: "Reconcile technical candidates with the brief; inspect the actual video/audio. Technical checks do not assess narrative, aesthetics or SFX audibility."
|
|
17607
|
-
};
|
|
17608
|
-
if (r.kind === "editorbrain.media-source-search") return {
|
|
17609
|
-
...base,
|
|
17610
|
-
complete: r.complete,
|
|
17611
|
-
results: r.results.slice(0, 5).map((item) => ({ provider: item.provider, id: item.id.slice(0, 180), title: item.title.slice(0, 180), sourceUrl: item.sourceUrl.length <= 1500 ? item.sourceUrl : null, author: item.author?.slice(0, 120) ?? null, license: { id: item.license.id, reason: item.license.reason, url: item.license.url && item.license.url.length <= 500 ? item.license.url : null }, contentReviewed: false, rightsVerified: false })),
|
|
17612
|
-
metadataAbbreviated: true,
|
|
17613
|
-
failures: r.failures.slice(0, 5),
|
|
17614
|
-
counts: r.counts,
|
|
17615
|
-
omitted: { results: Math.max(0, r.results.length - 5), failures: Math.max(0, r.failures.length - 5) },
|
|
17616
|
-
searchExhaustive: false,
|
|
17617
|
-
rightsVerified: false,
|
|
17618
|
-
next: "Inspect relevant source pages and media. These archives supplement web image/video search; empty results do not prove absence on the internet."
|
|
17619
|
-
};
|
|
17620
|
-
if (r.kind === "editorbrain.evidence-check") return {
|
|
17621
|
-
...base,
|
|
17622
|
-
valid: r.valid,
|
|
17623
|
-
inventoryComplete: r.inventoryComplete,
|
|
17624
|
-
observations: r.observations,
|
|
17625
|
-
sourceFilesReverified: false,
|
|
17626
|
-
factualTruth: "not_assessed",
|
|
17627
|
-
reuseRights: "not_assessed",
|
|
17628
|
-
findings: r.findings.slice(0, 8),
|
|
17629
|
-
omitted: { findings: Math.max(0, r.findings.length - 8) }
|
|
17630
|
-
};
|
|
17631
|
-
if (r.kind === "editorbrain.audio-check") return {
|
|
17632
|
-
...base,
|
|
17633
|
-
complete: r.complete,
|
|
17634
|
-
technicalStatus: r.technicalStatus,
|
|
17635
|
-
file: r.file,
|
|
17636
|
-
fileStable: r.fileStable,
|
|
17637
|
-
coverage: r.coverage,
|
|
17638
|
-
measurements: r.measurements.slice(0, 8),
|
|
17639
|
-
errorCode: r.errorCode,
|
|
17640
|
-
creativeQuality: r.creativeQuality,
|
|
17641
|
-
omitted: { measurements: Math.max(0, r.measurements.length - 8) },
|
|
17642
|
-
next: "Measurements describe this exact file. Listen for intelligibility, pronunciation, SFX and sync when the modality is available."
|
|
17643
|
-
};
|
|
17644
|
-
if (r.kind === "editorbrain.motion-review") return {
|
|
17645
|
-
...base,
|
|
17646
|
-
samples: r.samples ? { frameCount: r.samples.frameCount, fps: r.samples.fps, radiusFrames: r.samples.radiusFrames, samples: r.samples.samples.slice(0, 12), omittedSamples: Math.max(0, r.samples.samples.length - 12) } : null,
|
|
17647
|
-
geometryReviews: r.geometryReviews.slice(0, 3).map((g) => ({ scope: g.scope, status: g.status, candidates: g.candidates.slice(0, 8), omittedCandidates: Math.max(0, g.candidates.length - 8), checks: g.checks })),
|
|
17648
|
-
omittedGeometryReviews: Math.max(0, r.geometryReviews.length - 3),
|
|
17649
|
-
next: "This plans samples and checks declared boxes only. Render/view those frames; read saved details for omitted candidates and intentional overlaps."
|
|
17650
|
-
};
|
|
17651
|
-
throw new CliError("INVALID_REPORT", "Unknown production report kind.", "Use media inventory/sample or production check/plan-check.");
|
|
18157
|
+
if (!exists3 && !/^No MCP server named "editorbrain"[.\s]/.test(inspect.stderr + inspect.stdout)) throw new CliError("MCP_INSPECTION_FAILED", "Could not inspect Claude MCP configuration.", "Inspect it manually; setup did not overwrite it.");
|
|
18158
|
+
}
|
|
18159
|
+
if (exists3) return { agent, status: "preserved", next: "An existing editorbrain server was preserved. Check its endpoint and authentication, then " + next, hostVerified: false };
|
|
18160
|
+
const added = await runner(agent, args, directory, true);
|
|
18161
|
+
return { agent, status: added.code === 0 ? "registered" : "action_required", next, hostVerified: false };
|
|
18162
|
+
}
|
|
18163
|
+
async function setup(options, runner = runHost) {
|
|
18164
|
+
const steps = {};
|
|
18165
|
+
let selected = await options.getConfig();
|
|
18166
|
+
let client = new Client(selected);
|
|
18167
|
+
let me;
|
|
18168
|
+
try {
|
|
18169
|
+
if (!selected.apiKey) throw new CliError("AUTH_REQUIRED", "Login required.", "Run login.");
|
|
18170
|
+
me = await client.me();
|
|
18171
|
+
} catch (error) {
|
|
18172
|
+
if (!(error instanceof CliError) || error.code !== "AUTH_REQUIRED") throw error;
|
|
18173
|
+
await options.login();
|
|
18174
|
+
selected = await options.getConfig();
|
|
18175
|
+
client = new Client(selected);
|
|
18176
|
+
me = await client.me();
|
|
18177
|
+
}
|
|
18178
|
+
if (options.workspaceId && me.workspaceId !== options.workspaceId) throw new CliError("WORKSPACE_MISMATCH", "The authorized account selected a different workspace.", "Run login for the intended workspace. No project files were changed.");
|
|
18179
|
+
steps.account = { status: "verified", workspaceId: me.workspaceId, endpoint: client.url };
|
|
18180
|
+
const root = await ensureRoot(resolve4(options.directory), true);
|
|
18181
|
+
let folderReady = false, skillReady = false;
|
|
18182
|
+
await withWorkspaceLock(root, async () => {
|
|
18183
|
+
try {
|
|
18184
|
+
steps.documents = { status: "ready", ...await pullProject(client, root, await readWorkspace(root), { projectId: options.projectId }) };
|
|
18185
|
+
folderReady = true;
|
|
18186
|
+
} catch (error) {
|
|
18187
|
+
steps.documents = { status: error instanceof CliError && error.code === "PROJECT_SELECTION_REQUIRED" ? "selection_required" : "failed", ...errorOutput(error) };
|
|
18188
|
+
}
|
|
18189
|
+
try {
|
|
18190
|
+
const result2 = await installAgentGuidance(root, options.agent);
|
|
18191
|
+
steps.skill = { status: result2.conflicts.length ? "review_required" : "ready", ...result2 };
|
|
18192
|
+
skillReady = !result2.conflicts.length;
|
|
18193
|
+
} catch (error) {
|
|
18194
|
+
steps.skill = { status: "failed", ...errorOutput(error) };
|
|
18195
|
+
}
|
|
18196
|
+
});
|
|
18197
|
+
const hosts = [];
|
|
18198
|
+
if (!options.skipMcp && skillReady) {
|
|
18199
|
+
for (const agent of options.agent === "both" ? ["codex", "claude"] : [options.agent]) {
|
|
18200
|
+
try {
|
|
18201
|
+
hosts.push(await configureMcp(agent, root, client.url, !!options.noBrowser, runner));
|
|
18202
|
+
} catch (error) {
|
|
18203
|
+
hosts.push({ agent, status: "action_required", ...errorOutput(error), hostVerified: false });
|
|
18204
|
+
}
|
|
18205
|
+
}
|
|
18206
|
+
}
|
|
18207
|
+
steps.mcp = { status: options.skipMcp ? "skipped" : "verification_required", hosts, expectedWorkspaceId: me.workspaceId, next: "Inside your agent, call workspace_info and confirm this workspace ID. Configuration alone does not verify a connection." };
|
|
18208
|
+
const diagnosis = await diagnose({ directory: root, agent: options.agent, config: selected });
|
|
18209
|
+
steps.diagnostics = diagnosis;
|
|
18210
|
+
return { schemaVersion: 1, directory: root, workspaceId: me.workspaceId, cliReady: folderReady && skillReady && diagnosis.ok, status: folderReady && skillReady && diagnosis.ok && options.skipMcp ? "ready" : "action_required", steps, renderVerified: false };
|
|
17652
18211
|
}
|
|
17653
18212
|
|
|
18213
|
+
// packages/cli/src/onboarding.ts
|
|
18214
|
+
import { createServer } from "node:http";
|
|
18215
|
+
import { randomBytes, createHash as createHash4 } from "node:crypto";
|
|
18216
|
+
import { join as join5, dirname as dirname6 } from "node:path";
|
|
18217
|
+
import { mkdir as mkdir3 } from "node:fs/promises";
|
|
18218
|
+
|
|
18219
|
+
// packages/cli/src/onboarding-brand.ts
|
|
18220
|
+
var onboardingWordmark = '<svg xmlns="http://www.w3.org/2000/svg"\n width="873" height="131" viewBox="0 0 873.000000 131.000000"\n preserveAspectRatio="xMidYMid meet">\n<!-- Traced from approved Thought Loop lettering, excluding residual background. -->\n<g transform="translate(0.000000,131.000000) scale(0.100000,-0.100000)"\nfill="#f3ede3" stroke="none">\n<path d="M2116 1290 c-41 -13 -63 -36 -82 -87 -14 -36 -15 -50 -6 -81 31 -102\n157 -139 231 -68 50 48 58 116 23 183 -25 46 -107 72 -166 53z"/>\n<path d="M7491 1282 c-69 -34 -97 -113 -68 -185 30 -71 133 -101 204 -57 94\n57 80 202 -24 246 -43 19 -71 18 -112 -4z"/>\n<path d="M4771 1268 c-37 -13 -41 -39 -41 -314 0 -502 11 -894 27 -910 10 -10\n39 -14 97 -14 87 0 116 13 116 53 0 22 12 21 43 -3 75 -60 226 -86 338 -60\n129 30 250 123 302 233 31 66 57 171 57 230 -1 171 -105 339 -257 414 -67 33\n-85 38 -169 41 -113 5 -170 -7 -246 -53 -31 -19 -60 -35 -65 -35 -4 0 -6 84\n-5 188 5 230 5 229 -105 234 -40 2 -81 0 -92 -4z m552 -578 c59 -29 83 -51\n112 -105 32 -61 35 -157 6 -213 -25 -48 -77 -97 -123 -116 -51 -22 -153 -20\n-203 3 -145 66 -185 260 -77 375 67 72 198 97 285 56z"/>\n<path d="M1699 1243 c-11 -13 -13 -56 -11 -205 2 -114 0 -188 -6 -188 -5 0\n-35 15 -67 34 -87 53 -175 67 -285 46 -210 -40 -360 -232 -360 -461 0 -256\n193 -459 436 -459 77 0 155 25 227 74 l52 34 6 -29 c11 -52 24 -59 113 -59 44\n0 86 4 92 8 33 22 36 52 29 330 -3 152 -7 412 -10 576 -3 233 -7 301 -17 307\n-7 5 -52 9 -99 9 -69 0 -89 -3 -100 -17z m-133 -569 c137 -84 153 -274 33\n-377 -121 -104 -299 -65 -377 83 -21 38 -17 151 6 197 65 124 221 170 338 97z"/>\n<path d="M2543 1168 c-4 -7 -10 -65 -13 -128 l-5 -115 -60 -3 c-32 -2 -65 -8\n-72 -14 -9 -7 -13 -38 -13 -94 0 -103 8 -114 90 -114 l58 0 4 -232 c3 -261 9\n-284 82 -366 101 -111 320 -117 422 -11 l29 30 -35 65 c-47 89 -62 99 -111 73\n-49 -24 -80 -24 -112 1 -39 31 -47 77 -47 267 l0 171 98 4 c128 4 132 7 132\n110 0 106 -3 108 -129 108 l-101 0 0 114 c0 146 0 146 -120 146 -60 0 -91 -4\n-97 -12z"/>\n<path d="M4515 941 c-49 -12 -108 -47 -144 -85 -43 -44 -51 -45 -51 -5 0 62\n-11 69 -108 69 -55 0 -95 -5 -105 -12 -16 -12 -17 -46 -15 -431 3 -489 -8\n-447 123 -447 118 0 113 -12 117 260 3 189 6 231 21 265 39 87 122 144 212\n145 75 0 78 5 83 113 5 119 -3 137 -61 136 -23 -1 -55 -4 -72 -8z"/>\n<path d="M6218 940 c-47 -14 -135 -69 -152 -95 -23 -36 -36 -31 -36 13 0 21\n-5 43 -12 50 -16 16 -180 16 -196 0 -9 -9 -12 -119 -12 -435 0 -489 -12 -443\n118 -443 116 0 112 -8 112 241 0 238 9 286 68 351 48 53 102 78 173 78 30 0\n60 5 67 12 8 8 12 48 12 113 0 65 -4 105 -12 113 -13 13 -89 15 -130 2z"/>\n<path d="M8259 940 c-54 -9 -133 -48 -172 -84 -33 -30 -47 -26 -47 13 0 15 -5\n32 -12 39 -14 14 -166 16 -188 2 -13 -8 -15 -67 -15 -428 0 -337 3 -423 14\n-436 10 -12 32 -16 97 -16 129 0 124 -11 124 273 0 129 5 249 10 269 43 154\n261 187 343 53 l27 -46 0 -253 c0 -309 -5 -296 125 -296 135 0 127 -21 123\n343 -3 290 -4 304 -26 352 -34 74 -68 118 -115 152 -75 54 -192 80 -288 63z"/>\n<path d="M350 926 c-102 -29 -192 -95 -249 -184 -97 -153 -97 -381 0 -534 89\n-139 233 -206 422 -195 76 4 99 10 163 41 72 36 174 124 174 151 0 16 -47 57\n-107 91 l-45 27 -26 -22 c-90 -72 -101 -76 -193 -76 -81 0 -90 2 -131 30 -43\n31 -88 95 -88 128 0 16 22 17 314 17 314 0 315 0 330 22 41 58 -8 274 -79 350\n-14 15 -25 30 -25 34 0 20 -128 100 -190 118 -64 19 -205 20 -270 2z m239\n-215 c44 -23 86 -72 96 -113 l7 -28 -211 0 c-171 0 -211 3 -211 14 0 17 45 84\n73 107 58 50 170 58 246 20z"/>\n<path d="M3401 924 c-244 -65 -390 -320 -326 -567 88 -338 502 -461 774 -230\n98 84 151 205 151 348 0 50 -7 111 -15 138 -46 153 -170 271 -328 313 -66 18\n-187 17 -256 -2z m239 -242 c62 -32 115 -100 125 -163 33 -194 -166 -341 -343\n-253 -51 26 -79 55 -115 123 -22 43 -16 134 13 190 60 117 205 164 320 103z"/>\n<path d="M6723 930 c-47 -10 -146 -55 -175 -79 -11 -9 -28 -23 -37 -31 -10 -8\n-38 -45 -63 -81 -61 -90 -82 -168 -76 -296 3 -88 7 -106 41 -176 77 -164 225\n-257 407 -257 64 0 167 33 218 70 51 37 62 37 62 4 0 -42 25 -53 117 -54 73 0\n83 2 97 22 14 19 16 76 16 418 0 298 -3 401 -13 420 -12 23 -18 25 -101 28\n-95 3 -116 -5 -116 -43 0 -35 -11 -37 -48 -9 -75 58 -223 87 -329 64z m243\n-256 c73 -45 107 -100 112 -180 6 -85 -16 -141 -73 -192 -109 -95 -276 -79\n-358 36 -27 37 -32 54 -35 116 -3 48 1 84 10 106 57 136 221 190 344 114z"/>\n<path d="M2065 896 c-14 -20 -15 -76 -13 -431 3 -476 -8 -435 125 -435 123 0\n115 -32 112 447 -1 331 -4 402 -17 420 -13 21 -22 23 -103 23 -82 0 -90 -2\n-104 -24z"/>\n<path d="M7459 903 c-11 -13 -14 -94 -14 -430 0 -483 -10 -443 115 -443 119 0\n110 -35 110 447 0 482 10 443 -117 443 -63 0 -84 -4 -94 -17z"/>\n</g>\n</svg>\n';
|
|
18221
|
+
|
|
17654
18222
|
// packages/cli/src/onboarding-artifact.ts
|
|
17655
18223
|
function safeJson(value) {
|
|
17656
18224
|
return JSON.stringify(value).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
@@ -17669,12 +18237,11 @@ function renderOnboardingArtifact({ intake, submission }) {
|
|
|
17669
18237
|
return false;
|
|
17670
18238
|
}
|
|
17671
18239
|
}).map((example) => ({ ratio: example.ratio, url: example.url })) }));
|
|
17672
|
-
const data = { intake: validateOnboardingIntake(intake), submission: submission || null, options: profileOptions, styles };
|
|
18240
|
+
const data = { intake: validateOnboardingIntake(intake), submission: submission || null, options: profileOptions, styles, steps: localOnboardingSteps };
|
|
17673
18241
|
return `<!doctype html>
|
|
17674
18242
|
<html lang="es"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="referrer" content="no-referrer"><title>Tu cerebro editorial \xB7 EditorBrain</title><style>${artifactCss}</style></head>
|
|
17675
|
-
<body><div class="app"><header class="top"><a href="#main" class="product-label" aria-label="EditorBrain, ir a tus preferencias"
|
|
17676
|
-
<div class="layout"><
|
|
17677
|
-
<main id="main" tabindex="-1"><div class="progress" aria-hidden="true"><i id="progress-fill"></i></div><div class="page-top"><span id="step-count" class="eyebrow"></span><button type="button" id="download-top" class="text-button">Descargar respuestas</button></div><h2 id="page-title" tabindex="-1"></h2><p id="page-description" class="intro"></p><form id="preferences" novalidate><div id="panel"></div></form><div id="error" class="notice error" role="alert" hidden></div><div id="status" class="notice" role="status" hidden></div><footer class="actions"><button type="button" id="previous" class="button secondary">\u2190 Atr\xE1s</button><span id="save-hint">Puedes cambiar tus preferencias despu\xE9s.</span><button type="button" id="next" class="button primary">Continuar \u2192</button></footer></main></div><footer class="bottom">EditorBrain \xB7 Tu criterio, en cada edici\xF3n.<span>Las elecciones pendientes quedan a criterio de tu agente.</span></footer></div>
|
|
18243
|
+
<body data-theme="paper"><div class="app"><header class="top"><a href="#main" class="product-label" aria-label="EditorBrain, ir a tus preferencias">${onboardingWordmark}</a><span class="local-badge">Tu estudio \xB7 En tu computadora</span></header>
|
|
18244
|
+
<nav id="steps" aria-label="Pasos de tus preferencias"></nav><div class="layout"><main id="main" tabindex="-1"><div class="page-top"><span id="step-count" class="eyebrow"></span><button type="button" id="download-top" class="text-button">Descargar respuestas</button></div><h1 id="page-title" tabindex="-1"></h1><p id="page-description" class="intro"></p><form id="preferences" novalidate><div id="panel"></div></form><div id="error" class="notice error" role="alert" hidden></div><div id="status" class="notice" role="status" hidden></div><footer class="actions"><button type="button" id="previous" class="button secondary">\u2190 Atr\xE1s</button><span id="save-hint">Puedes cambiar tus preferencias despu\xE9s.</span><button type="button" id="next" class="button primary">Continuar \u2192</button></footer></main></div><footer class="bottom">EditorBrain \xB7 Tu criterio, en cada edici\xF3n.<span>Preferencias locales. No conecta servicios ni activa compras.</span></footer></div>
|
|
17678
18245
|
<dialog id="style-dialog" aria-labelledby="dialog-title"><div class="dialog-head"><div><span class="eyebrow">REFERENCIA DE APARIENCIA</span><h2 id="dialog-title"></h2></div><button type="button" id="close-dialog" class="button secondary">Cerrar</button></div><p id="dialog-description"></p><div id="dialog-examples"></div><p class="small">Ejemplos generados con IA. Orientan el tratamiento visual; no sustituyen tu marca ni acreditan hechos reales.</p></dialog>
|
|
17679
18246
|
<script id="onboarding-data" type="application/json">${safeJson(data)}</script><script>${artifactScript}</script></body></html>`;
|
|
17680
18247
|
}
|
|
@@ -17693,22 +18260,13 @@ h1,h2,h3,p{margin-top:0}
|
|
|
17693
18260
|
.top{height:88px;border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;gap:18px}
|
|
17694
18261
|
.product-label{font-size:17px;font-weight:600;letter-spacing:0;text-decoration:none;color:var(--accent);display:flex;align-items:center;gap:7px}
|
|
17695
18262
|
.local-badge{padding:6px 11px;border:1px solid #d9c7b8;border-radius:30px;color:#7c645c;font-size:11px}
|
|
17696
|
-
.layout{
|
|
17697
|
-
.
|
|
17698
|
-
|
|
17699
|
-
|
|
17700
|
-
.
|
|
17701
|
-
.
|
|
17702
|
-
#steps{
|
|
17703
|
-
#steps button{display:flex;align-items:center;gap:12px;text-align:left;border:1px solid transparent;background:transparent;border-radius:10px;padding:11px 13px;font-size:13px;color:var(--muted)}
|
|
17704
|
-
#steps button>span:first-child{border:1px solid #cfbcae;border-radius:50%;width:27px;height:27px;display:grid;place-items:center;font-size:11px;flex-shrink:0}
|
|
17705
|
-
#steps button[aria-current=step]{color:var(--accent);background:#fff9f1;border-color:#d9c4b5;box-shadow:0 8px 24px -18px #54293675}
|
|
17706
|
-
#steps button[aria-current=step]>span:first-child{background:var(--accent);color:#fff;border-color:var(--accent)}
|
|
17707
|
-
.privacy{border-top:1px solid var(--line);margin-top:32px;padding-top:22px;font-size:11px;color:var(--muted)}
|
|
17708
|
-
.privacy strong{color:#5f4a42;font-size:12px}
|
|
17709
|
-
.privacy p{margin-top:8px}
|
|
17710
|
-
.progress{height:3px;background:#e2d3c3;border-radius:3px;overflow:hidden;margin-bottom:23px}
|
|
17711
|
-
.progress i{display:block;height:100%;background:var(--accent);transition:width .25s}
|
|
18263
|
+
.layout{max-width:1120px;margin:auto;padding:44px 0 56px}
|
|
18264
|
+
.eyebrow{font-size:11px;letter-spacing:.13em;font-weight:650;text-transform:uppercase}
|
|
18265
|
+
#steps{display:flex;gap:8px;max-width:680px;margin:30px auto 0;padding:10px 18px;border:1px solid #85696135;border-radius:99px;background:#ffffff0a}
|
|
18266
|
+
#steps button{flex:1;min-width:0;border:0;background:transparent;padding:7px 0;color:inherit;font-size:10px}
|
|
18267
|
+
.step-bar{display:block;height:8px;border:1px solid #a78f8055;border-radius:3px;margin-bottom:7px}
|
|
18268
|
+
#steps [aria-current=step] .step-bar{background:#e89a4a;box-shadow:0 0 14px #e89a4a35;border-color:#e89a4a}
|
|
18269
|
+
#steps [data-done=true] .step-bar{background:#e89a4a65}
|
|
17712
18270
|
.page-top{display:flex;align-items:center;justify-content:space-between;gap:14px}
|
|
17713
18271
|
h2{font:400 clamp(30px,3.7vw,45px)/1.12 Georgia,serif;letter-spacing:-1.4px;margin:20px 0 14px}
|
|
17714
18272
|
h2:focus,#main:focus{outline:none}
|
|
@@ -17849,11 +18407,6 @@ dialog::backdrop{background:#291922bd;backdrop-filter:blur(5px)}
|
|
|
17849
18407
|
@media(max-width:780px){.app{padding:0 20px}
|
|
17850
18408
|
.top{height:70px}
|
|
17851
18409
|
.layout{display:block;padding-top:24px}
|
|
17852
|
-
.sidebar{position:static;margin-bottom:26px}
|
|
17853
|
-
.sidebar>.eyebrow,.sidebar h1,.sidebar>p,.privacy{display:none}
|
|
17854
|
-
#steps{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px;margin-top:0}
|
|
17855
|
-
#steps button{padding:8px;font-size:10px;gap:7px}
|
|
17856
|
-
#steps button>span:first-child{height:23px;width:23px;font-size:10px}
|
|
17857
18410
|
.page-top{align-items:flex-start}
|
|
17858
18411
|
h2{font-size:34px}
|
|
17859
18412
|
.section-head{flex-wrap:wrap}
|
|
@@ -17886,6 +18439,30 @@ dialog{padding:20px}
|
|
|
17886
18439
|
#dialog-examples{grid-template-columns:1fr}
|
|
17887
18440
|
.dialog-head h2{font-size:25px}
|
|
17888
18441
|
}
|
|
18442
|
+
|
|
18443
|
+
body{background:radial-gradient(ellipse at 85% 10%,#edb88420,transparent 55%),#f3ede3;transition:background-color .35s}
|
|
18444
|
+
body[data-theme=wine]{background-color:#351d29;color:#f3ede3}
|
|
18445
|
+
body[data-theme=porcelain]{background-color:#162a24;color:#f3ede3}
|
|
18446
|
+
body:not([data-theme=paper]) .intro,body:not([data-theme=paper]) .bottom,body:not([data-theme=paper]) .actions>span{color:#d4c4b5}
|
|
18447
|
+
body:not([data-theme=paper]) .eyebrow,body:not([data-theme=paper]) .text-button{color:#efb678}
|
|
18448
|
+
body:not([data-theme=paper]) .top,body:not([data-theme=paper]) .bottom,body:not([data-theme=paper]) .actions{border-color:#f3ede32b}
|
|
18449
|
+
body:not([data-theme=paper]) .local-badge{border-color:#f3ede32b;color:#f3ede3}
|
|
18450
|
+
.product-label svg{width:174px;height:auto}
|
|
18451
|
+
body[data-theme=paper] .product-label svg{filter:brightness(0)}
|
|
18452
|
+
#page-title{font:400 clamp(40px,5vw,70px)/1.04 Georgia,serif;letter-spacing:-.045em;max-width:850px;margin:22px 0;outline:none}
|
|
18453
|
+
.intro{font-size:16px;line-height:1.7;max-width:730px;margin-bottom:32px}
|
|
18454
|
+
#preferences{padding:clamp(20px,3vw,36px);border:1px solid #ffffffa6;border-radius:24px;background:#fbf7f0f5;color:var(--ink);box-shadow:0 24px 70px #100a0614,inset 0 1px 0 #ffffffcc}
|
|
18455
|
+
#panel{animation:studio-enter .45s ease-out both}
|
|
18456
|
+
#panel .section:last-child{margin-bottom:0}
|
|
18457
|
+
#panel .text-button{color:var(--accent)}
|
|
18458
|
+
.choice{background:#fffdf6b8;box-shadow:inset 0 1px 0 #ffffffcc;transition:transform .25s,border-color .25s,box-shadow .25s}
|
|
18459
|
+
.choice:hover{transform:translateY(-2px);box-shadow:0 8px 24px #682e3b10}
|
|
18460
|
+
.actions .button{border-radius:99px;padding:14px 24px}
|
|
18461
|
+
.actions .button.primary{background:#e89a4a;color:#211914;border-color:#e89a4a;font-weight:650}
|
|
18462
|
+
body:not([data-theme=paper]) .actions .secondary{color:#f3ede3;border-color:#f3ede32b}
|
|
18463
|
+
@keyframes studio-enter{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
|
|
18464
|
+
@media(max-width:600px){.layout{padding-top:30px}.step-label{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}#steps{gap:6px;margin-top:20px}.step-bar{margin-bottom:0}.top{height:72px}.product-label svg{width:142px}.local-badge{font-size:9px}#page-title{font-size:43px}#preferences{padding:18px;border-radius:18px}.intro{font-size:14px}}
|
|
18465
|
+
@media(prefers-reduced-transparency:reduce){#preferences,.choice{background:#fbf7f0}}
|
|
17889
18466
|
@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation:none!important;transition:none!important;scroll-behavior:auto!important}
|
|
17890
18467
|
}
|
|
17891
18468
|
|
|
@@ -17895,9 +18472,10 @@ var artifactScript = String.raw`
|
|
|
17895
18472
|
'use strict';
|
|
17896
18473
|
const data = JSON.parse(document.getElementById('onboarding-data').textContent), intake = structuredClone(data.intake), options = data.options, catalog = data.styles;
|
|
17897
18474
|
const $ = id => document.getElementById(id), panel = $('panel'), error = $('error'), status = $('status');
|
|
18475
|
+
let sfxExpanded = false;
|
|
17898
18476
|
let step = 0, mediaTab = 'image', showImages = false, busy = false, observer, nameEdited = false;
|
|
17899
18477
|
const editedTokens = new Set();
|
|
17900
|
-
const steps =
|
|
18478
|
+
const steps = data.steps;
|
|
17901
18479
|
const p = () => intake.profile || {}, delivery = () => p().editorialDelivery && typeof p().editorialDelivery === 'object' ? p().editorialDelivery : {}, audio = () => intake.audio || {};
|
|
17902
18480
|
const node = (tag, cls, text) => { const el = document.createElement(tag); if (cls)
|
|
17903
18481
|
el.className = cls; if (text !== undefined)
|
|
@@ -18155,7 +18733,7 @@ var artifactScript = String.raw`
|
|
|
18155
18733
|
panel.append(music);
|
|
18156
18734
|
const sfx = section('Efectos de sonido', 'Clics, acentos y transiciones cuando ayudan.', decide(!current.sfx?.density, () => setAudio('sfx', '')));
|
|
18157
18735
|
const blocked = (p().avoid || []).includes('Sin efectos de sonido');
|
|
18158
|
-
choices(sfx, [['none', 'Sin efectos', 'Nada añadido.'], ['sparse', '
|
|
18736
|
+
choices(sfx, [['none', 'Sin efectos', 'Nada añadido.'], ['sparse', 'Sutiles', 'Pocos acentos en momentos clave.'], ['moderate', 'Equilibrados', 'Acompañan las acciones importantes.'], ['dense', 'Expresivos', 'Más variedad sin tapar la voz.']], blocked ? 'none' : current.sfx?.density, v => setAudio('sfx', v), { columns: 2 });
|
|
18159
18737
|
if (blocked) {
|
|
18160
18738
|
sfx.querySelectorAll('button').forEach(b => { if (b.dataset.value !== 'none') {
|
|
18161
18739
|
b.disabled = true;
|
|
@@ -18163,6 +18741,13 @@ var artifactScript = String.raw`
|
|
|
18163
18741
|
} });
|
|
18164
18742
|
sfx.append(node('p', 'notice', 'Tienes activo «Sin efectos de sonido» en tus límites. Desactívalo antes de elegir otra densidad.'));
|
|
18165
18743
|
}
|
|
18744
|
+
const detail = node('details', ''); detail.append(node('summary', '', 'Personalizar el carácter y la apertura'));
|
|
18745
|
+
detail.open = sfxExpanded; detail.addEventListener('toggle', () => { if (detail.isConnected) sfxExpanded = detail.open; });
|
|
18746
|
+
const fields = node('fieldset', 'fields'); fields.disabled = blocked || current.sfx?.density === 'none';
|
|
18747
|
+
const setSfx = (key, value) => { intake.audio = { ...effectiveAudio(), sfx: { ...effectiveAudio().sfx, [key]: value } }; changed(); };
|
|
18748
|
+
select(fields, 'Carácter del sonido', current.sfx?.character, [['organic', 'Natural: papel y objetos'], ['digital', 'Digital: clics e interfaces'], ['cinematic', 'Cinematográfico'], ['mixed', 'Una combinación']], v => setSfx('character', v || null));
|
|
18749
|
+
select(fields, 'Apertura sin efectos añadidos', current.sfx?.hookWithoutEffects === true ? 'yes' : current.sfx?.hookWithoutEffects === false ? 'no' : '', [['yes', 'Sí, dejar el hook limpio'], ['no', 'Puede usar efectos si aportan']], v => setSfx('hookWithoutEffects', v ? v === 'yes' : null));
|
|
18750
|
+
fields.append(node('p', 'small', 'La cantidad no es volumen. Conserva voz y ambiente; revisa los acentos en el video final.')); detail.append(fields); sfx.append(detail);
|
|
18166
18751
|
panel.append(sfx, node('p', 'small', 'Seleccionar una voz en off no clona voces ni autoriza gastos. El agente verificará los archivos, las integraciones y los permisos disponibles.'));
|
|
18167
18752
|
}
|
|
18168
18753
|
function reviewStep() {
|
|
@@ -18177,7 +18762,7 @@ var artifactScript = String.raw`
|
|
|
18177
18762
|
['Marca', Object.entries(intake.tokens || {}).filter(([, v]) => v).map(([key, rule]) => key.replace('color.', '').replace('typography.', '') + ': ' + rule.value + ' · ' + ({ required: 'obligatorio', preferred: 'preferencia', example: 'ejemplo' }[rule.strength])).join('\n'), 1],
|
|
18178
18763
|
['Gustos visuales', selections.join('\n'), 2],
|
|
18179
18764
|
['Edición y recursos', [p().pace, delivery().visualSupport && 'Apoyo visual: ' + ({ sparse: 'poco', balanced: 'equilibrado', rich: 'abundante' }[delivery().visualSupport]), p().captions, sources?.allowed?.map(v => sourceNames[v]).join(' · '), sources?.preferred && 'Empezar por: ' + sourceNames[sources.preferred], delivery().ctaGoal && 'Cierre: ' + ({ adaptive: 'según el video', follow: 'seguir mi cuenta', comment: 'comentar', visit: 'visitar un enlace', contact: 'contactarme', none: 'sin llamada a la acción' }[delivery().ctaGoal]), !sources && p().assets, (p().avoid || []).join(' · ')].filter(Boolean).join('\n'), 3],
|
|
18180
|
-
['Narración y sonido', [delivery().narration && 'Narración: ' + ({ original: 'tu voz grabada', voiceover: 'voz en off', none: 'sin narración' }[delivery().narration]), effectiveAudio().music?.intensity && 'Música: ' + ({ none: 'sin música', subtle: 'de fondo', present: 'con presencia' }[effectiveAudio().music.intensity]), (p().avoid || []).includes('Sin efectos de sonido') ? 'Efectos: desactivados por tus límites' : effectiveAudio().sfx?.density && 'Efectos: ' + ({ none: 'sin efectos', sparse: '
|
|
18765
|
+
['Narración y sonido', [delivery().narration && 'Narración: ' + ({ original: 'tu voz grabada', voiceover: 'voz en off', none: 'sin narración' }[delivery().narration]), effectiveAudio().music?.intensity && 'Música: ' + ({ none: 'sin música', subtle: 'de fondo', present: 'con presencia' }[effectiveAudio().music.intensity]), (p().avoid || []).includes('Sin efectos de sonido') ? 'Efectos: desactivados por tus límites' : effectiveAudio().sfx?.density && 'Efectos: ' + ({ none: 'sin efectos', sparse: 'sutiles', moderate: 'equilibrados', dense: 'expresivos' }[effectiveAudio().sfx.density]), effectiveAudio().sfx?.character && 'Carácter: ' + ({organic:'natural',digital:'digital',cinematic:'cinematográfico',mixed:'combinado'}[effectiveAudio().sfx.character]), effectiveAudio().sfx?.hookWithoutEffects === true && 'Apertura sin efectos añadidos'].filter(Boolean).join('\n'), 4],
|
|
18181
18766
|
];
|
|
18182
18767
|
rows.forEach(([title, text, target]) => { const item = node('section', 'review-item'); append(item, node('h3', '', title), node('p', '', text || 'Pendiente: tu agente lo decidirá para cada proyecto.'), button('Cambiar ' + title.toLowerCase(), () => navigate(target), 'text-button')); list.append(item); });
|
|
18183
18768
|
panel.append(list);
|
|
@@ -18195,13 +18780,13 @@ var artifactScript = String.raw`
|
|
|
18195
18780
|
$('page-title').textContent = steps[step][1];
|
|
18196
18781
|
$('page-description').textContent = steps[step][2];
|
|
18197
18782
|
$('step-count').textContent = 'Paso ' + (step + 1) + ' de ' + steps.length;
|
|
18198
|
-
|
|
18783
|
+
document.body.dataset.theme = ['paper','paper','wine','wine','wine','porcelain'][step];
|
|
18199
18784
|
$('previous').hidden = step === 0;
|
|
18200
18785
|
$('next').hidden = step === steps.length - 1;
|
|
18201
18786
|
$('next').textContent = step === steps.length - 2 ? 'Revisar mis preferencias →' : 'Continuar →';
|
|
18202
18787
|
const nav = $('steps');
|
|
18203
18788
|
nav.replaceChildren();
|
|
18204
|
-
steps.forEach(([title], index) => { const b = button('', () => navigate(index)); append(b, node('span', '',
|
|
18789
|
+
steps.forEach(([title], index) => { const b = button('', () => navigate(index)); append(b, node('span', 'step-bar'), node('span', 'step-label', title)); b.setAttribute('aria-label', 'Paso ' + (index + 1) + ': ' + title); b.dataset.done = String(index < step); if (index === step)
|
|
18205
18790
|
b.setAttribute('aria-current', 'step'); nav.append(b); });
|
|
18206
18791
|
[contentStep, brandStep, styleStep, editingStep, soundStep, reviewStep][step]();
|
|
18207
18792
|
updateBrandPreview();
|
|
@@ -18341,7 +18926,7 @@ async function syncOnboardingDocuments(client, root, files, options) {
|
|
|
18341
18926
|
// packages/cli/src/onboarding.ts
|
|
18342
18927
|
var failure = (code, message, hint = "Reopen onboarding against the current files. Nothing is overwritten silently.") => new CliError(code, message, hint);
|
|
18343
18928
|
function starterOutputId(brandId2) {
|
|
18344
|
-
return `onboarding-${brandId2.slice(0, 48)}-${
|
|
18929
|
+
return `onboarding-${brandId2.slice(0, 48)}-${createHash4("sha256").update(brandId2).digest("hex").slice(0, 12)}`;
|
|
18345
18930
|
}
|
|
18346
18931
|
function scaffold(brandId2, projectId) {
|
|
18347
18932
|
const files = Object.fromEntries(Object.entries(createWorkspace({ brand: brandId2, project: projectId })).filter(([p]) => !p.startsWith(`.editorbrain/brands/${brandId2}/`)));
|
|
@@ -18397,14 +18982,14 @@ async function createOnboarding(options) {
|
|
|
18397
18982
|
const directory = options.out ?? `.editorbrain-local/onboarding/${brandId2}-${Date.now()}-${randomBytes(3).toString("hex")}`;
|
|
18398
18983
|
await productionOutputPath(root, `${directory}/session.json`);
|
|
18399
18984
|
const target = await checkedPath(root, directory, false);
|
|
18400
|
-
await mkdir3(
|
|
18985
|
+
await mkdir3(dirname6(target), { recursive: true });
|
|
18401
18986
|
await mkdir3(target);
|
|
18402
18987
|
const saved = await snapshots(root, brandId2, projectId), files = Object.fromEntries(Object.entries(saved).flatMap(([p, s]) => s ? [[p, s.content]] : []));
|
|
18403
18988
|
const intake = onboardingIntakeFromFiles(brandId2, files);
|
|
18404
18989
|
const session = { schemaVersion: 1, kind: "editorbrain.onboarding", brandId: brandId2, projectId, intake, snapshots: saved };
|
|
18405
18990
|
await atomicWrite(root, `${directory}/session.json`, stableJson(session), { document: false, overwrite: false, mode: 384 });
|
|
18406
18991
|
await atomicWrite(root, `${directory}/index.html`, renderOnboardingArtifact({ intake }), { document: false, overwrite: false, mode: 384 });
|
|
18407
|
-
return { schemaVersion: 1, brandId: brandId2, projectId, artifact:
|
|
18992
|
+
return { schemaVersion: 1, brandId: brandId2, projectId, artifact: join5(root, directory, "index.html"), session: `${directory}/session.json`, answers: `${directory}/answers.json`, savedToBrain: false, next: "Show the HTML artifact, or run onboarding serve --session <session>. Static artifacts download answers: put them in this workspace and run onboarding apply --session <session> --file <answers>. Never fill choices on the user\u2019s behalf." };
|
|
18408
18993
|
}
|
|
18409
18994
|
async function applyOnboarding(options) {
|
|
18410
18995
|
const root = await ensureRoot(options.directory);
|
|
@@ -18432,7 +19017,7 @@ async function applyOnboarding(options) {
|
|
|
18432
19017
|
if (now !== current2[item.path]?.content) throw failure("ONBOARDING_CONFLICT", `Document changed while applying: ${item.path}`);
|
|
18433
19018
|
await atomicWrite(root, item.path, item.content, { overwrite: now !== void 0 });
|
|
18434
19019
|
}
|
|
18435
|
-
await atomicWrite(root, `${
|
|
19020
|
+
await atomicWrite(root, `${dirname6(options.session)}/applied.json`, stableJson({ schemaVersion: 1, brandId: session.brandId, projectId: session.projectId, appliedAt: (/* @__PURE__ */ new Date()).toISOString(), paths: Object.keys(merged) }), { document: false, mode: 384 });
|
|
18436
19021
|
}
|
|
18437
19022
|
return { schemaVersion: 1, brandId: session.brandId, projectId: session.projectId, dryRun: !!options.dryRun, changed: pending.map((w) => w.path), documents: Object.keys(merged).length, brainGenerated: !options.dryRun, aiAnalyzed: false, cloudSynced: false, next: "Local documents are ready after apply. Install the skill if needed, authenticate, use onboarding sync for this session, then brain prepare. Review a proposal before brain accept. A brain is editorial context, not a rendered video." };
|
|
18438
19023
|
}, { migrateLegacy: !options.dryRun });
|
|
@@ -18441,7 +19026,7 @@ async function syncOnboarding(client, options) {
|
|
|
18441
19026
|
const root = await ensureRoot(options.directory);
|
|
18442
19027
|
return withWorkspaceLock(root, async () => {
|
|
18443
19028
|
const session = await readSession(root, options.session);
|
|
18444
|
-
const receipt = await readProductionJson(root, `${
|
|
19029
|
+
const receipt = await readProductionJson(root, `${dirname6(options.session)}/applied.json`);
|
|
18445
19030
|
if (receipt.brandId !== session.brandId || receipt.projectId !== session.projectId) throw failure("ONBOARDING_NOT_APPLIED", "Apply the reviewed choices locally before syncing.");
|
|
18446
19031
|
const files = {};
|
|
18447
19032
|
for (const path of paths(session.brandId, session.projectId)) {
|
|
@@ -18457,7 +19042,7 @@ async function syncOnboarding(client, options) {
|
|
|
18457
19042
|
async function serveOnboarding(options) {
|
|
18458
19043
|
const root = await ensureRoot(options.directory), session = await readSession(root, options.session), seconds = options.timeoutSeconds ?? 1800;
|
|
18459
19044
|
if (!Number.isInteger(seconds) || seconds < 1 || seconds > 3600) throw failure("INVALID_ARGUMENT", "Timeout must be between 1 and 3600 seconds.");
|
|
18460
|
-
const token = randomBytes(32).toString("hex"), csrf = randomBytes(32).toString("hex"), prefix = `/${token}`, answers = `${
|
|
19045
|
+
const token = randomBytes(32).toString("hex"), csrf = randomBytes(32).toString("hex"), prefix = `/${token}`, answers = `${dirname6(options.session)}/answers.json`;
|
|
18461
19046
|
let origin = "", consumed = false, busy = false, server;
|
|
18462
19047
|
let complete, reject;
|
|
18463
19048
|
const done = new Promise((yes, no) => {
|
|
@@ -18497,7 +19082,7 @@ async function serveOnboarding(options) {
|
|
|
18497
19082
|
await atomicWrite(root, answers, stableJson(intake), { document: false, overwrite: false, mode: 384 });
|
|
18498
19083
|
consumed = true;
|
|
18499
19084
|
send(200, { saved: true, path: answers });
|
|
18500
|
-
complete({ schemaVersion: 1, saved: true, answers:
|
|
19085
|
+
complete({ schemaVersion: 1, saved: true, answers: join5(root, answers), session: options.session, applied: false, next: "Review saved choices and run onboarding apply with this session and answers file." });
|
|
18501
19086
|
} catch (e) {
|
|
18502
19087
|
busy = false;
|
|
18503
19088
|
send(400, { error: e.message.slice(0, 300) });
|
|
@@ -18517,7 +19102,7 @@ async function serveOnboarding(options) {
|
|
|
18517
19102
|
process.once("SIGINT", stop);
|
|
18518
19103
|
process.once("SIGTERM", stop);
|
|
18519
19104
|
try {
|
|
18520
|
-
options.onReady?.({ url: `${origin}${prefix}/`, answers:
|
|
19105
|
+
options.onReady?.({ url: `${origin}${prefix}/`, answers: join5(root, answers) });
|
|
18521
19106
|
return await done;
|
|
18522
19107
|
} finally {
|
|
18523
19108
|
clearTimeout(timer);
|
|
@@ -18529,11 +19114,11 @@ async function serveOnboarding(options) {
|
|
|
18529
19114
|
}
|
|
18530
19115
|
|
|
18531
19116
|
// packages/cli/src/brain.ts
|
|
18532
|
-
var
|
|
19117
|
+
var object6 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
18533
19118
|
var integer = (value, min = 0) => Number.isSafeInteger(value) && Number(value) >= min;
|
|
18534
|
-
var
|
|
18535
|
-
var
|
|
18536
|
-
var date = (value) =>
|
|
19119
|
+
var text7 = (value, max) => typeof value === "string" && !!value.trim() && value.length <= max && !/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(value);
|
|
19120
|
+
var hash4 = (value) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
19121
|
+
var date = (value) => text7(value, 64) && Number.isFinite(Date.parse(value));
|
|
18537
19122
|
function invalidInput(message) {
|
|
18538
19123
|
return new CliError("INVALID_ARGUMENT", message, "Use brain status --brand ID; read the current design.md revision before preparing. Acceptance requires the reviewed run and its source revision.");
|
|
18539
19124
|
}
|
|
@@ -18546,10 +19131,10 @@ function brainRevision(value) {
|
|
|
18546
19131
|
return Number(value);
|
|
18547
19132
|
}
|
|
18548
19133
|
function validateInput(input, accept = false) {
|
|
18549
|
-
if (!
|
|
19134
|
+
if (!object6(input) || Object.keys(input).some((key) => !(accept ? ["brandId", "expectedDesignRevision", "runId", "reviewed"] : ["brandId", "expectedDesignRevision"]).includes(key))) throw invalidInput("Only the brand, document revision and explicit reviewed run are accepted.");
|
|
18550
19135
|
brandId(input.brandId);
|
|
18551
19136
|
if (!integer(input.expectedDesignRevision, 1)) throw invalidInput("A positive source design revision is required.");
|
|
18552
|
-
if (accept && (!
|
|
19137
|
+
if (accept && (!hash4(input.runId) || input.reviewed !== true)) throw invalidInput("Accept only a specific proposal already reviewed and approved by the user; --run and --reviewed are required.");
|
|
18553
19138
|
}
|
|
18554
19139
|
function invalidResponse(write) {
|
|
18555
19140
|
return new CliError(
|
|
@@ -18561,31 +19146,31 @@ function invalidResponse(write) {
|
|
|
18561
19146
|
);
|
|
18562
19147
|
}
|
|
18563
19148
|
function brainState(value, brand, write = false) {
|
|
18564
|
-
const
|
|
19149
|
+
const invalid5 = () => {
|
|
18565
19150
|
throw invalidResponse(write);
|
|
18566
19151
|
};
|
|
18567
|
-
if (!
|
|
19152
|
+
if (!object6(value) || value.brandId !== brand || typeof value.available !== "boolean" || typeof value.configured !== "boolean" || !text7(value.notice, 2e3) || value.contextVersion !== void 0 && !hash4(value.contextVersion) || value.contextChanged !== void 0 && typeof value.contextChanged !== "boolean") return invalid5();
|
|
18568
19153
|
let run = null;
|
|
18569
19154
|
if (value.run !== null) {
|
|
18570
19155
|
const row = value.run;
|
|
18571
|
-
if (!
|
|
18572
|
-
if (row.status === "accepted" ? row.acceptedDesignRevision !== row.sourceDesignRevision + 1 : row.acceptedDesignRevision !== null) return
|
|
19156
|
+
if (!object6(row) || !hash4(row.id) || !["queued", "running", "ready", "accepted", "failed"].includes(String(row.status)) || !integer(row.sourceDesignRevision, 1) || !(row.acceptedDesignRevision === null || integer(row.acceptedDesignRevision, 1)) || !(row.errorCode === null || typeof row.errorCode === "string" && /^[a-z][a-z0-9_]{0,119}$/.test(row.errorCode)) || !date(row.createdAt) || !date(row.updatedAt)) return invalid5();
|
|
19157
|
+
if (row.status === "accepted" ? row.acceptedDesignRevision !== row.sourceDesignRevision + 1 : row.acceptedDesignRevision !== null) return invalid5();
|
|
18573
19158
|
let proposal = null;
|
|
18574
19159
|
if (row.proposal !== null) {
|
|
18575
19160
|
const proposed = row.proposal;
|
|
18576
|
-
if (!
|
|
19161
|
+
if (!object6(proposed) || !text7(proposed.summary, 700) || !Array.isArray(proposed.recommendations) || proposed.recommendations.length < 1 || proposed.recommendations.length > 5) return invalid5();
|
|
18577
19162
|
const recommendations = [];
|
|
18578
19163
|
for (const item of proposed.recommendations) {
|
|
18579
|
-
if (!
|
|
19164
|
+
if (!object6(item) || !["images", "motion", "story", "audio"].includes(String(item.area)) || !text7(item.text, 500) || !text7(item.reason, 500) || !Array.isArray(item.sourceFields) || item.sourceFields.length < 1 || item.sourceFields.length > 5 || new Set(item.sourceFields).size !== item.sourceFields.length || item.sourceFields.some((field) => !text7(field, 400))) return invalid5();
|
|
18580
19165
|
recommendations.push({ area: item.area, text: item.text, reason: item.reason, sourceFields: [...item.sourceFields] });
|
|
18581
19166
|
}
|
|
18582
19167
|
proposal = { summary: proposed.summary, recommendations };
|
|
18583
19168
|
}
|
|
18584
|
-
if ((row.status === "ready" || row.status === "accepted") && !proposal) return
|
|
19169
|
+
if ((row.status === "ready" || row.status === "accepted") && !proposal) return invalid5();
|
|
18585
19170
|
let usage = null;
|
|
18586
19171
|
if (row.usage !== null) {
|
|
18587
19172
|
const measured = row.usage;
|
|
18588
|
-
if (!
|
|
19173
|
+
if (!object6(measured) || !["inputTokens", "outputTokens", "totalTokens"].every((field) => measured[field] === null || integer(measured[field])) || typeof measured.durationMs !== "number" || !Number.isFinite(measured.durationMs) || measured.durationMs < 0) return invalid5();
|
|
18589
19174
|
usage = { inputTokens: measured.inputTokens, outputTokens: measured.outputTokens, totalTokens: measured.totalTokens, durationMs: measured.durationMs };
|
|
18590
19175
|
}
|
|
18591
19176
|
run = { id: row.id, status: row.status, proposal, usage, errorCode: row.errorCode, sourceDesignRevision: row.sourceDesignRevision, acceptedDesignRevision: row.acceptedDesignRevision, createdAt: row.createdAt, updatedAt: row.updatedAt };
|
|
@@ -18624,7 +19209,7 @@ async function acceptBrainProposal(client, input) {
|
|
|
18624
19209
|
}
|
|
18625
19210
|
|
|
18626
19211
|
// packages/cli/src/device-auth.ts
|
|
18627
|
-
import { spawn as
|
|
19212
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
18628
19213
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
18629
19214
|
var DEFAULT_API_URL = "https://api.editorbrain.com";
|
|
18630
19215
|
var DeviceLoginError = class extends Error {
|
|
@@ -18642,12 +19227,12 @@ function browserCommand(url2, platform = process.platform) {
|
|
|
18642
19227
|
}
|
|
18643
19228
|
async function openSystemBrowser(url2) {
|
|
18644
19229
|
const { command, args } = browserCommand(url2);
|
|
18645
|
-
await new Promise((
|
|
18646
|
-
const child =
|
|
19230
|
+
await new Promise((resolve9, reject) => {
|
|
19231
|
+
const child = spawn3(command, args, { shell: false, stdio: "ignore", detached: true, windowsHide: true });
|
|
18647
19232
|
child.once("error", reject);
|
|
18648
19233
|
child.once("spawn", () => {
|
|
18649
19234
|
child.unref();
|
|
18650
|
-
|
|
19235
|
+
resolve9();
|
|
18651
19236
|
});
|
|
18652
19237
|
});
|
|
18653
19238
|
}
|
|
@@ -18804,10 +19389,10 @@ async function loginWithDevice(options, overrides = {}) {
|
|
|
18804
19389
|
}
|
|
18805
19390
|
|
|
18806
19391
|
// packages/cli/src/index.ts
|
|
18807
|
-
import { chmod, lstat as
|
|
19392
|
+
import { chmod as chmod2, lstat as lstat11, mkdir as mkdir8, open as open9 } from "node:fs/promises";
|
|
18808
19393
|
import { constants as constants9 } from "node:fs";
|
|
18809
19394
|
import { homedir } from "node:os";
|
|
18810
|
-
import { basename as
|
|
19395
|
+
import { basename as basename3, dirname as dirname11, extname as extname5, join as join8, resolve as resolve8 } from "node:path";
|
|
18811
19396
|
|
|
18812
19397
|
// packages/cli/src/direction-context.ts
|
|
18813
19398
|
async function prepareDirectionContext(root, client, projectId) {
|
|
@@ -18830,10 +19415,10 @@ async function prepareDirectionContext(root, client, projectId) {
|
|
|
18830
19415
|
}
|
|
18831
19416
|
|
|
18832
19417
|
// packages/cli/src/style-authoring.ts
|
|
18833
|
-
import { createHash as
|
|
18834
|
-
import { constants as
|
|
18835
|
-
import { lstat as
|
|
18836
|
-
import { dirname as
|
|
19418
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
19419
|
+
import { constants as constants6 } from "node:fs";
|
|
19420
|
+
import { lstat as lstat6, mkdir as mkdir4, open as open6, readdir as readdir4, rmdir, unlink as unlink3 } from "node:fs/promises";
|
|
19421
|
+
import { dirname as dirname7 } from "node:path";
|
|
18837
19422
|
function styleId(id2) {
|
|
18838
19423
|
try {
|
|
18839
19424
|
return assertIdentifier(id2);
|
|
@@ -18843,7 +19428,7 @@ function styleId(id2) {
|
|
|
18843
19428
|
}
|
|
18844
19429
|
async function exists(path) {
|
|
18845
19430
|
try {
|
|
18846
|
-
await
|
|
19431
|
+
await lstat6(path);
|
|
18847
19432
|
return true;
|
|
18848
19433
|
} catch (error) {
|
|
18849
19434
|
if (error.code === "ENOENT") return false;
|
|
@@ -18873,11 +19458,11 @@ async function makeDirectories(root, path, created) {
|
|
|
18873
19458
|
const target = await checkedPath(root, relative2, false);
|
|
18874
19459
|
try {
|
|
18875
19460
|
await mkdir4(target);
|
|
18876
|
-
const info = await
|
|
19461
|
+
const info = await lstat6(target);
|
|
18877
19462
|
created.push({ path: relative2, dev: info.dev, ino: info.ino });
|
|
18878
19463
|
} catch (error) {
|
|
18879
19464
|
if (error.code !== "EEXIST") throw error;
|
|
18880
|
-
if (!(await
|
|
19465
|
+
if (!(await lstat6(await checkedPath(root, relative2, false))).isDirectory()) throw new Error(`Not a directory: ${relative2}`);
|
|
18881
19466
|
}
|
|
18882
19467
|
}
|
|
18883
19468
|
}
|
|
@@ -18885,7 +19470,7 @@ async function cleanCreated(root, paths2, directory) {
|
|
|
18885
19470
|
for (const item of [...paths2].reverse()) {
|
|
18886
19471
|
try {
|
|
18887
19472
|
const target = await checkedPath(root, item.path, false);
|
|
18888
|
-
const info = await
|
|
19473
|
+
const info = await lstat6(target);
|
|
18889
19474
|
if (info.dev !== item.dev || info.ino !== item.ino || info.isSymbolicLink()) continue;
|
|
18890
19475
|
if (directory) await rmdir(target);
|
|
18891
19476
|
else await unlink3(target);
|
|
@@ -18907,9 +19492,9 @@ async function createLocalStyle(options) {
|
|
|
18907
19492
|
const createdDirectories = [];
|
|
18908
19493
|
try {
|
|
18909
19494
|
for (const [path, content] of Object.entries(files)) {
|
|
18910
|
-
await makeDirectories(root,
|
|
19495
|
+
await makeDirectories(root, dirname7(path), createdDirectories);
|
|
18911
19496
|
const target = await checkedPath(root, path);
|
|
18912
|
-
const handle = await
|
|
19497
|
+
const handle = await open6(target, constants6.O_WRONLY | constants6.O_CREAT | constants6.O_EXCL | constants6.O_NOFOLLOW, 420);
|
|
18913
19498
|
try {
|
|
18914
19499
|
const info = await handle.stat();
|
|
18915
19500
|
createdFiles.push({ path, dev: info.dev, ino: info.ino });
|
|
@@ -18942,321 +19527,124 @@ async function readStyle(root, id2) {
|
|
|
18942
19527
|
try {
|
|
18943
19528
|
const target = await checkedPath(root, path, false);
|
|
18944
19529
|
if (!await exists(target)) continue;
|
|
18945
|
-
if (!(await
|
|
19530
|
+
if (!(await lstat6(target)).isDirectory()) {
|
|
18946
19531
|
issues.push({ path, message: "Style path is not a directory." });
|
|
18947
19532
|
continue;
|
|
18948
19533
|
}
|
|
18949
19534
|
directories.push(path);
|
|
18950
|
-
const entries = await
|
|
19535
|
+
const entries = await readdir4(target, { withFileTypes: true });
|
|
18951
19536
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
18952
|
-
if (!/\.(json|md)$/.test(entry.name)) continue;
|
|
18953
|
-
const document2 = `${path}/${entry.name}`;
|
|
18954
|
-
if (!entry.isFile()) {
|
|
18955
|
-
issues.push({ path: document2, message: "Style document must be a regular file, not a symlink or directory." });
|
|
18956
|
-
continue;
|
|
18957
|
-
}
|
|
18958
|
-
try {
|
|
18959
|
-
files[document2] = await readText(root, document2);
|
|
18960
|
-
} catch (error) {
|
|
18961
|
-
issues.push({ path: document2, message: `Cannot safely read style document: ${error.message}` });
|
|
18962
|
-
}
|
|
18963
|
-
}
|
|
18964
|
-
} catch (error) {
|
|
18965
|
-
issues.push({ path, message: `Cannot safely read style directory: ${error.message}` });
|
|
18966
|
-
}
|
|
18967
|
-
}
|
|
18968
|
-
if (directories.length > 1) issues.push({ path: `.editorbrain/styles/${id2}`, message: "Style exists in both current and legacy directories. Reconcile the duplicate before checking it." });
|
|
18969
|
-
return { files, issues };
|
|
18970
|
-
}
|
|
18971
|
-
async function checkLocalResource(root, path, expected) {
|
|
18972
|
-
safeStyleResourcePath(path);
|
|
18973
|
-
const target = await checkedPath(root, path, false);
|
|
18974
|
-
const handle = await open5(target, constants5.O_RDONLY | constants5.O_NOFOLLOW | constants5.O_NONBLOCK);
|
|
18975
|
-
try {
|
|
18976
|
-
const before = await handle.stat();
|
|
18977
|
-
if (!before.isFile()) throw new Error("Resource is not a regular file.");
|
|
18978
|
-
if (!expected) return {};
|
|
18979
|
-
const hash4 = createHash3("sha256");
|
|
18980
|
-
for await (const chunk of handle.createReadStream({ autoClose: false })) hash4.update(chunk);
|
|
18981
|
-
const after = await handle.stat();
|
|
18982
|
-
const current2 = await lstat4(await checkedPath(root, path, false));
|
|
18983
|
-
if (before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs || current2.dev !== before.dev || current2.ino !== before.ino) {
|
|
18984
|
-
throw new Error("Resource changed while its hash was being checked; check it again.");
|
|
18985
|
-
}
|
|
18986
|
-
return { actualSha256: hash4.digest("hex") };
|
|
18987
|
-
} finally {
|
|
18988
|
-
await handle.close();
|
|
18989
|
-
}
|
|
18990
|
-
}
|
|
18991
|
-
async function checkLocalStyle(options) {
|
|
18992
|
-
const id2 = styleId(options.id);
|
|
18993
|
-
const root = await ensureRoot(options.directory);
|
|
18994
|
-
const local = await readStyle(root, id2);
|
|
18995
|
-
const structural = checkStyle(local.files, id2);
|
|
18996
|
-
const localIssues = [...local.issues];
|
|
18997
|
-
const warnings = [...structural.warnings];
|
|
18998
|
-
const resources = [];
|
|
18999
|
-
const resourcePath = Object.keys(local.files).find((path) => path.endsWith("/resources.json"));
|
|
19000
|
-
let document2;
|
|
19001
|
-
if (resourcePath) {
|
|
19002
|
-
try {
|
|
19003
|
-
const parsed = JSON.parse(local.files[resourcePath]);
|
|
19004
|
-
if (validateStyleResources(parsed, id2).valid) document2 = parsed;
|
|
19005
|
-
} catch {
|
|
19006
|
-
}
|
|
19007
|
-
}
|
|
19008
|
-
for (const resource of document2?.resources ?? []) {
|
|
19009
|
-
const result2 = { id: resource.id, source: resource.source.type, required: resource.required, status: "unverified" };
|
|
19010
|
-
resources.push(result2);
|
|
19011
|
-
if (resource.source.type !== "local") {
|
|
19012
|
-
warnings.push(`Resource ${resource.id}: ${resource.source.type === "asset" ? "remote asset availability and access" : "installed or external requirement"} not verified by this local check.`);
|
|
19013
|
-
continue;
|
|
19014
|
-
}
|
|
19015
|
-
result2.path = resource.source.path;
|
|
19016
|
-
result2.expectedSha256 = resource.source.sha256;
|
|
19017
|
-
let message;
|
|
19018
|
-
try {
|
|
19019
|
-
const checked = await checkLocalResource(root, resource.source.path, resource.source.sha256);
|
|
19020
|
-
result2.actualSha256 = checked.actualSha256;
|
|
19021
|
-
result2.status = resource.source.sha256 ? "verified" : "present";
|
|
19022
|
-
if (resource.source.sha256 && checked.actualSha256 !== resource.source.sha256) {
|
|
19023
|
-
result2.status = "hash_mismatch";
|
|
19024
|
-
message = "Resource SHA-256 does not match resources.json.";
|
|
19025
|
-
} else if (!resource.source.sha256) warnings.push(`Resource ${resource.id}: local file is present, but its content hash is not pinned.`);
|
|
19026
|
-
} catch (error) {
|
|
19027
|
-
result2.status = error.code === "ENOENT" ? "missing" : "invalid";
|
|
19028
|
-
message = result2.status === "missing" ? "Local resource is missing." : `Cannot safely verify resource: ${error.message}`;
|
|
19029
|
-
}
|
|
19030
|
-
if (message) {
|
|
19031
|
-
const detail = `Resource ${resource.id}: ${message}`;
|
|
19032
|
-
if (resource.required) localIssues.push({ path: resource.source.path, message: detail });
|
|
19033
|
-
else warnings.push(`Optional ${detail}`);
|
|
19034
|
-
}
|
|
19035
|
-
}
|
|
19036
|
-
const issues = [...structural.issues, ...localIssues];
|
|
19037
|
-
return {
|
|
19038
|
-
root,
|
|
19039
|
-
id: id2,
|
|
19040
|
-
...structural,
|
|
19041
|
-
valid: issues.length === 0,
|
|
19042
|
-
issues,
|
|
19043
|
-
warnings,
|
|
19044
|
-
localIssues,
|
|
19045
|
-
resources,
|
|
19046
|
-
mediaUploaded: false,
|
|
19047
|
-
quality: "not_assessed"
|
|
19048
|
-
};
|
|
19049
|
-
}
|
|
19050
|
-
|
|
19051
|
-
// packages/cli/src/media-runtime.ts
|
|
19052
|
-
import { spawn as spawn3 } from "node:child_process";
|
|
19053
|
-
import { createHash as createHash4 } from "node:crypto";
|
|
19054
|
-
import { constants as constants6 } from "node:fs";
|
|
19055
|
-
import { lstat as lstat5, open as open6, realpath as realpath2 } from "node:fs/promises";
|
|
19056
|
-
import { dirname as dirname5, extname, parse, resolve as resolve4 } from "node:path";
|
|
19057
|
-
var MediaToolError = class extends CliError {
|
|
19058
|
-
constructor(code, message) {
|
|
19059
|
-
super(code, message, code === "MEDIA_TOOL_NOT_FOUND" ? "Install FFmpeg/FFprobe separately and make them available on PATH; no account or model is needed." : "Review the local input and reported limit. An incomplete check does not validate the media.");
|
|
19060
|
-
this.name = "MediaToolError";
|
|
19061
|
-
}
|
|
19062
|
-
};
|
|
19063
|
-
var SUPPORTED_MEDIA_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
19064
|
-
".mp4",
|
|
19065
|
-
".mov",
|
|
19066
|
-
".m4v",
|
|
19067
|
-
".mkv",
|
|
19068
|
-
".webm",
|
|
19069
|
-
".avi",
|
|
19070
|
-
".mpg",
|
|
19071
|
-
".mpeg",
|
|
19072
|
-
".mts",
|
|
19073
|
-
".m2ts",
|
|
19074
|
-
".ts",
|
|
19075
|
-
".ogv",
|
|
19076
|
-
".wmv",
|
|
19077
|
-
".wav",
|
|
19078
|
-
".mp3",
|
|
19079
|
-
".m4a",
|
|
19080
|
-
".aac",
|
|
19081
|
-
".flac",
|
|
19082
|
-
".ogg",
|
|
19083
|
-
".opus",
|
|
19084
|
-
".aif",
|
|
19085
|
-
".aiff",
|
|
19086
|
-
".ac3",
|
|
19087
|
-
".eac3",
|
|
19088
|
-
".jpg",
|
|
19089
|
-
".jpeg",
|
|
19090
|
-
".png",
|
|
19091
|
-
".webp",
|
|
19092
|
-
".bmp",
|
|
19093
|
-
".tif",
|
|
19094
|
-
".tiff",
|
|
19095
|
-
".gif",
|
|
19096
|
-
".avif",
|
|
19097
|
-
".heic",
|
|
19098
|
-
".heif"
|
|
19099
|
-
]);
|
|
19100
|
-
var PROTOCOLS = "file,pipe";
|
|
19101
|
-
var FORMATS = "mov,matroska,webm,avi,mpeg,mpegts,ogg,wav,flac,mp3,aac,aiff,ac3,eac3,asf,jpeg_pipe,png_pipe,webp_pipe,bmp_pipe,tiff_pipe,gif,heif,avif";
|
|
19102
|
-
var DEFAULT_TIMEOUT = 3e4;
|
|
19103
|
-
var DEFAULT_OUTPUT_LIMIT = 2 * 1024 * 1024;
|
|
19104
|
-
function resolveLocalMediaPath(value) {
|
|
19105
|
-
if (!value || value.includes("\0") || /[\r\n]/.test(value) || /^[a-z][a-z\d+.-]*:/i.test(value) && !(process.platform === "win32" && /^[a-z]:[\\/]/i.test(value)) || value.startsWith("\\\\")) {
|
|
19106
|
-
throw new MediaToolError("MEDIA_LOCAL_PATH_REQUIRED", "Provide a local filesystem path, not a URL, protocol or network share.");
|
|
19107
|
-
}
|
|
19108
|
-
return resolve4(value);
|
|
19109
|
-
}
|
|
19110
|
-
async function assertNoMediaSymlinks(value) {
|
|
19111
|
-
const path = resolveLocalMediaPath(value);
|
|
19112
|
-
let cursor = path;
|
|
19113
|
-
while (cursor !== parse(cursor).root) {
|
|
19114
|
-
const info = await lstat5(cursor);
|
|
19115
|
-
if (info.isSymbolicLink()) {
|
|
19116
|
-
const systemAlias = process.platform === "darwin" && (cursor === "/tmp" || cursor === "/var") && await realpath2(cursor) === `/private${cursor}`;
|
|
19117
|
-
if (!systemAlias) throw new MediaToolError("MEDIA_SYMLINK", "Symlink media paths are not followed. Use the original local path.");
|
|
19118
|
-
}
|
|
19119
|
-
cursor = dirname5(cursor);
|
|
19120
|
-
}
|
|
19121
|
-
return path;
|
|
19122
|
-
}
|
|
19123
|
-
async function assertLocalMediaFile(value) {
|
|
19124
|
-
const path = await assertNoMediaSymlinks(value);
|
|
19125
|
-
const info = await lstat5(path);
|
|
19126
|
-
if (!info.isFile()) throw new MediaToolError("MEDIA_NOT_REGULAR", "The media input must be a regular local file.");
|
|
19127
|
-
if (!SUPPORTED_MEDIA_EXTENSIONS.has(extname(path).toLowerCase())) throw new MediaToolError("MEDIA_FORMAT_UNSUPPORTED", "This file extension is not supported for local media inspection.");
|
|
19128
|
-
return path;
|
|
19129
|
-
}
|
|
19130
|
-
function localInputArgs(path) {
|
|
19131
|
-
return ["-protocol_whitelist", PROTOCOLS, "-format_whitelist", FORMATS, "-i", resolveLocalMediaPath(path)];
|
|
19132
|
-
}
|
|
19133
|
-
async function runMediaTool(tool, args, options = {}) {
|
|
19134
|
-
if (tool !== "ffmpeg" && tool !== "ffprobe") throw new MediaToolError("MEDIA_TOOL_INVALID", "Unsupported media executable.");
|
|
19135
|
-
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT;
|
|
19136
|
-
const maxOutputBytes = options.maxOutputBytes ?? DEFAULT_OUTPUT_LIMIT;
|
|
19137
|
-
if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 10 * 6e4 || !Number.isInteger(maxOutputBytes) || maxOutputBytes < 1 || maxOutputBytes > 16 * 1024 * 1024) {
|
|
19138
|
-
throw new MediaToolError("MEDIA_TOOL_LIMITS_INVALID", "Media runtime limits are outside the supported range.");
|
|
19139
|
-
}
|
|
19140
|
-
if (!Array.isArray(args) || args.length > 200 || args.some((arg) => typeof arg !== "string" || arg.length > 8192 || arg.includes("\0"))) {
|
|
19141
|
-
throw new MediaToolError("MEDIA_TOOL_ARGUMENTS_INVALID", "Invalid media tool arguments.");
|
|
19142
|
-
}
|
|
19143
|
-
for (let i = 0; i < args.length; i++) {
|
|
19144
|
-
const arg = args[i];
|
|
19145
|
-
if (/(?:^|[=,;\s])(?:https?|ftp|sftp|tcp|udp|rtsp|rtmp|smb|data|concat|subfile|crypto):/i.test(arg) || arg === "-safe" && args[i + 1] === "0" || arg === "-enable_drefs" && args[i + 1] !== "0") {
|
|
19146
|
-
throw new MediaToolError("MEDIA_REMOTE_INPUT_FORBIDDEN", "Only self-contained local media inputs are allowed.");
|
|
19147
|
-
}
|
|
19148
|
-
if (arg === "-protocol_whitelist" && args[i + 1] !== PROTOCOLS || arg === "-format_whitelist" && args[i + 1] !== FORMATS) {
|
|
19149
|
-
throw new MediaToolError("MEDIA_TOOL_ARGUMENTS_INVALID", "The local media allowlists cannot be widened.");
|
|
19150
|
-
}
|
|
19151
|
-
if (arg === "-i") {
|
|
19152
|
-
await assertLocalMediaFile(args[i + 1] ?? "");
|
|
19153
|
-
if (args[i - 4] !== "-protocol_whitelist" || args[i - 3] !== PROTOCOLS || args[i - 2] !== "-format_whitelist" || args[i - 1] !== FORMATS) {
|
|
19154
|
-
throw new MediaToolError("MEDIA_TOOL_ARGUMENTS_INVALID", "Every media input must use localInputArgs.");
|
|
19537
|
+
if (!/\.(json|md)$/.test(entry.name)) continue;
|
|
19538
|
+
const document2 = `${path}/${entry.name}`;
|
|
19539
|
+
if (!entry.isFile()) {
|
|
19540
|
+
issues.push({ path: document2, message: "Style document must be a regular file, not a symlink or directory." });
|
|
19541
|
+
continue;
|
|
19542
|
+
}
|
|
19543
|
+
try {
|
|
19544
|
+
files[document2] = await readText(root, document2);
|
|
19545
|
+
} catch (error) {
|
|
19546
|
+
issues.push({ path: document2, message: `Cannot safely read style document: ${error.message}` });
|
|
19547
|
+
}
|
|
19155
19548
|
}
|
|
19549
|
+
} catch (error) {
|
|
19550
|
+
issues.push({ path, message: `Cannot safely read style directory: ${error.message}` });
|
|
19156
19551
|
}
|
|
19157
19552
|
}
|
|
19158
|
-
|
|
19159
|
-
|
|
19160
|
-
let size = 0, done = false;
|
|
19161
|
-
const child = spawn3(tool, ["-hide_banner", ...tool === "ffmpeg" ? ["-nostdin"] : [], ...args], { shell: false, stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
19162
|
-
const fail2 = (error) => {
|
|
19163
|
-
if (done) return;
|
|
19164
|
-
done = true;
|
|
19165
|
-
clearTimeout(timer);
|
|
19166
|
-
child.kill("SIGKILL");
|
|
19167
|
-
reject(error);
|
|
19168
|
-
};
|
|
19169
|
-
const timer = setTimeout(() => fail2(new MediaToolError("MEDIA_TOOL_TIMEOUT", "The local media check exceeded its time limit. No complete result is available.")), timeoutMs);
|
|
19170
|
-
const collect = (target) => (chunk) => {
|
|
19171
|
-
if (done) return;
|
|
19172
|
-
size += chunk.length;
|
|
19173
|
-
if (size > maxOutputBytes) return fail2(new MediaToolError("MEDIA_TOOL_OUTPUT_LIMIT", "The local media check exceeded its output limit. No complete result is available."));
|
|
19174
|
-
target.push(chunk);
|
|
19175
|
-
};
|
|
19176
|
-
child.stdout.on("data", collect(stdout));
|
|
19177
|
-
child.stderr.on("data", collect(stderr));
|
|
19178
|
-
child.on("error", (error) => fail2(new MediaToolError(error.code === "ENOENT" ? "MEDIA_TOOL_NOT_FOUND" : "MEDIA_TOOL_FAILED", error.code === "ENOENT" ? `${tool} is not installed or available on PATH. Install it separately to use local media checks.` : "The local media executable could not run.")));
|
|
19179
|
-
child.on("close", (code) => {
|
|
19180
|
-
if (done) return;
|
|
19181
|
-
done = true;
|
|
19182
|
-
clearTimeout(timer);
|
|
19183
|
-
if (code !== 0) return reject(new MediaToolError("MEDIA_TOOL_FAILED", "The local media tool could not inspect or decode this input. No complete result is available."));
|
|
19184
|
-
resolveResult({ stdout: Buffer.concat(stdout).toString("utf8"), stderr: Buffer.concat(stderr).toString("utf8") });
|
|
19185
|
-
});
|
|
19186
|
-
});
|
|
19553
|
+
if (directories.length > 1) issues.push({ path: `.editorbrain/styles/${id2}`, message: "Style exists in both current and legacy directories. Reconcile the duplicate before checking it." });
|
|
19554
|
+
return { files, issues };
|
|
19187
19555
|
}
|
|
19188
|
-
|
|
19189
|
-
|
|
19190
|
-
const
|
|
19191
|
-
|
|
19192
|
-
};
|
|
19193
|
-
var nonnegative2 = (value) => {
|
|
19194
|
-
const n = finite2(value);
|
|
19195
|
-
return n !== null && n >= 0 ? n : null;
|
|
19196
|
-
};
|
|
19197
|
-
var positiveInteger = (value) => {
|
|
19198
|
-
const n = finite2(value);
|
|
19199
|
-
return n !== null && Number.isInteger(n) && n > 0 ? n : void 0;
|
|
19200
|
-
};
|
|
19201
|
-
var rate2 = (value) => {
|
|
19202
|
-
if (typeof value !== "string" || !/^\d+(?:\/\d+)?$/.test(value)) return null;
|
|
19203
|
-
const [n, d = "1"] = value.split("/");
|
|
19204
|
-
const result2 = Number(n) / Number(d);
|
|
19205
|
-
return Number.isFinite(result2) && result2 > 0 ? result2 : null;
|
|
19206
|
-
};
|
|
19207
|
-
var sameFile = (a, b) => a.dev === b.dev && a.ino === b.ino && a.size === b.size && a.mtimeMs === b.mtimeMs && a.ctimeMs === b.ctimeMs;
|
|
19208
|
-
async function probeLocalMedia(value) {
|
|
19209
|
-
const path = await assertLocalMediaFile(value);
|
|
19210
|
-
const handle = await open6(path, constants6.O_RDONLY | (constants6.O_NOFOLLOW ?? 0));
|
|
19556
|
+
async function checkLocalResource(root, path, expected) {
|
|
19557
|
+
safeStyleResourcePath(path);
|
|
19558
|
+
const target = await checkedPath(root, path, false);
|
|
19559
|
+
const handle = await open6(target, constants6.O_RDONLY | constants6.O_NOFOLLOW | constants6.O_NONBLOCK);
|
|
19211
19560
|
try {
|
|
19212
19561
|
const before = await handle.stat();
|
|
19213
|
-
if (!before.isFile()) throw new
|
|
19214
|
-
|
|
19215
|
-
const
|
|
19216
|
-
const
|
|
19217
|
-
for await (const chunk of handle.createReadStream({ autoClose: false })) hash4.update(chunk);
|
|
19562
|
+
if (!before.isFile()) throw new Error("Resource is not a regular file.");
|
|
19563
|
+
if (!expected) return {};
|
|
19564
|
+
const hash6 = createHash5("sha256");
|
|
19565
|
+
for await (const chunk of handle.createReadStream({ autoClose: false })) hash6.update(chunk);
|
|
19218
19566
|
const after = await handle.stat();
|
|
19219
|
-
await
|
|
19220
|
-
|
|
19221
|
-
|
|
19222
|
-
let raw;
|
|
19223
|
-
try {
|
|
19224
|
-
raw = JSON.parse(stdout);
|
|
19225
|
-
} catch {
|
|
19226
|
-
throw new MediaToolError("MEDIA_PROBE_INVALID", "The media probe returned invalid metadata.");
|
|
19567
|
+
const current2 = await lstat6(await checkedPath(root, path, false));
|
|
19568
|
+
if (before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs || current2.dev !== before.dev || current2.ino !== before.ino) {
|
|
19569
|
+
throw new Error("Resource changed while its hash was being checked; check it again.");
|
|
19227
19570
|
}
|
|
19228
|
-
|
|
19229
|
-
const streams = raw.streams.map((stream, offset) => {
|
|
19230
|
-
const type = typeof stream.codec_type === "string" && /^(video|audio|subtitle|data|attachment|unknown)$/.test(stream.codec_type) ? stream.codec_type : "unknown";
|
|
19231
|
-
const codec = typeof stream.codec_name === "string" && /^[a-zA-Z0-9_]{1,64}$/.test(stream.codec_name) ? stream.codec_name : null;
|
|
19232
|
-
const index = finite2(stream.index);
|
|
19233
|
-
return {
|
|
19234
|
-
index: index !== null && Number.isInteger(index) && index >= 0 ? index : offset,
|
|
19235
|
-
type,
|
|
19236
|
-
codec,
|
|
19237
|
-
durationSeconds: nonnegative2(stream.duration),
|
|
19238
|
-
...type === "video" ? { width: positiveInteger(stream.width), height: positiveInteger(stream.height), fps: rate2(stream.avg_frame_rate), attachedPicture: stream.disposition?.attached_pic === 1 } : {},
|
|
19239
|
-
...type === "audio" ? { sampleRate: nonnegative2(stream.sample_rate), channels: positiveInteger(stream.channels) ?? null } : {}
|
|
19240
|
-
};
|
|
19241
|
-
});
|
|
19242
|
-
return { path, sha256: hash4.digest("hex"), bytes: after.size, durationSeconds: nonnegative2(raw.format?.duration), startTimeSeconds: finite2(raw.format?.start_time), streams };
|
|
19571
|
+
return { actualSha256: hash6.digest("hex") };
|
|
19243
19572
|
} finally {
|
|
19244
19573
|
await handle.close();
|
|
19245
19574
|
}
|
|
19246
19575
|
}
|
|
19576
|
+
async function checkLocalStyle(options) {
|
|
19577
|
+
const id2 = styleId(options.id);
|
|
19578
|
+
const root = await ensureRoot(options.directory);
|
|
19579
|
+
const local = await readStyle(root, id2);
|
|
19580
|
+
const structural = checkStyle(local.files, id2);
|
|
19581
|
+
const localIssues = [...local.issues];
|
|
19582
|
+
const warnings = [...structural.warnings];
|
|
19583
|
+
const resources = [];
|
|
19584
|
+
const resourcePath = Object.keys(local.files).find((path) => path.endsWith("/resources.json"));
|
|
19585
|
+
let document2;
|
|
19586
|
+
if (resourcePath) {
|
|
19587
|
+
try {
|
|
19588
|
+
const parsed = JSON.parse(local.files[resourcePath]);
|
|
19589
|
+
if (validateStyleResources(parsed, id2).valid) document2 = parsed;
|
|
19590
|
+
} catch {
|
|
19591
|
+
}
|
|
19592
|
+
}
|
|
19593
|
+
for (const resource of document2?.resources ?? []) {
|
|
19594
|
+
const result2 = { id: resource.id, source: resource.source.type, required: resource.required, status: "unverified" };
|
|
19595
|
+
resources.push(result2);
|
|
19596
|
+
if (resource.source.type !== "local") {
|
|
19597
|
+
warnings.push(`Resource ${resource.id}: ${resource.source.type === "asset" ? "remote asset availability and access" : "installed or external requirement"} not verified by this local check.`);
|
|
19598
|
+
continue;
|
|
19599
|
+
}
|
|
19600
|
+
result2.path = resource.source.path;
|
|
19601
|
+
result2.expectedSha256 = resource.source.sha256;
|
|
19602
|
+
let message;
|
|
19603
|
+
try {
|
|
19604
|
+
const checked = await checkLocalResource(root, resource.source.path, resource.source.sha256);
|
|
19605
|
+
result2.actualSha256 = checked.actualSha256;
|
|
19606
|
+
result2.status = resource.source.sha256 ? "verified" : "present";
|
|
19607
|
+
if (resource.source.sha256 && checked.actualSha256 !== resource.source.sha256) {
|
|
19608
|
+
result2.status = "hash_mismatch";
|
|
19609
|
+
message = "Resource SHA-256 does not match resources.json.";
|
|
19610
|
+
} else if (!resource.source.sha256) warnings.push(`Resource ${resource.id}: local file is present, but its content hash is not pinned.`);
|
|
19611
|
+
} catch (error) {
|
|
19612
|
+
result2.status = error.code === "ENOENT" ? "missing" : "invalid";
|
|
19613
|
+
message = result2.status === "missing" ? "Local resource is missing." : `Cannot safely verify resource: ${error.message}`;
|
|
19614
|
+
}
|
|
19615
|
+
if (message) {
|
|
19616
|
+
const detail = `Resource ${resource.id}: ${message}`;
|
|
19617
|
+
if (resource.required) localIssues.push({ path: resource.source.path, message: detail });
|
|
19618
|
+
else warnings.push(`Optional ${detail}`);
|
|
19619
|
+
}
|
|
19620
|
+
}
|
|
19621
|
+
const issues = [...structural.issues, ...localIssues];
|
|
19622
|
+
return {
|
|
19623
|
+
root,
|
|
19624
|
+
id: id2,
|
|
19625
|
+
...structural,
|
|
19626
|
+
valid: issues.length === 0,
|
|
19627
|
+
issues,
|
|
19628
|
+
warnings,
|
|
19629
|
+
localIssues,
|
|
19630
|
+
resources,
|
|
19631
|
+
mediaUploaded: false,
|
|
19632
|
+
quality: "not_assessed"
|
|
19633
|
+
};
|
|
19634
|
+
}
|
|
19247
19635
|
|
|
19248
19636
|
// packages/cli/src/audio-check.ts
|
|
19249
19637
|
function invalid3() {
|
|
19250
19638
|
throw new CliError("AUDIO_PROFILE_INVALID", "Invalid audio check profile.", "Use explicit numeric loudness targets and bounded runtime limits.");
|
|
19251
19639
|
}
|
|
19252
|
-
function
|
|
19640
|
+
function object7(value, keys2) {
|
|
19253
19641
|
if (!value || typeof value !== "object" || Array.isArray(value)) invalid3();
|
|
19254
19642
|
const result2 = value;
|
|
19255
19643
|
if (Object.keys(result2).some((key) => !keys2.includes(key))) invalid3();
|
|
19256
19644
|
return result2;
|
|
19257
19645
|
}
|
|
19258
19646
|
function validateAudioCheckProfile(value) {
|
|
19259
|
-
const input =
|
|
19647
|
+
const input = object7(value, ["schemaVersion", "loudness", "limits"]);
|
|
19260
19648
|
if (input.schemaVersion !== 1) invalid3();
|
|
19261
19649
|
let loudness;
|
|
19262
19650
|
try {
|
|
@@ -19264,7 +19652,7 @@ function validateAudioCheckProfile(value) {
|
|
|
19264
19652
|
} catch {
|
|
19265
19653
|
invalid3();
|
|
19266
19654
|
}
|
|
19267
|
-
const limits = input.limits === void 0 ? {} :
|
|
19655
|
+
const limits = input.limits === void 0 ? {} : object7(input.limits, ["timeoutMs", "maxOutputBytes"]);
|
|
19268
19656
|
const timeoutMs = limits.timeoutMs === void 0 ? 6e5 : limits.timeoutMs;
|
|
19269
19657
|
const maxOutputBytes = limits.maxOutputBytes === void 0 ? 4 * 1024 * 1024 : limits.maxOutputBytes;
|
|
19270
19658
|
if (typeof timeoutMs !== "number" || !Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 6e5 || typeof maxOutputBytes !== "number" || !Number.isInteger(maxOutputBytes) || maxOutputBytes < 1024 || maxOutputBytes > 16 * 1024 * 1024) invalid3();
|
|
@@ -19396,11 +19784,11 @@ var record10 = (value) => value !== null && typeof value === "object" && !Array.
|
|
|
19396
19784
|
var array2 = (value) => Array.isArray(value) ? value : [];
|
|
19397
19785
|
function plain(value, max = 2e3) {
|
|
19398
19786
|
if (typeof value !== "string" || value.length > 16e3) return null;
|
|
19399
|
-
const
|
|
19787
|
+
const text9 = value.replace(/<[^>]*>/g, " ").replace(/&#(x[0-9a-f]+|[0-9]+);/gi, (_, number2) => {
|
|
19400
19788
|
const code = number2[0].toLowerCase() === "x" ? parseInt(number2.slice(1), 16) : Number(number2);
|
|
19401
19789
|
return code > 0 && code <= 1114111 ? String.fromCodePoint(code) : "";
|
|
19402
19790
|
}).replace(/&(amp|quot|apos|lt|gt|nbsp);/g, (_, entity) => ({ amp: "&", quot: '"', apos: "'", lt: "<", gt: ">", nbsp: " " })[entity]).replace(/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g, " ").replace(/\s+/g, " ").trim();
|
|
19403
|
-
return
|
|
19791
|
+
return text9 ? text9.slice(0, max) : null;
|
|
19404
19792
|
}
|
|
19405
19793
|
function texts(value) {
|
|
19406
19794
|
return (Array.isArray(value) ? value.slice(0, 8) : [value]).map((item) => plain(item)).filter((item) => item !== null);
|
|
@@ -19599,7 +19987,7 @@ async function searchProvider(provider, query, limit, client, report) {
|
|
|
19599
19987
|
item.author = firstText(metadata.photographer, metadata.secondary_creator);
|
|
19600
19988
|
item.credit = firstText(metadata.center);
|
|
19601
19989
|
item.dates.created = firstText(metadata.date_created);
|
|
19602
|
-
item.previewUrl = array2(entry.links).map(record10).filter((
|
|
19990
|
+
item.previewUrl = array2(entry.links).map(record10).filter((link5) => link5.rel === "preview").map((link5) => safeUrl(link5.href, HOSTS[provider])).find(Boolean) ?? null;
|
|
19603
19991
|
}
|
|
19604
19992
|
add(item);
|
|
19605
19993
|
}
|
|
@@ -19720,9 +20108,9 @@ async function searchMediaSources(options, dependencies = {}) {
|
|
|
19720
20108
|
}
|
|
19721
20109
|
|
|
19722
20110
|
// packages/cli/src/media-inventory.ts
|
|
19723
|
-
import { createHash as
|
|
19724
|
-
import { link as
|
|
19725
|
-
import { basename, dirname as
|
|
20111
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
20112
|
+
import { link as link4, lstat as lstat7, mkdir as mkdir5, mkdtemp as mkdtemp3, opendir, readFile as readFile3, rm as rm3 } from "node:fs/promises";
|
|
20113
|
+
import { basename as basename2, dirname as dirname8, extname as extname3, join as join6 } from "node:path";
|
|
19726
20114
|
var GENERATED_DIRECTORIES = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "output", "outputs", "renders", "render", "cache", "caches", "proxies", "proxy", "samples", "thumbnails", "coverage", "venv", "__pycache__"]);
|
|
19727
20115
|
function mediaFailure(error) {
|
|
19728
20116
|
if (error instanceof MediaToolError) return { code: error.code, message: error.message };
|
|
@@ -19748,19 +20136,19 @@ async function inventoryMedia(options) {
|
|
|
19748
20136
|
}
|
|
19749
20137
|
report.scannedEntries++;
|
|
19750
20138
|
try {
|
|
19751
|
-
const stat = await
|
|
20139
|
+
const stat = await lstat7(path);
|
|
19752
20140
|
if (stat.isSymbolicLink()) {
|
|
19753
20141
|
if (explicit) throw new MediaToolError("MEDIA_SYMLINK", "Symlink inputs are not followed. Use the original local path.");
|
|
19754
20142
|
skip(path, "symlink_not_followed");
|
|
19755
20143
|
return;
|
|
19756
20144
|
}
|
|
19757
|
-
if (!explicit &&
|
|
20145
|
+
if (!explicit && basename2(path).startsWith(".")) {
|
|
19758
20146
|
skip(path, "hidden_entry");
|
|
19759
20147
|
return;
|
|
19760
20148
|
}
|
|
19761
20149
|
await assertNoMediaSymlinks(path);
|
|
19762
20150
|
if (stat.isDirectory()) {
|
|
19763
|
-
if (!explicit && GENERATED_DIRECTORIES.has(
|
|
20151
|
+
if (!explicit && GENERATED_DIRECTORIES.has(basename2(path).toLowerCase())) {
|
|
19764
20152
|
skip(path, "generated_directory");
|
|
19765
20153
|
return;
|
|
19766
20154
|
}
|
|
@@ -19774,12 +20162,12 @@ async function inventoryMedia(options) {
|
|
|
19774
20162
|
skip(path, "remaining_entries_not_scanned", true);
|
|
19775
20163
|
break;
|
|
19776
20164
|
}
|
|
19777
|
-
await visit(
|
|
20165
|
+
await visit(join6(path, entry.name), depth + 1);
|
|
19778
20166
|
}
|
|
19779
20167
|
} else if (!stat.isFile()) {
|
|
19780
20168
|
if (explicit) throw new MediaToolError("MEDIA_NOT_REGULAR", "The explicit media input must be a regular local file or directory.");
|
|
19781
20169
|
skip(path, "not_regular_file");
|
|
19782
|
-
} else if (!SUPPORTED_MEDIA_EXTENSIONS.has(
|
|
20170
|
+
} else if (!SUPPORTED_MEDIA_EXTENSIONS.has(extname3(path).toLowerCase())) {
|
|
19783
20171
|
if (explicit) throw new MediaToolError("MEDIA_FORMAT_UNSUPPORTED", "This explicit input is not a supported media format.");
|
|
19784
20172
|
skip(path, "unsupported_extension");
|
|
19785
20173
|
} else if (candidates.length >= maxFiles) skip(path, "file_limit_reached", true);
|
|
@@ -19809,35 +20197,35 @@ async function sampleMedia(options) {
|
|
|
19809
20197
|
if (!source.streams.some((stream) => stream.type === "video")) throw new MediaToolError("MEDIA_VIDEO_REQUIRED", "Frame sampling requires a video or image stream.");
|
|
19810
20198
|
if (source.durationSeconds !== null && options.times.some((time) => time >= source.durationSeconds)) throw new MediaToolError("MEDIA_SAMPLE_OUT_OF_RANGE", "A requested sample is outside the known media duration.");
|
|
19811
20199
|
const outputDirectory = resolveLocalMediaPath(options.outputDirectory);
|
|
19812
|
-
await assertNoMediaSymlinks(
|
|
20200
|
+
await assertNoMediaSymlinks(dirname8(outputDirectory));
|
|
19813
20201
|
try {
|
|
19814
20202
|
await mkdir5(outputDirectory);
|
|
19815
20203
|
} catch (error) {
|
|
19816
20204
|
if (error.code !== "EEXIST") throw error;
|
|
19817
20205
|
}
|
|
19818
20206
|
await assertNoMediaSymlinks(outputDirectory);
|
|
19819
|
-
if (!(await
|
|
19820
|
-
const temp = await
|
|
19821
|
-
const tempStat = await
|
|
20207
|
+
if (!(await lstat7(outputDirectory)).isDirectory()) throw new MediaToolError("MEDIA_OUTPUT_DIRECTORY_INVALID", "Frame output must be a local directory.");
|
|
20208
|
+
const temp = await mkdtemp3(join6(outputDirectory, ".editorbrain-samples-"));
|
|
20209
|
+
const tempStat = await lstat7(temp);
|
|
19822
20210
|
const frames = [];
|
|
19823
20211
|
const created = [];
|
|
19824
20212
|
try {
|
|
19825
20213
|
for (let index = 0; index < options.times.length; index++) {
|
|
19826
20214
|
const time = options.times[index];
|
|
19827
20215
|
const filename = `${source.sha256.slice(0, 12)}-${String(index + 1).padStart(2, "0")}-${Math.round(time * 1e3)}ms.jpg`;
|
|
19828
|
-
const temporary =
|
|
20216
|
+
const temporary = join6(temp, filename), output2 = join6(outputDirectory, filename);
|
|
19829
20217
|
await runMediaTool("ffmpeg", ["-v", "error", "-n", "-ss", String(time), ...localInputArgs(file), "-map", "0:v:0", "-frames:v", "1", "-an", "-sn", "-dn", "-vf", "scale=960:960:force_original_aspect_ratio=decrease", "-q:v", "3", "-update", "1", temporary], { timeoutMs: 6e4 });
|
|
19830
|
-
const bytes = await
|
|
20218
|
+
const bytes = await readFile3(temporary);
|
|
19831
20219
|
if (!bytes.length) throw new MediaToolError("MEDIA_SAMPLE_EMPTY", "The requested frame did not produce a usable image.");
|
|
19832
20220
|
try {
|
|
19833
|
-
await
|
|
20221
|
+
await link4(temporary, output2);
|
|
19834
20222
|
} catch (error) {
|
|
19835
20223
|
if (error.code === "EEXIST") throw new MediaToolError("MEDIA_OUTPUT_EXISTS", "A sample output already exists. Choose a new output directory; existing files are never overwritten.");
|
|
19836
20224
|
throw error;
|
|
19837
20225
|
}
|
|
19838
|
-
const outputStat = await
|
|
20226
|
+
const outputStat = await lstat7(output2);
|
|
19839
20227
|
created.push({ path: output2, ino: outputStat.ino, dev: outputStat.dev, size: outputStat.size, mtimeMs: outputStat.mtimeMs, ctimeMs: outputStat.ctimeMs });
|
|
19840
|
-
frames.push({ path: output2, sha256:
|
|
20228
|
+
frames.push({ path: output2, sha256: createHash6("sha256").update(bytes).digest("hex"), bytes: bytes.length, requestedTimeSeconds: time, actualPresentationTimeSeconds: null });
|
|
19841
20229
|
}
|
|
19842
20230
|
const after = await probeLocalMedia(file);
|
|
19843
20231
|
if (source.sha256 !== after.sha256) throw new MediaToolError("MEDIA_FILE_CHANGED", "The source changed while frames were being sampled. Retry with a stable source.");
|
|
@@ -19846,8 +20234,8 @@ async function sampleMedia(options) {
|
|
|
19846
20234
|
for (const file2 of created) {
|
|
19847
20235
|
try {
|
|
19848
20236
|
await assertNoMediaSymlinks(file2.path);
|
|
19849
|
-
const current2 = await
|
|
19850
|
-
if (current2.ino === file2.ino && current2.dev === file2.dev && current2.size === file2.size && current2.mtimeMs === file2.mtimeMs && current2.ctimeMs === file2.ctimeMs) await
|
|
20237
|
+
const current2 = await lstat7(file2.path);
|
|
20238
|
+
if (current2.ino === file2.ino && current2.dev === file2.dev && current2.size === file2.size && current2.mtimeMs === file2.mtimeMs && current2.ctimeMs === file2.ctimeMs) await rm3(file2.path, { force: true });
|
|
19851
20239
|
} catch {
|
|
19852
20240
|
}
|
|
19853
20241
|
}
|
|
@@ -19855,8 +20243,8 @@ async function sampleMedia(options) {
|
|
|
19855
20243
|
} finally {
|
|
19856
20244
|
try {
|
|
19857
20245
|
await assertNoMediaSymlinks(temp);
|
|
19858
|
-
const current2 = await
|
|
19859
|
-
if (current2.ino === tempStat.ino && current2.dev === tempStat.dev) await
|
|
20246
|
+
const current2 = await lstat7(temp);
|
|
20247
|
+
if (current2.ino === tempStat.ino && current2.dev === tempStat.dev) await rm3(temp, { recursive: true, force: true });
|
|
19860
20248
|
} catch {
|
|
19861
20249
|
}
|
|
19862
20250
|
}
|
|
@@ -19867,21 +20255,21 @@ import { resolve as resolve5 } from "node:path";
|
|
|
19867
20255
|
function invalidProfile(message) {
|
|
19868
20256
|
throw new CliError("RENDER_PROFILE_INVALID", message, "Use a schemaVersion: 1 render profile with explicit technical requirements.");
|
|
19869
20257
|
}
|
|
19870
|
-
function
|
|
20258
|
+
function object8(value, path, keys2) {
|
|
19871
20259
|
if (!value || typeof value !== "object" || Array.isArray(value)) invalidProfile(`${path} must be an object.`);
|
|
19872
20260
|
const result2 = value;
|
|
19873
20261
|
for (const key of Object.keys(result2)) if (!keys2.includes(key)) invalidProfile(`${path} contains unsupported field ${key}.`);
|
|
19874
20262
|
return result2;
|
|
19875
20263
|
}
|
|
19876
20264
|
function validateRenderCheckProfile(value) {
|
|
19877
|
-
const root =
|
|
20265
|
+
const root = object8(value, "profile", ["schemaVersion", "expected", "tolerance", "audio", "silence", "black", "peak", "limits"]);
|
|
19878
20266
|
if (root.schemaVersion !== 1) invalidProfile("profile.schemaVersion must be 1.");
|
|
19879
|
-
const expected = root.expected === void 0 ? {} :
|
|
19880
|
-
const tolerance = root.tolerance === void 0 ? {} :
|
|
19881
|
-
const silence = root.silence === void 0 ? {} :
|
|
19882
|
-
const black = root.black === void 0 ? {} :
|
|
19883
|
-
const peak = root.peak === void 0 ? {} :
|
|
19884
|
-
const limits = root.limits === void 0 ? {} :
|
|
20267
|
+
const expected = root.expected === void 0 ? {} : object8(root.expected, "expected", ["width", "height", "fps", "durationSeconds"]);
|
|
20268
|
+
const tolerance = root.tolerance === void 0 ? {} : object8(root.tolerance, "tolerance", ["fps", "durationSeconds"]);
|
|
20269
|
+
const silence = root.silence === void 0 ? {} : object8(root.silence, "silence", ["enabled", "minDurationSeconds", "thresholdDb"]);
|
|
20270
|
+
const black = root.black === void 0 ? {} : object8(root.black, "black", ["enabled", "minDurationSeconds", "pixelThreshold", "pictureBlackRatio"]);
|
|
20271
|
+
const peak = root.peak === void 0 ? {} : object8(root.peak, "peak", ["enabled", "thresholdDbFS"]);
|
|
20272
|
+
const limits = root.limits === void 0 ? {} : object8(root.limits, "limits", ["timeoutMs", "maxOutputBytes"]);
|
|
19885
20273
|
const number2 = (v, name, min, max, fallback, integer2 = false) => {
|
|
19886
20274
|
if (v === void 0 && fallback !== void 0) return fallback;
|
|
19887
20275
|
if (typeof v !== "number" || !Number.isFinite(v) || v < min || v > max || integer2 && !Number.isInteger(v)) invalidProfile(`${name} must be ${integer2 ? "an integer" : "a finite number"} between ${min} and ${max}.`);
|
|
@@ -20128,8 +20516,8 @@ async function checkRender(options) {
|
|
|
20128
20516
|
|
|
20129
20517
|
// packages/cli/src/integrations.ts
|
|
20130
20518
|
import { constants as constants7 } from "node:fs";
|
|
20131
|
-
import { lstat as
|
|
20132
|
-
function
|
|
20519
|
+
import { lstat as lstat8, open as open7 } from "node:fs/promises";
|
|
20520
|
+
function object9(value) {
|
|
20133
20521
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
20134
20522
|
}
|
|
20135
20523
|
function invalidState(write) {
|
|
@@ -20142,13 +20530,13 @@ function invalidState(write) {
|
|
|
20142
20530
|
);
|
|
20143
20531
|
}
|
|
20144
20532
|
function integrationState(value, write = false) {
|
|
20145
|
-
if (!
|
|
20533
|
+
if (!object9(value) || !["member", "connection"].includes(value.scope) || !Number.isSafeInteger(value.revision) || value.revision < 0 || !(value.updatedAt === null || typeof value.updatedAt === "string" && value.updatedAt.length <= 64 && Number.isFinite(Date.parse(value.updatedAt))) || !Array.isArray(value.entries) || !Array.isArray(value.catalog)) throw invalidState(write);
|
|
20146
20534
|
const providers = /* @__PURE__ */ new Set();
|
|
20147
20535
|
for (const entry of value.entries) {
|
|
20148
|
-
if (!
|
|
20536
|
+
if (!object9(entry) || typeof entry.providerId !== "string" || !entry.providerId || entry.providerId.length > 120 || providers.has(entry.providerId) || typeof entry.enabled !== "boolean" || typeof entry.accessMethod !== "string" || !entry.accessMethod || entry.accessMethod.length > 64 || !object9(entry.preferences)) throw invalidState(write);
|
|
20149
20537
|
providers.add(entry.providerId);
|
|
20150
20538
|
}
|
|
20151
|
-
if (value.catalog.some((provider) => !
|
|
20539
|
+
if (value.catalog.some((provider) => !object9(provider) || typeof provider.id !== "string" || !provider.id)) throw invalidState(write);
|
|
20152
20540
|
return {
|
|
20153
20541
|
scope: value.scope,
|
|
20154
20542
|
revision: value.revision,
|
|
@@ -20161,13 +20549,13 @@ async function readIntegrationSettings(file) {
|
|
|
20161
20549
|
const invalidFile = () => new CliError("INVALID_ARGUMENT", "Choose a regular integration settings JSON file under 16 KiB.", "Store provider credentials in the provider connection, not this file.");
|
|
20162
20550
|
const changedFile = () => new CliError("INVALID_ARGUMENT", "Integration settings changed while being read.", "Choose a stable regular settings file and try again.");
|
|
20163
20551
|
const limit = 16 * 1024;
|
|
20164
|
-
const before = await
|
|
20552
|
+
const before = await lstat8(file);
|
|
20165
20553
|
if (!before.isFile() || before.isSymbolicLink() || before.size > limit) throw invalidFile();
|
|
20166
20554
|
const unchanged2 = (info) => info.isFile() && !info.isSymbolicLink() && info.dev === before.dev && info.ino === before.ino && info.size === before.size && info.mtimeMs === before.mtimeMs && info.ctimeMs === before.ctimeMs;
|
|
20167
20555
|
const handle = await open7(file, constants7.O_RDONLY | constants7.O_NOFOLLOW | constants7.O_NONBLOCK);
|
|
20168
20556
|
try {
|
|
20169
20557
|
const opened = await handle.stat();
|
|
20170
|
-
const current2 = await
|
|
20558
|
+
const current2 = await lstat8(file);
|
|
20171
20559
|
if (!unchanged2(opened) || !unchanged2(current2)) throw changedFile();
|
|
20172
20560
|
const bytes = Buffer.alloc(limit + 1);
|
|
20173
20561
|
let length = 0;
|
|
@@ -20177,7 +20565,7 @@ async function readIntegrationSettings(file) {
|
|
|
20177
20565
|
length += bytesRead;
|
|
20178
20566
|
}
|
|
20179
20567
|
const after = await handle.stat();
|
|
20180
|
-
const finalPath = await
|
|
20568
|
+
const finalPath = await lstat8(file);
|
|
20181
20569
|
if (length > limit || length !== opened.size || !unchanged2(after) || !unchanged2(finalPath)) throw changedFile();
|
|
20182
20570
|
try {
|
|
20183
20571
|
return JSON.parse(bytes.subarray(0, length).toString("utf8"));
|
|
@@ -20235,10 +20623,10 @@ async function setIntegration(client, providerId, settings, expectedRevision) {
|
|
|
20235
20623
|
}
|
|
20236
20624
|
|
|
20237
20625
|
// packages/cli/src/motion-components.ts
|
|
20238
|
-
import { createHash as
|
|
20626
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
20239
20627
|
import { constants as constants8 } from "node:fs";
|
|
20240
|
-
import { lstat as
|
|
20241
|
-
import { dirname as
|
|
20628
|
+
import { lstat as lstat9, mkdir as mkdir6, open as open8, rmdir as rmdir2, unlink as unlink4 } from "node:fs/promises";
|
|
20629
|
+
import { dirname as dirname9, extname as extname4, resolve as resolve6 } from "node:path";
|
|
20242
20630
|
|
|
20243
20631
|
// packages/core/src/motion-liquid.ts
|
|
20244
20632
|
function createLiquidRenderer(config2, context) {
|
|
@@ -20418,8 +20806,8 @@ function createLiquidRenderer(config2, context) {
|
|
|
20418
20806
|
context.strokeStyle = rgba(palette.foreground, 0.14);
|
|
20419
20807
|
context.lineWidth = Math.max(0.5, min * 1e-3);
|
|
20420
20808
|
context.stroke();
|
|
20421
|
-
const
|
|
20422
|
-
if (config2.subtitle)
|
|
20809
|
+
const text9 = [{ key: "title", text: config2.title, x: w * 0.065, y: h * 0.055, width: w * 0.87, height: h * (tall ? 0.095 : 0.12), size: min * 0.062, weight: 760 }];
|
|
20810
|
+
if (config2.subtitle) text9.push({ key: "subtitle", text: config2.subtitle, x: w * 0.067, y: h * (tall ? 0.159 : 0.176), width: w * 0.85, height: h * (tall ? 0.057 : 0.065), size: min * 0.025, opacity: 0.79 });
|
|
20423
20811
|
const active = config2.motion === "reduced" ? items.length - 1 : Math.min(items.length - 1, Math.floor(p * items.length));
|
|
20424
20812
|
items.forEach((item, index) => {
|
|
20425
20813
|
const col = wide ? 0 : index % 2, row = wide ? index : Math.floor(index / 2);
|
|
@@ -20441,11 +20829,11 @@ function createLiquidRenderer(config2, context) {
|
|
|
20441
20829
|
context.lineWidth = min * 3e-3;
|
|
20442
20830
|
context.stroke();
|
|
20443
20831
|
}
|
|
20444
|
-
|
|
20445
|
-
|
|
20446
|
-
if (item.detail)
|
|
20832
|
+
text9.push({ key: `number-${index}`, text: String(index + 1).padStart(2, "0"), x, y, width: min * 0.049, height: min * 0.04, size: min * 0.022, weight: 700, opacity: 0.65 });
|
|
20833
|
+
text9.push({ key: `label-${index}`, text: item.label, x: x + min * 0.06, y, width: width - min * 0.06, height: h * (wide ? 0.062 : tall ? 0.045 : 0.055), size: min * 0.03, weight: 720, opacity: active === index || p >= 0.88 ? 1 : 0.75 });
|
|
20834
|
+
if (item.detail) text9.push({ key: `detail-${index}`, text: item.detail, x, y: y + h * (wide ? 0.065 : tall ? 0.048 : 0.06), width, height: h * (wide ? 0.067 : tall ? 0.072 : 0.065), size: min * 0.022, opacity: 0.72 });
|
|
20447
20835
|
});
|
|
20448
|
-
return
|
|
20836
|
+
return text9;
|
|
20449
20837
|
} };
|
|
20450
20838
|
}
|
|
20451
20839
|
|
|
@@ -20493,7 +20881,7 @@ function createGlassRenderer(config2, context) {
|
|
|
20493
20881
|
if (!Number.isFinite(time)) throw new Error("Glass time must be finite.");
|
|
20494
20882
|
const p = config2.motion === "reduced" ? 1 : clamp(time / config2.duration);
|
|
20495
20883
|
const phase = config2.motion === "reduced" ? 1.85 : p * Math.PI * 1.7;
|
|
20496
|
-
const
|
|
20884
|
+
const text9 = [{
|
|
20497
20885
|
key: "title",
|
|
20498
20886
|
text: config2.title,
|
|
20499
20887
|
x: w * 0.065,
|
|
@@ -20503,7 +20891,7 @@ function createGlassRenderer(config2, context) {
|
|
|
20503
20891
|
size: min * 0.066,
|
|
20504
20892
|
weight: 550
|
|
20505
20893
|
}];
|
|
20506
|
-
if (config2.subtitle)
|
|
20894
|
+
if (config2.subtitle) text9.push({
|
|
20507
20895
|
key: "subtitle",
|
|
20508
20896
|
text: config2.subtitle,
|
|
20509
20897
|
x: w * 0.067,
|
|
@@ -20603,8 +20991,8 @@ function createGlassRenderer(config2, context) {
|
|
|
20603
20991
|
context.moveTo(x, y - min * 0.014);
|
|
20604
20992
|
context.lineTo(x + width * (0.07 + 0.18 * settle), y - min * 0.014);
|
|
20605
20993
|
stroke(color(palette.accent, 0.85), Math.max(1, min * 2e-3));
|
|
20606
|
-
|
|
20607
|
-
if (item.detail)
|
|
20994
|
+
text9.push({ key: `label-${index}`, text: item.label, x, y, width, height: h * (wide ? 0.044 : tall ? 0.043 : 0.048), size: min * 0.031, weight: 620, opacity: 0.74 + settle * 0.26 });
|
|
20995
|
+
if (item.detail) text9.push({
|
|
20608
20996
|
key: `detail-${index}`,
|
|
20609
20997
|
text: item.detail,
|
|
20610
20998
|
x,
|
|
@@ -20615,7 +21003,7 @@ function createGlassRenderer(config2, context) {
|
|
|
20615
21003
|
opacity: 0.72
|
|
20616
21004
|
});
|
|
20617
21005
|
});
|
|
20618
|
-
return
|
|
21006
|
+
return text9;
|
|
20619
21007
|
} };
|
|
20620
21008
|
}
|
|
20621
21009
|
|
|
@@ -20700,8 +21088,8 @@ function createPixelRenderer(config2, context) {
|
|
|
20700
21088
|
const p = clamp(stepped / config2.duration), final = p >= 0.9;
|
|
20701
21089
|
const travel = clamp((p - 0.09) / 0.72) * (items.length - 1), frame = Math.floor(stepped * 10);
|
|
20702
21090
|
const camera = Math.floor(Math.min(p, 0.9) * 24);
|
|
20703
|
-
const
|
|
20704
|
-
if (config2.subtitle)
|
|
21091
|
+
const text9 = [{ key: "title", text: config2.title, x: w * 0.065, y: h * 0.055, width: w * 0.87, height: h * (tall ? 0.095 : 0.12), size: min * 0.06, weight: 750 }];
|
|
21092
|
+
if (config2.subtitle) text9.push({ key: "subtitle", text: config2.subtitle, x: w * 0.067, y: h * (tall ? 0.161 : 0.181), width: w * 0.85, height: h * (tall ? 0.057 : 0.065), size: min * 0.025, opacity: 0.8 });
|
|
20705
21093
|
const sunX = Math.round(gw * 0.77 - camera * 0.08), sunY = world.top + 8;
|
|
20706
21094
|
for (let y = -10; y <= 10; y++) {
|
|
20707
21095
|
const half = Math.floor(Math.sqrt(Math.max(0, 100 - y * y)));
|
|
@@ -20765,7 +21153,7 @@ function createPixelRenderer(config2, context) {
|
|
|
20765
21153
|
sprite(["aaaaaaa", "aaaaaaa", "aaaaaa.", "aaaa..."], markerX + 1, y - 18 + (wave === 2 ? 1 : 0), visited ? 1 : 0.26);
|
|
20766
21154
|
rect2(markerX - 2, y - 1, 5, 1, palette.foreground, 0.5);
|
|
20767
21155
|
}
|
|
20768
|
-
|
|
21156
|
+
text9.push({ key: `milestone-${index}`, text: String(index + 1).padStart(2, "0"), x: (node.x - 5) * unit, y: (node.y + 19) * unit, width: unit * 10, height: unit * 7, size: min * 0.022, align: "center", weight: 750, opacity: visited ? 1 : 0.4 });
|
|
20769
21157
|
});
|
|
20770
21158
|
const fromIndex = Math.min(items.length - 2, Math.floor(travel)), amount = travel - fromIndex;
|
|
20771
21159
|
const from = nodes[fromIndex], to = nodes[fromIndex + 1];
|
|
@@ -20782,11 +21170,11 @@ function createPixelRenderer(config2, context) {
|
|
|
20782
21170
|
const boxW = cellW - w * 0.025, visited = travel >= index;
|
|
20783
21171
|
rect2(x / unit, (y - min * 0.018) / unit, boxW / unit, 1, palette.foreground, 0.16);
|
|
20784
21172
|
rect2(x / unit, (y - min * 0.018) / unit, Math.min(boxW / unit, visited ? 14 : 3), 1, visited ? palette.accent : palette.foreground, visited ? 1 : 0.3);
|
|
20785
|
-
|
|
20786
|
-
if (item.detail)
|
|
21173
|
+
text9.push({ key: `label-${index}`, text: item.label, x, y, width: boxW, height: h * (wide ? 0.054 : tall ? 0.036 : 0.049), size: min * 0.029, weight: 700, opacity: visited ? 1 : 0.6 });
|
|
21174
|
+
if (item.detail) text9.push({ key: `detail-${index}`, text: item.detail, x, y: y + h * (wide ? 0.058 : tall ? 0.039 : 0.053), width: boxW, height: h * (wide ? 0.075 : tall ? 0.058 : 0.09), size: min * 0.022, opacity: visited ? 0.82 : 0.52 });
|
|
20787
21175
|
});
|
|
20788
21176
|
context.globalAlpha = 1;
|
|
20789
|
-
return
|
|
21177
|
+
return text9;
|
|
20790
21178
|
} };
|
|
20791
21179
|
}
|
|
20792
21180
|
|
|
@@ -20868,14 +21256,14 @@ function visualDocumentBoot(config2, factory) {
|
|
|
20868
21256
|
ctx.fillStyle = config2.palette.background;
|
|
20869
21257
|
ctx.fillRect(0, 0, W, H);
|
|
20870
21258
|
ctx.save();
|
|
20871
|
-
let
|
|
21259
|
+
let text9;
|
|
20872
21260
|
try {
|
|
20873
|
-
|
|
21261
|
+
text9 = renderer.draw(current2);
|
|
20874
21262
|
} finally {
|
|
20875
21263
|
ctx.restore();
|
|
20876
21264
|
}
|
|
20877
21265
|
for (const node of nodes.values()) node.style.display = "none";
|
|
20878
|
-
|
|
21266
|
+
text9.forEach(textNode);
|
|
20879
21267
|
scrubber.value = String(current2);
|
|
20880
21268
|
timeLabel.textContent = current2.toFixed(1) + " / " + config2.duration.toFixed(1) + " s";
|
|
20881
21269
|
}
|
|
@@ -21032,8 +21420,8 @@ function createIsometricRenderer(config2, context) {
|
|
|
21032
21420
|
return { draw(time) {
|
|
21033
21421
|
if (!Number.isFinite(time)) throw new Error("Isometric time must be finite.");
|
|
21034
21422
|
const p = config2.motion === "reduced" ? 1 : clamp(time / config2.duration);
|
|
21035
|
-
const
|
|
21036
|
-
if (config2.subtitle)
|
|
21423
|
+
const text9 = [{ key: "title", text: config2.title, x: w * 0.065, y: h * 0.055, width: w * 0.87, height: h * (tall ? 0.095 : 0.12), size: min * (wide ? 0.059 : 0.06), weight: 750 }];
|
|
21424
|
+
if (config2.subtitle) text9.push({ key: "subtitle", text: config2.subtitle, x: w * 0.067, y: h * (tall ? 0.159 : 0.176), width: w * 0.85, height: h * (tall ? 0.057 : 0.065), size: min * 0.025, opacity: 0.78 });
|
|
21037
21425
|
const tiles = [];
|
|
21038
21426
|
for (let ix = 0; ix < 6; ix++) for (let iy = 0; iy < 6; iy++) tiles.push({ x: -1.75 + ix * 0.7, y: -1.6 + iy * 0.7, depth: ix + iy });
|
|
21039
21427
|
tiles.sort((a, b) => a.depth - b.depth).forEach((tile) => {
|
|
@@ -21077,7 +21465,7 @@ function createIsometricRenderer(config2, context) {
|
|
|
21077
21465
|
polygon([project(x - 0.24, y - 0.24, z + 4e-3), project(x + 0.24, y - 0.24, z + 4e-3), project(x + 0.24, y + 0.24, z + 4e-3), project(x - 0.24, y + 0.24, z + 4e-3)], rgba(palette.background, 0.22));
|
|
21078
21466
|
}
|
|
21079
21467
|
const anchor = project(x, y, roofHeights[index] + 0.47);
|
|
21080
|
-
|
|
21468
|
+
text9.push({ key: `node-${index}`, text: String(index + 1).padStart(2, "0"), x: anchor[0] - min * 0.035, y: anchor[1] - min * 0.017, width: min * 0.07, height: min * 0.035, size: min * 0.024, opacity: smooth((p - start2 - 0.11) / 0.11), align: "center", weight: 750 });
|
|
21081
21469
|
});
|
|
21082
21470
|
items.forEach((item, index) => {
|
|
21083
21471
|
const columns = wide ? 1 : 2, col = index % columns, row = Math.floor(index / columns);
|
|
@@ -21088,11 +21476,11 @@ function createIsometricRenderer(config2, context) {
|
|
|
21088
21476
|
line([[x, y - min * 0.012], [x + width, y - min * 0.012]], rgba(palette.foreground, 0.17), strokeWidth);
|
|
21089
21477
|
const bar = ease((p - 0.12 - index * 0.105) / 0.19);
|
|
21090
21478
|
line([[x, y - min * 0.012], [x + width * bar, y - min * 0.012]], rgba(palette.accent, 0.95), strokeWidth * 2.1);
|
|
21091
|
-
|
|
21092
|
-
|
|
21093
|
-
if (item.detail)
|
|
21479
|
+
text9.push({ key: `index-${index}`, text: String(index + 1).padStart(2, "0"), x, y, width: min * 0.049, height: min * 0.04, size: min * 0.023, opacity: 0.72 + reveal * 0.28, weight: 650 });
|
|
21480
|
+
text9.push({ key: `label-${index}`, text: item.label, x: x + min * 0.06, y, width: width - min * 0.06, height: h * (wide ? 0.062 : tall ? 0.045 : 0.055), size: min * 0.03, weight: 700, opacity: 0.58 + reveal * 0.42 });
|
|
21481
|
+
if (item.detail) text9.push({ key: `detail-${index}`, text: item.detail, x, y: y + h * (wide ? 0.063 : tall ? 0.048 : 0.06), width, height: h * (wide ? 0.067 : tall ? 0.072 : 0.065), size: min * 0.022, opacity: 0.52 + reveal * 0.25 });
|
|
21094
21482
|
});
|
|
21095
|
-
return
|
|
21483
|
+
return text9;
|
|
21096
21484
|
} };
|
|
21097
21485
|
}
|
|
21098
21486
|
|
|
@@ -21216,7 +21604,7 @@ function createSketchRenderer(config2, context) {
|
|
|
21216
21604
|
const phase = reduced ? 1 : clampedTime / config2.duration;
|
|
21217
21605
|
const frame = reduced ? 0 : Math.floor(clampedTime * 8);
|
|
21218
21606
|
const titleOpacity = reduced ? 1 : 0.25 + 0.75 * ease(phase / 0.045);
|
|
21219
|
-
const
|
|
21607
|
+
const text9 = [
|
|
21220
21608
|
{ key: "title", text: config2.title, x: W * 0.065, y: H * 0.055, width: W * 0.87, height: H * 0.145, size: S * 0.081, weight: 600, opacity: titleOpacity },
|
|
21221
21609
|
{ key: "subtitle", text: config2.subtitle || "", x: W * 0.067, y: H * 0.216, width: W * 0.84, height: H * 0.052, size: S * 0.026, weight: 400, opacity: titleOpacity }
|
|
21222
21610
|
];
|
|
@@ -21249,9 +21637,9 @@ function createSketchRenderer(config2, context) {
|
|
|
21249
21637
|
context.restore();
|
|
21250
21638
|
}
|
|
21251
21639
|
const numberSize = Math.min(S * 0.039, node.radius * 0.57);
|
|
21252
|
-
|
|
21253
|
-
|
|
21254
|
-
|
|
21640
|
+
text9.push({ key: `number-${index}`, text: String(index + 1).padStart(2, "0"), x: node.x - node.radius * 0.52, y: node.y - numberSize * 0.64, width: node.radius * 1.04, height: numberSize * 1.4, size: numberSize, weight: 400, align: "center", opacity: appear });
|
|
21641
|
+
text9.push({ key: `label-${index}`, text: config2.items[index].label, ...node.label, size: S * (vertical ? 0.043 : 0.046), weight: 600, align: node.align, opacity: appear });
|
|
21642
|
+
text9.push({ key: `detail-${index}`, text: config2.items[index].detail || "", ...node.detail, size: S * (portrait ? 0.028 : 0.024), weight: 400, align: node.align, opacity: appear });
|
|
21255
21643
|
const underline = ease((phase - nodeStart(index) - 0.2) / 0.12);
|
|
21256
21644
|
const lineY = node.label.y + node.label.height + S * 4e-3;
|
|
21257
21645
|
const lineWidth = node.label.width * (vertical ? 0.43 : 0.66);
|
|
@@ -21263,7 +21651,7 @@ function createSketchRenderer(config2, context) {
|
|
|
21263
21651
|
stroke(ellipse(last.x, last.y, last.radius * 1.13, last.radius * 1.115, -0.66, Math.PI * 1.76), finalEmphasis, frame, 66, palette.accent, S * 26e-4, 0.85);
|
|
21264
21652
|
stroke([{ x: W * 0.067, y: H * 0.94 }, { x: W * 0.933, y: H * 0.94 }], 1, frame, 96, palette.foreground, S * 13e-4, 0.14, false);
|
|
21265
21653
|
stroke([{ x: W * 0.067, y: H * 0.94 }, { x: W * 0.933, y: H * 0.94 }], reduced ? 1 : clamp(phase / 0.88), frame, 95, palette.accent, S * 24e-4, 0.8, false);
|
|
21266
|
-
return
|
|
21654
|
+
return text9;
|
|
21267
21655
|
}
|
|
21268
21656
|
};
|
|
21269
21657
|
}
|
|
@@ -21534,10 +21922,10 @@ function motionDocumentBoot(config2) {
|
|
|
21534
21922
|
return p * p * p * (p * (p * 6 - 15) + 10);
|
|
21535
21923
|
};
|
|
21536
21924
|
const lerp = (a, b, p) => a + (b - a) * p;
|
|
21537
|
-
const element = (tag, className, parent = stage,
|
|
21925
|
+
const element = (tag, className, parent = stage, text9) => {
|
|
21538
21926
|
const node = document.createElement(tag);
|
|
21539
21927
|
node.className = className;
|
|
21540
|
-
if (
|
|
21928
|
+
if (text9 !== void 0) node.textContent = text9;
|
|
21541
21929
|
parent.appendChild(node);
|
|
21542
21930
|
return node;
|
|
21543
21931
|
};
|
|
@@ -21608,9 +21996,9 @@ function motionDocumentBoot(config2) {
|
|
|
21608
21996
|
}
|
|
21609
21997
|
config2.items.forEach((item, index) => {
|
|
21610
21998
|
const pos = layout.overview[index];
|
|
21611
|
-
const
|
|
21612
|
-
stickerObjects.push(
|
|
21613
|
-
box(
|
|
21999
|
+
const object11 = element("div", "sticker-object", world);
|
|
22000
|
+
stickerObjects.push(object11);
|
|
22001
|
+
box(object11, pos.x - pos.size / 2, pos.y - pos.size / 2, pos.size, pos.size);
|
|
21614
22002
|
if (item.image) {
|
|
21615
22003
|
const image2 = document.createElement("img");
|
|
21616
22004
|
image2.src = item.image;
|
|
@@ -21619,7 +22007,7 @@ function motionDocumentBoot(config2) {
|
|
|
21619
22007
|
image2.draggable = false;
|
|
21620
22008
|
image2.dataset.stickerIndex = String(index);
|
|
21621
22009
|
image2.style.visibility = "hidden";
|
|
21622
|
-
|
|
22010
|
+
object11.appendChild(image2);
|
|
21623
22011
|
images.push(image2);
|
|
21624
22012
|
} else {
|
|
21625
22013
|
const illustration = svgNode("svg", { viewBox: "-125 -125 250 250", class: "illustration", role: "img", "aria-label": "Illustration: " + item.label });
|
|
@@ -21627,7 +22015,7 @@ function motionDocumentBoot(config2) {
|
|
|
21627
22015
|
illustration.appendChild(svgNode("path", { d: pathOf(contours[index]), fill: "none", stroke: "var(--fg)", "stroke-width": "1.4", "stroke-linejoin": "round" }));
|
|
21628
22016
|
const glyph = svgNode("path", { d: index % 2 ? "M-24,0 L-6,18 L30,-22" : "M-28,0 H28 M0,-28 V28", fill: "none", stroke: "var(--fg)", "stroke-width": "6", "stroke-linecap": "round", "stroke-linejoin": "round" });
|
|
21629
22017
|
illustration.appendChild(glyph);
|
|
21630
|
-
|
|
22018
|
+
object11.appendChild(illustration);
|
|
21631
22019
|
}
|
|
21632
22020
|
const overviewLabel = element("div", "overview-label measured", world, item.label);
|
|
21633
22021
|
box(overviewLabel, pos.x - pos.labelWidth / 2, pos.y + pos.size / 2 + s * 0.02, pos.labelWidth, pos.labelHeight);
|
|
@@ -21702,9 +22090,9 @@ function motionDocumentBoot(config2) {
|
|
|
21702
22090
|
const ca = cameraFor(a.index), cb = cameraFor(b.index), focus = lerp(ca.focus, cb.focus, blend);
|
|
21703
22091
|
const x = lerp(ca.x, cb.x, blend), y = lerp(ca.y, cb.y, blend), z = lerp(ca.z, cb.z, blend);
|
|
21704
22092
|
stickerPaint.clearRect(0, 0, W, H);
|
|
21705
|
-
stickerObjects.forEach((
|
|
22093
|
+
stickerObjects.forEach((object11, index) => {
|
|
21706
22094
|
const pos = layout.overview[index];
|
|
21707
|
-
box(
|
|
22095
|
+
box(object11, x + (pos.x - pos.size / 2) * z, y + (pos.y - pos.size / 2) * z, pos.size * z, pos.size * z);
|
|
21708
22096
|
});
|
|
21709
22097
|
overviewLabels.forEach((node, index) => {
|
|
21710
22098
|
const pos = layout.overview[index];
|
|
@@ -21785,7 +22173,7 @@ function motionDocumentBoot(config2) {
|
|
|
21785
22173
|
const width = ink.width / scale, height = ink.height / scale;
|
|
21786
22174
|
return { text: node.textContent, x, y, width, height, fontSize: parseFloat(getComputedStyle(node).fontSize), container: { x: (rect2.x - stageRect.x) / scale, y: (rect2.y - stageRect.y) / scale, width: rect2.width / scale, height: rect2.height / scale }, outOfBounds: x < -1 || y < -1 || x + width > W + 1 || y + height > H + 1, overflow: node.scrollWidth > node.clientWidth + 1 || node.scrollHeight > node.clientHeight + 1 };
|
|
21787
22175
|
});
|
|
21788
|
-
return { ready: runtime.__ready, error: runtime.__motionError || null, time: currentTime, format: layout.format, activeItem: currentIndex, overview: overviewState, textBounds, outOfBounds: textBounds.some((
|
|
22176
|
+
return { ready: runtime.__ready, error: runtime.__motionError || null, time: currentTime, format: layout.format, activeItem: currentIndex, overview: overviewState, textBounds, outOfBounds: textBounds.some((text9) => text9.outOfBounds || text9.overflow), illustrationFallbacks: config2.items.map((item, index) => !item.image && config2.componentId === "sticker-explainer" ? index : -1).filter((index) => index >= 0), fontLoaded: document.fonts.check("16px EditorBrainEmbedded"), imagesDecoded: images.every((image2) => image2.complete && image2.naturalWidth > 0) };
|
|
21789
22177
|
};
|
|
21790
22178
|
let playing = false, frame = 0, playStart = 0, timeStart = 0;
|
|
21791
22179
|
const pause = () => {
|
|
@@ -21932,18 +22320,18 @@ ${motionSceneLayout.toString()}
|
|
|
21932
22320
|
|
|
21933
22321
|
// packages/cli/src/motion-components.ts
|
|
21934
22322
|
var fail = (message) => new CliError("MOTION_COMPONENT_INVALID", message, "Use a pinned component, explicit resolved token bindings, a local WOFF2/TTF font and 2\u20134 plain-text items.");
|
|
21935
|
-
function
|
|
22323
|
+
function object10(value, name, allowed) {
|
|
21936
22324
|
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => !allowed.includes(key))) throw fail(`${name} has invalid or unsupported fields.`);
|
|
21937
22325
|
}
|
|
21938
22326
|
function inputDocument(value) {
|
|
21939
|
-
|
|
22327
|
+
object10(value, "Input", ["componentId", "componentVersion", "bindings", "fontFile", "title", "subtitle", "duration", "items", "motion"]);
|
|
21940
22328
|
if (typeof value.componentId !== "string" || value.componentVersion !== "1.0.0" || !getMotionComponent(value.componentId, value.componentVersion)) throw fail("Unknown component or unsupported pinned componentVersion.");
|
|
21941
|
-
|
|
22329
|
+
object10(value.bindings, "bindings", ["background", "foreground", "accent", "fontFamily"]);
|
|
21942
22330
|
for (const role of ["background", "foreground", "accent", "fontFamily"]) if (typeof value.bindings[role] !== "string" || !/^[a-z][a-z0-9_.-]{0,79}$/.test(value.bindings[role])) throw fail(`bindings.${role} must name an explicit semantic token key.`);
|
|
21943
22331
|
if (typeof value.fontFile !== "string" || !value.fontFile) throw fail("fontFile is required.");
|
|
21944
22332
|
if (!Array.isArray(value.items) || value.items.length < 2 || value.items.length > 4) throw fail("Input requires 2\u20134 items.");
|
|
21945
22333
|
for (const item of value.items) {
|
|
21946
|
-
|
|
22334
|
+
object10(item, "item", ["label", "detail", "imageFile", "shape"]);
|
|
21947
22335
|
if (item.imageFile !== void 0 && (typeof item.imageFile !== "string" || !item.imageFile)) throw fail("imageFile must be an explicit local image path.");
|
|
21948
22336
|
}
|
|
21949
22337
|
return value;
|
|
@@ -21967,22 +22355,22 @@ async function sourceFile(path, role, max) {
|
|
|
21967
22355
|
}
|
|
21968
22356
|
const after = await handle.stat();
|
|
21969
22357
|
await assertNoMediaSymlinks(path);
|
|
21970
|
-
const current2 = await
|
|
22358
|
+
const current2 = await lstat9(path);
|
|
21971
22359
|
if (before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs || current2.dev !== before.dev || current2.ino !== before.ino || current2.size !== after.size || current2.mtimeMs !== after.mtimeMs) throw fail(`${role} changed during reading.`);
|
|
21972
|
-
return { source: { role, path, bytes: content.length, sha256:
|
|
22360
|
+
return { source: { role, path, bytes: content.length, sha256: createHash7("sha256").update(content).digest("hex") }, content };
|
|
21973
22361
|
} finally {
|
|
21974
22362
|
await handle.close();
|
|
21975
22363
|
}
|
|
21976
22364
|
}
|
|
21977
22365
|
function embed(content, path, kind) {
|
|
21978
22366
|
const formats = kind === "font" ? { ".woff2": "font/woff2", ".ttf": "font/ttf" } : { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp" };
|
|
21979
|
-
const mime = formats[
|
|
22367
|
+
const mime = formats[extname4(path).toLowerCase()];
|
|
21980
22368
|
if (!mime) throw fail(`${kind} file type is unsupported. Use ${kind === "font" ? "WOFF2/TTF" : "PNG/JPEG/WebP"}.`);
|
|
21981
22369
|
return `data:${mime};base64,${content.toString("base64")}`;
|
|
21982
22370
|
}
|
|
21983
22371
|
async function exists2(path) {
|
|
21984
22372
|
try {
|
|
21985
|
-
await
|
|
22373
|
+
await lstat9(path);
|
|
21986
22374
|
return true;
|
|
21987
22375
|
} catch (error) {
|
|
21988
22376
|
if (error.code === "ENOENT") return false;
|
|
@@ -21996,18 +22384,18 @@ async function makeParents(root, path, created) {
|
|
|
21996
22384
|
const target = await checkedPath(root, relative2, false);
|
|
21997
22385
|
try {
|
|
21998
22386
|
await mkdir6(target, { mode: 448 });
|
|
21999
|
-
const info = await
|
|
22387
|
+
const info = await lstat9(target);
|
|
22000
22388
|
created.push({ path: relative2, dev: info.dev, ino: info.ino, directory: true });
|
|
22001
22389
|
} catch (error) {
|
|
22002
22390
|
if (error.code !== "EEXIST") throw error;
|
|
22003
|
-
if (!(await
|
|
22391
|
+
if (!(await lstat9(await checkedPath(root, relative2, false))).isDirectory()) throw fail("Output parent must be a directory.");
|
|
22004
22392
|
}
|
|
22005
22393
|
}
|
|
22006
22394
|
}
|
|
22007
22395
|
async function cleanCreated2(root, created) {
|
|
22008
22396
|
for (const item of [...created].reverse()) {
|
|
22009
22397
|
try {
|
|
22010
|
-
const path = await checkedPath(root, item.path, false), info = await
|
|
22398
|
+
const path = await checkedPath(root, item.path, false), info = await lstat9(path);
|
|
22011
22399
|
if (info.dev !== item.dev || info.ino !== item.ino || info.isSymbolicLink()) continue;
|
|
22012
22400
|
if (item.directory) await rmdir2(path);
|
|
22013
22401
|
else await unlink4(path);
|
|
@@ -22035,14 +22423,14 @@ async function prepareMotionComponent(options) {
|
|
|
22035
22423
|
if (typeof value !== "string" || !value.trim()) throw fail(`Required binding ${role} references missing or non-string token ${key}. No demo default was applied.`);
|
|
22036
22424
|
return [role, value];
|
|
22037
22425
|
}));
|
|
22038
|
-
const fontPath = inputPath(
|
|
22426
|
+
const fontPath = inputPath(dirname9(input.source.path), document2.fontFile);
|
|
22039
22427
|
const font = await sourceFile(fontPath, "font", MOTION_INPUT_LIMITS.fontBytes);
|
|
22040
22428
|
const sources = [input.source, font.source];
|
|
22041
22429
|
const items = [];
|
|
22042
22430
|
for (const [index, item] of document2.items.entries()) {
|
|
22043
22431
|
let image2;
|
|
22044
22432
|
if (item.imageFile) {
|
|
22045
|
-
const path = inputPath(
|
|
22433
|
+
const path = inputPath(dirname9(input.source.path), item.imageFile);
|
|
22046
22434
|
const file = await sourceFile(path, `items[${index}].image`, MOTION_INPUT_LIMITS.imageBytes);
|
|
22047
22435
|
image2 = embed(file.content, path, "image");
|
|
22048
22436
|
sources.push(file.source);
|
|
@@ -22083,7 +22471,7 @@ async function prepareMotionComponent(options) {
|
|
|
22083
22471
|
resolvedBindings: Object.fromEntries(Object.entries(document2.bindings).map(([role, key]) => [role, { key, value: bound[role], source: resolved.tokenSources[key] }])),
|
|
22084
22472
|
sources,
|
|
22085
22473
|
context: { sourceHashes: resolved.lock.sourceHashes, output: resolved.output, mediaPreferences: resolved.mediaPreferences },
|
|
22086
|
-
files: { "config.json": { sha256:
|
|
22474
|
+
files: { "config.json": { sha256: createHash7("sha256").update(configJson).digest("hex") }, "index.html": { sha256: createHash7("sha256").update(html).digest("hex") } },
|
|
22087
22475
|
warnings: resolved.warnings
|
|
22088
22476
|
};
|
|
22089
22477
|
const readme = `# EditorBrain motion component
|
|
@@ -22103,9 +22491,9 @@ Preparation is not a rendered video or a quality certification. Inspect text fit
|
|
|
22103
22491
|
if (await exists2(await productionOutputPath(root, options.outDirectory))) throw new CliError("MOTION_OUTPUT_EXISTS", "The output directory already exists; nothing was overwritten.", "Choose a new directory under .editorbrain-local/.");
|
|
22104
22492
|
const created = [];
|
|
22105
22493
|
try {
|
|
22106
|
-
await makeParents(root,
|
|
22494
|
+
await makeParents(root, dirname9(options.outDirectory), created);
|
|
22107
22495
|
await mkdir6(await productionOutputPath(root, options.outDirectory), { mode: 448 });
|
|
22108
|
-
const info = await
|
|
22496
|
+
const info = await lstat9(output2);
|
|
22109
22497
|
created.push({ path: options.outDirectory, dev: info.dev, ino: info.ino, directory: true });
|
|
22110
22498
|
for (const [name, content] of Object.entries(files)) {
|
|
22111
22499
|
const path = `${options.outDirectory}/${name}`;
|
|
@@ -22141,12 +22529,12 @@ Preparation is not a rendered video or a quality certification. Inspect text fit
|
|
|
22141
22529
|
}
|
|
22142
22530
|
|
|
22143
22531
|
// packages/cli/src/authored-motion.ts
|
|
22144
|
-
import { lstat as
|
|
22532
|
+
import { lstat as lstat10 } from "node:fs/promises";
|
|
22145
22533
|
async function downloadAuthoredSource(client, pin, root, out, fetcher = fetch) {
|
|
22146
22534
|
if (!/^[a-z0-9][a-z0-9-]{0,79}$/.test(pin.id) || !/^\d+\.\d+\.\d+$/.test(pin.version) || !/^[a-f0-9]{64}$/.test(pin.sha256) || !Number.isSafeInteger(pin.bytes) || pin.bytes < 1 || pin.bytes > 16 * 1024 * 1024) throw new CliError("INVALID_ARGUMENT", "Invalid pinned scene bundle.", "Retrieve one authored scene from the catalog.");
|
|
22147
22535
|
const target = await checkedPath(root, out, false);
|
|
22148
22536
|
try {
|
|
22149
|
-
await
|
|
22537
|
+
await lstat10(target);
|
|
22150
22538
|
throw new CliError("FILE_EXISTS", "The output already exists.", "Choose a new archive path.");
|
|
22151
22539
|
} catch (error) {
|
|
22152
22540
|
if (error.code !== "ENOENT") throw error;
|
|
@@ -22177,6 +22565,294 @@ async function downloadAuthoredSource(client, pin, root, out, fetcher = fetch) {
|
|
|
22177
22565
|
return { id: pin.id, version: pin.version, path: target, sha256: pin.sha256, bytes: size, extracted: false, executed: false, rendered: false, next: "Inspect bundle README and selected card; extract into a new local directory. Bind current project colors, fonts, language, content, timing and sound before rendering. The demo is not your finished edit." };
|
|
22178
22566
|
}
|
|
22179
22567
|
|
|
22568
|
+
// packages/cli/src/production-verify.ts
|
|
22569
|
+
import { createHash as createHash8, randomUUID as randomUUID3 } from "node:crypto";
|
|
22570
|
+
import { mkdir as mkdir7, readFile as readFile4 } from "node:fs/promises";
|
|
22571
|
+
import { dirname as dirname10, join as join7, resolve as resolve7 } from "node:path";
|
|
22572
|
+
var digest2 = (value) => createHash8("sha256").update(value).digest("hex");
|
|
22573
|
+
var errorCode2 = (error) => error && typeof error === "object" && "code" in error && typeof error.code === "string" && /^[A-Z0-9_]+$/.test(error.code) ? error.code : "PRODUCTION_VERIFY_FAILED";
|
|
22574
|
+
function productionVerificationRequirements(options) {
|
|
22575
|
+
const { resolved } = options;
|
|
22576
|
+
const voice = resolved.audioProduction?.voice;
|
|
22577
|
+
const loudness = resolved.audioProduction?.loudness ?? null;
|
|
22578
|
+
if (loudness) validateAudioCheckProfile({ schemaVersion: 1, loudness });
|
|
22579
|
+
const voiceRequired = !!voice && ["original", "catalog", "cloned"].includes(voice.mode);
|
|
22580
|
+
const audio = options.audio ?? (voiceRequired || loudness ? "required" : "optional");
|
|
22581
|
+
if (audio === "forbidden" && loudness) throw new CliError("PRODUCTION_REQUIREMENTS_CONFLICT", "Forbidden audio conflicts with the project loudness target.", "Resolve the explicit project audio requirements before verification.");
|
|
22582
|
+
const renderProfile = {
|
|
22583
|
+
schemaVersion: 1,
|
|
22584
|
+
expected: { width: resolved.output.width, height: resolved.output.height, fps: resolved.output.fps, ...options.expectedDurationSeconds !== void 0 ? { durationSeconds: options.expectedDurationSeconds } : {} },
|
|
22585
|
+
audio
|
|
22586
|
+
};
|
|
22587
|
+
validateRenderCheckProfile(renderProfile);
|
|
22588
|
+
return { renderProfile, audioBasis: options.audio !== void 0 ? "explicit_verification_requirement" : voiceRequired ? "configured_voice" : loudness ? "configured_loudness" : "no_explicit_track_requirement", loudness, durationBasis: options.expectedDurationSeconds !== void 0 ? "explicit" : "unspecified" };
|
|
22589
|
+
}
|
|
22590
|
+
function parseVerificationSamplePts(stderr) {
|
|
22591
|
+
const match = /\[[^\]\r\n]*showinfo[^\]\r\n]*\]\s+n:\s*0\s+pts:\s*-?\d+\s+pts_time:([-\d.e+]+)/i.exec(stderr);
|
|
22592
|
+
const value = match ? Number(match[1]) : NaN;
|
|
22593
|
+
return Number.isFinite(value) ? value : null;
|
|
22594
|
+
}
|
|
22595
|
+
async function sampleFinalArtifact(file, probe, directory, requested) {
|
|
22596
|
+
const video = probe.streams.find((stream) => stream.type === "video" && !stream.attachedPicture);
|
|
22597
|
+
const result2 = { status: "not_available", streamIndex: video?.index ?? null, items: [], failures: [] };
|
|
22598
|
+
if (!video) return result2;
|
|
22599
|
+
const duration = video.durationSeconds ?? probe.durationSeconds;
|
|
22600
|
+
const times = requested ? [...new Set(requested)] : duration && duration > 0 ? [.../* @__PURE__ */ new Set([0, duration / 2, Math.max(0, duration - Math.min(0.25, duration / 4))])] : [0];
|
|
22601
|
+
for (const time of times) {
|
|
22602
|
+
if (duration !== null && duration !== void 0 && time >= duration) {
|
|
22603
|
+
result2.failures.push({ requestedTimeSeconds: time, code: "MEDIA_SAMPLE_OUT_OF_RANGE" });
|
|
22604
|
+
continue;
|
|
22605
|
+
}
|
|
22606
|
+
const path = join7(directory, `sample-${String(result2.items.length + result2.failures.length + 1).padStart(2, "0")}.png`);
|
|
22607
|
+
try {
|
|
22608
|
+
const output2 = await runMediaTool("ffmpeg", [
|
|
22609
|
+
"-loglevel",
|
|
22610
|
+
"info",
|
|
22611
|
+
"-nostats",
|
|
22612
|
+
"-n",
|
|
22613
|
+
"-copyts",
|
|
22614
|
+
"-start_at_zero",
|
|
22615
|
+
...localInputArgs(file),
|
|
22616
|
+
"-map",
|
|
22617
|
+
`0:${video.index}`,
|
|
22618
|
+
"-an",
|
|
22619
|
+
"-sn",
|
|
22620
|
+
"-dn",
|
|
22621
|
+
"-vf",
|
|
22622
|
+
`select=gte(t\\,${time}),showinfo,scale='min(1920,iw)':'min(1920,ih)':force_original_aspect_ratio=decrease`,
|
|
22623
|
+
"-frames:v",
|
|
22624
|
+
"1",
|
|
22625
|
+
"-fps_mode",
|
|
22626
|
+
"passthrough",
|
|
22627
|
+
"-update",
|
|
22628
|
+
"1",
|
|
22629
|
+
path
|
|
22630
|
+
], { timeoutMs: 6e4, maxOutputBytes: 1024 * 1024 });
|
|
22631
|
+
const bytes = await readFile4(path);
|
|
22632
|
+
if (!bytes.length) throw new CliError("MEDIA_SAMPLE_EMPTY", "The selected frame produced no image.", "Review the final video directly.");
|
|
22633
|
+
result2.items.push({ path, sha256: digest2(bytes), bytes: bytes.length, requestedTimeSeconds: time, actualPresentationTimeSeconds: parseVerificationSamplePts(output2.stderr), timestampBasis: "seconds_from_container_start", contentReviewed: false });
|
|
22634
|
+
} catch (error) {
|
|
22635
|
+
result2.failures.push({ requestedTimeSeconds: time, code: errorCode2(error) });
|
|
22636
|
+
}
|
|
22637
|
+
}
|
|
22638
|
+
result2.status = result2.failures.length ? "incomplete" : "complete";
|
|
22639
|
+
return result2;
|
|
22640
|
+
}
|
|
22641
|
+
function pendingReviews(resolved) {
|
|
22642
|
+
const items = [
|
|
22643
|
+
{ code: "EDITORIAL_REVIEW", message: "Watch the delivered video for message, timing, continuity and closure. Technical checks do not establish editorial quality." },
|
|
22644
|
+
{ code: "VISUAL_REVIEW", message: "View samples and final output at delivery size for text, captions, crop, safe areas and visual defects. Samples are bounded previews, not continuous playback evidence." },
|
|
22645
|
+
{ code: "BRAND_REVIEW", message: "Verify actual fonts, logo, palette and style in the rendered output; configured tokens do not prove rendered identity." },
|
|
22646
|
+
{ code: "AUDIO_REVIEW", message: "If audio exists, listen to the final mix for voice identity, intelligibility, synchronization, music and SFX. Track presence and loudness do not identify or approve these contents." }
|
|
22647
|
+
];
|
|
22648
|
+
if (resolved.audioProduction?.music?.intensity === "none" || resolved.audioProduction?.sfx?.density === "none") items.push({ code: "AUDIO_EXCLUSIONS", message: "Verify the requested music/SFX exclusions by listening. These preferences do not forbid original voice or establish absence of mixed sounds." });
|
|
22649
|
+
return items.map((item) => ({ ...item, status: "pending" }));
|
|
22650
|
+
}
|
|
22651
|
+
function summarizeProductionVerification(report) {
|
|
22652
|
+
return {
|
|
22653
|
+
schemaVersion: report.schemaVersion,
|
|
22654
|
+
kind: report.kind,
|
|
22655
|
+
summary: true,
|
|
22656
|
+
projectId: report.projectId,
|
|
22657
|
+
contextSha256: report.context.sha256,
|
|
22658
|
+
contextVersion: report.context.version,
|
|
22659
|
+
artifact: report.artifact,
|
|
22660
|
+
complete: report.complete,
|
|
22661
|
+
technicalStatus: report.technicalStatus,
|
|
22662
|
+
technicalPass: report.technicalPass,
|
|
22663
|
+
perceptualPending: true,
|
|
22664
|
+
creativeQuality: report.creativeQuality,
|
|
22665
|
+
checks: report.render?.checks ?? [],
|
|
22666
|
+
loudness: report.audio ? { status: report.audio.technicalStatus, streams: report.audio.measurements.slice(0, 4), omitted: Math.max(0, report.audio.measurements.length - 4) } : { status: "not_requested" },
|
|
22667
|
+
samples: report.samples,
|
|
22668
|
+
issues: report.issues,
|
|
22669
|
+
findingCounts: report.render?.findingsSummary ?? null,
|
|
22670
|
+
findings: report.render?.findings.slice(0, 5) ?? [],
|
|
22671
|
+
omittedFindings: Math.max(0, (report.render?.findings.length ?? 0) - 5),
|
|
22672
|
+
pendingReviews: report.pendingReviews.map((item) => item.code),
|
|
22673
|
+
agentInstructions: report.agentInstructions,
|
|
22674
|
+
mediaUploaded: false,
|
|
22675
|
+
mediaModified: false,
|
|
22676
|
+
modelCalls: 0
|
|
22677
|
+
};
|
|
22678
|
+
}
|
|
22679
|
+
async function verifyProduction(options) {
|
|
22680
|
+
const root = await ensureRoot(options.root);
|
|
22681
|
+
assertIdentifier(options.resolved.project.id);
|
|
22682
|
+
const requirements = productionVerificationRequirements(options);
|
|
22683
|
+
if (options.contextVersion !== void 0 && (typeof options.contextVersion !== "string" || !options.contextVersion || options.contextVersion.length > 256 || /[\x00-\x1f\x7f]/.test(options.contextVersion))) throw new CliError("INVALID_CONTEXT_VERSION", "Invalid context version.", "Pass the current project context version.");
|
|
22684
|
+
if (options.sampleTimesSeconds !== void 0 && (!Array.isArray(options.sampleTimesSeconds) || options.sampleTimesSeconds.length < 1 || options.sampleTimesSeconds.length > 6 || Array.from(options.sampleTimesSeconds).some((time) => typeof time !== "number" || !Number.isFinite(time) || time < 0 || time > 86400))) throw new CliError("INVALID_SAMPLE_TIMES", "Provide one to six finite sample times between 0 and 86400 seconds.", "Select a few timestamps that answer a review question.");
|
|
22685
|
+
const relativeDirectory = options.outDirectory ?? `.editorbrain-local/${options.resolved.project.id}/verify-${randomUUID3()}`;
|
|
22686
|
+
const directory = await productionOutputPath(root, relativeDirectory);
|
|
22687
|
+
await mkdir7(dirname10(directory), { recursive: true, mode: 448 });
|
|
22688
|
+
await productionOutputPath(root, relativeDirectory);
|
|
22689
|
+
try {
|
|
22690
|
+
await mkdir7(directory, { mode: 448 });
|
|
22691
|
+
} catch (error) {
|
|
22692
|
+
if (error.code === "EEXIST") throw new CliError("PRODUCTION_OUTPUT_EXISTS", "Verification output already exists; nothing was overwritten.", "Choose a new directory under .editorbrain-local/.");
|
|
22693
|
+
throw error;
|
|
22694
|
+
}
|
|
22695
|
+
const file = resolve7(root, options.file);
|
|
22696
|
+
const report = {
|
|
22697
|
+
schemaVersion: 1,
|
|
22698
|
+
kind: "editorbrain.production-verification",
|
|
22699
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
22700
|
+
projectId: options.resolved.project.id,
|
|
22701
|
+
context: { sha256: digest2(stableJson(options.resolved)), version: options.contextVersion ?? null, sourceHashes: { ...options.resolved.lock.sourceHashes }, validAtStart: null, validAtEnd: null },
|
|
22702
|
+
artifact: { path: file, sha256: null, bytes: null, stableAcrossVerification: null },
|
|
22703
|
+
requirements,
|
|
22704
|
+
complete: false,
|
|
22705
|
+
technicalStatus: "incomplete",
|
|
22706
|
+
technicalPass: false,
|
|
22707
|
+
perceptualPending: true,
|
|
22708
|
+
render: null,
|
|
22709
|
+
audio: null,
|
|
22710
|
+
audioMeasurement: "not_requested",
|
|
22711
|
+
samples: { status: "not_available", streamIndex: null, items: [], failures: [] },
|
|
22712
|
+
issues: [],
|
|
22713
|
+
pendingReviews: pendingReviews(options.resolved),
|
|
22714
|
+
agentInstructions: [
|
|
22715
|
+
"Open the saved sample images and actual final file before making visual or audio claims. Samples have not been viewed automatically.",
|
|
22716
|
+
"Reconcile black, silence and peak candidates with the brief. A technical pass never approves storytelling, readability, music/SFX, fonts or synchronization.",
|
|
22717
|
+
"Keep human approval separate. A changed artifact or project context needs a new verification; reuse unchanged source observations instead of reloading every document."
|
|
22718
|
+
],
|
|
22719
|
+
mediaUploaded: false,
|
|
22720
|
+
mediaModified: false,
|
|
22721
|
+
modelCalls: 0,
|
|
22722
|
+
creativeQuality: "not_assessed"
|
|
22723
|
+
};
|
|
22724
|
+
const contextCheck = async (phase) => {
|
|
22725
|
+
if (!options.validateContext) return true;
|
|
22726
|
+
try {
|
|
22727
|
+
const valid = await options.validateContext();
|
|
22728
|
+
if (typeof valid !== "boolean") throw new Error("Invalid context validator result.");
|
|
22729
|
+
report.context[phase] = valid;
|
|
22730
|
+
if (!valid) report.issues.push({ code: "PRODUCTION_CONTEXT_CHANGED", severity: "error", message: "Project context changed; this verification is stale for the current project." });
|
|
22731
|
+
return valid;
|
|
22732
|
+
} catch {
|
|
22733
|
+
report.issues.push({ code: "PRODUCTION_CONTEXT_UNVERIFIED", severity: "incomplete", message: "Current project context could not be revalidated." });
|
|
22734
|
+
return false;
|
|
22735
|
+
}
|
|
22736
|
+
};
|
|
22737
|
+
if (await contextCheck("validAtStart")) {
|
|
22738
|
+
report.render = await checkRender({ file, profile: requirements.renderProfile });
|
|
22739
|
+
report.artifact = { ...report.render.file, stableAcrossVerification: null };
|
|
22740
|
+
const probe = report.render.measurements.probe;
|
|
22741
|
+
if (requirements.loudness) {
|
|
22742
|
+
report.audioMeasurement = "checked";
|
|
22743
|
+
report.audio = await checkAudio({ file, profile: { schemaVersion: 1, loudness: requirements.loudness } });
|
|
22744
|
+
}
|
|
22745
|
+
if (probe && report.render.checks.find((check) => check.id === "decode")?.status === "pass") report.samples = await sampleFinalArtifact(file, probe, directory, options.sampleTimesSeconds);
|
|
22746
|
+
if (probe) {
|
|
22747
|
+
try {
|
|
22748
|
+
const final = await probeLocalMedia(file);
|
|
22749
|
+
const stable = final.sha256 === probe.sha256 && final.bytes === probe.bytes && (!report.audio || report.audio.file.sha256 === probe.sha256 && report.audio.file.bytes === probe.bytes);
|
|
22750
|
+
report.artifact.stableAcrossVerification = stable;
|
|
22751
|
+
if (!stable) report.issues.push({ code: "PRODUCTION_ARTIFACT_CHANGED", severity: "error", message: "The final artifact changed between technical checks or samples; evidence cannot validate the current file." });
|
|
22752
|
+
} catch {
|
|
22753
|
+
report.issues.push({ code: "PRODUCTION_ARTIFACT_UNVERIFIED", severity: "incomplete", message: "The final artifact fingerprint could not be reverified." });
|
|
22754
|
+
}
|
|
22755
|
+
}
|
|
22756
|
+
await contextCheck("validAtEnd");
|
|
22757
|
+
}
|
|
22758
|
+
report.complete = !!report.render?.complete && (!report.audio || report.audio.complete) && report.artifact.stableAcrossVerification === true && report.samples.status === "complete" && !report.issues.length;
|
|
22759
|
+
const failed = report.issues.some((issue) => issue.severity === "error") || report.render?.technicalStatus === "fail" || report.audio?.technicalStatus === "fail";
|
|
22760
|
+
report.technicalStatus = failed ? "fail" : !report.complete ? "incomplete" : report.render?.technicalStatus === "warning" ? "warning" : "pass";
|
|
22761
|
+
report.technicalPass = report.complete && !failed;
|
|
22762
|
+
const reportPath = await saveProductionReport(root, `${relativeDirectory}/report.json`, report);
|
|
22763
|
+
const reportSha256 = digest2(stableJson(report));
|
|
22764
|
+
return { report, summary: { ...summarizeProductionVerification(report), reportPath, reportSha256 }, reportPath, reportSha256 };
|
|
22765
|
+
}
|
|
22766
|
+
|
|
22767
|
+
// packages/cli/src/production-review-client.ts
|
|
22768
|
+
var productionReviewCommands = ["reviews", "sample", "approve", "change", "feedback", "resolve-feedback", "revoke-sample"];
|
|
22769
|
+
var invalid4 = (message) => {
|
|
22770
|
+
throw new CliError("INVALID_ARGUMENT", message, "Read editorbrain help and references/production-continuity.md.");
|
|
22771
|
+
};
|
|
22772
|
+
var uuid = (value, name) => {
|
|
22773
|
+
if (typeof value !== "string" || !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(value)) invalid4(`${name} must be a revision or feedback UUID.`);
|
|
22774
|
+
return value;
|
|
22775
|
+
};
|
|
22776
|
+
var hash5 = (value, name) => {
|
|
22777
|
+
if (typeof value !== "string" || !/^[a-f0-9]{64}$/.test(value)) invalid4(`${name} must be an exact SHA-256 hash.`);
|
|
22778
|
+
return value;
|
|
22779
|
+
};
|
|
22780
|
+
var text8 = (value, name, max, optional = false) => {
|
|
22781
|
+
if (optional && (value === void 0 || value === null)) return "";
|
|
22782
|
+
if (typeof value !== "string" || value.length > max || !optional && !value.trim()) invalid4(`${name} must contain ${optional ? "0" : "1"}\u2013${max} characters.`);
|
|
22783
|
+
return value.trim();
|
|
22784
|
+
};
|
|
22785
|
+
function reviewBody(value, command) {
|
|
22786
|
+
const fields = command === "sample" ? ["assetId", "contextVersion", "title", "notes", "parentRevisionId", "revisionId"] : ["resultRevisionId", "contextVersion", "resultNote", "preservedElements"];
|
|
22787
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => !fields.includes(key))) invalid4("Use only documented review fields; project and feedback IDs come from command flags, never the JSON body.");
|
|
22788
|
+
const body = value;
|
|
22789
|
+
hash5(body.contextVersion, "contextVersion");
|
|
22790
|
+
if (command === "sample") {
|
|
22791
|
+
hash5(body.assetId, "assetId");
|
|
22792
|
+
text8(body.title, "title", 120);
|
|
22793
|
+
text8(body.notes, "notes", 2e3, true);
|
|
22794
|
+
for (const key of ["parentRevisionId", "revisionId"]) if (body[key] !== void 0) uuid(body[key], key);
|
|
22795
|
+
} else {
|
|
22796
|
+
uuid(body.resultRevisionId, "resultRevisionId");
|
|
22797
|
+
text8(body.resultNote, "resultNote", 1500);
|
|
22798
|
+
text8(body.preservedElements, "preservedElements", 2e3);
|
|
22799
|
+
}
|
|
22800
|
+
if (Buffer.byteLength(JSON.stringify(body)) > 8192) invalid4("Review metadata exceeds the 8 KiB service limit.");
|
|
22801
|
+
return body;
|
|
22802
|
+
}
|
|
22803
|
+
async function productionReviewCommand(client, root, command, flags) {
|
|
22804
|
+
if (!productionReviewCommands.includes(command)) invalid4("Unknown production review command.");
|
|
22805
|
+
const required = (name) => {
|
|
22806
|
+
const value = flags[name];
|
|
22807
|
+
if (typeof value !== "string" || !value) throw new CliError("INVALID_ARGUMENT", `--${name} is required.`, "Read editorbrain help.");
|
|
22808
|
+
return value;
|
|
22809
|
+
};
|
|
22810
|
+
const allowed = {
|
|
22811
|
+
reviews: ["after"],
|
|
22812
|
+
feedback: ["status", "revision", "after"],
|
|
22813
|
+
sample: ["file"],
|
|
22814
|
+
approve: ["revision", "context-version", "user-confirmed"],
|
|
22815
|
+
change: ["revision", "context-version", "text", "at", "id"],
|
|
22816
|
+
"resolve-feedback": ["id", "file"],
|
|
22817
|
+
"revoke-sample": ["revision"]
|
|
22818
|
+
};
|
|
22819
|
+
for (const name of Object.keys(flags)) if (!["project", "dir", "url", ...allowed[command] ?? []].includes(name)) throw new CliError("INVALID_ARGUMENT", `--${name} is not supported for production ${command}.`, "Read editorbrain help.");
|
|
22820
|
+
const projectId = required("project");
|
|
22821
|
+
assertIdentifier(projectId);
|
|
22822
|
+
const base = `/v1/projects/${encodeURIComponent(projectId)}`;
|
|
22823
|
+
const revision = () => encodeURIComponent(uuid(required("revision"), "--revision"));
|
|
22824
|
+
const contextVersion = () => hash5(required("context-version"), "--context-version");
|
|
22825
|
+
const after = flags.after === void 0 ? void 0 : uuid(required("after"), "--after");
|
|
22826
|
+
if (command === "reviews") return client.request(`${base}/production-reviews${after ? "?" + new URLSearchParams({ after }) : ""}`);
|
|
22827
|
+
if (command === "feedback") {
|
|
22828
|
+
const query = new URLSearchParams();
|
|
22829
|
+
if (flags.status !== void 0) {
|
|
22830
|
+
const status = required("status");
|
|
22831
|
+
if (!["pending", "resolved"].includes(status)) invalid4("--status must be pending or resolved.");
|
|
22832
|
+
query.set("status", status);
|
|
22833
|
+
}
|
|
22834
|
+
if (flags.revision !== void 0) query.set("revisionId", uuid(required("revision"), "--revision"));
|
|
22835
|
+
if (after) query.set("after", after);
|
|
22836
|
+
return client.request(`${base}/production-feedback?${query}`);
|
|
22837
|
+
}
|
|
22838
|
+
if (command === "sample" || command === "resolve-feedback") {
|
|
22839
|
+
const feedbackId = command === "resolve-feedback" ? uuid(required("id"), "--id") : void 0;
|
|
22840
|
+
const body = reviewBody(await readProductionJson(root, required("file")), command);
|
|
22841
|
+
return client.request(command === "sample" ? `${base}/production-reviews` : `${base}/production-feedback/${encodeURIComponent(feedbackId)}/resolve`, { method: "POST", body });
|
|
22842
|
+
}
|
|
22843
|
+
if (command === "approve") {
|
|
22844
|
+
if (flags["user-confirmed"] !== true) throw new CliError("USER_CONFIRMATION_REQUIRED", "Record approval only after the user accepted this exact sample.", "Use --user-confirmed to record existing explicit approval, never to bypass a requested review.");
|
|
22845
|
+
return client.request(`${base}/production-reviews/${revision()}/approve`, { method: "POST", body: { contextVersion: contextVersion(), userConfirmed: true } });
|
|
22846
|
+
}
|
|
22847
|
+
if (command === "revoke-sample") return client.request(`${base}/production-reviews/${revision()}`, { method: "DELETE" });
|
|
22848
|
+
if (command === "change") {
|
|
22849
|
+
const times = flags.at === void 0 ? [] : required("at").split(",");
|
|
22850
|
+
if (times.length > 2 || times.some((t) => !/^(?:\d+(?:\.\d*)?|\.\d+)$/.test(t) || !Number.isFinite(Number(t)) || Number(t) > 36e3) || times.length === 2 && Number(times[1]) < Number(times[0])) throw new CliError("INVALID_ARGUMENT", "--at accepts one second or a start,end range between 0 and 36000 seconds.", "Example: --at 12,15.5. Omit it for the whole sample.");
|
|
22851
|
+
return client.request(`${base}/production-reviews/${revision()}/changes`, { method: "POST", body: { contextVersion: contextVersion(), text: text8(required("text"), "--text", 1500), target: times.length ? { startMs: Math.round(Number(times[0]) * 1e3), ...times.length === 2 ? { endMs: Math.round(Number(times[1]) * 1e3) } : {} } : {}, ...flags.id !== void 0 ? { feedbackId: uuid(required("id"), "--id") } : {} } });
|
|
22852
|
+
}
|
|
22853
|
+
throw new CliError("INVALID_ARGUMENT", "Unknown production review command.", "Read editorbrain help.");
|
|
22854
|
+
}
|
|
22855
|
+
|
|
22180
22856
|
// packages/cli/src/index.ts
|
|
22181
22857
|
var HELP = `EditorBrain \u2014 portable editorial context for AI video agents
|
|
22182
22858
|
|
|
@@ -22184,6 +22860,7 @@ editorbrain version
|
|
|
22184
22860
|
editorbrain setup --dir folder --agent codex|claude|both [--project id] [--workspace id] [--no-browser] [--skip-mcp]
|
|
22185
22861
|
editorbrain doctor [--dir folder] [--agent codex|claude|both] [--offline]
|
|
22186
22862
|
editorbrain init [--dir folder] [--brand id] [--style id] [--project id] [--topic text] [--goal text] [--agent codex|claude|both]
|
|
22863
|
+
editorbrain onboarding guide
|
|
22187
22864
|
editorbrain onboarding create --brand ID [--project ID] [--dir folder] [--out .editorbrain-local/onboarding/new]
|
|
22188
22865
|
editorbrain onboarding serve --session path [--dir folder] [--timeout seconds] [--no-browser]
|
|
22189
22866
|
editorbrain onboarding apply --session path --file answers.json [--dir folder] [--dry-run]
|
|
@@ -22216,6 +22893,13 @@ editorbrain review list --project ID
|
|
|
22216
22893
|
editorbrain review create --project ID --asset SHA --title TEXT --kind sample|render --days 7 --share-authorized
|
|
22217
22894
|
editorbrain review revoke ID
|
|
22218
22895
|
editorbrain workflow check [--project id] [--previous-version hash] [--previous-context-version hash]
|
|
22896
|
+
editorbrain studio prepare --file source.mp4 --out .editorbrain-local/studio/audio.wav [--audio-stream N]
|
|
22897
|
+
editorbrain studio catalog
|
|
22898
|
+
editorbrain billing status
|
|
22899
|
+
editorbrain studio quote --file request.json [--dir folder]
|
|
22900
|
+
editorbrain studio run QUOTE_ID --max-credits N --user-confirmed
|
|
22901
|
+
editorbrain studio get JOB_ID
|
|
22902
|
+
editorbrain studio jobs
|
|
22219
22903
|
editorbrain integrations catalog
|
|
22220
22904
|
editorbrain integrations list [--details]
|
|
22221
22905
|
editorbrain integrations set <provider-id> --file settings.json --expected-revision N
|
|
@@ -22231,6 +22915,10 @@ editorbrain styles create <style-id> [--name name] [--dir folder]
|
|
|
22231
22915
|
editorbrain styles check <style-id> [--dir folder]
|
|
22232
22916
|
editorbrain media styles [--category image|motion]
|
|
22233
22917
|
editorbrain media style <style-id> [--style-version 1.0.0]
|
|
22918
|
+
editorbrain sfx index --input folder --bank ID [--dir folder]
|
|
22919
|
+
editorbrain sfx search --project ID --query text [--bank ID] [--limit 6] [--scene-role hook|body|cta] [--local --dir folder --include-unverified]
|
|
22920
|
+
editorbrain sfx get SOUND_ID --project ID --bank ID --catalog-sha256 HASH [--scene-role hook|body|cta] [--local --dir folder --include-unverified]
|
|
22921
|
+
editorbrain sfx check --project ID --plan .editorbrain-local/sfx-plan.json [--dir folder]
|
|
22234
22922
|
editorbrain motion scenes [query] [--limit 6] [--after cursor]
|
|
22235
22923
|
editorbrain motion scene <scene-id> [--scene-version <version>]
|
|
22236
22924
|
editorbrain motion preview <scene-id> --scene-version <version> --ratio wide|square|tall [--preview-kind video|poster|first|last] [--preset <name>]
|
|
@@ -22241,6 +22929,14 @@ editorbrain motion prepare --file scene.json --project ID --out .editorbrain-loc
|
|
|
22241
22929
|
editorbrain media inventory --input file-or-folder [--max-files 200] [--dir folder] [--out .editorbrain-local/report.json] [--details]
|
|
22242
22930
|
editorbrain media sample --file source --at 0,5,12 --out .editorbrain-local/frames [--dir folder] [--details]
|
|
22243
22931
|
editorbrain production check --file render [--profile local-profile.json] [--dir folder] [--out .editorbrain-local/report.json] [--details]
|
|
22932
|
+
editorbrain production verify --project ID --file final.mp4 [--duration seconds] [--at 0,5,12] [--dir folder] [--out .editorbrain-local/new-check] [--offline] [--details]
|
|
22933
|
+
editorbrain production reviews --project ID [--after UUID]
|
|
22934
|
+
editorbrain production sample --project ID --file sample.json [--dir folder]
|
|
22935
|
+
editorbrain production approve --project ID --revision ID --context-version HASH --user-confirmed
|
|
22936
|
+
editorbrain production change --project ID --revision ID --context-version HASH --text TEXT [--at start,end] [--id UUID]
|
|
22937
|
+
editorbrain production feedback --project ID [--revision ID] [--status pending|resolved] [--after UUID]
|
|
22938
|
+
editorbrain production resolve-feedback --project ID --id UUID --file correction.json [--dir folder]
|
|
22939
|
+
editorbrain production revoke-sample --project ID --revision ID
|
|
22244
22940
|
editorbrain media search --query text [--provider all|wikimedia|nasa|loc|internet-archive] [--limit 10] [--licenses public-domain,cc0,cc-by] [--include-unknown] [--out .editorbrain-local/sources.json] [--dir folder] [--details]
|
|
22245
22941
|
editorbrain production audio-check --file final.mp4 [--profile local-profile.json] [--dir folder] [--out .editorbrain-local/audio.json] [--details]
|
|
22246
22942
|
editorbrain production evidence-check --inventory local-inventory.json --manifest local-evidence.json [--dir folder] [--out .editorbrain-local/evidence.json] [--details]
|
|
@@ -22249,7 +22945,10 @@ editorbrain production plan-check --inventory local-inventory.json --plan local-
|
|
|
22249
22945
|
editorbrain styles recommend <project-id> [--assets tag,tag] [--capabilities tag,tag] [--style pack-id] [--duration seconds]
|
|
22250
22946
|
editorbrain direction draft <project-id> --style pack-id [--assets tag,tag] [--capabilities tag,tag] [--duration seconds] [--out new-folder]
|
|
22251
22947
|
editorbrain assets list [--limit 200] [--cursor opaque-cursor]
|
|
22252
|
-
editorbrain
|
|
22948
|
+
editorbrain media preview --file source.mp4 --out .editorbrain-local/previews/review.mp4 [--quality 720|1080] [--audio-stream N]
|
|
22949
|
+
editorbrain storage status
|
|
22950
|
+
editorbrain storage cleanup --ids sha256,sha256 --confirm
|
|
22951
|
+
editorbrain assets upload <file> [--mime supported-media-type] [--purpose original|preview|final|temporary] [--source-sha256 hash]
|
|
22253
22952
|
editorbrain assets download <sha256> --out file
|
|
22254
22953
|
editorbrain keys list
|
|
22255
22954
|
editorbrain keys create --label name [--role reader|editor|owner]
|
|
@@ -22265,14 +22964,16 @@ Motion list/get discover reusable components; prepare creates an editable HTML s
|
|
|
22265
22964
|
Media inspection/production checks run locally without installation or media upload. Media search sends only your explicit query to public archive APIs; it does not replace the agent's web image/video search or verify rights.
|
|
22266
22965
|
Integrations store personal preferences, not credentials. Declared access is checked by your agent before use.
|
|
22267
22966
|
Production output is compact by default; --details expands it. --out saves full private reports without overwriting.
|
|
22967
|
+
Production verify uses local project requirements and rechecks the web context when logged in; --offline explicitly checks local documents only. Media stays local. Technical success is not audiovisual approval.
|
|
22968
|
+
Production reviews/sample/feedback preserve exact revisions privately in the workspace. Approval requires existing user confirmation. They do not render, change brand preferences or create public sharing links.
|
|
22268
22969
|
Keys create stores the returned secret in the private user config; stdout has no key.
|
|
22269
22970
|
Output is JSON except help; login progress/code goes to stderr. --url overrides the configured endpoint.
|
|
22270
22971
|
`;
|
|
22271
22972
|
function parseArgs(argv) {
|
|
22272
22973
|
const positional = [];
|
|
22273
22974
|
const flags = {};
|
|
22274
|
-
const booleans = /* @__PURE__ */ new Set(["help", "version", "offline", "dry-run", "key-stdin", "invite-stdin", "no-browser", "skip-mcp", "all", "share-authorized", "details", "include-unknown", "reviewed"]);
|
|
22275
|
-
const known = /* @__PURE__ */ new Set(["manifest", "query", "provider", "licenses", "input", "max-files", "at", "profile", "inventory", "plan", "dir", "brand", "style", "project", "topic", "goal", "name", "adapter", "out", "url", "document", "mime", "label", "role", "limit", "cursor", "timeout", "assets", "capabilities", "duration", "agent", "workspace", "prefix", "sha256", "kind", "channel", "epistemic-status", "event-offset", "relation-offset", "script", "video", "asset", "requirements", "mode", "review", "previous-version", "previous-context-version", "category", "style-version", "file", "revision", "expected-revision", "before", "scope", "id", "text", "conditions", "status", "index", "title", "days", ...booleans]);
|
|
22975
|
+
const booleans = /* @__PURE__ */ new Set(["local", "include-unverified", "help", "version", "offline", "dry-run", "key-stdin", "invite-stdin", "no-browser", "skip-mcp", "all", "share-authorized", "details", "include-unknown", "reviewed", "user-confirmed", "confirm"]);
|
|
22976
|
+
const known = /* @__PURE__ */ new Set(["bank", "scene-role", "catalog-sha256", "quality", "purpose", "source-sha256", "ids", "max-credits", "audio-stream", "manifest", "query", "provider", "licenses", "input", "max-files", "at", "profile", "inventory", "plan", "dir", "brand", "style", "project", "topic", "goal", "name", "adapter", "out", "url", "document", "mime", "label", "role", "limit", "cursor", "timeout", "assets", "capabilities", "duration", "agent", "workspace", "prefix", "sha256", "kind", "channel", "epistemic-status", "event-offset", "relation-offset", "script", "video", "asset", "requirements", "mode", "review", "previous-version", "previous-context-version", "category", "style-version", "file", "revision", "expected-revision", "before", "scope", "id", "text", "conditions", "status", "index", "title", "days", ...booleans]);
|
|
22276
22977
|
known.add("session");
|
|
22277
22978
|
known.add("run");
|
|
22278
22979
|
known.add("component-version");
|
|
@@ -22281,6 +22982,7 @@ function parseArgs(argv) {
|
|
|
22281
22982
|
known.add("ratio");
|
|
22282
22983
|
known.add("preview-kind");
|
|
22283
22984
|
known.add("preset");
|
|
22985
|
+
known.add("context-version");
|
|
22284
22986
|
for (let i = 0; i < argv.length; i++) {
|
|
22285
22987
|
const arg = argv[i];
|
|
22286
22988
|
if (!arg.startsWith("--")) {
|
|
@@ -22306,16 +23008,16 @@ function output(value) {
|
|
|
22306
23008
|
process.stdout.write(stableJson(value));
|
|
22307
23009
|
}
|
|
22308
23010
|
async function configRoot(create = false) {
|
|
22309
|
-
let path = process.env.EDITORBRAIN_CONFIG_DIR ?? process.env.EDITORHARNESS_CONFIG_DIR ??
|
|
23011
|
+
let path = process.env.EDITORBRAIN_CONFIG_DIR ?? process.env.EDITORHARNESS_CONFIG_DIR ?? join8(homedir(), ".config", "editorbrain");
|
|
22310
23012
|
if (!create && !process.env.EDITORBRAIN_CONFIG_DIR && !process.env.EDITORHARNESS_CONFIG_DIR) {
|
|
22311
23013
|
try {
|
|
22312
|
-
await
|
|
23014
|
+
await lstat11(path);
|
|
22313
23015
|
} catch (error) {
|
|
22314
23016
|
if (error.code !== "ENOENT") throw error;
|
|
22315
|
-
path =
|
|
23017
|
+
path = join8(homedir(), ".config", "editorharness");
|
|
22316
23018
|
}
|
|
22317
23019
|
}
|
|
22318
|
-
if (create) await
|
|
23020
|
+
if (create) await mkdir8(path, { recursive: true, mode: 448 });
|
|
22319
23021
|
return ensureRoot(path);
|
|
22320
23022
|
}
|
|
22321
23023
|
async function loadConfig() {
|
|
@@ -22332,7 +23034,7 @@ async function loadConfig() {
|
|
|
22332
23034
|
async function saveConfig(config2) {
|
|
22333
23035
|
const root = await configRoot(true);
|
|
22334
23036
|
await atomicWrite(root, "config.json", stableJson(config2), { document: false, mode: 384 });
|
|
22335
|
-
await
|
|
23037
|
+
await chmod2(join8(root, "config.json"), 384);
|
|
22336
23038
|
}
|
|
22337
23039
|
async function config(args, requireKey = true) {
|
|
22338
23040
|
const stored = await loadConfig();
|
|
@@ -22360,7 +23062,7 @@ async function init(args) {
|
|
|
22360
23062
|
for (const path of Object.keys(files)) {
|
|
22361
23063
|
const target = await checkedPath(root, path);
|
|
22362
23064
|
try {
|
|
22363
|
-
await
|
|
23065
|
+
await lstat11(target);
|
|
22364
23066
|
throw new Error(`Refusing to replace existing file: ${path}`);
|
|
22365
23067
|
} catch (error) {
|
|
22366
23068
|
if (error.code !== "ENOENT") throw error;
|
|
@@ -22372,13 +23074,13 @@ async function init(args) {
|
|
|
22372
23074
|
for (const path of directories) {
|
|
22373
23075
|
const target = await checkedPath(root, path, false);
|
|
22374
23076
|
try {
|
|
22375
|
-
if (!(await
|
|
23077
|
+
if (!(await lstat11(target)).isDirectory()) throw new Error(`Not a directory: ${path}`);
|
|
22376
23078
|
} catch (error) {
|
|
22377
23079
|
if (error.code !== "ENOENT") throw error;
|
|
22378
23080
|
}
|
|
22379
23081
|
}
|
|
22380
23082
|
for (const [path, content] of Object.entries(files)) await atomicWrite(root, path, content, { overwrite: false });
|
|
22381
|
-
for (const path of directories) await
|
|
23083
|
+
for (const path of directories) await mkdir8(await checkedPath(root, path, false), { recursive: true });
|
|
22382
23084
|
const guidance = await installGuidance();
|
|
22383
23085
|
const entryPoints = guidance.readInstructions.filter((item) => item.entryPointStatus === "created").map((item) => item.entryPoint);
|
|
22384
23086
|
output({ root, created: Object.keys(files), entryPoints, guidance, next: `Edit brand tokens and brief, then run editorbrain resolve ${project}` });
|
|
@@ -22433,7 +23135,7 @@ async function main() {
|
|
|
22433
23135
|
return;
|
|
22434
23136
|
}
|
|
22435
23137
|
if (args.flags["dry-run"] && !["push", "pull"].includes(command) && !(command === "onboarding" && ["apply", "sync"].includes(subcommand))) throw new CliError("INVALID_ARGUMENT", "--dry-run is supported by push, pull and onboarding apply/sync.", "Read editorbrain help.");
|
|
22436
|
-
if (args.flags.offline && command !== "doctor") throw new CliError("INVALID_ARGUMENT", "--offline is supported
|
|
23138
|
+
if (args.flags.offline && command !== "doctor" && !(command === "production" && subcommand === "verify")) throw new CliError("INVALID_ARGUMENT", "--offline is supported by doctor and production verify.", "Read editorbrain help.");
|
|
22437
23139
|
if (!command || command === "help" || args.flags.help) {
|
|
22438
23140
|
process.stdout.write(HELP);
|
|
22439
23141
|
return;
|
|
@@ -22441,9 +23143,13 @@ async function main() {
|
|
|
22441
23143
|
if (args.flags.details && !["media", "production", "integrations"].includes(command) && !(command === "search" && subcommand === "brief")) throw new CliError("INVALID_ARGUMENT", "--details is supported only by media, production, integrations list and search brief.", "Read editorbrain help.");
|
|
22442
23144
|
if (command === "onboarding") {
|
|
22443
23145
|
const allowed = new Set(subcommand === "create" ? ["brand", "project", "dir", "out"] : subcommand === "serve" ? ["session", "dir", "timeout", "no-browser"] : subcommand === "apply" ? ["session", "file", "dir", "dry-run"] : subcommand === "sync" ? ["session", "dir", "dry-run", "url"] : []);
|
|
22444
|
-
if (!["create", "serve", "apply", "sync"].includes(subcommand) || args.positional.length !== 2) throw new Error("Use onboarding create, serve, apply or sync.");
|
|
23146
|
+
if (!["guide", "create", "serve", "apply", "sync"].includes(subcommand) || args.positional.length !== 2) throw new Error("Use onboarding guide, create, serve, apply or sync.");
|
|
22445
23147
|
for (const flag of Object.keys(args.flags)) if (!allowed.has(flag)) throw new Error(`--${flag} is not supported here.`);
|
|
22446
|
-
|
|
23148
|
+
if (subcommand === "guide") {
|
|
23149
|
+
output(getOnboardingGuide());
|
|
23150
|
+
return;
|
|
23151
|
+
}
|
|
23152
|
+
const directory = resolve8(str(args, "dir") ?? process.cwd());
|
|
22447
23153
|
if (subcommand === "create") {
|
|
22448
23154
|
const brandId2 = str(args, "brand");
|
|
22449
23155
|
if (!brandId2) throw new Error("--brand is required.");
|
|
@@ -22491,6 +23197,38 @@ async function main() {
|
|
|
22491
23197
|
output(await acceptBrainProposal(client, { brandId: brandId2, runId, expectedDesignRevision: brainRevision(revision), reviewed: true }));
|
|
22492
23198
|
return;
|
|
22493
23199
|
}
|
|
23200
|
+
if (command === "sfx") {
|
|
23201
|
+
const allowed = new Set(subcommand === "index" ? ["input", "bank", "dir"] : subcommand === "check" ? ["project", "plan", "dir"] : subcommand === "search" ? ["project", "query", "bank", "limit", "scene-role", "local", "dir", "include-unverified", "url"] : subcommand === "get" ? ["project", "bank", "catalog-sha256", "scene-role", "local", "dir", "include-unverified", "url"] : []);
|
|
23202
|
+
if (!["index", "search", "get", "check"].includes(subcommand)) throw new Error("Use sfx index, search, get or check.");
|
|
23203
|
+
for (const key of Object.keys(args.flags)) if (!allowed.has(key)) throw new Error(`--${key} is not supported for sfx ${subcommand}.`);
|
|
23204
|
+
if (args.positional.length !== (subcommand === "get" ? 3 : 2)) throw new Error("Unexpected SFX arguments.");
|
|
23205
|
+
const directory = str(args, "dir") ?? process.cwd(), bankId = str(args, "bank"), projectId = str(args, "project");
|
|
23206
|
+
if (subcommand === "index") {
|
|
23207
|
+
const input = str(args, "input");
|
|
23208
|
+
if (!input || !bankId) throw new Error("Provide --input and --bank.");
|
|
23209
|
+
output(await indexSfx(directory, input, bankId));
|
|
23210
|
+
return;
|
|
23211
|
+
}
|
|
23212
|
+
if (!projectId) throw new Error("--project is required to resolve audio preferences.");
|
|
23213
|
+
if (subcommand === "check") {
|
|
23214
|
+
const plan = str(args, "plan");
|
|
23215
|
+
if (!plan) throw new Error("--plan is required.");
|
|
23216
|
+
const report = await checkLocalSfx(directory, projectId, plan);
|
|
23217
|
+
output(report);
|
|
23218
|
+
if (!report.passed) process.exitCode = 1;
|
|
23219
|
+
return;
|
|
23220
|
+
}
|
|
23221
|
+
const sceneRole = str(args, "scene-role") ?? "body", query = str(args, "query") ?? "", limit = str(args, "limit");
|
|
23222
|
+
if (!["hook", "body", "cta"].includes(sceneRole) || limit !== void 0 && (!/^\d+$/.test(limit) || Number(limit) < 1 || Number(limit) > 20)) throw new Error("Invalid scene role or limit.");
|
|
23223
|
+
if (subcommand === "get" && (!argument || !str(args, "catalog-sha256"))) throw new Error("Provide sound ID and --catalog-sha256 from search.");
|
|
23224
|
+
if (args.flags.local) {
|
|
23225
|
+
output(await localSfx({ directory, projectId, bankId, query, limit: limit ? Number(limit) : void 0, sceneRole, includeUnverified: !!args.flags["include-unverified"], soundId: subcommand === "get" ? argument : void 0, catalogSha256: str(args, "catalog-sha256") }));
|
|
23226
|
+
return;
|
|
23227
|
+
}
|
|
23228
|
+
if (args.flags["include-unverified"] || args.flags.dir) throw new Error("--dir and --include-unverified require --local.");
|
|
23229
|
+
output(await remoteSfx(new Client(await config(args)), subcommand, { projectId, query, sceneRole, ...bankId ? { bankId } : {}, ...limit ? { limit } : {}, ...subcommand === "get" ? { soundId: argument, catalogSha256: str(args, "catalog-sha256") } : {} }));
|
|
23230
|
+
return;
|
|
23231
|
+
}
|
|
22494
23232
|
if (command === "motion" && ["scenes", "scene", "source", "preview"].includes(subcommand)) {
|
|
22495
23233
|
const allowed = new Set(subcommand === "scenes" ? ["limit", "after"] : subcommand === "scene" ? ["scene-version"] : subcommand === "preview" ? ["scene-version", "ratio", "preview-kind", "preset", "url"] : ["scene-version", "out", "dir", "url"]);
|
|
22496
23234
|
for (const key of Object.keys(args.flags)) if (!allowed.has(key)) throw new CliError("INVALID_ARGUMENT", `--${key} is not supported here.`, "Read editorbrain help.");
|
|
@@ -22542,6 +23280,48 @@ async function main() {
|
|
|
22542
23280
|
}
|
|
22543
23281
|
return;
|
|
22544
23282
|
}
|
|
23283
|
+
if (command === "billing") {
|
|
23284
|
+
if (subcommand !== "status" || argument || Object.keys(args.flags).some((k) => !["url", "dir"].includes(k))) throw new CliError("INVALID_ARGUMENT", "Use editorbrain billing status.", "Manage payments at https://editorbrain.com/app/billing.");
|
|
23285
|
+
output(await new Client(await config(args)).request("/v1/billing/status"));
|
|
23286
|
+
return;
|
|
23287
|
+
}
|
|
23288
|
+
if (command === "media" && subcommand === "preview") {
|
|
23289
|
+
const allowed = /* @__PURE__ */ new Set(["file", "out", "quality", "audio-stream", "dir"]);
|
|
23290
|
+
if (argument || Object.keys(args.flags).some((f) => !allowed.has(f)) || !str(args, "file") || !str(args, "out")) throw new CliError("INVALID_ARGUMENT", "Use media preview --file source --out .editorbrain-local/previews/review.mp4.", "This command creates a local copy only.");
|
|
23291
|
+
const quality = Number(str(args, "quality") ?? 720), audio = str(args, "audio-stream");
|
|
23292
|
+
if (![720, 1080].includes(quality) || audio !== void 0 && (!Number.isInteger(Number(audio)) || Number(audio) < 0)) throw new CliError("INVALID_ARGUMENT", "Invalid quality or audio stream.", "Use quality 720 or 1080.");
|
|
23293
|
+
const root2 = await ensureRoot(str(args, "dir") ?? process.cwd());
|
|
23294
|
+
output(await prepareMediaPreview(root2, resolve8(root2, str(args, "file")), str(args, "out"), quality, audio === void 0 ? void 0 : Number(audio)));
|
|
23295
|
+
return;
|
|
23296
|
+
}
|
|
23297
|
+
if (command === "storage") {
|
|
23298
|
+
const allowed = new Set(subcommand === "status" ? ["url", "dir"] : ["url", "dir", "ids", "confirm"]);
|
|
23299
|
+
if (argument || Object.keys(args.flags).some((f) => !allowed.has(f))) throw new CliError("INVALID_ARGUMENT", "Unsupported storage argument.", "Use storage status or storage cleanup --ids sha256,sha256 --confirm.");
|
|
23300
|
+
const client = new Client(await config(args));
|
|
23301
|
+
if (subcommand === "status") {
|
|
23302
|
+
output(await client.request("/v1/storage"));
|
|
23303
|
+
return;
|
|
23304
|
+
}
|
|
23305
|
+
if (subcommand === "cleanup" && args.flags.confirm === true && str(args, "ids")) {
|
|
23306
|
+
output(await client.request("/v1/storage/cleanup", { method: "POST", body: { assetIds: str(args, "ids").split(","), userConfirmed: true } }));
|
|
23307
|
+
return;
|
|
23308
|
+
}
|
|
23309
|
+
throw new CliError("INVALID_ARGUMENT", "Use storage status, or storage cleanup --ids sha256,sha256 --confirm.", "Review the exact unreferenced previews/temporaries first; deletion cannot be undone.");
|
|
23310
|
+
}
|
|
23311
|
+
if (command === "studio" && subcommand === "prepare") {
|
|
23312
|
+
const allowed = /* @__PURE__ */ new Set(["file", "out", "audio-stream", "dir"]);
|
|
23313
|
+
if (argument || Object.keys(args.flags).some((flag) => !allowed.has(flag)) || !str(args, "file") || !str(args, "out")) throw new CliError("INVALID_ARGUMENT", "Use studio prepare --file source --out .editorbrain-local/audio.wav.", "Preparation is local and does not need login.");
|
|
23314
|
+
const stream = str(args, "audio-stream");
|
|
23315
|
+
if (stream !== void 0 && !/^\d+$/.test(stream)) throw new CliError("INVALID_ARGUMENT", "--audio-stream must be an integer.", "Use media inventory to inspect available tracks.");
|
|
23316
|
+
const root2 = resolve8(str(args, "dir") ?? process.cwd());
|
|
23317
|
+
output(await prepareStudioAudio(root2, resolve8(root2, str(args, "file")), str(args, "out"), stream === void 0 ? void 0 : Number(stream)));
|
|
23318
|
+
return;
|
|
23319
|
+
}
|
|
23320
|
+
if (command === "studio") {
|
|
23321
|
+
if (args.positional.length > 3) throw new CliError("INVALID_ARGUMENT", "Too many studio arguments.", "Read editorbrain help.");
|
|
23322
|
+
output(await studioCommand(new Client(await config(args)), resolve8(str(args, "dir") ?? process.cwd()), subcommand, argument, args.flags));
|
|
23323
|
+
return;
|
|
23324
|
+
}
|
|
22545
23325
|
if (command === "integrations") {
|
|
22546
23326
|
const allowed = new Set(subcommand === "catalog" ? [] : subcommand === "list" ? ["url", "details"] : subcommand === "set" ? ["url", "file", "expected-revision", "dir"] : []);
|
|
22547
23327
|
if (!["catalog", "list", "set"].includes(subcommand) || args.positional.length > (subcommand === "set" ? 3 : 2)) throw new CliError("INVALID_ARGUMENT", "Use integrations catalog, list or set <provider-id>.", "Read editorbrain help.");
|
|
@@ -22554,7 +23334,7 @@ async function main() {
|
|
|
22554
23334
|
const file = str(args, "file");
|
|
22555
23335
|
if (!argument || !file) throw new CliError("INVALID_ARGUMENT", "integrations set requires a provider ID and --file.", "Use integrations list first, then supply --expected-revision.");
|
|
22556
23336
|
const revision = integrationRevision(str(args, "expected-revision"));
|
|
22557
|
-
const settings = await readIntegrationSettings(
|
|
23337
|
+
const settings = await readIntegrationSettings(resolve8(str(args, "dir") ?? process.cwd(), file));
|
|
22558
23338
|
output(await setIntegration(new Client(await config(args)), argument, settings, revision));
|
|
22559
23339
|
} else output(await listIntegrations(new Client(await config(args)), !!args.flags.details));
|
|
22560
23340
|
return;
|
|
@@ -22574,6 +23354,34 @@ async function main() {
|
|
|
22574
23354
|
}
|
|
22575
23355
|
return;
|
|
22576
23356
|
}
|
|
23357
|
+
if (command === "production" && productionReviewCommands.includes(subcommand)) {
|
|
23358
|
+
if (argument) throw new CliError("INVALID_ARGUMENT", "Use named flags for production reviews.", "Read editorbrain help.");
|
|
23359
|
+
output(await productionReviewCommand(new Client(await config(args)), await ensureRoot(str(args, "dir") ?? process.cwd()), subcommand, args.flags));
|
|
23360
|
+
return;
|
|
23361
|
+
}
|
|
23362
|
+
if (command === "production" && subcommand === "verify") {
|
|
23363
|
+
if (argument) throw new CliError("INVALID_ARGUMENT", "Use named flags for production verify.", "Read editorbrain help.");
|
|
23364
|
+
const allowed = /* @__PURE__ */ new Set(["project", "file", "duration", "at", "dir", "out", "offline", "details", "url"]);
|
|
23365
|
+
for (const flag of Object.keys(args.flags)) if (!allowed.has(flag)) throw new CliError("INVALID_ARGUMENT", `--${flag} is not supported for production verify.`, "Read editorbrain help.");
|
|
23366
|
+
const projectId = str(args, "project"), file = str(args, "file");
|
|
23367
|
+
if (!projectId || !file) throw new CliError("INVALID_ARGUMENT", "--project and --file are required.", "Read editorbrain help.");
|
|
23368
|
+
const times = str(args, "at")?.split(",");
|
|
23369
|
+
if (times?.some((time) => !/^(?:\d+(?:\.\d*)?|\.\d+)$/.test(time))) throw new CliError("INVALID_ARGUMENT", "--at requires comma-separated nonnegative seconds.", "Example: --at 0,5.5,12");
|
|
23370
|
+
if (times && (times.length > 6 || times.some((time) => !Number.isFinite(Number(time)) || Number(time) > 86400))) throw new CliError("INVALID_ARGUMENT", "--at accepts up to six times within 86400 seconds.", "Choose only the moments needed for review.");
|
|
23371
|
+
const duration = str(args, "duration");
|
|
23372
|
+
if (duration !== void 0 && (!duration.trim() || !Number.isFinite(Number(duration)) || Number(duration) <= 0 || Number(duration) > 864e4)) throw new CliError("INVALID_ARGUMENT", "--duration must be a finite positive duration in seconds.", "Use the required final duration, not an estimate.");
|
|
23373
|
+
const root2 = await ensureRoot(str(args, "dir") ?? process.cwd());
|
|
23374
|
+
const credentials = args.flags.offline ? void 0 : await config(args, false);
|
|
23375
|
+
const client = credentials?.apiKey ? new Client(credentials) : void 0;
|
|
23376
|
+
await withWorkspaceLock(root2, async () => {
|
|
23377
|
+
const context = await prepareProductionContext(root2, projectId, client);
|
|
23378
|
+
const result2 = await verifyProduction({ root: root2, file, ...context, ...str(args, "out") ? { outDirectory: str(args, "out") } : {}, ...str(args, "duration") !== void 0 ? { expectedDurationSeconds: Number(str(args, "duration")) } : {}, ...times ? { sampleTimesSeconds: times.map(Number) } : {} });
|
|
23379
|
+
output({ ...args.flags.details ? result2.report : result2.summary, contextScope: context.contextScope, reportPath: result2.reportPath, reportSha256: result2.reportSha256 });
|
|
23380
|
+
if (result2.report.technicalStatus === "fail") process.exitCode = 1;
|
|
23381
|
+
else if (!result2.report.complete) process.exitCode = 2;
|
|
23382
|
+
}, { migrateLegacy: false });
|
|
23383
|
+
return;
|
|
23384
|
+
}
|
|
22577
23385
|
if (command === "media" || command === "production") {
|
|
22578
23386
|
const required = (name) => {
|
|
22579
23387
|
const value = str(args, name);
|
|
@@ -22590,17 +23398,17 @@ async function main() {
|
|
|
22590
23398
|
let report;
|
|
22591
23399
|
if (command === "media" && subcommand === "inventory") {
|
|
22592
23400
|
const limit = str(args, "max-files");
|
|
22593
|
-
report = await inventoryMedia({ input:
|
|
23401
|
+
report = await inventoryMedia({ input: resolve8(root2, required("input")), ...limit ? { maxFiles: Number(limit) } : {} });
|
|
22594
23402
|
if (!report.complete) process.exitCode = 2;
|
|
22595
23403
|
} else if (command === "media" && subcommand === "sample") {
|
|
22596
23404
|
const times = required("at").split(",");
|
|
22597
23405
|
if (times.some((t) => !t.trim() || !/^(?:\d+(?:\.\d*)?|\.\d+)$/.test(t.trim()))) throw new CliError("INVALID_ARGUMENT", "--at requires comma-separated nonnegative seconds.", "For example: --at 0,5.5,12");
|
|
22598
23406
|
const directory = await prepareSampleDirectory(root2, required("out"));
|
|
22599
|
-
report = await sampleMedia({ file:
|
|
23407
|
+
report = await sampleMedia({ file: resolve8(root2, required("file")), times: times.map(Number), outputDirectory: directory });
|
|
22600
23408
|
} else if (command === "production" && subcommand === "check") {
|
|
22601
23409
|
const profilePath = str(args, "profile");
|
|
22602
23410
|
const profile = profilePath ? validateRenderCheckProfile(await readProductionJson(root2, profilePath)) : void 0;
|
|
22603
|
-
report = await checkRender({ file:
|
|
23411
|
+
report = await checkRender({ file: resolve8(root2, required("file")), profile });
|
|
22604
23412
|
if (report.technicalStatus === "fail") process.exitCode = 1;
|
|
22605
23413
|
else if (!report.complete) process.exitCode = 2;
|
|
22606
23414
|
} else if (command === "media" && subcommand === "search") {
|
|
@@ -22608,7 +23416,7 @@ async function main() {
|
|
|
22608
23416
|
if (!report.complete) process.exitCode = 2;
|
|
22609
23417
|
} else if (command === "production" && subcommand === "audio-check") {
|
|
22610
23418
|
const profilePath = str(args, "profile");
|
|
22611
|
-
report = await checkAudio({ file:
|
|
23419
|
+
report = await checkAudio({ file: resolve8(root2, required("file")), ...profilePath ? { profile: validateAudioCheckProfile(await readProductionJson(root2, profilePath)) } : {} });
|
|
22612
23420
|
if (report.technicalStatus === "fail") process.exitCode = 1;
|
|
22613
23421
|
else if (!report.complete) process.exitCode = 2;
|
|
22614
23422
|
} else if (command === "production" && subcommand === "evidence-check") {
|
|
@@ -22716,10 +23524,10 @@ async function main() {
|
|
|
22716
23524
|
const destination = str(args, "out");
|
|
22717
23525
|
if (destination) {
|
|
22718
23526
|
if (command !== "direction" || !result2.files) throw new Error("--out is supported only for direction draft.");
|
|
22719
|
-
const outRoot = await ensureRoot(
|
|
23527
|
+
const outRoot = await ensureRoot(resolve8(str(args, "dir") ?? process.cwd(), destination), true);
|
|
22720
23528
|
for (const path of Object.keys(result2.files)) {
|
|
22721
23529
|
try {
|
|
22722
|
-
await
|
|
23530
|
+
await lstat11(await checkedPath(outRoot, path, false));
|
|
22723
23531
|
throw new Error(`Refusing to replace existing proposal: ${path}`);
|
|
22724
23532
|
} catch (error) {
|
|
22725
23533
|
if (error.code !== "ENOENT") throw error;
|
|
@@ -22745,7 +23553,7 @@ async function main() {
|
|
|
22745
23553
|
if (!/^[a-zA-Z0-9_-]{1,100}$/.test(result2.id) || typeof result2.apiKey !== "string") throw new Error("Invalid key response");
|
|
22746
23554
|
const file = `keys/${result2.id}.json`;
|
|
22747
23555
|
await atomicWrite(root2, file, stableJson({ url: client.url, apiKey: result2.apiKey, id: result2.id, label, role }), { document: false, mode: 384, overwrite: false });
|
|
22748
|
-
output({ id: result2.id, label, role, credentialFile:
|
|
23556
|
+
output({ id: result2.id, label, role, credentialFile: join8(root2, file) });
|
|
22749
23557
|
return;
|
|
22750
23558
|
}
|
|
22751
23559
|
if (subcommand === "revoke" && argument) {
|
|
@@ -22832,7 +23640,7 @@ async function main() {
|
|
|
22832
23640
|
return;
|
|
22833
23641
|
}
|
|
22834
23642
|
if (subcommand === "upload" && argument) {
|
|
22835
|
-
const target =
|
|
23643
|
+
const target = resolve8(str(args, "dir") ?? process.cwd(), argument);
|
|
22836
23644
|
const handle = await open9(target, constants9.O_RDONLY | constants9.O_NOFOLLOW);
|
|
22837
23645
|
let bytes;
|
|
22838
23646
|
try {
|
|
@@ -22843,24 +23651,27 @@ async function main() {
|
|
|
22843
23651
|
await handle.close();
|
|
22844
23652
|
}
|
|
22845
23653
|
const types = { ".mp4": "video/mp4", ".mov": "video/quicktime", ".webm": "video/webm", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp", ".mp3": "audio/mpeg", ".wav": "audio/wav", ".m4a": "audio/mp4", ".ogg": "audio/ogg" };
|
|
22846
|
-
const mimeType = str(args, "mime") ?? types[
|
|
23654
|
+
const mimeType = str(args, "mime") ?? types[extname5(target).toLowerCase()];
|
|
22847
23655
|
if (!mimeType) throw new Error("Unsupported extension; specify --mime for a supported media format");
|
|
22848
|
-
const
|
|
22849
|
-
const
|
|
23656
|
+
const hash6 = await sha256(bytes);
|
|
23657
|
+
const params = new URLSearchParams({ name: basename3(target) });
|
|
23658
|
+
if (str(args, "purpose")) params.set("purpose", str(args, "purpose"));
|
|
23659
|
+
if (str(args, "source-sha256")) params.set("sourceSha256", str(args, "source-sha256"));
|
|
23660
|
+
const result2 = await client.media(`/v1/assets/${hash6}?${params}`, { content: bytes, mimeType });
|
|
22850
23661
|
const body = await result2.json();
|
|
22851
|
-
if (body.sha256 !==
|
|
23662
|
+
if (body.sha256 !== hash6) throw new Error("Upload response hash mismatch");
|
|
22852
23663
|
output(body);
|
|
22853
23664
|
return;
|
|
22854
23665
|
}
|
|
22855
23666
|
if (subcommand === "download" && argument && str(args, "out")) {
|
|
22856
23667
|
if (!/^[a-f0-9]{64}$/.test(argument)) throw new Error("Asset ID must be its SHA-256");
|
|
22857
|
-
const destination =
|
|
22858
|
-
const root2 = await ensureRoot(
|
|
22859
|
-
await checkedPath(root2,
|
|
23668
|
+
const destination = resolve8(str(args, "dir") ?? process.cwd(), str(args, "out"));
|
|
23669
|
+
const root2 = await ensureRoot(dirname11(destination), true);
|
|
23670
|
+
await checkedPath(root2, basename3(destination), false);
|
|
22860
23671
|
const response = await client.media(`/v1/assets/${argument}`);
|
|
22861
23672
|
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
22862
23673
|
if (await sha256(bytes) !== argument) throw new Error("Downloaded asset failed SHA-256 verification");
|
|
22863
|
-
await atomicWrite(root2,
|
|
23674
|
+
await atomicWrite(root2, basename3(destination), bytes, { document: false, overwrite: false });
|
|
22864
23675
|
output({ path: destination, sha256: argument, bytes: bytes.length });
|
|
22865
23676
|
return;
|
|
22866
23677
|
}
|
|
@@ -22890,7 +23701,7 @@ async function main() {
|
|
|
22890
23701
|
if (command === "search" && subcommand === "brief") {
|
|
22891
23702
|
const path = str(args, "file");
|
|
22892
23703
|
if (!path) throw new Error("Provide --file with the editorial brief JSON after inspecting the actual material.");
|
|
22893
|
-
const handle = await open9(
|
|
23704
|
+
const handle = await open9(resolve8(path), constants9.O_RDONLY | constants9.O_NOFOLLOW);
|
|
22894
23705
|
let brief;
|
|
22895
23706
|
try {
|
|
22896
23707
|
const stat = await handle.stat();
|
|
@@ -22912,7 +23723,7 @@ async function main() {
|
|
|
22912
23723
|
const scriptPath = str(args, "script"), videoPath = str(args, "video");
|
|
22913
23724
|
let script, assetId = str(args, "asset");
|
|
22914
23725
|
const readInput = async (path, max) => {
|
|
22915
|
-
const handle = await open9(
|
|
23726
|
+
const handle = await open9(resolve8(path), constants9.O_RDONLY | constants9.O_NOFOLLOW);
|
|
22916
23727
|
try {
|
|
22917
23728
|
const stat = await handle.stat();
|
|
22918
23729
|
if (!stat.isFile() || stat.size < 1 || stat.size > max) throw new Error(`Input must be a regular file of at most ${max} bytes.`);
|
|
@@ -22932,10 +23743,10 @@ async function main() {
|
|
|
22932
23743
|
if (assetId && !/^[a-f0-9]{64}$/.test(assetId)) throw new Error("--asset requires an owned production asset SHA-256.");
|
|
22933
23744
|
if (videoPath) {
|
|
22934
23745
|
const bytes = await readInput(videoPath, 12 * 1024 * 1024);
|
|
22935
|
-
const mimeType = { ".mp4": "video/mp4", ".mov": "video/quicktime", ".webm": "video/webm" }[
|
|
23746
|
+
const mimeType = { ".mp4": "video/mp4", ".mov": "video/quicktime", ".webm": "video/webm" }[extname5(videoPath).toLowerCase()];
|
|
22936
23747
|
if (!mimeType) throw new Error("Use MP4, MOV or WebM.");
|
|
22937
23748
|
assetId = await sha256(bytes);
|
|
22938
|
-
const uploaded = await client.media(`/v1/assets/${assetId}?name=${encodeURIComponent(
|
|
23749
|
+
const uploaded = await client.media(`/v1/assets/${assetId}?name=${encodeURIComponent(basename3(videoPath))}`, { content: bytes, mimeType });
|
|
22939
23750
|
if ((await uploaded.json()).sha256 !== assetId) throw new Error("Upload hash mismatch.");
|
|
22940
23751
|
}
|
|
22941
23752
|
if (!script && !assetId && !str(args, "project")) throw new Error("Provide --script, --video, --asset or --project with a saved script.");
|
|
@@ -22961,20 +23772,20 @@ async function main() {
|
|
|
22961
23772
|
}
|
|
22962
23773
|
if (subcommand === "get" && argument) {
|
|
22963
23774
|
const document2 = str(args, "document");
|
|
22964
|
-
const
|
|
22965
|
-
if (
|
|
22966
|
-
const result2 = await client.request(`/v1/library/${encodeURIComponent(argument)}${document2 ? `/documents?${new URLSearchParams({ path: document2, ...
|
|
22967
|
-
if (document2 && (typeof result2.content !== "string" || await sha256(result2.content) !== result2.sha256 ||
|
|
23775
|
+
const hash6 = str(args, "sha256");
|
|
23776
|
+
if (hash6 && !document2) throw new Error("--sha256 requires --document for library get.");
|
|
23777
|
+
const result2 = await client.request(`/v1/library/${encodeURIComponent(argument)}${document2 ? `/documents?${new URLSearchParams({ path: document2, ...hash6 ? { sha256: hash6 } : {} })}` : ""}`);
|
|
23778
|
+
if (document2 && (typeof result2.content !== "string" || await sha256(result2.content) !== result2.sha256 || hash6 && result2.sha256 !== hash6)) throw new CliError("HASH_MISMATCH", "Reference document verification failed.", "Do not use this evidence; retry the pinned version.");
|
|
22968
23779
|
output(result2);
|
|
22969
23780
|
return;
|
|
22970
23781
|
}
|
|
22971
23782
|
if (subcommand === "media" && argument && str(args, "out")) {
|
|
22972
|
-
const
|
|
22973
|
-
if (!
|
|
22974
|
-
const destination =
|
|
22975
|
-
const root2 = await ensureRoot(
|
|
22976
|
-
await checkedPath(root2,
|
|
22977
|
-
const response = await client.media(`/v1/library/${encodeURIComponent(argument)}/media/${
|
|
23783
|
+
const hash6 = str(args, "sha256");
|
|
23784
|
+
if (!hash6 || !/^[a-f0-9]{64}$/.test(hash6)) throw new Error("--sha256 must be a media SHA-256 from library get.");
|
|
23785
|
+
const destination = resolve8(str(args, "dir") ?? process.cwd(), str(args, "out"));
|
|
23786
|
+
const root2 = await ensureRoot(dirname11(destination), true);
|
|
23787
|
+
await checkedPath(root2, basename3(destination), false);
|
|
23788
|
+
const response = await client.media(`/v1/library/${encodeURIComponent(argument)}/media/${hash6}`);
|
|
22978
23789
|
const reader = response.body?.getReader();
|
|
22979
23790
|
if (!reader) throw new Error("Media body missing.");
|
|
22980
23791
|
const chunks = [];
|
|
@@ -22990,9 +23801,9 @@ async function main() {
|
|
|
22990
23801
|
chunks.push(value);
|
|
22991
23802
|
}
|
|
22992
23803
|
const bytes = Buffer.concat(chunks);
|
|
22993
|
-
if (await sha256(bytes) !==
|
|
22994
|
-
await atomicWrite(root2,
|
|
22995
|
-
output({ videoId: argument, path: destination, sha256:
|
|
23804
|
+
if (await sha256(bytes) !== hash6) throw new CliError("HASH_MISMATCH", "Reference media verification failed.", "Do not use this file as evidence.");
|
|
23805
|
+
await atomicWrite(root2, basename3(destination), bytes, { document: false, overwrite: false });
|
|
23806
|
+
output({ videoId: argument, path: destination, sha256: hash6, bytes: size, mimeType: response.headers.get("Content-Type"), reviewed: false });
|
|
22996
23807
|
return;
|
|
22997
23808
|
}
|
|
22998
23809
|
throw new Error("Use library search, library get, or library media <video-id> --sha256 hash --out file.");
|
|
@@ -23031,7 +23842,7 @@ async function main() {
|
|
|
23031
23842
|
const adapter = str(args, "adapter") ?? "neutral";
|
|
23032
23843
|
if (!["neutral", "hyperframes", "remotion"].includes(adapter)) throw new Error("Adapter must be neutral, hyperframes or remotion");
|
|
23033
23844
|
const generated = exportProject(resolved, adapter);
|
|
23034
|
-
const destination =
|
|
23845
|
+
const destination = resolve8(root, str(args, "out") ?? `videos/${project}/plans/${adapter}`);
|
|
23035
23846
|
const outRoot = await ensureRoot(destination, true);
|
|
23036
23847
|
for (const [path, content] of Object.entries(generated)) await atomicWrite(outRoot, path, content, { document: false });
|
|
23037
23848
|
output({ adapter, kind: "editorial_handoff", rendered: false, directory: outRoot, files: Object.keys(generated), warnings: resolved.warnings });
|