@praeko/cli 0.0.0-stage → 0.1.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.
@@ -0,0 +1,34 @@
1
+ import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);
2
+
3
+ // ../core/src/contrato.mjs
4
+ var REDES = { yt: "youtube", youtube: "youtube", ig: "instagram", instagram: "instagram", tt: "tiktok", tiktok: "tiktok" };
5
+ var REDES_VALIDAS = ["youtube", "instagram", "tiktok"];
6
+ var MODOS = { youtube: ["privado", "agendado", "publico"], tiktok: ["rascunho", "direto"], instagram: ["agora"] };
7
+ var PROVEDORES = ["buffer", "local", "remoto"];
8
+ function validarPedido(p) {
9
+ const e = [];
10
+ if (!p || typeof p !== "object") return ["pedido vazio"];
11
+ if (!p.midia?.video) e.push("midia.video \xE9 obrigat\xF3rio");
12
+ if (!p.titulo && !p.textos?.youtube?.titulo) e.push("falta o t\xEDtulo");
13
+ if (!Array.isArray(p.destinos) || !p.destinos.length) e.push("destinos vazio");
14
+ for (const d of p.destinos || []) {
15
+ if (!REDES_VALIDAS.includes(d.rede)) {
16
+ e.push(`rede desconhecida: ${d.rede}`);
17
+ continue;
18
+ }
19
+ if (!p.textos?.[d.rede]?.texto) e.push(`falta textos.${d.rede}.texto`);
20
+ if (d.quando != null && Number.isNaN(Date.parse(d.quando))) e.push(`${d.rede}: quando inv\xE1lido (${d.quando})`);
21
+ if (d.midia != null && (typeof d.midia !== "object" || Array.isArray(d.midia) || ["video", "capa"].some((k) => d.midia[k] != null && typeof d.midia[k] !== "string"))) e.push(`${d.rede}: midia do destino precisa ser { video?: <id>, capa?: <id> }`);
22
+ if (d.modo && !MODOS[d.rede].includes(d.modo)) e.push(`${d.rede}: modo "${d.modo}" inv\xE1lido (use ${MODOS[d.rede].join(", ")})`);
23
+ }
24
+ const todos = JSON.stringify(p.textos || {});
25
+ const marc = [...new Set(todos.match(/\[[A-Z][A-Z_]{3,}\]/g) || [])];
26
+ if (marc.length) e.push(`texto com marcador n\xE3o preenchido: ${marc.join(", ")}`);
27
+ return e;
28
+ }
29
+
30
+ export {
31
+ REDES,
32
+ PROVEDORES,
33
+ validarPedido
34
+ };
@@ -0,0 +1,415 @@
1
+ import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);
2
+ import {
3
+ caminhoEstado,
4
+ caminhoLeitura,
5
+ pastaEstado
6
+ } from "./chunk-TTGQU5CG.mjs";
7
+ import {
8
+ segredos
9
+ } from "./chunk-QDZLRBTP.mjs";
10
+ import {
11
+ validarPedido
12
+ } from "./chunk-Z325UM27.mjs";
13
+ import {
14
+ __export
15
+ } from "./chunk-LCFK6X55.mjs";
16
+
17
+ // ../core/src/adaptadores/buffer.mjs
18
+ var buffer_exports = {};
19
+ __export(buffer_exports, {
20
+ REF_CHAVE: () => REF_CHAVE,
21
+ criar: () => criar
22
+ });
23
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
24
+
25
+ // ../core/src/hospedagem.mjs
26
+ import { randomBytes } from "node:crypto";
27
+ import { spawnSync } from "node:child_process";
28
+ import { basename } from "node:path";
29
+ var tipo = (arq) => /\.mp4$/i.test(arq) ? "video/mp4" : /\.mov$/i.test(arq) ? "video/quicktime" : "application/octet-stream";
30
+ function chaveObjeto(slug, arquivo, aleatorio = randomBytes(8).toString("hex")) {
31
+ return `${slug}/${aleatorio}/${basename(arquivo)}`;
32
+ }
33
+ function configHospedagem(config) {
34
+ const h = config.hospedagem;
35
+ if (!h) throw new Error('sem hospedagem: defina em praeko.json "hospedagem": { "tipo": "r2", "bucket": "...", "urlBase": "https://..." } ou passe --url');
36
+ if (h.tipo !== "r2") throw new Error(`hospedagem "${h.tipo}" n\xE3o suportada (use "r2")`);
37
+ if (!h.bucket || !/^https:\/\//.test(h.urlBase || "")) throw new Error("hospedagem r2 precisa de bucket e urlBase https://");
38
+ return h;
39
+ }
40
+ function subir({ h, slug, arquivo, cwd, rodar = spawnSync }) {
41
+ const chave = chaveObjeto(slug, arquivo);
42
+ const r = rodar("npx", ["--yes", "wrangler@4", "r2", "object", "put", `${h.bucket}/${chave}`, "--file", arquivo, "--content-type", tipo(arquivo), "--remote"], { cwd, stdio: "inherit" });
43
+ if (r.status !== 0) throw new Error("wrangler n\xE3o conseguiu subir o arquivo (voc\xEA fez `npx wrangler login`?)");
44
+ return { url: `${h.urlBase.replace(/\/$/, "")}/${chave}`, chave };
45
+ }
46
+ function apagar({ h, chave, cwd, rodar = spawnSync }) {
47
+ const r = rodar("npx", ["--yes", "wrangler@4", "r2", "object", "delete", `${h.bucket}/${chave}`, "--remote"], { cwd, stdio: "inherit" });
48
+ return r.status === 0;
49
+ }
50
+
51
+ // ../core/src/capa-quadro.mjs
52
+ var capa_quadro_exports = {};
53
+ __export(capa_quadro_exports, {
54
+ argsFfmpeg: () => argsFfmpeg,
55
+ dimensoesImagem: () => dimensoesImagem,
56
+ embutirCapa: () => embutirCapa,
57
+ mesmaProporcao: () => mesmaProporcao,
58
+ sondar: () => sondar
59
+ });
60
+ import { spawnSync as spawnSync2 } from "node:child_process";
61
+ var num = (s) => {
62
+ const [a, b] = String(s).split("/").map(Number);
63
+ return b ? a / b : a;
64
+ };
65
+ function sondar(arq, rodar = spawnSync2) {
66
+ const r = rodar("ffprobe", ["-v", "error", "-select_streams", "v:0", "-show_entries", "stream=width,height,r_frame_rate", "-show_entries", "format=duration", "-of", "json", arq], { encoding: "utf8" });
67
+ if (r.status !== 0) throw new Error(`ffprobe falhou em ${arq}`);
68
+ const j = JSON.parse(r.stdout), s = j.streams[0];
69
+ return { largura: s.width, altura: s.height, fps: num(s.r_frame_rate), duracao: +j.format.duration };
70
+ }
71
+ function dimensoesImagem(arq, rodar = spawnSync2) {
72
+ const r = rodar("ffprobe", ["-v", "error", "-show_entries", "stream=width,height", "-of", "json", arq], { encoding: "utf8" });
73
+ if (r.status !== 0) throw new Error(`ffprobe falhou em ${arq}`);
74
+ const s = JSON.parse(r.stdout).streams[0];
75
+ return { largura: s.width, altura: s.height };
76
+ }
77
+ var mesmaProporcao = (a, b) => Math.abs(a.largura / a.altura - b.largura / b.altura) < 0.02 * (b.largura / b.altura);
78
+ function argsFfmpeg({ video, capa, saida, largura, altura, quadros = 2, duracao = 3600, fps = 30 }) {
79
+ const filtro = `[1:v]scale=${largura}:${altura}:force_original_aspect_ratio=increase,crop=${largura}:${altura},setsar=1,format=yuv420p[c];[0:v][c]overlay=enable='lt(n,${quadros})':format=auto,format=yuv420p[v]`;
80
+ return ["-v", "error", "-y", "-i", video, "-loop", "1", "-framerate", String(fps), "-t", String(duracao + 1), "-i", capa, "-filter_complex", filtro, "-map", "[v]", "-map", "0:a?", "-c:v", "libx264", "-crf", "16", "-preset", "medium", "-c:a", "copy", "-movflags", "+faststart", "-t", String(duracao), saida];
81
+ }
82
+ function embutirCapa({ video, capa, saida, quadros = 2, rodar = spawnSync2 }) {
83
+ const v = sondar(video, rodar), c = dimensoesImagem(capa, rodar);
84
+ if (!mesmaProporcao(c, v)) throw new Error(`a capa (${c.largura}x${c.altura}) n\xE3o tem a propor\xE7\xE3o do v\xEDdeo (${v.largura}x${v.altura})`);
85
+ const r = rodar("ffmpeg", argsFfmpeg({ video, capa, saida, largura: v.largura, altura: v.altura, quadros, duracao: v.duracao, fps: v.fps }), { stdio: "inherit" });
86
+ if (r.status !== 0) throw new Error("ffmpeg falhou ao colar a capa");
87
+ const d = sondar(saida, rodar);
88
+ if (Math.abs(d.duracao - v.duracao) > 0.1) throw new Error(`a dura\xE7\xE3o mudou (${v.duracao.toFixed(2)} s \u2192 ${d.duracao.toFixed(2)} s); abortei`);
89
+ return { saida, quadros };
90
+ }
91
+
92
+ // ../core/src/config.mjs
93
+ import { existsSync, readFileSync } from "node:fs";
94
+ import { dirname, join, resolve } from "node:path";
95
+ var ARQUIVOS_CONFIG = ["praeko.json", "arauto.json", "marca.json"];
96
+ function normalizarConfig(bruto = {}) {
97
+ if (bruto.publicacao && typeof bruto.publicacao === "object") {
98
+ const { publicacao, links, nome, handle } = bruto;
99
+ return { nome, handle, links, ...publicacao };
100
+ }
101
+ return { ...bruto };
102
+ }
103
+ function acharConfig(de = process.cwd()) {
104
+ let d = resolve(de);
105
+ for (; ; ) {
106
+ for (const nome of ARQUIVOS_CONFIG) {
107
+ const f = join(d, nome);
108
+ if (existsSync(f)) return { raiz: d, arquivo: f, config: normalizarConfig(JSON.parse(readFileSync(f, "utf8"))) };
109
+ }
110
+ const p = dirname(d);
111
+ if (p === d) return null;
112
+ d = p;
113
+ }
114
+ }
115
+
116
+ // ../core/src/contexto.mjs
117
+ function criarContexto({ config, raiz = process.cwd(), log = console.log, fetchFn = globalThis.fetch, segredos: segredos2 = segredos, agora = () => /* @__PURE__ */ new Date() } = {}) {
118
+ const h = () => configHospedagem(config);
119
+ return {
120
+ config,
121
+ raiz,
122
+ log,
123
+ fetchFn,
124
+ segredos: segredos2,
125
+ agora,
126
+ /** Sobe um arquivo para a hospedagem pública temporária (R2). Devolve { url, chave }. */
127
+ hospedar: async (arquivo, slug = "midia") => subir({ h: h(), slug, arquivo, cwd: raiz }),
128
+ removerHospedado: async (chave) => apagar({ h: h(), chave, cwd: raiz })
129
+ };
130
+ }
131
+
132
+ // ../core/src/index.mjs
133
+ var PADRAO = {
134
+ utm: { instagram: ["instagram", "bio"], youtube: ["youtube", "short"], tiktok: ["tiktok", "bio"] },
135
+ limites: { youtubeTitulo: 100, youtubeDescricao: 5e3, instagram: 2200, tiktok: 2200, hashtagsInstagram: 30 },
136
+ hashtagsProibidas: [],
137
+ // marcas de texto escrito por IA (a skill copywriting do projeto proíbe)
138
+ marcasDeIA: [
139
+ { re: /\bnão é [^.!?]{1,45}, é\s/i, aviso: 'estrutura "n\xE3o \xE9 X, \xE9 Y"' },
140
+ { re: /\bsem [^,.!?]{1,25}, sem [^,.!?]{1,25}, sem\b/i, aviso: 'lista "sem X, sem Y, sem Z"' }
141
+ ]
142
+ };
143
+ function link(config, { plataforma, destino = "jogo", campanha, formato }) {
144
+ const base = config.links?.[destino];
145
+ if (!base) throw new Error(`links.${destino} n\xE3o definido na configura\xE7\xE3o (praeko.json)`);
146
+ const u = { ...PADRAO.utm, ...config.utm || {} };
147
+ let [fonte, midia] = u[plataforma] || [plataforma, "bio"];
148
+ if (plataforma === "youtube" && formato) midia = formato === "longo" ? "long" : formato;
149
+ const q = new URLSearchParams({ utm_source: fonte, utm_medium: midia });
150
+ if (campanha) q.set("utm_campaign", campanha);
151
+ return `${base}${base.includes("?") ? "&" : "?"}${q.toString()}`;
152
+ }
153
+ var tags = (l) => (l || []).join(" ");
154
+ var mmss = (s) => {
155
+ const t = Math.round(s), h = Math.floor(t / 3600), m = Math.floor(t % 3600 / 60), x = t % 60;
156
+ return h ? `${h}:${String(m).padStart(2, "0")}:${String(x).padStart(2, "0")}` : `${m}:${String(x).padStart(2, "0")}`;
157
+ };
158
+ var capitulos = (lista) => lista.map((c) => `${mmss(c.t)} ${c.titulo}`).join("\n");
159
+ function validarCapitulos(lista) {
160
+ const e = [];
161
+ if (!lista?.length) return e;
162
+ if (lista.length < 3) e.push(`${lista.length} cap\xEDtulo(s): o YouTube s\xF3 cria cap\xEDtulos com 3 ou mais`);
163
+ if (Math.round(lista[0].t) !== 0) e.push("o primeiro cap\xEDtulo precisa come\xE7ar em 0:00");
164
+ for (let i = 1; i < lista.length; i++) if (lista[i].t - lista[i - 1].t < 10) e.push(`cap\xEDtulo "${lista[i - 1].titulo}" dura ${(lista[i].t - lista[i - 1].t).toFixed(1)} s (m\xEDnimo 10 s)`);
165
+ return e;
166
+ }
167
+ function validar({ config, ep }) {
168
+ const L = { ...PADRAO.limites, ...config.limites || {} };
169
+ const proibidas = [...PADRAO.hashtagsProibidas, ...config.hashtags?.proibidas || []].map((h) => h.toLowerCase());
170
+ const permitidas = (config.hashtags?.permitidas || []).map((h) => h.toLowerCase());
171
+ const erros = [], avisos = [];
172
+ const yt = `${ep.yt?.descricao || ""}
173
+ ${ep.capitulos ? capitulos(ep.capitulos) : ""}
174
+ ${tags(ep.hashtags?.youtube)}`;
175
+ for (const x of validarCapitulos(ep.capitulos)) erros.push(`cap\xEDtulos: ${x}`);
176
+ if ((ep.titulo || "").length > L.youtubeTitulo) erros.push(`t\xEDtulo com ${ep.titulo.length} caracteres (limite ${L.youtubeTitulo})`);
177
+ if (yt.length > L.youtubeDescricao) erros.push(`descri\xE7\xE3o do YouTube com ${yt.length} caracteres (limite ${L.youtubeDescricao})`);
178
+ const ig = `${ep.ig?.legenda || ""}
179
+ ${tags(ep.hashtags?.instagram)}`;
180
+ if (ig.length > L.instagram) erros.push(`legenda do Instagram com ${ig.length} caracteres (limite ${L.instagram})`);
181
+ const tk = `${ep.tiktok?.legenda || ""}
182
+ ${tags(ep.hashtags?.tiktok)}`;
183
+ if (tk.length > L.tiktok) avisos.push(`legenda do TikTok com ${tk.length} caracteres (limite sugerido ${L.tiktok})`);
184
+ if ((ep.hashtags?.instagram || []).length > L.hashtagsInstagram) erros.push(`mais de ${L.hashtagsInstagram} hashtags no Instagram`);
185
+ for (const [pl, hs] of Object.entries(ep.hashtags || {})) for (const h of hs) {
186
+ if (proibidas.includes(h.toLowerCase())) erros.push(`hashtag proibida em ${pl}: ${h}`);
187
+ else if (permitidas.length && !permitidas.includes(h.toLowerCase())) avisos.push(`hashtag fora da lista permitida em ${pl}: ${h}`);
188
+ }
189
+ const tudo = [ep.titulo, ...ep.alternativas || [], ep.yt?.descricao, ep.ig?.legenda, ep.ig?.comentarioFixo, ep.tiktok?.legenda, ep.respostaComentario].filter(Boolean).join("\n");
190
+ for (const m of PADRAO.marcasDeIA) if (m.re.test(tudo)) avisos.push(`marca de texto de IA: ${m.aviso}`);
191
+ return { erros, avisos };
192
+ }
193
+
194
+ // ../core/src/buffer-api.mjs
195
+ var ENDPOINT = "https://api.buffer.com";
196
+ var ANTECEDENCIA_MIN_MS = 5 * 6e4;
197
+ async function gql(consulta, variaveis, { chave, fetchFn = fetch } = {}) {
198
+ if (!chave) throw new Error("sem chave do Buffer (PRAEKO_KEY_BUFFER_PRINCIPAL, Chaveiro do macOS ou `praeko agendar --chave`)");
199
+ const r = await fetchFn(ENDPOINT, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${chave}` }, body: JSON.stringify({ query: consulta, variables: variaveis || {} }) });
200
+ const txt = await r.text();
201
+ let j;
202
+ try {
203
+ j = JSON.parse(txt);
204
+ } catch {
205
+ throw new Error(`Buffer respondeu ${r.status}: ${txt.slice(0, 200)}`);
206
+ }
207
+ if (!r.ok || j.errors?.length) throw new Error(`Buffer: ${(j.errors || []).map((e) => e.message).join("; ") || r.status}`);
208
+ return j.data;
209
+ }
210
+ var Q_CANAIS = `query { account { organizations { id name } } }`;
211
+ var Q_CANAIS_ORG = `query($org: OrganizationId!) { channels(input: { organizationId: $org }) { id name displayName service avatar isQueuePaused } }`;
212
+ async function listarCanais(op) {
213
+ const d = await gql(Q_CANAIS, {}, op);
214
+ const lista = [];
215
+ for (const o of d.account.organizations) {
216
+ const c = await gql(Q_CANAIS_ORG, { org: o.id }, op);
217
+ for (const x of c.channels) lista.push({ id: x.id, nome: x.displayName || x.name, servico: x.service, avatar: x.avatar || null, org: o.name, pausado: !!x.isQueuePaused });
218
+ }
219
+ return lista;
220
+ }
221
+ var M_CRIAR = `mutation($input: CreatePostInput!) { createPost(input: $input) { ... on PostActionSuccess { post { id dueAt } } ... on MutationError { message } } }`;
222
+ var M_APAGAR = `mutation($id: PostId!) { deletePost(input: { id: $id }) { ... on DeletePostSuccess { __typename } ... on MutationError { message } } }`;
223
+ async function criarPost(input, op) {
224
+ const d = await gql(M_CRIAR, { input }, op);
225
+ const r = d.createPost;
226
+ if (r.message) throw new Error(`Buffer recusou o post: ${r.message}`);
227
+ return r.post;
228
+ }
229
+ async function apagarPost(id, op) {
230
+ const d = await gql(M_APAGAR, { id }, op);
231
+ if (d.deletePost?.message) throw new Error(`Buffer n\xE3o apagou ${id}: ${d.deletePost.message}`);
232
+ return true;
233
+ }
234
+
235
+ // ../core/src/buffer.mjs
236
+ var tags2 = (l) => (l || []).join(" ");
237
+ function parseQuando(txt, { fuso = "-03:00", agora = /* @__PURE__ */ new Date() } = {}) {
238
+ const s = String(txt).trim();
239
+ let m = s.match(/^(\d{1,2})\/(\d{1,2})(?:\/(\d{4}))?[ T]+(\d{1,2}):(\d{2})$/);
240
+ let iso;
241
+ if (m) {
242
+ let ano = m[3] ? +m[3] : agora.getFullYear();
243
+ const mk = (a) => `${a}-${String(m[2]).padStart(2, "0")}-${String(m[1]).padStart(2, "0")}T${String(m[4]).padStart(2, "0")}:${m[5]}:00${fuso}`;
244
+ iso = mk(ano);
245
+ if (!m[3] && new Date(iso) < agora) iso = mk(ano + 1);
246
+ } else if (/^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(:\d{2})?$/.test(s)) iso = s.replace(" ", "T") + (s.length === 16 ? ":00" : "") + fuso;
247
+ else if (/^\d{4}-\d{2}-\d{2}T[\d:.]+(Z|[+-]\d{2}:\d{2})$/.test(s)) iso = s;
248
+ else throw new Error(`data inv\xE1lida: "${txt}" (use "06/11 17:30", "06/11/2026 17:30" ou "2026-11-06T17:30")`);
249
+ const d = new Date(iso);
250
+ if (Number.isNaN(d.getTime())) throw new Error(`data inv\xE1lida: "${txt}"`);
251
+ return d.toISOString();
252
+ }
253
+ var privacidadeBuffer = (modo, padrao) => modo === "privado" ? "private" : modo === "publico" || modo === "agendado" ? void 0 : padrao;
254
+ function textosPorRede({ config, ep }) {
255
+ const lk = (plataforma) => link(config, { plataforma, campanha: ep.campanha, formato: ep.formato || "short" });
256
+ const longo = ep.formato === "longo";
257
+ const yt = [ep.yt?.descricao || ""];
258
+ if (ep.capitulos?.length) yt.push("", "\u23F1 Cap\xEDtulos", capitulos(ep.capitulos));
259
+ if (!longo && ep.artigo) yt.push("", `\u{1F4D6} Artigo completo: ${ep.artigo}`);
260
+ if (!longo && ep.videoCompleto) yt.push(`\u25B6\uFE0F Completo: ${ep.videoCompleto}`);
261
+ yt.push("", `\u{1F517} O jogo: ${lk("youtube")}`, "", tags2(ep.hashtags?.youtube));
262
+ return {
263
+ youtube: { titulo: ep.titulo, texto: yt.join("\n").trim(), comentarioFixo: ep.yt?.comentarioFixo },
264
+ instagram: { texto: `${ep.ig?.legenda || ""}${ep.hashtags?.instagram ? " " + tags2(ep.hashtags.instagram) : ""}`.trim(), comentarioFixo: ep.ig?.comentarioFixo },
265
+ tiktok: { texto: `${ep.tiktok?.legenda || ep.ig?.legenda || ""}${ep.hashtags?.tiktok ? " " + tags2(ep.hashtags.tiktok) : ""}`.trim() }
266
+ };
267
+ }
268
+ function montarPosts({ config, ep, textos, modos = {}, canais, videoUrl, quando, redes = ["youtube", "instagram", "tiktok"], lembrete = false, miniaturaMs }) {
269
+ const t = textos || textosPorRede({ config, ep });
270
+ const cfg = config.buffer || {};
271
+ const miniatura = miniaturaMs ?? cfg.miniaturaMs ?? 1e3;
272
+ const base = (rede) => ({
273
+ channelId: canais[rede],
274
+ schedulingType: lembrete ? "notification" : "automatic",
275
+ mode: "customScheduled",
276
+ dueAt: quando[rede] || quando.todas,
277
+ needsApproval: false,
278
+ source: "arauto",
279
+ assets: [{ video: { url: videoUrl, metadata: rede === "youtube" ? void 0 : { thumbnailOffset: miniatura } } }]
280
+ });
281
+ const out = [];
282
+ for (const rede of redes) {
283
+ if (!t[rede]) throw new Error(`rede desconhecida: ${rede}`);
284
+ const p = { ...base(rede), text: t[rede].texto };
285
+ if (rede === "youtube") {
286
+ p.metadata = { youtube: { title: t.youtube.titulo, madeForKids: false, notifySubscribers: cfg.youtube?.avisarInscritos ?? true, categoryId: String(cfg.youtube?.categoriaId ?? "28"), ...privacidadeBuffer(modos.youtube, cfg.youtube?.privacidade) ? { privacy: privacidadeBuffer(modos.youtube, cfg.youtube?.privacidade) } : {} } };
287
+ } else if (rede === "instagram") {
288
+ p.metadata = { instagram: { type: "reel", shouldShareToFeed: true, ...cfg.primeiroComentario && t.instagram.comentarioFixo ? { firstComment: t.instagram.comentarioFixo } : {} } };
289
+ } else p.metadata = { tiktok: {} };
290
+ if (!p.dueAt) throw new Error(`sem hor\xE1rio para ${rede}`);
291
+ out.push({ rede, input: JSON.parse(JSON.stringify(p)) });
292
+ }
293
+ return out;
294
+ }
295
+ function escolherCanais({ config, canais, redes }) {
296
+ const cfg = config.buffer?.canais || {};
297
+ const saida = {}, faltam = [];
298
+ for (const r of redes) {
299
+ if (cfg[r]) {
300
+ saida[r] = cfg[r];
301
+ continue;
302
+ }
303
+ const ach = canais.filter((c) => c.servico === r);
304
+ if (ach.length === 1) saida[r] = ach[0].id;
305
+ else faltam.push(ach.length ? `${r}: ${ach.length} canais (${ach.map((c) => `${c.nome}=${c.id}`).join(", ")}); fixe um em buffer.canais.${r} (praeko.json)` : `${r}: nenhum canal conectado no Buffer`);
306
+ }
307
+ return { canais: saida, faltam };
308
+ }
309
+
310
+ // ../core/src/adaptadores/buffer.mjs
311
+ import { randomBytes as randomBytes2 } from "node:crypto";
312
+ var REF_CHAVE = "buffer/principal";
313
+ function criar({ config, ctx }) {
314
+ const arq = caminhoEstado(ctx.raiz, "buffer.json"), arqLeitura = () => caminhoLeitura(ctx.raiz, "buffer.json");
315
+ const ler = () => existsSync2(arqLeitura()) ? JSON.parse(readFileSync2(arqLeitura(), "utf8")) : { pedidos: {} };
316
+ const gravar = (r) => {
317
+ mkdirSync(pastaEstado(ctx.raiz), { recursive: true });
318
+ writeFileSync(arq, JSON.stringify(r, null, 2) + "\n");
319
+ };
320
+ const op = () => {
321
+ const chave = ctx.segredos.obter(REF_CHAVE);
322
+ if (!chave) throw new Error("sem chave do Buffer: `echo $CHAVE | praeko agendar --chave` (publish.buffer.com/settings/api)");
323
+ return { chave, fetchFn: ctx.fetchFn };
324
+ };
325
+ const cq = ctx.deps?.capaQuadro || capa_quadro_exports;
326
+ return {
327
+ nome: "buffer",
328
+ async publicar(pedido) {
329
+ const erros = validarPedido(pedido);
330
+ if (erros.length) throw new Error(`pedido inv\xE1lido: ${erros.join("; ")}`);
331
+ const sem = pedido.destinos.filter((d) => !d.quando).map((d) => d.rede);
332
+ if (sem.length) throw new Error(`o Buffer precisa de hor\xE1rio (falta em ${sem.join(", ")})`);
333
+ const id = pedido.id || `p${randomBytes2(3).toString("hex")}`, reg = ler();
334
+ const redes = pedido.destinos.map((d) => d.rede), quando = Object.fromEntries(pedido.destinos.map((d) => [d.rede, d.quando]));
335
+ const modos = Object.fromEntries(pedido.destinos.filter((d) => d.modo).map((d) => [d.rede, d.modo]));
336
+ const o = op();
337
+ const { canais, faltam } = escolherCanais({ config, canais: await listarCanais(o), redes });
338
+ if (faltam.length) throw new Error(faltam.join("\n"));
339
+ let videoUrl = pedido.opcoes?.videoUrl, chaveObjeto2, arquivo = pedido.midia.video, capaEmbutida = false;
340
+ const previo = reg.pedidos[id];
341
+ if (!videoUrl && previo?.videoUrl) {
342
+ videoUrl = previo.videoUrl;
343
+ chaveObjeto2 = previo.chaveObjeto;
344
+ }
345
+ if (!videoUrl) {
346
+ const capa = pedido.midia.capa;
347
+ if (capa && pedido.opcoes?.capaNoQuadro !== false && pedido.opcoes?.formato !== "longo") {
348
+ const v = cq.sondar(arquivo), c = cq.dimensoesImagem(capa);
349
+ if (cq.mesmaProporcao(c, v)) {
350
+ const saida = caminhoEstado(ctx.raiz, `${id}.capa.mp4`);
351
+ mkdirSync(pastaEstado(ctx.raiz), { recursive: true });
352
+ cq.embutirCapa({ video: arquivo, capa, saida });
353
+ arquivo = saida;
354
+ capaEmbutida = true;
355
+ }
356
+ }
357
+ const s = await ctx.hospedar(arquivo, id);
358
+ videoUrl = s.url;
359
+ chaveObjeto2 = s.chave;
360
+ }
361
+ const posts = montarPosts({ config, textos: pedido.textos, modos, canais, videoUrl, quando, redes, lembrete: !!pedido.opcoes?.lembrete, ...capaEmbutida ? { miniaturaMs: 0 } : {} });
362
+ const r = reg.pedidos[id] = { id, titulo: pedido.titulo, videoUrl, chaveObjeto: chaveObjeto2, criadoEm: (/* @__PURE__ */ new Date()).toISOString(), posts: previo?.posts || [] };
363
+ for (const x of posts) {
364
+ try {
365
+ const p = await criarPost(x.input, o);
366
+ r.posts.push({ rede: x.rede, id: p.id, dueAt: x.input.dueAt, canalId: x.input.channelId });
367
+ gravar(reg);
368
+ } catch (e) {
369
+ gravar(reg);
370
+ throw new Error(`${x.rede}: ${e.message} (j\xE1 criado: ${r.posts.map((q) => q.rede).join(", ") || "nada"})`);
371
+ }
372
+ }
373
+ return { id, itens: r.posts.map((p) => ({ rede: p.rede, id: p.id, estado: "agendado", quando: p.dueAt })) };
374
+ },
375
+ async status(id) {
376
+ const p = ler().pedidos[id];
377
+ if (!p) throw new Error(`pedido ${id} n\xE3o existe`);
378
+ return { id, itens: p.posts.map((x) => ({ rede: x.rede, id: x.id, estado: "agendado", quando: x.dueAt })) };
379
+ },
380
+ async cancelar(id, { redes } = {}) {
381
+ const reg = ler(), p = reg.pedidos[id];
382
+ if (!p) throw new Error(`pedido ${id} n\xE3o existe`);
383
+ const o = op(), cancelados = [], mantidos = [];
384
+ for (const x of p.posts) {
385
+ if (!redes || redes.includes(x.rede)) {
386
+ await apagarPost(x.id, o);
387
+ cancelados.push(x.id);
388
+ } else mantidos.push(x.id);
389
+ }
390
+ p.posts = p.posts.filter((x) => !cancelados.includes(x.id));
391
+ if (!p.posts.length) {
392
+ if (p.chaveObjeto && ctx.removerHospedado) await ctx.removerHospedado(p.chaveObjeto);
393
+ delete reg.pedidos[id];
394
+ }
395
+ gravar(reg);
396
+ return { cancelados, mantidos };
397
+ },
398
+ async canais() {
399
+ return (await listarCanais(op())).map((c) => ({ rede: c.servico, id: c.id, conectado: true, detalhe: `${c.nome} [${c.org}]${c.pausado ? " (fila pausada)" : ""}` }));
400
+ }
401
+ };
402
+ }
403
+
404
+ export {
405
+ parseQuando,
406
+ textosPorRede,
407
+ capa_quadro_exports,
408
+ normalizarConfig,
409
+ acharConfig,
410
+ criarContexto,
411
+ REF_CHAVE,
412
+ criar,
413
+ buffer_exports,
414
+ validar
415
+ };