@barbapapazes/content-creation 0.19.0 → 0.20.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 CHANGED
@@ -23,6 +23,7 @@ content-creation <command> --help
23
23
  - `content upcoming` — list upcoming publications
24
24
  - `content rebuild-indexes` — regenerate content indexes
25
25
  - `series new` — create a new series directory
26
+ - `series video process` — process a series video into subtitles and optional thumbnails
26
27
  - `series article new` — insert a new article in a series
27
28
  - `series article ready` — mark a series article as ready and refresh reading time
28
29
  - `series article estimate-time` — compute reading time for a series article
@@ -46,6 +47,28 @@ Environment variables used by advanced workflows:
46
47
  - `AUTOMATION_ENDPOINT`
47
48
  - `CF_ACCESS_CLIENT_ID`
48
49
  - `CF_ACCESS_CLIENT_SECRET`
50
+ - `OPENAI_API_KEY`
51
+ - `OPENAI_TRANSCRIPTION_LANGUAGE`
52
+ - `OPENAI_TRANSCRIPTION_MODEL`
53
+
54
+ Example configuration for the `linkedin ready` command only:
55
+
56
+ ```ts
57
+ import { defineConfig } from '@barbapapazes/content-creation'
58
+
59
+ export default defineConfig({
60
+ linkedin: {
61
+ ready: {
62
+ copilot: {
63
+ model: 'gpt-5',
64
+ reasoningEffort: 'medium',
65
+ },
66
+ },
67
+ },
68
+ })
69
+ ```
70
+
71
+ For series video processing, the command scans the selected series directory recursively for `.mp4` files, generates an `.srt` transcription next to the chosen video, and can optionally render thumbnails into a `thumbnails/` subdirectory.
49
72
 
50
73
  ## License
51
74
 
package/dist/cli.mjs CHANGED
@@ -1,9 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import e from"node:process";import{intro as t,isCancel as n,log as r,multiselect as i,outro as a,select as o,text as s}from"@clack/prompts";import{Command as c}from"commander";import{existsSync as l,mkdirSync as u,readFileSync as d,readdirSync as f,renameSync as ee,statSync as p,writeFileSync as m}from"node:fs";import{basename as te,dirname as ne,isAbsolute as re,join as h,resolve as g}from"node:path";import{addDays as _,format as v,parseISO as ie,setHours as ae,setMinutes as oe,setSeconds as se}from"date-fns";import y from"gray-matter";import{loadConfig as ce}from"c12";import{execFileSync as le}from"node:child_process";import{createHash as ue}from"node:crypto";import{fileURLToPath as de}from"node:url";var fe=`@barbapapazes/content-creation`,pe=`0.19.0`,me=`CLI tool for content creation and management`;const b=[`linkedin`,`x`,`youtube`,`instagram`],x={linkedin:`LinkedIn`,x:`X`,youtube:`YouTube`,instagram:`Instagram`},S={linkedin:{mainFile:`linkedin.md`,additionalFiles:[`linkedin-script.md`]},x:{mainFile:`x.md`},youtube:{mainFile:`youtube-script.md`,additionalFiles:[`youtube-description.md`]},instagram:{mainFile:`instagram-script.md`,additionalFiles:[`instagram-description.md`]}},he={linkedin:{fileName:`linkedin-posts.md`,heading:`LinkedIn Posts`},x:{fileName:`x-posts.md`,heading:`X Posts`},youtube:{fileName:`youtube-videos.md`,heading:`YouTube Videos`},instagram:{fileName:`instagram-posts.md`,heading:`Instagram Posts`}},C=`__load-more__`,ge=[`.jpg`,`.jpeg`,`.png`],w={hours:11,minutes:0,seconds:0},_e={thematic:[],templatesDir:void 0,templates:{},scheduling:{},calendar:{},reading:{wordsPerMinute:100}};function ve(t,n){return t?re(t)?t:g(n?ne(n):e.cwd(),t):n?ne(n):e.cwd()}function T(e,t,n){if(!t)return;let i=g(e,t);if(!l(i)){r.warn(`Configured ${n} template was not found: ${i}`);return}return i}function ye(e,t){return{footerPath:T(t,e?.footerPath,`LinkedIn footer`)}}function be(e,t){return{templatePath:T(t,e?.templatePath,`video description`)}}async function E(){let{config:t,configFile:n}=await ce({name:`content-creation`,defaults:_e,globalRc:!0,dotenv:!0}),r=ve(t.templatesDir,n),i={linkedin:ye(t.templates?.linkedin,r),youtube:be(t.templates?.youtube,r),instagram:be(t.templates?.instagram,r)},a={automationEndpoint:t.scheduling?.automationEndpoint??e.env.AUTOMATION_ENDPOINT,cfAccessClientId:t.scheduling?.cfAccessClientId??e.env.CF_ACCESS_CLIENT_ID,cfAccessClientSecret:t.scheduling?.cfAccessClientSecret??e.env.CF_ACCESS_CLIENT_SECRET},o={publicUrl:t.calendar?.publicUrl??e.env.CALENDAR_PUBLIC_URL,token:t.calendar?.token??e.env.CALENDAR_TOKEN},s=t.reading?.wordsPerMinute??_e.reading?.wordsPerMinute??100;if(!Number.isFinite(s)||s<=0)throw RangeError(`Reading wordsPerMinute must be a positive number`);return{thematic:t.thematic??[],templatesDir:t.templatesDir??``,templates:i,scheduling:a,calendar:o,reading:{wordsPerMinute:s}}}function D(e){return e?l(e)?d(e,`utf-8`):(r.warn(`Template file not found: ${e}`),``):``}function xe(e,t,n,i,a){let o=g(h(a,v(e,`yyyy`),v(e,`MM`),v(e,`dd`)));l(o)||(u(o,{recursive:!0}),r.success(`Created directory: ${o}`));let s=Se(n);for(let e of t)Ce(e,o,s,i);return o}function Se(e){return e.hasVideo?{...e,hasImages:!1}:{...e,hasImages:!0}}function Ce(e,t,n,i){let a=S[e],o=h(t,a.mainFile),s=`additionalFiles`in a?a.additionalFiles:void 0;if(l(o)?r.info(`${a.mainFile} already exists, skipping`):we(e,o,n,i),s)for(let a of s){let o=h(t,a);Ee(a,e,n)&&(l(o)?r.info(`${a} already exists, skipping`):Te(e,o,a,i))}}function we(e,t,n,i){let a={title:n.title};e===`linkedin`&&(n.theme&&(a.theme=n.theme),n.hasVideo&&(a.video=!0),n.hasImages&&(a.images=[null]));let o=``;e===`linkedin`&&(o=D(i.templates.linkedin?.footerPath)),m(t,y.stringify(o,a),`utf-8`),r.success(`Created ${t}`)}function Te(e,t,n,i){let a=``;n.endsWith(`-description.md`)&&(a=D(i.templates[e]?.templatePath)),m(t,a,`utf-8`),r.success(`Created ${t}`)}function Ee(e,t,n){return t===`linkedin`&&e===`linkedin-script.md`?n.hasVideo||!1:!0}function De(e,t,n){let r=n===`date-asc`?e.date.getTime()-t.date.getTime():t.date.getTime()-e.date.getTime();if(r!==0)return r;let i=b.indexOf(e.contentType)-b.indexOf(t.contentType);return i===0?e.relativePath.localeCompare(t.relativePath):i}function O(e,t={}){let n=[],i=t.contentTypes?.length?t.contentTypes:[...b],a=t.fromDate,o=t.sort||`date-desc`;try{let t=f(e).filter(t=>p(h(e,t)).isDirectory()&&/^\d{4}$/.test(t));for(let o of t){let t=h(e,o),s=f(t).filter(e=>p(h(t,e)).isDirectory()&&/^\d{2}$/.test(e));for(let e of s){let s=h(t,e),c=f(s).filter(e=>p(h(s,e)).isDirectory()&&/^\d{2}$/.test(e));for(let t of c){let c=h(s,t),u=`${o}-${e}-${t}`,f=new Date(Number(o),Number(e)-1,Number(t));if(!(a&&f.getTime()<a.getTime()))for(let a of i){let i=S[a].mainFile,s=g(c,i);if(l(s))try{let{data:r}=y(d(s,`utf-8`)),l=r,ee=l.title||`Untitled (${u})`;n.push({contentType:a,path:s,folderPath:c,date:f,title:ee,relativePath:`${o}/${e}/${t}/${i}`,frontmatter:l})}catch(e){r.warn(`Error reading ${s}: ${e}`)}}}}}}catch(e){return r.error(`Error reading directories: ${e}`),[]}return n.sort((e,t)=>De(e,t,o))}function Oe(t){let n=new Date;n.setHours(0,0,0,0);let i=O(t,{fromDate:n,sort:`date-asc`});if(i.length===0){r.info(`No upcoming publications found from today onward.`);return}r.info(`Found ${i.length} upcoming publication${i.length===1?``:`s`}:`),e.stdout.write(`
3
- `);for(let t of i){let n=v(t.date,`EEEE, MMMM d, yyyy`),r=v(t.date,`yyyy-MM-dd`);e.stdout.write(`${n} (${r}) · ${x[t.contentType]} · ${t.title}\n`),e.stdout.write(`${t.path}\n\n`)}}function ke(e,t){let n=O(e,{contentTypes:[t],sort:`date-desc`});if(n.length===0){r.info(`No ${t} posts found`);return}let{fileName:i,heading:a}=he[t],o=[`# ${a}`,``,`_Generated on ${new Date().toLocaleDateString()}_`,``,`Total posts: ${n.length}`,``];for(let e of n){let t=e.date.toLocaleDateString(`en-US`,{year:`numeric`,month:`long`,day:`numeric`});o.push(`- [${e.title}](${e.relativePath}) - _${t}_`)}m(h(e,i),`${o.join(`
4
- `)}\n`,`utf-8`),r.success(`Created ${i} with ${n.length} posts`)}function Ae(e){for(let t of b)ke(e,t)}function je(t=`Operation cancelled.`){r.error(t),e.exit(0)}function k(t){r.error(t),e.exit(1)}function A(e,t){return n(e)&&je(t),e}function j(e){let{content:t,data:n}=y(d(e,`utf-8`));return{content:t,frontmatter:n}}function M(e,t){m(e,y.stringify(t.content,t.frontmatter),`utf-8`)}async function N(e){return A(await s({message:e.message,placeholder:e.placeholder,validate:t=>{if(!t||t.trim().length===0)return e.requiredMessage||`Value is required`}})).trim()}const Me=/<!--([\s\S]*?)-->/g;function Ne(e,t){let n=e.replace(Me,``).replace(/\s+/g,``);if(n.length===0)return 0;let r=n.length/5;return Math.ceil(r/t)}function Pe(e){return e.charAt(0).toUpperCase()+e.slice(1)}function P(e){return e.split(`-`).filter(Boolean).map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join(` `)}function F(e){return e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`)}function I(e,t){try{let{content:n,frontmatter:i}=j(e.path);if(i.ready===!0){r.warn(`This ${t} is already marked as ready.`);return}let a={...i,ready:!0};M(e.path,{content:n,frontmatter:a}),r.success(`${Pe(t)} marked as ready: ${e.relativePath}`)}catch(e){k(`Failed to mark ${t} as ready: ${e instanceof Error?e.message:String(e)}`)}}async function Fe(){let e=new Date,t=7;for(;;){let n=A(await o({message:`When do you want to create content?`,options:Ie(e,t)}));if(n===C){t+=7;continue}return typeof n!=`string`&&k(`Unable to resolve selected date: ${String(n)}`),ie(n)}}function Ie(e,t){let n=[];for(let r=0;r<t;r++){let t=_(e,r),i=v(t,`yyyy-MM-dd`),a=v(t,`EEEE`),o=i,s=`Create content for ${a}, ${i}`;r===0?(o=`Today - ${a} (${i})`,s=`Create content for today (${a})`):r===1?(o=`Tomorrow - ${a} (${i})`,s=`Create content for tomorrow (${a})`):(o=`In ${r} days - ${a} (${i})`,s=`Create content for ${a}, ${i}`),n.push({label:o,value:i,hint:s})}return n.push({label:`Show 7 more dates (${t+1}-${t+7} days ahead)`,value:C,hint:`Extend the list up to ${v(_(e,t+7-1),`yyyy-MM-dd`)}`}),n}async function Le(){return N({message:`What is the title of your content?`,placeholder:`Enter content title`,requiredMessage:`Title is required`})}async function Re(){let e=A(await o({message:`Is a LinkedIn video planned?`,options:[{label:`Yes`,value:!0,hint:`Add video: true to frontmatter`},{label:`No`,value:!1,hint:`No video metadata`}]}));return typeof e!=`boolean`&&k(`Unable to resolve LinkedIn video choice: ${String(e)}`),e}async function ze(e){if(e.length===0)return;let t=A(await o({message:`What is the content theme? (optional)`,options:[{label:`No theme`,value:``,hint:`Leave theme empty`},...e.map(e=>({label:e,value:e,hint:`Use theme: ${e}`}))]}));typeof t!=`string`&&k(`Unable to resolve selected theme: ${String(t)}`);let n=t;return n.length>0?n:void 0}async function Be(){let e=A(await o({message:`What type of resource is this?`,options:[{label:`Article`,value:`article`,hint:`Create article.md`},{label:`Video`,value:`video`,hint:`Create video.md`},{label:`Audio`,value:`audio`,hint:`Create audio.md`},{label:`Tweet`,value:`tweet`,hint:`Create tweet.md`}]}));return[`article`,`video`,`audio`,`tweet`].includes(String(e))||k(`Unable to resolve resource type: ${String(e)}`),e}async function Ve(){let e=A(await i({message:`Which content types do you want to create?`,options:[{label:`LinkedIn`,value:`linkedin`,hint:`Create LinkedIn post and optional script`},{label:`X (Twitter)`,value:`x`,hint:`Create X post`},{label:`YouTube`,value:`youtube`,hint:`Create YouTube script and description`},{label:`Instagram`,value:`instagram`,hint:`Create Instagram script and description`}],required:!0}));return(!Array.isArray(e)||e.some(e=>!b.includes(e)))&&k(`Unable to resolve selected content types.`),e}async function He(e){return L({basePath:e,contentTypes:[`x`],message:`Which X publication do you want to schedule a reminder for?`,emptyMessage:`No X publications found in dated folders`,sort:`date-desc`})}async function Ue(e){return L({basePath:e,contentTypes:[`linkedin`],message:`Which LinkedIn publication do you want to mark as ready?`,emptyMessage:`No LinkedIn publications found in dated folders`,sort:`date-desc`})}async function L(e){let t=O(e.basePath,{contentTypes:e.contentTypes,sort:e.sort});t.length===0&&k(e.emptyMessage);let n=20;for(;;){let r=A(await o({message:e.message,options:We(t,n)}));if(r===C){n+=20;continue}typeof r!=`string`&&k(`Unable to resolve selected publication: ${String(r)}`);let i=t.find(e=>e.path===r);return i||k(`Unable to resolve selected publication: ${r}`),i}}function We(e,t){let n=e.slice(0,t).map(e=>Ge(e));if(t>=e.length)return n;let r=Math.min(t+20,e.length);return[...n,{label:`Show ${r-t} more publications (${t+1}-${r} of ${e.length})`,value:C,hint:`Extend the list to ${e[r-1]?.relativePath}`}]}function Ge(e){return{label:`${v(e.date,`yyyy-MM-dd`)} · ${x[e.contentType]} · ${e.title}`,value:e.path,hint:e.relativePath}}async function Ke(e){t(`Content Creation - Create dated content directory`);let n=await E(),r=await Le(),i=await Ve(),o={title:r};i.includes(`linkedin`)&&(o.hasVideo=await Re(),o.hasImages=!o.hasVideo,n.thematic.length>0&&(o.theme=await ze(n.thematic)));let s=await Fe();xe(s,i,o,n,e);let c=i.join(`, `);a(`✓ Content directory created: ${v(s,`yyyy/MM/dd`)} (${c})`)}function qe(e){t(`Content Creation - List Upcoming Publications`),Oe(e),a(`✓ Upcoming publications listed`)}function Je(e){t(`Content Creation - Rebuild Content Indexes`),Ae(e),a(`✓ Content indexes rebuilt successfully`)}const R=de(new URL(`../../../../`,import.meta.url)),z=`content-creation.ics`;function B(e){return e.replace(/\\/g,`\\\\`).replace(/\r\n|\r|\n/g,`\\n`).replace(/;/g,`\\;`).replace(/,/g,`\\,`)}function Ye(e){if(e.length<=75)return e;let t=[],n=e;for(;n.length>75;)t.push(n.slice(0,75)),n=` ${n.slice(75)}`;return t.push(n),t.join(`\r
5
- `)}function Xe(e){return e.toISOString().replace(/[-:]/g,``).replace(/\.\d{3}Z$/,`Z`)}function Ze(e){return v(e,`yyyyMMdd`)}function Qe(e){return e.split(`/`).map(e=>encodeURIComponent(e)).join(`/`)}function $e(e){return`https://github.com/barbapapazes/content-creation/blob/main/${Qe(e)}`}function et(e){let t=Qe(e);return`vscode://file${t.startsWith(`/`)?``:`/`}${t}`}function tt(e){let t=[`GitHub: ${$e(e.relativePath)}`,`VS Code: ${et(e.path)}`,`Status: ${e.ready?`ready`:`not ready`}`];return e.theme&&t.push(`Theme: ${e.theme}`),typeof e.video==`boolean`&&t.push(`Video planned: ${e.video?`yes`:`no`}`),e.imageCount>0&&t.push(`Images planned: ${e.imageCount}`),t.join(`
6
- `)}function nt(e){return`${e.ready?`Ready`:`Not ready`} · ${e.title} · ${x.linkedin}`}function rt(e){return`linkedin-${ue(`sha1`).update(e).digest(`hex`)}@barbapapazes`}function it(e){let t=[`BEGIN:VCALENDAR`,`VERSION:2.0`,`PRODID:-//Barbapapazes//Content Creation LinkedIn Calendar//EN`,`CALSCALE:GREGORIAN`,`METHOD:PUBLISH`,`X-WR-CALNAME:${B(`content-creation.ics`)}`];for(let n of e){let e=Xe(new Date(Date.UTC(n.date.getFullYear(),n.date.getMonth(),n.date.getDate()))),r=_(n.date,1);t.push(`BEGIN:VEVENT`,`UID:${rt(n.relativePath)}`,`DTSTAMP:${e}`,`SUMMARY:${B(nt(n))}`,`DTSTART;VALUE=DATE:${Ze(n.date)}`,`DTEND;VALUE=DATE:${Ze(r)}`,`TRANSP:TRANSPARENT`,`DESCRIPTION:${B(tt(n))}`,`END:VEVENT`)}return t.push(`END:VCALENDAR`),`${t.map(Ye).join(`\r
7
- `)}\r\n`}function at(){let t=h(R,`node_modules`,`.bin`,e.platform===`win32`?`wrangler.cmd`:`wrangler`);return l(t)?t:`wrangler`}function ot(t){(!t.calendar.publicUrl||!t.calendar.token)&&(r.error(`Calendar publishing configuration is missing. Please set CALENDAR_PUBLIC_URL and CALENDAR_TOKEN in your .env file or config.`),e.exit(1));let n=t.calendar.publicUrl.endsWith(`/`)?t.calendar.publicUrl:`${t.calendar.publicUrl}/`,i=new URL(z,n);return i.searchParams.set(`token`,t.calendar.token),i.toString()}function st(e){return O(e,{contentTypes:[`linkedin`],sort:`date-asc`}).map(e=>{let t=e.frontmatter;return{...e,title:t.title||e.title,ready:t.ready===!0,theme:t.theme,video:t.video,imageCount:t.images?.filter(Boolean).length??0}})}function ct(t){let n=at();try{le(n,[`r2`,`object`,`put`,`content-creation/${z}`,`--pipe`,`--content-type`,`text/calendar; charset=utf-8`,`--remote`],{cwd:R,stdio:[`pipe`,`inherit`,`inherit`],env:e.env,input:t})}catch(t){r.error(`Failed to upload LinkedIn calendar: ${t instanceof Error?t.message:String(t)}`),e.exit(1)}}async function lt(e,t){let n=st(e);if(n.length===0)return r.info(`No LinkedIn publications found. Skipping calendar upload.`),null;let i=ot(t);return ct(it(n)),r.success(`✓ LinkedIn calendar uploaded to R2 as ${z}`),r.info(`Subscription URL: ${i}`),i}function ut(e){let t=[];try{let n=f(e);for(let r of n){if(!p(h(e,r)).isFile())continue;let n=r.toLowerCase().substring(r.lastIndexOf(`.`));ge.includes(n)&&t.push(r)}}catch(e){k(`Error reading directory: ${e instanceof Error?e.message:String(e)}`)}return t.sort()}function dt(e,t){try{let{content:n,frontmatter:i}=j(e.path);M(e.path,{content:n,frontmatter:{...i,images:t.length>0?t:[null]}}),r.success(`LinkedIn publication updated: ${e.relativePath} (${t.length} image(s))`)}catch(e){k(`Failed to update LinkedIn publication: ${e instanceof Error?e.message:String(e)}`)}}function ft(e){let t=ut(e.folderPath);t.length===0?r.info(`No images found for LinkedIn publication: ${e.relativePath}`):r.info(`Found ${t.length} image(s) for ${e.relativePath}: ${t.join(`, `)}`),dt(e,t),I({path:e.path,relativePath:e.relativePath},`linkedin publication`);let n=h(e.folderPath,`linkedin-script.md`),i=e.relativePath.slice(0,e.relativePath.lastIndexOf(`/`)),a=l(n);return a&&I({path:n,relativePath:`${i}/linkedin-script.md`},`linkedin script`),{imageCount:t.length,scriptMarked:a}}async function pt(e){if(t(`Content Creation - Publish LinkedIn Calendar`),await lt(e,await E())){a(`✓ LinkedIn calendar published successfully`);return}a(`✓ No LinkedIn publications found, calendar was not updated`)}async function mt(e){t(`Content Creation - Mark LinkedIn Publication Ready`);let n=await Ue(e),r=ft(n),i=r.scriptMarked?`script marked ready`:`no script found`;a(`✓ LinkedIn publication ready: ${n.relativePath} (${r.imageCount} image(s), ${i})`)}function V(e){return e.toLowerCase().trim().normalize(`NFD`).replace(/[\u0300-\u036F]/g,``).replace(/[^a-z0-9\s-]/g,``).replace(/\s+/g,`-`).replace(/-+/g,`-`).replace(/^-+|-+$/g,``)||`untitled`}function ht(e,t,n){let i=V(e),a=h(g(h(n,`resources`)),i),o=`${t}.md`,s=h(a,o);if(l(s))return r.info(`${o} already exists at: ${s}`),a;u(a,{recursive:!0}),r.success(`Created directory: ${a}`);let c={title:e,url:``,date:``};return m(s,y.stringify(``,c),`utf-8`),r.success(`Created ${o} at: ${s}`),a}async function gt(e){t(`Content Creation - Create Resource`);let n=await Le(),r=await Be();ht(n,r,e),a(`✓ Resource created: resources/${V(n)}/${r}.md`)}const H=`## Index`,U=`<!-- content-creation:series-index:start -->`,W=`<!-- content-creation:series-index:end -->`,_t=/^(\d+)\.([^.]+(?:\.[^.]+)*)\.md$/;function G(e){return g(h(e,`series`))}function vt(e){let t=G(e);return l(t)?f(t).filter(e=>St(h(t,e))).map(e=>Ct(t,e)).sort((e,t)=>e.title.localeCompare(t.title,`fr`)):[]}function K(e){return f(e).map(Dt).filter(e=>e!==null).map(t=>wt(e,t)).sort((e,t)=>e.index-t.index)}function q(e){let t=K(e),n=h(e,`README.md`);m(n,yt(l(n)?d(n,`utf-8`):``,t,e),`utf-8`),r.success(`Generated series index: ${n}`)}function yt(e,t,n){let r=bt(t);if(e.includes(U)&&e.includes(W))return e.replace(xt(),r);let i=e.trimEnd();return i.length>0?`${i}\n\n${r}\n`:`# ${P(n.split(`/`).pop()||`series`)}\n\n${r}\n`}function bt(e){let t=[H,``,U];if(e.length===0)t.push(``,`_No articles yet._`);else{t.push(``);for(let n of e)t.push(`${n.index}. [${n.title}](./${n.fileName})`)}return t.push(``,W),t.join(`
8
- `)}function xt(){return RegExp(`${F(H)}\n\n${F(U)}[\\s\\S]*?${F(W)}`)}function St(e){return p(e).isDirectory()}function Ct(e,t){let n=h(e,t);return{name:t,path:n,title:Tt(n,t)}}function wt(e,t){let n=h(e,t.fileName);return{...t,path:n,title:Et(n,t.slug)}}function Tt(e,t){let n=h(e,`README.md`);if(l(n)){let e=d(n,`utf-8`).split(`
9
- `).map(e=>e.trim()).find(e=>e.startsWith(`# `));if(e)return e.slice(2).trim()}return P(t)}function Et(e,t){try{let{data:t}=y(d(e,`utf-8`)),n=typeof t.title==`string`?t.title.trim():``;if(n.length>0)return n}catch(t){r.warn(`Unable to read article title from ${e}: ${t}`)}return P(t)}function Dt(e){let t=e.match(_t);return t?{index:Number(t[1]),slug:t[2],fileName:e}:null}function Ot(e,t,n){let i=K(e),a=At(n,i.length),o=h(e,`${a}.${V(t)}.md`);if(l(o))throw Error(`Article already exists at ${o}`);return kt(e,i,a),m(o,y.stringify(``,{title:t}),`utf-8`),r.success(`Created article: ${o}`),q(e),o}function kt(e,t,n){let i=t.filter(e=>e.index>=n).sort((e,t)=>t.index-e.index);for(let t of i){let n=`${t.index+1}.${t.slug}.md`,i=h(e,n);ee(t.path,i),r.info(`Renamed ${t.fileName} → ${n}`)}}function At(e,t){if(!Number.isInteger(e))throw TypeError(`Article index must be an integer`);if(e<1||e>t+1)throw RangeError(`Article index must be between 1 and ${t+1}`);return e}function jt(e,t){let n=t.trim();if(n.length===0)throw Error(`Series title is required`);let i=V(n),a=G(e),o=h(a,i);if(u(a,{recursive:!0}),l(o))throw Error(`Series already exists: series/${i}`);return u(o),m(h(o,`README.md`),`# ${n}\n`,`utf-8`),q(o),r.success(`Created series: ${o}`),{name:i,path:o,title:n}}function Mt(e,t){let{content:n,frontmatter:i}=j(e),a=Ne(n,t);return M(e,{content:n,frontmatter:{...i,time:a}}),r.success(`Estimated time set to ${a} min: ${e}`),a}function Nt(e){I({path:e.path,relativePath:e.fileName},`series article`)}async function Pt(){return N({message:`What is the title of your series?`,placeholder:`Enter series title`,requiredMessage:`Title is required`})}async function Ft(){return N({message:`What is the title of your article?`,placeholder:`Enter article title`,requiredMessage:`Title is required`})}async function J(e){return Rt({entries:vt(e),message:`Which series do you want to work on?`,emptyMessage:`No series directories found`,resolveMessage:`Unable to resolve selected series`,buildOption:e=>({label:e.title,value:e.path,hint:`series/${e.name}`})})}async function It(e){let t=K(e),n=t.map(e=>({label:`Insert at #${e.index} · before ${e.title}`,value:String(e.index),hint:e.fileName}));n.push({label:`Append as #${t.length+1}`,value:String(t.length+1),hint:`Add the new article at the end of the series`});let r=A(await o({message:`Where should the new article be inserted?`,options:n}));return typeof r!=`string`&&k(`Unable to resolve selected insert position: ${String(r)}`),Number(r)}async function Lt(e){return Rt({entries:K(e),message:`Which series article do you want to update?`,emptyMessage:`No series articles found`,resolveMessage:`Unable to resolve selected series article`,buildOption:e=>({label:`#${e.index} · ${e.title}`,value:e.path,hint:e.fileName})})}async function Rt({entries:e,message:t,emptyMessage:n,resolveMessage:r,buildOption:i}){if(e.length===0&&k(n),e.length===1)return e[0];let a=A(await o({message:t,options:e.map(i)}));typeof a!=`string`&&k(`${r}: ${String(a)}`);let s=e.find(e=>e.path===a);return s||k(`${r}: ${a}`),s}async function zt(e){t(`Content Creation - Estimate Series Article Time`);let n=await E(),r=await J(e),i=await Lt(r.path),o=Mt(i.path,n.reading.wordsPerMinute);a(`✓ Estimated time updated for ${r.name}/${i.fileName}: ${o} min`)}async function Bt(e){t(`Content Creation - Create Series`),a(`✓ Series created: series/${jt(e,await Pt()).name}`)}async function Vt(e){t(`Content Creation - Create Series Article`);let n=await J(e),r=await Ft(),i=await It(n.path),o=Ot(n.path,r,i);a(`✓ Series article created: ${n.name}/${te(o)}`)}async function Ht(e){t(`Content Creation - Mark Series Article Ready`);let n=await E(),r=await J(e),i=await Lt(r.path);Nt(i);let o=Mt(i.path,n.reading.wordsPerMinute);a(`✓ Series article updated for ${r.name}/${i.fileName}: ready + ${o} min`)}async function Ut(e,t){let n=e.path,{content:i,frontmatter:a}=j(n);if(a.scheduled===!0)return r.warn(`This X publication is already scheduled. Skipping.`),!1;(!t.scheduling.automationEndpoint||!t.scheduling.cfAccessClientId||!t.scheduling.cfAccessClientSecret)&&k(`Scheduling configuration is missing. Please set AUTOMATION_ENDPOINT, CF_ACCESS_CLIENT_ID, and CF_ACCESS_CLIENT_SECRET in your .env file or config.`);let o=new Date(e.date);o=ae(o,w.hours),o=oe(o,w.minutes),o=se(o,w.seconds);let s={content:i.trim(),scheduleAt:o.getTime()};try{let e=await fetch(t.scheduling.automationEndpoint,{method:`POST`,headers:{"Content-Type":`application/json`,Accept:`application/json`,"CF-Access-Client-Id":t.scheduling.cfAccessClientId,"CF-Access-Client-Secret":t.scheduling.cfAccessClientSecret},body:JSON.stringify(s)});if(!e.ok){let t=await e.text();k(`Failed to schedule reminder: ${e.status} ${e.statusText}\n${t}`)}let o=await e.json();M(n,{content:i,frontmatter:{...a,scheduled:!0}});let c=new Date(o.scheduledAt);return r.success(`✓ X publication reminder scheduled`),r.info(`Reminder scheduled for: ${v(c,`yyyy-MM-dd HH:mm:ss`)} UTC`),!0}catch(e){k(`Failed to schedule X publication reminder: ${e instanceof Error?e.message:String(e)}`)}}async function Wt(e){t(`Content Creation - Schedule X Publication Reminder`);let n=await E();a(await Ut(await He(e),n)?`✓ X publication reminder scheduled`:`✓ X publication was already scheduled`)}const Y=new c;Y.name(fe).description(me).version(pe).showHelpAfterError();const X=Y.command(`content`);X.description(`Manage dated content`),X.command(`new`).description(`Create a dated content directory`).action(()=>Ke(e.cwd())),X.command(`upcoming`).description(`List upcoming publications`).action(()=>qe(e.cwd())),X.command(`rebuild-indexes`).description(`Rebuild all content index files`).action(()=>Je(e.cwd()));const Z=Y.command(`series`);Z.description(`Manage article series`),Z.command(`new`).description(`Create a new series`).action(()=>Bt(e.cwd()));const Q=Z.command(`article`).description(`Manage series articles`);Q.command(`new`).description(`Create a new article in a series`).action(()=>Vt(e.cwd())),Q.command(`ready`).description(`Mark a series article as ready`).action(()=>Ht(e.cwd())),Q.command(`estimate-time`).description(`Estimate the reading time of a series article`).action(()=>zt(e.cwd()));const $=Y.command(`linkedin`);$.description(`Manage LinkedIn publications`),$.command(`ready`).description(`Prepare a LinkedIn publication and mark it as ready`).action(()=>mt(e.cwd())),$.command(`publish-calendar`).description(`Publish the LinkedIn calendar`).action(()=>pt(e.cwd()));const Gt=Y.command(`x`);Gt.description(`Manage X publications`),Gt.command(`schedule-reminder`).description(`Schedule an X publication reminder`).action(()=>Wt(e.cwd()));const Kt=Y.command(`resource`);Kt.description(`Manage resources`),Kt.command(`new`).description(`Create a new resource`).action(()=>gt(e.cwd())),Y.parseAsync().catch(t=>{r.error(t instanceof Error?t.message:String(t)),e.exit(1)});export{};
2
+ import{C as e,S as t,a as n,g as r,i,o as a,r as o,s,t as c,v as l,w as u,y as d}from"./x-DuV_hNhq.mjs";import f from"node:process";import{log as p}from"@clack/prompts";import{Command as m}from"commander";var h=`@barbapapazes/content-creation`,g=`0.20.1`,_=`CLI tool for content creation and management`;const v=new m;v.name(h).description(_).version(g).showHelpAfterError();const y=v.command(`content`);y.description(`Manage dated content`),y.command(`new`).description(`Create a dated content directory`).action(()=>u(f.cwd())),y.command(`upcoming`).description(`List upcoming publications`).action(()=>e(f.cwd())),y.command(`rebuild-indexes`).description(`Rebuild all content index files`).action(()=>t(f.cwd()));const b=v.command(`series`);b.description(`Manage article series`),b.command(`new`).description(`Create a new series`).action(()=>a(f.cwd())),b.command(`video`).description(`Manage series videos`).command(`process`).description(`Process a video from a series directory`).action(()=>i(f.cwd()));const x=b.command(`article`).description(`Manage series articles`);x.command(`new`).description(`Create a new article in a series`).action(()=>n(f.cwd())),x.command(`ready`).description(`Mark a series article as ready`).action(()=>o(f.cwd())),x.command(`estimate-time`).description(`Estimate the reading time of a series article`).action(()=>s(f.cwd()));const S=v.command(`linkedin`);S.description(`Manage LinkedIn publications`),S.command(`ready`).description(`Proofread a LinkedIn publication, create a checkpoint commit, and mark it as ready`).action(()=>l(f.cwd())),S.command(`publish-calendar`).description(`Publish the LinkedIn calendar`).action(()=>d(f.cwd()));const C=v.command(`x`);C.description(`Manage X publications`),C.command(`schedule-reminder`).description(`Schedule an X publication reminder`).action(()=>c(f.cwd()));const w=v.command(`resource`);w.description(`Manage resources`),w.command(`new`).description(`Create a new resource`).action(()=>r(f.cwd())),v.parseAsync().catch(e=>{p.error(e instanceof Error?e.message:String(e)),f.exit(1)});export{};
package/dist/index.d.mts CHANGED
@@ -1,11 +1,36 @@
1
- //#region src/types.d.ts
1
+ import { SessionConfig } from "@github/copilot-sdk";
2
2
 
3
+ //#region src/constants.d.ts
4
+ declare const CONTENT_TYPES: readonly ["linkedin", "x", "youtube", "instagram"];
5
+ //#endregion
6
+ //#region src/types.d.ts
7
+ type ContentType = (typeof CONTENT_TYPES)[number];
3
8
  interface LinkedInTemplateConfig {
4
9
  footerPath?: string;
5
10
  }
11
+ interface LinkedInReadyCopilotConfig {
12
+ model?: string;
13
+ reasoningEffort?: SessionConfig['reasoningEffort'];
14
+ }
15
+ interface LinkedInReadyConfig {
16
+ copilot?: LinkedInReadyCopilotConfig;
17
+ }
18
+ interface LinkedInConfig {
19
+ ready?: LinkedInReadyConfig;
20
+ }
6
21
  interface VideoTemplateConfig {
7
22
  templatePath?: string;
8
23
  }
24
+ interface VideoProcessingConfig {
25
+ openaiApiKey?: string;
26
+ language?: string;
27
+ model?: string;
28
+ }
29
+ interface ResolvedVideoProcessingConfig {
30
+ openaiApiKey: string;
31
+ language: string;
32
+ model: string;
33
+ }
9
34
  interface SchedulingConfig {
10
35
  automationEndpoint?: string;
11
36
  cfAccessClientId?: string;
@@ -21,17 +46,188 @@ interface ReadingConfig {
21
46
  interface Config {
22
47
  thematic?: string[];
23
48
  templatesDir?: string;
49
+ linkedin?: LinkedInConfig;
24
50
  templates?: {
25
51
  linkedin?: LinkedInTemplateConfig;
26
52
  youtube?: VideoTemplateConfig;
27
53
  instagram?: VideoTemplateConfig;
28
54
  };
55
+ videoProcessing?: VideoProcessingConfig;
29
56
  scheduling?: SchedulingConfig;
30
57
  calendar?: CalendarPublishingConfig;
31
58
  reading?: ReadingConfig;
32
59
  }
60
+ interface ResolvedConfig extends Omit<Required<Config>, 'linkedin' | 'reading' | 'videoProcessing'> {
61
+ linkedin: {
62
+ ready: {
63
+ copilot: LinkedInReadyCopilotConfig;
64
+ };
65
+ };
66
+ reading: {
67
+ wordsPerMinute: number;
68
+ };
69
+ videoProcessing: ResolvedVideoProcessingConfig;
70
+ }
71
+ interface ContentFrontmatter {
72
+ title: string;
73
+ theme?: string;
74
+ video?: boolean;
75
+ images?: (string | null)[];
76
+ scheduled?: boolean;
77
+ ready?: boolean;
78
+ }
79
+ interface ContentEntry {
80
+ contentType: ContentType;
81
+ path: string;
82
+ folderPath: string;
83
+ date: Date;
84
+ title: string;
85
+ relativePath: string;
86
+ frontmatter: ContentFrontmatter;
87
+ }
88
+ //#endregion
89
+ //#region src/common/frontmatter.d.ts
90
+ interface MarkdownDocument<TFrontmatter extends object = Record<string, unknown>> {
91
+ content: string;
92
+ frontmatter: TFrontmatter;
93
+ }
94
+ declare function readMarkdownDocument<TFrontmatter extends object = Record<string, unknown>>(filePath: string): MarkdownDocument<TFrontmatter>;
95
+ declare function writeMarkdownDocument<TFrontmatter extends object>(filePath: string, document: MarkdownDocument<TFrontmatter>): void;
96
+ //#endregion
97
+ //#region src/common/prompts.d.ts
98
+ interface PromptForRequiredTextOptions {
99
+ message: string;
100
+ placeholder: string;
101
+ requiredMessage?: string;
102
+ }
103
+ declare function promptForRequiredText(options: PromptForRequiredTextOptions): Promise<string>;
104
+ //#endregion
105
+ //#region src/common/reading.d.ts
106
+ declare function estimateReadingMinutesFromMarkdown(markdownContent: string, wordsPerMinute: number): number;
107
+ //#endregion
108
+ //#region src/common/ready.d.ts
109
+ interface ReadyEntry {
110
+ path: string;
111
+ relativePath: string;
112
+ }
113
+ declare function markEntryAsReady(entry: ReadyEntry, entryLabel: string): void;
114
+ //#endregion
115
+ //#region src/common/slugify.d.ts
116
+ declare function slugify(title: string): string;
117
+ //#endregion
118
+ //#region src/common/strings.d.ts
119
+ declare function capitalize(value: string): string;
120
+ declare function humanizeSlug(value: string): string;
121
+ declare function escapeRegExp(value: string): string;
122
+ //#endregion
123
+ //#region src/config.d.ts
124
+ declare function loadContentCreationConfig(): Promise<ResolvedConfig>;
125
+ //#endregion
126
+ //#region src/modules/content/steps/create-content-directory.d.ts
127
+ interface ContentCreationOptions {
128
+ title: string;
129
+ hasVideo?: boolean;
130
+ hasImages?: boolean;
131
+ theme?: string;
132
+ }
133
+ declare function createContentDirectory(date: Date, selectedTypes: ContentType[], options: ContentCreationOptions, config: ResolvedConfig, basePath: string): string;
134
+ //#endregion
135
+ //#region src/modules/content/steps/list-upcoming-content.d.ts
136
+ declare function listUpcomingContent(basePath: string): void;
137
+ //#endregion
138
+ //#region src/modules/content/steps/rebuild-content-indexes.d.ts
139
+ declare function rebuildContentIndexes(basePath: string): void;
140
+ //#endregion
141
+ //#region src/modules/content/workflows/content-new.d.ts
142
+ declare function contentNewWorkflow(basePath: string): Promise<void>;
143
+ //#endregion
144
+ //#region src/modules/content/workflows/content-upcoming.d.ts
145
+ declare function contentUpcomingWorkflow(basePath: string): void;
146
+ //#endregion
147
+ //#region src/modules/content/workflows/rebuild-content-indexes.d.ts
148
+ declare function rebuildContentIndexesWorkflow(basePath: string): void;
149
+ //#endregion
150
+ //#region src/modules/linkedin/steps/publish-linkedin-calendar.d.ts
151
+ declare function publishLinkedInCalendar(basePath: string, config: ResolvedConfig): Promise<string | null>;
152
+ //#endregion
153
+ //#region src/modules/linkedin/steps/ready-linkedin-publication.d.ts
154
+ interface LinkedInReadyResult {
155
+ imageCount: number;
156
+ scriptMarked: boolean;
157
+ }
158
+ declare function readyLinkedInPublication(entry: ContentEntry): LinkedInReadyResult;
159
+ //#endregion
160
+ //#region src/modules/linkedin/workflows/linkedin-publish-calendar.d.ts
161
+ declare function linkedinPublishCalendarWorkflow(basePath: string): Promise<void>;
162
+ //#endregion
163
+ //#region src/modules/linkedin/workflows/linkedin-ready.d.ts
164
+ declare function linkedinReadyWorkflow(basePath: string): Promise<void>;
165
+ //#endregion
166
+ //#region src/modules/resource/steps/create-resource-directory.d.ts
167
+ declare function createResourceDirectory(title: string, resourceType: 'article' | 'video' | 'audio' | 'tweet', basePath: string): string;
168
+ //#endregion
169
+ //#region src/modules/resource/workflows/resource-new.d.ts
170
+ declare function resourceNewWorkflow(basePath: string): Promise<void>;
171
+ //#endregion
172
+ //#region src/modules/series/steps/create-series-article.d.ts
173
+ declare function createSeriesArticle(seriesPath: string, title: string, insertAtIndex: number): string;
174
+ //#endregion
175
+ //#region src/modules/series/types.d.ts
176
+ interface SeriesDirectoryEntry {
177
+ name: string;
178
+ path: string;
179
+ title: string;
180
+ }
181
+ interface SeriesArticleEntry {
182
+ index: number;
183
+ slug: string;
184
+ title: string;
185
+ fileName: string;
186
+ path: string;
187
+ }
188
+ interface SeriesVideoEntry {
189
+ name: string;
190
+ path: string;
191
+ relativePath: string;
192
+ }
193
+ //#endregion
194
+ //#region src/modules/series/steps/create-series-directory.d.ts
195
+ declare function createSeriesDirectory(basePath: string, title: string): SeriesDirectoryEntry;
196
+ //#endregion
197
+ //#region src/modules/series/steps/estimate-series-article-time.d.ts
198
+ declare function estimateSeriesArticleTime(filePath: string, wordsPerMinute: number): number;
199
+ //#endregion
200
+ //#region src/modules/series/steps/mark-series-article-ready.d.ts
201
+ declare function markSeriesArticleReady(entry: SeriesArticleEntry): void;
202
+ //#endregion
203
+ //#region src/modules/series/steps/series.d.ts
204
+ declare function getSeriesRootPath(basePath: string): string;
205
+ declare function getSeriesDirectories(basePath: string): SeriesDirectoryEntry[];
206
+ declare function getSeriesArticles(seriesPath: string): SeriesArticleEntry[];
207
+ declare function createSeriesIndex(seriesPath: string): void;
208
+ //#endregion
209
+ //#region src/modules/series/workflows/estimate-series-article-time.d.ts
210
+ declare function seriesArticleEstimateTimeWorkflow(basePath: string): Promise<void>;
211
+ //#endregion
212
+ //#region src/modules/series/workflows/new-series.d.ts
213
+ declare function seriesNewWorkflow(basePath: string): Promise<void>;
214
+ //#endregion
215
+ //#region src/modules/series/workflows/new-series-article.d.ts
216
+ declare function seriesArticleNewWorkflow(basePath: string): Promise<void>;
217
+ //#endregion
218
+ //#region src/modules/series/workflows/process-series-video.d.ts
219
+ declare function seriesVideoProcessWorkflow(basePath: string): Promise<void>;
220
+ //#endregion
221
+ //#region src/modules/series/workflows/ready-series-article.d.ts
222
+ declare function seriesArticleReadyWorkflow(basePath: string): Promise<void>;
223
+ //#endregion
224
+ //#region src/modules/x/steps/schedule-reminder.d.ts
225
+ declare function scheduleReminder(entry: ContentEntry, config: ResolvedConfig): Promise<boolean>;
226
+ //#endregion
227
+ //#region src/modules/x/workflows/x-schedule-reminder.d.ts
228
+ declare function xScheduleReminderWorkflow(basePath: string): Promise<void>;
33
229
  //#endregion
34
230
  //#region src/index.d.ts
35
231
  declare function defineConfig(config: Config): Config;
36
232
  //#endregion
37
- export { defineConfig };
233
+ export { type Config, ContentCreationOptions, LinkedInReadyResult, MarkdownDocument, type ResolvedConfig, SeriesArticleEntry, SeriesDirectoryEntry, SeriesVideoEntry, capitalize, contentNewWorkflow, contentUpcomingWorkflow, createContentDirectory, createResourceDirectory, createSeriesArticle, createSeriesDirectory, createSeriesIndex, defineConfig, escapeRegExp, estimateReadingMinutesFromMarkdown, estimateSeriesArticleTime, getSeriesArticles, getSeriesDirectories, getSeriesRootPath, humanizeSlug, linkedinPublishCalendarWorkflow, linkedinReadyWorkflow, listUpcomingContent, loadContentCreationConfig, markEntryAsReady, markSeriesArticleReady, promptForRequiredText, publishLinkedInCalendar, readMarkdownDocument, readyLinkedInPublication, rebuildContentIndexes, rebuildContentIndexesWorkflow, resourceNewWorkflow, scheduleReminder, seriesArticleEstimateTimeWorkflow, seriesArticleNewWorkflow, seriesArticleReadyWorkflow, seriesNewWorkflow, seriesVideoProcessWorkflow, slugify, writeMarkdownDocument, xScheduleReminderWorkflow };
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- function e(e){return e}export{e as defineConfig};
1
+ import{A as e,C as t,D as n,E as r,F as i,I as a,L as o,M as s,N as c,O as l,P as u,S as d,T as f,_ as p,a as m,b as h,c as g,d as _,f as v,g as y,h as b,i as x,j as S,k as C,l as w,m as T,n as E,o as D,p as O,r as k,s as A,t as j,u as M,v as N,w as P,x as F,y as I}from"./x-DuV_hNhq.mjs";function L(e){return e}export{n as capitalize,P as contentNewWorkflow,t as contentUpcomingWorkflow,a as createContentDirectory,p as createResourceDirectory,_ as createSeriesArticle,M as createSeriesDirectory,v as createSeriesIndex,L as defineConfig,l as escapeRegExp,e as estimateReadingMinutesFromMarkdown,w as estimateSeriesArticleTime,O as getSeriesArticles,T as getSeriesDirectories,b as getSeriesRootPath,C as humanizeSlug,I as linkedinPublishCalendarWorkflow,N as linkedinReadyWorkflow,i as listUpcomingContent,o as loadContentCreationConfig,r as markEntryAsReady,g as markSeriesArticleReady,S as promptForRequiredText,F as publishLinkedInCalendar,s as readMarkdownDocument,h as readyLinkedInPublication,u as rebuildContentIndexes,d as rebuildContentIndexesWorkflow,y as resourceNewWorkflow,E as scheduleReminder,A as seriesArticleEstimateTimeWorkflow,m as seriesArticleNewWorkflow,k as seriesArticleReadyWorkflow,D as seriesNewWorkflow,x as seriesVideoProcessWorkflow,f as slugify,c as writeMarkdownDocument,j as xScheduleReminderWorkflow};
@@ -0,0 +1,17 @@
1
+ import e from"node:process";import{intro as t,isCancel as n,log as r,multiselect as i,outro as a,select as o,text as s}from"@clack/prompts";import{existsSync as c,mkdirSync as l,mkdtempSync as u,readFileSync as d,readdirSync as f,renameSync as ee,rmSync as te,statSync as p,writeFileSync as m}from"node:fs";import{basename as h,dirname as g,extname as _,isAbsolute as ne,join as v,relative as y,resolve as b}from"node:path";import{addDays as x,format as S,parseISO as re,setHours as ie,setMinutes as ae,setSeconds as oe}from"date-fns";import C from"gray-matter";import{loadConfig as se}from"c12";import{execFileSync as ce}from"node:child_process";import{createHash as le}from"node:crypto";import{fileURLToPath as ue}from"node:url";import{tmpdir as de}from"node:os";import{xSync as fe}from"tinyexec";import{CopilotClient as pe,approveAll as me}from"@github/copilot-sdk";import{cleanupFile as he,extractAudio as ge,generateThumbnail as _e,generateTranscription as ve}from"@barbapapazes/video-toolkit";const w={thematic:[],templatesDir:void 0,linkedin:{ready:{copilot:{}}},templates:{},videoProcessing:{openaiApiKey:``,language:`fr`,model:`whisper-1`},scheduling:{},calendar:{},reading:{wordsPerMinute:100}};function ye(t,n){return t?ne(t)?t:b(n?g(n):e.cwd(),t):n?g(n):e.cwd()}function be(e,t,n){if(!t)return;let i=b(e,t);if(!c(i)){r.warn(`Configured ${n} template was not found: ${i}`);return}return i}function xe(e,t){return{footerPath:be(t,e?.footerPath,`LinkedIn footer`)}}function Se(e,t){return{templatePath:be(t,e?.templatePath,`video description`)}}function Ce(e){let t=e?.model?.trim();return{model:t&&t.length>0?t:void 0,reasoningEffort:e?.reasoningEffort}}function we(e){return{ready:{copilot:Ce(e?.ready?.copilot)}}}async function T(){let{config:t,configFile:n}=await se({name:`content-creation`,defaults:w,globalRc:!0,dotenv:!0}),r=ye(t.templatesDir,n),i={linkedin:xe(t.templates?.linkedin,r),youtube:Se(t.templates?.youtube,r),instagram:Se(t.templates?.instagram,r)},a={automationEndpoint:t.scheduling?.automationEndpoint??e.env.AUTOMATION_ENDPOINT,cfAccessClientId:t.scheduling?.cfAccessClientId??e.env.CF_ACCESS_CLIENT_ID,cfAccessClientSecret:t.scheduling?.cfAccessClientSecret??e.env.CF_ACCESS_CLIENT_SECRET},o={openaiApiKey:t.videoProcessing?.openaiApiKey??e.env.OPENAI_API_KEY??``,language:t.videoProcessing?.language??e.env.OPENAI_TRANSCRIPTION_LANGUAGE??w.videoProcessing?.language??`fr`,model:t.videoProcessing?.model??e.env.OPENAI_TRANSCRIPTION_MODEL??w.videoProcessing?.model??`whisper-1`},s={publicUrl:t.calendar?.publicUrl??e.env.CALENDAR_PUBLIC_URL,token:t.calendar?.token??e.env.CALENDAR_TOKEN},c=t.reading?.wordsPerMinute??w.reading?.wordsPerMinute??100;if(!Number.isFinite(c)||c<=0)throw RangeError(`Reading wordsPerMinute must be a positive number`);return{thematic:t.thematic??[],templatesDir:t.templatesDir??``,linkedin:we(t.linkedin),templates:i,videoProcessing:o,scheduling:a,calendar:s,reading:{wordsPerMinute:c}}}function E(e){return e?c(e)?d(e,`utf-8`):(r.warn(`Template file not found: ${e}`),``):``}const D=[`linkedin`,`x`,`youtube`,`instagram`],O={linkedin:`LinkedIn`,x:`X`,youtube:`YouTube`,instagram:`Instagram`},k={linkedin:{mainFile:`linkedin.md`,additionalFiles:[`linkedin-script.md`]},x:{mainFile:`x.md`},youtube:{mainFile:`youtube-script.md`,additionalFiles:[`youtube-description.md`]},instagram:{mainFile:`instagram-script.md`,additionalFiles:[`instagram-description.md`]}},Te={linkedin:{fileName:`linkedin-posts.md`,heading:`LinkedIn Posts`},x:{fileName:`x-posts.md`,heading:`X Posts`},youtube:{fileName:`youtube-videos.md`,heading:`YouTube Videos`},instagram:{fileName:`instagram-posts.md`,heading:`Instagram Posts`}},A=`__load-more__`,Ee=[`.jpg`,`.jpeg`,`.png`],j={hours:11,minutes:0,seconds:0};function M(e,t,n,i,a){let o=b(v(a,S(e,`yyyy`),S(e,`MM`),S(e,`dd`)));c(o)||(l(o,{recursive:!0}),r.success(`Created directory: ${o}`));let s=De(n);for(let e of t)Oe(e,o,s,i);return o}function De(e){return e.hasVideo?{...e,hasImages:!1}:{...e,hasImages:!0}}function Oe(e,t,n,i){let a=k[e],o=v(t,a.mainFile),s=`additionalFiles`in a?a.additionalFiles:void 0;if(c(o)?r.info(`${a.mainFile} already exists, skipping`):ke(e,o,n,i),s)for(let a of s){let o=v(t,a);je(a,e,n)&&(c(o)?r.info(`${a} already exists, skipping`):Ae(e,o,a,i))}}function ke(e,t,n,i){let a={title:n.title};e===`linkedin`&&(n.theme&&(a.theme=n.theme),n.hasVideo&&(a.video=!0),n.hasImages&&(a.images=[null]));let o=``;e===`linkedin`&&(o=E(i.templates.linkedin?.footerPath)),m(t,C.stringify(o,a),`utf-8`),r.success(`Created ${t}`)}function Ae(e,t,n,i){let a=``;n.endsWith(`-description.md`)&&(a=E(i.templates[e]?.templatePath)),m(t,a,`utf-8`),r.success(`Created ${t}`)}function je(e,t,n){return t===`linkedin`&&e===`linkedin-script.md`?n.hasVideo||!1:!0}function Me(e,t,n){let r=n===`date-asc`?e.date.getTime()-t.date.getTime():t.date.getTime()-e.date.getTime();if(r!==0)return r;let i=D.indexOf(e.contentType)-D.indexOf(t.contentType);return i===0?e.relativePath.localeCompare(t.relativePath):i}function N(e,t={}){let n=[],i=t.contentTypes?.length?t.contentTypes:[...D],a=t.fromDate,o=t.sort||`date-desc`;try{let t=f(e).filter(t=>p(v(e,t)).isDirectory()&&/^\d{4}$/.test(t));for(let o of t){let t=v(e,o),s=f(t).filter(e=>p(v(t,e)).isDirectory()&&/^\d{2}$/.test(e));for(let e of s){let s=v(t,e),l=f(s).filter(e=>p(v(s,e)).isDirectory()&&/^\d{2}$/.test(e));for(let t of l){let l=v(s,t),u=`${o}-${e}-${t}`,f=new Date(Number(o),Number(e)-1,Number(t));if(!(a&&f.getTime()<a.getTime()))for(let a of i){let i=k[a].mainFile,s=b(l,i);if(c(s))try{let{data:r}=C(d(s,`utf-8`)),c=r,ee=c.title||`Untitled (${u})`;n.push({contentType:a,path:s,folderPath:l,date:f,title:ee,relativePath:`${o}/${e}/${t}/${i}`,frontmatter:c})}catch(e){r.warn(`Error reading ${s}: ${e}`)}}}}}}catch(e){return r.error(`Error reading directories: ${e}`),[]}return n.sort((e,t)=>Me(e,t,o))}function Ne(t){let n=new Date;n.setHours(0,0,0,0);let i=N(t,{fromDate:n,sort:`date-asc`});if(i.length===0){r.info(`No upcoming publications found from today onward.`);return}r.info(`Found ${i.length} upcoming publication${i.length===1?``:`s`}:`),e.stdout.write(`
2
+ `);for(let t of i){let n=S(t.date,`EEEE, MMMM d, yyyy`),r=S(t.date,`yyyy-MM-dd`);e.stdout.write(`${n} (${r}) · ${O[t.contentType]} · ${t.title}\n`),e.stdout.write(`${t.path}\n\n`)}}function Pe(e,t){let n=N(e,{contentTypes:[t],sort:`date-desc`});if(n.length===0){r.info(`No ${t} posts found`);return}let{fileName:i,heading:a}=Te[t],o=[`# ${a}`,``,`_Generated on ${new Date().toLocaleDateString()}_`,``,`Total posts: ${n.length}`,``];for(let e of n){let t=e.date.toLocaleDateString(`en-US`,{year:`numeric`,month:`long`,day:`numeric`});o.push(`- [${e.title}](${e.relativePath}) - _${t}_`)}m(v(e,i),`${o.join(`
3
+ `)}\n`,`utf-8`),r.success(`Created ${i} with ${n.length} posts`)}function Fe(e){for(let t of D)Pe(e,t)}function Ie(t=`Operation cancelled.`){r.error(t),e.exit(0)}function P(t){r.error(t),e.exit(1)}function Le(e){return e instanceof Error?e.message:String(e)}function F(e,t){return n(e)&&Ie(t),e}async function Re(e){let t=N(e.basePath,{contentTypes:e.contentTypes,sort:e.sort});t.length===0&&P(e.emptyMessage);let n=20;for(;;){let r=F(await o({message:e.message,options:ze(t,n)}));if(r===`__load-more__`){n+=20;continue}typeof r!=`string`&&P(`Unable to resolve selected publication: ${String(r)}`);let i=t.find(e=>e.path===r);return i||P(`Unable to resolve selected publication: ${r}`),i}}function ze(e,t){let n=e.slice(0,t).map(e=>Be(e));if(t>=e.length)return n;let r=Math.min(t+20,e.length);return[...n,{label:`Show ${r-t} more publications (${t+1}-${r} of ${e.length})`,value:A,hint:`Extend the list to ${e[r-1]?.relativePath}`}]}function Be(e){return{label:`${S(e.date,`yyyy-MM-dd`)} · ${O[e.contentType]} · ${e.title}`,value:e.path,hint:e.relativePath}}async function Ve(){let e=F(await i({message:`Which content types do you want to create?`,options:[{label:`LinkedIn`,value:`linkedin`,hint:`Create LinkedIn post and optional script`},{label:`X (Twitter)`,value:`x`,hint:`Create X post`},{label:`YouTube`,value:`youtube`,hint:`Create YouTube script and description`},{label:`Instagram`,value:`instagram`,hint:`Create Instagram script and description`}],required:!0}));return(!Array.isArray(e)||e.some(e=>!D.includes(e)))&&P(`Unable to resolve selected content types.`),e}async function He(){let e=new Date,t=7;for(;;){let n=F(await o({message:`When do you want to create content?`,options:Ue(e,t)}));if(n===`__load-more__`){t+=7;continue}return typeof n!=`string`&&P(`Unable to resolve selected date: ${String(n)}`),re(n)}}function Ue(e,t){let n=[];for(let r=0;r<t;r++){let t=x(e,r),i=S(t,`yyyy-MM-dd`),a=S(t,`EEEE`),o=i;o=r===0?`${a.padEnd(10)} ${i} - Today`:r===1?`${a.padEnd(10)} ${i} - Tomorrow`:`${a.padEnd(10)} ${i} - ${r} days ahead`,n.push({label:o,value:i})}return n.push({label:`Show 7 more dates (${t+1}-${t+7} days ahead)`,value:A,hint:`Extend the list up to ${S(x(e,t+7-1),`yyyy-MM-dd`)}`}),n}async function We(e){if(e.length===0)return;let t=F(await o({message:`What is the content theme? (optional)`,options:[{label:`No theme`,value:``,hint:`Leave theme empty`},...e.map(e=>({label:e,value:e,hint:`Use theme: ${e}`}))]}));typeof t!=`string`&&P(`Unable to resolve selected theme: ${String(t)}`);let n=t;return n.length>0?n:void 0}function I(e){let{content:t,data:n}=C(d(e,`utf-8`));return{content:t,frontmatter:n}}function L(e,t){m(e,C.stringify(t.content,t.frontmatter),`utf-8`)}async function R(e){return F(await s({message:e.message,placeholder:e.placeholder,validate:t=>{if(!t||t.trim().length===0)return e.requiredMessage||`Value is required`}})).trim()}const Ge=/<!--([\s\S]*?)-->/g;function Ke(e,t){let n=e.replace(Ge,``).replace(/\s+/g,``);if(n.length===0)return 0;let r=n.length/5;return Math.ceil(r/t)}function qe(e){return e.charAt(0).toUpperCase()+e.slice(1)}function z(e){return e.split(`-`).filter(Boolean).map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join(` `)}function B(e){return e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`)}function V(e,t){try{let{content:n,frontmatter:i}=I(e.path);if(i.ready===!0){r.warn(`This ${t} is already marked as ready.`);return}let a={...i,ready:!0};L(e.path,{content:n,frontmatter:a}),r.success(`${qe(t)} marked as ready: ${e.relativePath}`)}catch(e){P(`Failed to mark ${t} as ready: ${e instanceof Error?e.message:String(e)}`)}}function H(e){return e.toLowerCase().trim().normalize(`NFD`).replace(/[\u0300-\u036F]/g,``).replace(/[^a-z0-9\s-]/g,``).replace(/\s+/g,`-`).replace(/-+/g,`-`).replace(/^-+|-+$/g,``)||`untitled`}async function Je(){return R({message:`What is the title of your content?`,placeholder:`Enter content title`,requiredMessage:`Title is required`})}async function Ye(){let e=F(await o({message:`Is a LinkedIn video planned?`,options:[{label:`Yes`,value:!0,hint:`Add video: true to frontmatter`},{label:`No`,value:!1,hint:`No video metadata`}]}));return typeof e!=`boolean`&&P(`Unable to resolve LinkedIn video choice: ${String(e)}`),e}async function Xe(e){t(`Content Creation - Create dated content directory`);let n=await T(),r=await Je(),i=await Ve(),o={title:r};i.includes(`linkedin`)&&(o.hasVideo=await Ye(),o.hasImages=!o.hasVideo,n.thematic.length>0&&(o.theme=await We(n.thematic)));let s=await He();M(s,i,o,n,e);let c=i.join(`, `);a(`✓ Content directory created: ${S(s,`yyyy/MM/dd`)} (${c})`)}function Ze(e){t(`Content Creation - List Upcoming Publications`),Ne(e),a(`✓ Upcoming publications listed`)}function Qe(e){t(`Content Creation - Rebuild Content Indexes`),Fe(e),a(`✓ Content indexes rebuilt successfully`)}const $e=ue(new URL(`../../../../`,import.meta.url)),U=`content-creation.ics`;function W(e){return e.replace(/\\/g,`\\\\`).replace(/\r\n|\r|\n/g,`\\n`).replace(/;/g,`\\;`).replace(/,/g,`\\,`)}function et(e){if(e.length<=75)return e;let t=[],n=e;for(;n.length>75;)t.push(n.slice(0,75)),n=` ${n.slice(75)}`;return t.push(n),t.join(`\r
4
+ `)}function tt(e){return e.toISOString().replace(/[-:]/g,``).replace(/\.\d{3}Z$/,`Z`)}function nt(e){return S(e,`yyyyMMdd`)}function G(e){return e.split(`/`).map(e=>encodeURIComponent(e)).join(`/`)}function rt(e){return`https://github.com/barbapapazes/content-creation/blob/main/${G(e)}`}function it(e){let t=G(e);return`vscode://file${t.startsWith(`/`)?``:`/`}${t}`}function at(e){let t=[`GitHub: ${rt(e.relativePath)}`,`VS Code: ${it(e.path)}`,`Status: ${e.ready?`ready`:`not ready`}`];return e.theme&&t.push(`Theme: ${e.theme}`),typeof e.video==`boolean`&&t.push(`Video planned: ${e.video?`yes`:`no`}`),e.imageCount>0&&t.push(`Images planned: ${e.imageCount}`),t.join(`
5
+ `)}function ot(e){return`${e.ready?`Ready`:`Not ready`} · ${e.title} · ${O.linkedin}`}function st(e){return`linkedin-${le(`sha1`).update(e).digest(`hex`)}@barbapapazes`}function ct(e){let t=[`BEGIN:VCALENDAR`,`VERSION:2.0`,`PRODID:-//Barbapapazes//Content Creation LinkedIn Calendar//EN`,`CALSCALE:GREGORIAN`,`METHOD:PUBLISH`,`X-WR-CALNAME:${W(`content-creation.ics`)}`];for(let n of e){let e=tt(new Date(Date.UTC(n.date.getFullYear(),n.date.getMonth(),n.date.getDate()))),r=x(n.date,1);t.push(`BEGIN:VEVENT`,`UID:${st(n.relativePath)}`,`DTSTAMP:${e}`,`SUMMARY:${W(ot(n))}`,`DTSTART;VALUE=DATE:${nt(n.date)}`,`DTEND;VALUE=DATE:${nt(r)}`,`TRANSP:TRANSPARENT`,`DESCRIPTION:${W(at(n))}`,`END:VEVENT`)}return t.push(`END:VCALENDAR`),`${t.map(et).join(`\r
6
+ `)}\r\n`}function lt(){let t=v($e,`node_modules`,`.bin`,e.platform===`win32`?`wrangler.cmd`:`wrangler`);return c(t)?t:`wrangler`}function ut(t){(!t.calendar.publicUrl||!t.calendar.token)&&(r.error(`Calendar publishing configuration is missing. Please set CALENDAR_PUBLIC_URL and CALENDAR_TOKEN in your .env file or config.`),e.exit(1));let n=t.calendar.publicUrl.endsWith(`/`)?t.calendar.publicUrl:`${t.calendar.publicUrl}/`,i=new URL(U,n);return i.searchParams.set(`token`,t.calendar.token),i.toString()}function dt(e){return N(e,{contentTypes:[`linkedin`],sort:`date-asc`}).map(e=>{let t=e.frontmatter;return{...e,title:t.title||e.title,ready:t.ready===!0,theme:t.theme,video:t.video,imageCount:t.images?.filter(Boolean).length??0}})}function ft(t){let n=lt();try{ce(n,[`r2`,`object`,`put`,`content-creation/${U}`,`--pipe`,`--content-type`,`text/calendar; charset=utf-8`,`--remote`],{cwd:$e,stdio:[`pipe`,`inherit`,`inherit`],env:e.env,input:t})}catch(t){r.error(`Failed to upload LinkedIn calendar: ${t instanceof Error?t.message:String(t)}`),e.exit(1)}}async function pt(e,t){let n=dt(e);if(n.length===0)return r.info(`No LinkedIn publications found. Skipping calendar upload.`),null;let i=ut(t);return ft(ct(n)),r.success(`✓ LinkedIn calendar uploaded to R2 as ${U}`),r.info(`Subscription URL: ${i}`),i}function mt(e){let t=[];try{let n=f(e);for(let r of n){if(!p(v(e,r)).isFile())continue;let n=r.toLowerCase().substring(r.lastIndexOf(`.`));Ee.includes(n)&&t.push(r)}}catch(e){P(`Error reading directory: ${e instanceof Error?e.message:String(e)}`)}return t.sort()}function ht(e,t){try{let{content:n,frontmatter:i}=I(e.path);L(e.path,{content:n,frontmatter:{...i,images:t.length>0?t:[null]}}),r.success(`LinkedIn publication updated: ${e.relativePath} (${t.length} image(s))`)}catch(e){P(`Failed to update LinkedIn publication: ${e instanceof Error?e.message:String(e)}`)}}function gt(e){let t=mt(e.folderPath);t.length===0?r.info(`No images found for LinkedIn publication: ${e.relativePath}`):r.info(`Found ${t.length} image(s) for ${e.relativePath}: ${t.join(`, `)}`),ht(e,t),V({path:e.path,relativePath:e.relativePath},`linkedin publication`);let n=v(e.folderPath,`linkedin-script.md`),i=e.relativePath.slice(0,e.relativePath.lastIndexOf(`/`)),a=c(n);return a&&V({path:n,relativePath:`${i}/linkedin-script.md`},`linkedin script`),{imageCount:t.length,scriptMarked:a}}async function _t(e){if(t(`Content Creation - Publish LinkedIn Calendar`),await pt(e,await T())){a(`✓ LinkedIn calendar published successfully`);return}a(`✓ No LinkedIn publications found, calendar was not updated`)}function vt(e){let t=k.linkedin,n=e.relativePath.slice(0,e.relativePath.lastIndexOf(`/`)),r=[{kind:`publication`,path:e.path,relativePath:e.relativePath,isMain:!0}];for(let i of t.additionalFiles??[]){let t=v(e.folderPath,i);c(t)&&r.push({kind:i===`linkedin-script.md`?`script`:`publication`,path:t,relativePath:`${n}/${i}`,isMain:!1})}return r}async function yt(e){return Re({basePath:e,contentTypes:[`linkedin`],message:`Which LinkedIn publication do you want to polish and mark as ready?`,emptyMessage:`No LinkedIn publications found in dated folders`,sort:`date-desc`})}function K(t,n,r={}){return fe(`git`,n,{throwOnError:!0,nodeOptions:{cwd:t,env:{...e.env,...r}}}).stdout.trim()}function bt(e,t,n){let r=u(v(de(),`content-creation-linkedin-ready-`)),i={GIT_INDEX_FILE:v(r,`index`)};try{K(e,[`rev-parse`,`--is-inside-work-tree`]),K(e,[`read-tree`,`HEAD`],i);let r=n.map(t=>y(e,t.path));if(r.length===0)return{created:!1,message:`No related LinkedIn files found to checkpoint.`};K(e,[`add`,`--`,...r],i);try{return K(e,[`diff`,`--cached`,`--quiet`,`--exit-code`],i),{created:!1,message:`No changes detected in the LinkedIn publication files. Skipping checkpoint commit.`}}catch{let n=`chore(linkedin): checkpoint ${t} before ready polish`;return K(e,[`commit`,`--no-gpg-sign`,`-m`,n],i),{created:!0,commitHash:K(e,[`rev-parse`,`HEAD`]),message:n}}}catch(e){throw Error(`Failed to create LinkedIn checkpoint commit: ${Le(e)}`)}finally{te(r,{recursive:!0,force:!0})}}const xt=`linkedin-ready-editor`;function St(e){let t=e.trim();if(!t.startsWith("```")||!t.endsWith("```"))return t;let n=t.split(`
7
+ `);return n.length<3?t:n.slice(1,-1).join(`
8
+ `).trim()}function Ct(e,t,n){let r=b(e,t);return c(r)||P(`Missing ${n}: ${r}`),r}function wt(e,t){let n=b(e,t);return c(n)?{type:`file`,path:n,displayName:h(n)}:null}function Tt(e){return[wt(e,`guidelines-linkedin.md`),wt(e,`ligne-editoriale.md`)].filter(e=>e!==null)}function Et(e,t){let n=St(e),r=C(n);return(typeof r.data.title!=`string`||r.data.title.trim().length===0)&&P(`Invalid Copilot response for ${t}: missing frontmatter title.`),n.endsWith(`
9
+ `)?n:`${n}\n`}function Dt(e,t){let n=St(e);return n.length===0&&P(`Invalid Copilot response for ${t}: empty content.`),n.endsWith(`
10
+ `)?n:`${n}\n`}function Ot(){return[`You are polishing LinkedIn publication files before they are marked as ready.`,`Use the preloaded skills to improve the attached files while preserving the author's meaning, tone, style, structure, and emojis.`,`Return only the corrected file content.`,`Never add explanations, summaries, or Markdown code fences around the file content.`].join(`
11
+
12
+ `)}function kt(e,t){let n=t.filter(e=>!e.isMain).map(e=>e.relativePath),r=n.length>0?`Related files in this publication folder: ${n.join(`, `)}.`:`There are no related Markdown files in this publication folder.`;return[`Prepare the attached LinkedIn publication file for social publishing: ${e.relativePath}.`,r,`Use the LinkedIn correction skill and the provided guidance to fix typos, grammar, punctuation, spacing, and minor formatting issues.`,`Make the post social-network ready without rewriting its meaning or personality.`,`You may improve the frontmatter title, but keep all other frontmatter keys intact unless correcting an obvious formatting issue.`,`Return only the complete corrected linkedin.md content.`].join(`
13
+ `)}function At(e){return[`Proofread the attached Markdown file: ${e.relativePath}.`,`Fix typos, grammar, punctuation, spacing, and light formatting issues only.`,`Preserve the original meaning, structure, tone, and voice.`,`Return only the complete corrected file content.`].join(`
14
+ `)}async function jt(e,t,n,r){let i=Ct(e,`.github/skills`,`Copilot skills directory`),a=Tt(e),o=new pe;await o.start();let s;try{s=await o.createSession({...r.model?{model:r.model}:{},...r.reasoningEffort?{reasoningEffort:r.reasoningEffort}:{},workingDirectory:e,availableTools:[],skillDirectories:[i],customAgents:[{name:xt,displayName:`LinkedIn Ready Editor`,description:`Proofreads and polishes LinkedIn publication files before they are marked as ready.`,prompt:Ot(),skills:[`correct-linkedin-post`,`proofread-text`]}],agent:xt,onPermissionRequest:me});let c=n.find(e=>e.isMain);c||P(`Unable to find the main LinkedIn publication file to polish.`);let l=(await s.sendAndWait({prompt:kt(t,n),attachments:[{type:`file`,path:c.path,displayName:h(c.path)},...a]}))?.data.content;typeof l!=`string`&&P(`Copilot did not return corrected content for ${c.relativePath}.`),m(c.path,Et(l,c.relativePath),`utf-8`);for(let e of n.filter(e=>!e.isMain)){let t=(await s.sendAndWait({prompt:At(e),attachments:[{type:`file`,path:e.path,displayName:h(e.path)}]}))?.data.content;typeof t!=`string`&&P(`Copilot did not return corrected content for ${e.relativePath}.`),m(e.path,Dt(t,e.relativePath),`utf-8`)}}catch(e){P(`Failed to polish the LinkedIn publication with Copilot: ${e instanceof Error?e.message:String(e)}`)}finally{await s?.disconnect(),await o.stop()}}async function Mt(e){t(`Content Creation - Polish and Mark LinkedIn Publication Ready`);let n=await T(),i=await yt(e),o=vt(i),s=y(e,i.folderPath);r.info(`Preparing ${i.relativePath} with ${o.length} Markdown file(s).`);let c=bt(e,s,o);c.created?r.success(`Checkpoint commit created: ${c.commitHash}`):r.warn(c.message),await jt(e,i,o,n.linkedin.ready.copilot);let l=gt(i),u=l.scriptMarked?`script marked ready`:`no script found`,d=c.created?`checkpoint ${c.commitHash} created`:`no checkpoint commit created`;a(`✓ LinkedIn publication ready: ${i.relativePath} (${o.length} file(s) reviewed, ${l.imageCount} image(s), ${u}, ${d}; final changes left uncommitted)`)}function Nt(e,t,n){let i=H(e),a=v(b(v(n,`resources`)),i),o=`${t}.md`,s=v(a,o);if(c(s))return r.info(`${o} already exists at: ${s}`),a;l(a,{recursive:!0}),r.success(`Created directory: ${a}`);let u={title:e,url:``,date:``};return m(s,C.stringify(``,u),`utf-8`),r.success(`Created ${o} at: ${s}`),a}async function Pt(){let e=F(await o({message:`What type of resource is this?`,options:[{label:`Article`,value:`article`,hint:`Create article.md`},{label:`Video`,value:`video`,hint:`Create video.md`},{label:`Audio`,value:`audio`,hint:`Create audio.md`},{label:`Tweet`,value:`tweet`,hint:`Create tweet.md`}]}));return[`article`,`video`,`audio`,`tweet`].includes(String(e))||P(`Unable to resolve resource type: ${String(e)}`),e}async function Ft(){return R({message:`What is the title of your content?`,placeholder:`Enter content title`,requiredMessage:`Title is required`})}async function It(e){t(`Content Creation - Create Resource`);let n=await Ft(),r=await Pt();Nt(n,r,e),a(`✓ Resource created: resources/${H(n)}/${r}.md`)}const Lt=`## Index`,q=`<!-- content-creation:series-index:start -->`,J=`<!-- content-creation:series-index:end -->`,Rt=/^(\d+)\.([^.]+(?:\.[^.]+)*)\.md$/;function Y(e){return b(v(e,`series`))}function zt(e){let t=Y(e);return c(t)?f(t).filter(e=>Ut(v(t,e))).map(e=>Wt(t,e)).sort((e,t)=>e.title.localeCompare(t.title,`fr`)):[]}function X(e){return f(e).map(Jt).filter(e=>e!==null).map(t=>Gt(e,t)).sort((e,t)=>e.index-t.index)}function Z(e){let t=X(e),n=v(e,`README.md`);m(n,Bt(c(n)?d(n,`utf-8`):``,t,e),`utf-8`),r.success(`Generated series index: ${n}`)}function Bt(e,t,n){let r=Vt(t);if(e.includes(q)&&e.includes(J))return e.replace(Ht(),r);let i=e.trimEnd();return i.length>0?`${i}\n\n${r}\n`:`# ${z(n.split(`/`).pop()||`series`)}\n\n${r}\n`}function Vt(e){let t=[Lt,``,q];if(e.length===0)t.push(``,`_No articles yet._`);else{t.push(``);for(let n of e)t.push(`${n.index}. [${n.title}](./${n.fileName})`)}return t.push(``,J),t.join(`
15
+ `)}function Ht(){return RegExp(`${B(Lt)}\n\n${B(q)}[\\s\\S]*?${B(J)}`)}function Ut(e){return p(e).isDirectory()}function Wt(e,t){let n=v(e,t);return{name:t,path:n,title:Kt(n,t)}}function Gt(e,t){let n=v(e,t.fileName);return{...t,path:n,title:qt(n,t.slug)}}function Kt(e,t){let n=v(e,`README.md`);if(c(n)){let e=d(n,`utf-8`).split(`
16
+ `).map(e=>e.trim()).find(e=>e.startsWith(`# `));if(e)return e.slice(2).trim()}return z(t)}function qt(e,t){try{let{data:t}=C(d(e,`utf-8`)),n=typeof t.title==`string`?t.title.trim():``;if(n.length>0)return n}catch(t){r.warn(`Unable to read article title from ${e}: ${t}`)}return z(t)}function Jt(e){let t=e.match(Rt);return t?{index:Number(t[1]),slug:t[2],fileName:e}:null}function Yt(e,t,n){let i=X(e),a=Zt(n,i.length),o=v(e,`${a}.${H(t)}.md`);if(c(o))throw Error(`Article already exists at ${o}`);return Xt(e,i,a),m(o,C.stringify(``,{title:t}),`utf-8`),r.success(`Created article: ${o}`),Z(e),o}function Xt(e,t,n){let i=t.filter(e=>e.index>=n).sort((e,t)=>t.index-e.index);for(let t of i){let n=`${t.index+1}.${t.slug}.md`,i=v(e,n);ee(t.path,i),r.info(`Renamed ${t.fileName} → ${n}`)}}function Zt(e,t){if(!Number.isInteger(e))throw TypeError(`Article index must be an integer`);if(e<1||e>t+1)throw RangeError(`Article index must be between 1 and ${t+1}`);return e}function Qt(e,t){let n=t.trim();if(n.length===0)throw Error(`Series title is required`);let i=H(n),a=Y(e),o=v(a,i);if(l(a,{recursive:!0}),c(o))throw Error(`Series already exists: series/${i}`);return l(o),m(v(o,`README.md`),`# ${n}\n`,`utf-8`),Z(o),r.success(`Created series: ${o}`),{name:i,path:o,title:n}}function Q(e,t){let{content:n,frontmatter:i}=I(e),a=Ke(n,t);return L(e,{content:n,frontmatter:{...i,time:a}}),r.success(`Estimated time set to ${a} min: ${e}`),a}function $t(e){V({path:e.path,relativePath:e.fileName},`series article`)}async function en(e){let t=X(e);if(t.length===0&&P(`No series articles found`),t.length===1)return t[0];let n=F(await o({message:`Which series article do you want to update?`,options:t.map(e=>({label:`#${e.index} · ${e.title}`,value:e.path,hint:e.fileName}))}));typeof n!=`string`&&P(`Unable to resolve selected series article: ${String(n)}`);let r=t.find(e=>e.path===n);return r||P(`Unable to resolve selected series article: ${n}`),r}async function tn(){return R({message:`What is the title of your article?`,placeholder:`Enter article title`,requiredMessage:`Title is required`})}async function $(e){let t=zt(e);if(t.length===0&&P(`No series directories found`),t.length===1)return t[0];let n=F(await o({message:`Which series do you want to work on?`,options:t.map(e=>({label:e.title,value:e.path,hint:`series/${e.name}`}))}));typeof n!=`string`&&P(`Unable to resolve selected series: ${String(n)}`);let r=t.find(e=>e.path===n);return r||P(`Unable to resolve selected series: ${n}`),r}async function nn(e){let t=X(e),n=t.map(e=>({label:`Insert at #${e.index} · before ${e.title}`,value:String(e.index),hint:e.fileName}));n.push({label:`Append as #${t.length+1}`,value:String(t.length+1),hint:`Add the new article at the end of the series`});let r=F(await o({message:`Where should the new article be inserted?`,options:n}));return typeof r!=`string`&&P(`Unable to resolve selected insert position: ${String(r)}`),Number(r)}async function rn(){return R({message:`What is the title of your series?`,placeholder:`Enter series title`,requiredMessage:`Title is required`})}function an(e){return on(e,e).sort((e,t)=>e.relativePath.localeCompare(t.relativePath))}function on(e,t){let n=f(t,{withFileTypes:!0}),r=[];for(let i of n){let n=v(t,i.name);if(i.isDirectory()){r.push(...on(e,n));continue}!i.isFile()||_(i.name).toLowerCase()!==`.mp4`||r.push({name:i.name,path:n,relativePath:y(e,n)})}return r}async function sn(e){let t=an(e);if(t.length===0&&P(`No .mp4 files found in series directory: ${e}`),t.length===1)return t[0];let n=F(await o({message:`Which series video do you want to process?`,options:t.map(e=>({label:e.relativePath,value:e.path,hint:e.name}))}));typeof n!=`string`&&P(`Unable to resolve selected series video: ${String(n)}`);let r=t.find(e=>e.path===n);return r||P(`Unable to resolve selected series video: ${n}`),r}async function cn(){let e=await s({message:`Enter text for the thumbnail (or press Enter to skip):`,placeholder:`Optional thumbnail text`});if(typeof e!=`symbol`)return e.trim()||void 0}async function ln(e){let t=un(e);if(t.length===0){r.error(`No templates found in ${e||`the configured templates directory`}`),r.info(`Please add SVG templates before generating thumbnails.`);return}let n=await o({message:`Choose a template:`,options:t.map(e=>({label:e,value:e}))});if(typeof n!=`symbol`)return n}function un(e){if(!e||!c(e))return[];try{return f(e,{withFileTypes:!0}).filter(e=>e.isFile()&&_(e.name).toLowerCase()===`.svg`).map(e=>e.name.replace(/\.svg$/i,``)).sort((e,t)=>e.localeCompare(t))}catch{return[]}}async function dn(e){t(`Content Creation - Estimate Series Article Time`);let n=await T(),r=await $(e),i=await en(r.path),o=Q(i.path,n.reading.wordsPerMinute);a(`✓ Estimated time updated for ${r.name}/${i.fileName}: ${o} min`)}async function fn(e){t(`Content Creation - Create Series`),a(`✓ Series created: series/${Qt(e,await rn()).name}`)}async function pn(e){t(`Content Creation - Create Series Article`);let n=await $(e),r=await tn(),i=await nn(n.path),o=Yt(n.path,r,i);a(`✓ Series article created: ${n.name}/${h(o)}`)}async function mn(e){let n=await T();n.videoProcessing.openaiApiKey||P(`OpenAI API key is required. Set OPENAI_API_KEY in .env or configure videoProcessing.openaiApiKey in content-creation.config.{ts,js,mjs,json}.`);let i=await $(e),o=await sn(i.path);t(`Content Creation - Process Series Video: ${h(o.path)}`);let{videoPath:s,audioPath:c}=await ge(o.path),{srtPath:l}=await ve(s,c,{openaiApiKey:n.videoProcessing.openaiApiKey,language:n.videoProcessing.language,model:n.videoProcessing.model,templatesDir:n.templatesDir});he(c,`audio file`);let u=await cn();if(!u){a(`✓ Transcription saved to: ${l}`);return}let d=await ln(n.templatesDir);if(!d){r.warn(`No template selected. Skipping thumbnail generation.`),a(`✓ Transcription saved to: ${l}`);return}let{thumbnailPaths:f}=await _e(s,u,d,n.templatesDir);a(`✓ Series video processed: series/${i.name}/${o.relativePath}\n✓ Transcription saved to: ${l}\n✓ Thumbnails saved:\n - ${f.join(`
17
+ - `)}`)}async function hn(e){t(`Content Creation - Mark Series Article Ready`);let n=await T(),r=await $(e),i=await en(r.path);$t(i);let o=Q(i.path,n.reading.wordsPerMinute);a(`✓ Series article updated for ${r.name}/${i.fileName}: ready + ${o} min`)}async function gn(e,t){let n=e.path,{content:i,frontmatter:a}=I(n);if(a.scheduled===!0)return r.warn(`This X publication is already scheduled. Skipping.`),!1;(!t.scheduling.automationEndpoint||!t.scheduling.cfAccessClientId||!t.scheduling.cfAccessClientSecret)&&P(`Scheduling configuration is missing. Please set AUTOMATION_ENDPOINT, CF_ACCESS_CLIENT_ID, and CF_ACCESS_CLIENT_SECRET in your .env file or config.`);let o=new Date(e.date);o=ie(o,j.hours),o=ae(o,j.minutes),o=oe(o,j.seconds);let s={content:i.trim(),scheduleAt:o.getTime()};try{let e=await fetch(t.scheduling.automationEndpoint,{method:`POST`,headers:{"Content-Type":`application/json`,Accept:`application/json`,"CF-Access-Client-Id":t.scheduling.cfAccessClientId,"CF-Access-Client-Secret":t.scheduling.cfAccessClientSecret},body:JSON.stringify(s)});if(!e.ok){let t=await e.text();P(`Failed to schedule reminder: ${e.status} ${e.statusText}\n${t}`)}let o=await e.json();L(n,{content:i,frontmatter:{...a,scheduled:!0}});let c=new Date(o.scheduledAt);return r.success(`✓ X publication reminder scheduled`),r.info(`Reminder scheduled for: ${S(c,`yyyy-MM-dd HH:mm:ss`)} UTC`),!0}catch(e){P(`Failed to schedule X publication reminder: ${e instanceof Error?e.message:String(e)}`)}}async function _n(e){return Re({basePath:e,contentTypes:[`x`],message:`Which X publication do you want to schedule a reminder for?`,emptyMessage:`No X publications found in dated folders`,sort:`date-desc`})}async function vn(e){t(`Content Creation - Schedule X Publication Reminder`);let n=await T();a(await gn(await _n(e),n)?`✓ X publication reminder scheduled`:`✓ X publication was already scheduled`)}export{Ke as A,Ze as C,qe as D,V as E,Ne as F,M as I,T as L,I as M,L as N,B as O,Fe as P,Qe as S,H as T,Nt as _,pn as a,gt as b,$t as c,Yt as d,Z as f,It as g,Y as h,mn as i,R as j,z as k,Q as l,zt as m,gn as n,fn as o,X as p,hn as r,dn as s,vn as t,Qt as u,Mt as v,Xe as w,pt as x,_t as y};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@barbapapazes/content-creation",
3
3
  "type": "module",
4
- "version": "0.19.0",
4
+ "version": "0.20.1",
5
5
  "description": "CLI tool for content creation and management",
6
6
  "author": "Estéban Soubiran <esteban@soubiran.dev>",
7
7
  "license": "MIT",
@@ -31,17 +31,20 @@
31
31
  "node": ">=24"
32
32
  },
33
33
  "dependencies": {
34
- "@clack/prompts": "^0.10.1",
34
+ "@clack/prompts": "^1.4.0",
35
+ "@github/copilot-sdk": "1.0.0-beta.4",
35
36
  "c12": "^3.3.4",
36
37
  "commander": "15.0.0-0",
37
38
  "date-fns": "^4.1.0",
38
39
  "gray-matter": "^4.0.3",
39
- "wrangler": "^4.61.1"
40
+ "tinyexec": "1.1.2",
41
+ "wrangler": "^4.61.1",
42
+ "@barbapapazes/video-toolkit": "0.20.1"
40
43
  },
41
44
  "devDependencies": {
42
45
  "@tsconfig/node24": "^24.0.4",
43
46
  "@types/node": "^22.19.17",
44
- "tsdown": "^0.18.4",
47
+ "tsdown": "^0.22.0",
45
48
  "typescript": "^5.9.3"
46
49
  },
47
50
  "scripts": {