@barbapapazes/content-creation 0.18.4 → 0.18.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.mjs +4 -4
  2. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
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{cac as c}from"cac";import{addDays as l,format as u,parse as d,parseISO as f,setHours as ee,setMinutes as te,setSeconds as ne}from"date-fns";import{existsSync as p,mkdirSync as m,readFileSync as h,readdirSync as g,statSync as _,writeFileSync as v}from"node:fs";import{dirname as y,isAbsolute as re,join as b,resolve as x}from"node:path";import S from"gray-matter";import{loadConfig as C}from"c12";import{execFileSync as w}from"node:child_process";import{createHash as T}from"node:crypto";import{fileURLToPath as E}from"node:url";var D=`0.18.4`;const O={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`]}},ie={thematic:[],templatesDir:void 0,templates:{},scheduling:{},calendar:{}};function ae(t,n){return t?re(t)?t:x(n?y(n):e.cwd(),t):n?y(n):e.cwd()}function oe(e,t){let n={};if(e?.footerPath){let r=x(t,e.footerPath);p(r)&&(n.footerPath=r)}return n}function k(e,t){let n={};if(e?.templatePath){let r=x(t,e.templatePath);p(r)&&(n.templatePath=r)}return n}async function A(){let{config:t,configFile:n}=await C({name:`content-creation`,defaults:ie,globalRc:!0,dotenv:!0}),r=ae(t.templatesDir,n),i={linkedin:oe(t.templates?.linkedin,r),youtube:k(t.templates?.youtube,r),instagram:k(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};return{thematic:t.thematic||[],templatesDir:t.templatesDir||``,templates:i,scheduling:a,calendar:o}}function j(e){return p(e)?h(e,`utf-8`):``}function se(t,n,i,a,o=e.cwd()){let s=x(b(o,u(t,`yyyy`),u(t,`MM`),u(t,`dd`)));p(s)||(m(s,{recursive:!0}),r.success(`Created directory: ${s}`));let c=ce(i);for(let e of n)le(e,s,c,a);return s}function ce(e){return e.hasVideo?{...e,hasImages:!1}:{...e,hasImages:!0}}function le(e,t,n,i){let a=O[e],o=b(t,a.mainFile);if(p(o)?r.info(`${a.mainFile} already exists, skipping`):M(e,o,n,i),a.additionalFiles)for(let o of a.additionalFiles){let a=b(t,o);P(o,e,n)&&(p(a)?r.info(`${o} already exists, skipping`):N(e,a,o,i))}}function M(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=``;if(e===`linkedin`){let e=i.templates.linkedin?.footerPath;o=e?j(e):``}v(t,S.stringify(o,a),`utf-8`),r.success(`Created ${t}`)}function N(e,t,n,i){let a=``;if(n.endsWith(`-description.md`)){let t=i.templates[e]?.templatePath;t&&(a=j(t))}v(t,a,`utf-8`),r.success(`Created ${t}`)}function P(e,t,n){return t===`linkedin`&&e===`linkedin-script.md`?n.hasVideo||!1:!0}const F=[`linkedin`,`x`,`youtube`,`instagram`],I={linkedin:`LinkedIn`,x:`X`,youtube:`YouTube`,instagram:`Instagram`};function L(e,t){let n=[],i=O[t].mainFile;try{let t=g(e).filter(t=>_(b(e,t)).isDirectory()&&/^\d{4}$/.test(t));for(let a of t){let t=b(e,a),o=g(t).filter(e=>_(b(t,e)).isDirectory()&&/^\d{2}$/.test(e));for(let e of o){let o=b(t,e),s=g(o).filter(e=>_(b(o,e)).isDirectory()&&/^\d{2}$/.test(e));for(let t of s){let s=x(b(o,t),i);if(!p(s))continue;let c=`${a}-${e}-${t}`,l=new Date(Number(a),Number(e)-1,Number(t));try{let{data:r}=S(h(s,`utf-8`)),o=r.title||`Untitled (${c})`;n.push({path:s,date:l,title:o,relativePath:`${a}/${e}/${t}/${i}`})}catch(e){r.warn(`Error reading ${s}: ${e}`)}}}}}catch(e){return r.error(`Error reading directories: ${e}`),[]}return n.sort((e,t)=>t.date.getTime()-e.date.getTime())}function R(e){switch(e){case`linkedin`:return`linkedin-posts.md`;case`x`:return`x-posts.md`;case`youtube`:return`youtube-videos.md`;case`instagram`:return`instagram-posts.md`}}function z(e){switch(e){case`linkedin`:return`LinkedIn Posts`;case`x`:return`X Posts`;case`youtube`:return`YouTube Videos`;case`instagram`:return`Instagram Posts`}}function B(e,t){let n=L(e,t);if(n.length===0){r.info(`No ${t} posts found`);return}let i=z(t),a=R(t),o=`# ${i}\n\n`;o+=`_Generated on ${new Date().toLocaleDateString()}_\n\n`,o+=`Total posts: ${n.length}\n\n`;for(let e of n){let t=e.date.toLocaleDateString(`en-US`,{year:`numeric`,month:`long`,day:`numeric`});o+=`- [${e.title}](${e.relativePath}) - _${t}_\n`}v(b(e,a),o,`utf-8`),r.success(`Created ${a} with ${n.length} posts`)}function V(t=e.cwd(),n){let r=n||[...F];for(let e of r)B(t,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`}function U(t,n,i=e.cwd()){let a=H(t),o=b(x(b(i,`resources`)),a),s=`${n}.md`,c=b(o,s);if(p(c))return r.info(`${s} already exists at: ${c}`),o;m(o,{recursive:!0}),r.success(`Created directory: ${o}`);let l={title:t,url:``,date:``};return v(c,S.stringify(``,l),`utf-8`),r.success(`Created ${s} at: ${c}`),o}const W=`load-more`;function ue(t=e.cwd()){let n=[];try{let e=g(t).filter(e=>_(b(t,e)).isDirectory()&&/^\d{4}$/.test(e));for(let r of e){let e=b(t,r),i=g(e).filter(t=>_(b(e,t)).isDirectory()&&/^\d{2}$/.test(t));for(let t of i){let i=b(e,t),a=g(i).filter(e=>_(b(i,e)).isDirectory()&&/^\d{2}$/.test(e));for(let e of a){let a=b(i,e);if(p(b(a,`linkedin.md`))){let i=`${r}-${t}-${e}`;n.push({path:a,date:new Date(i),displayPath:`${r}/${t}/${e}`})}}}}}catch(e){return r.error(`Error reading directories: ${e}`),[]}return n.sort((e,t)=>t.date.getTime()-e.date.getTime())}function de(e){let t=[`.jpg`,`.jpeg`,`.png`],n=[];try{let r=g(e);for(let i of r)if(_(b(e,i)).isFile()){let e=i.toLowerCase().substring(i.lastIndexOf(`.`));t.includes(e)&&n.push(i)}}catch(e){r.error(`Error reading directory: ${e}`)}return n.sort()}function G(e,t){try{let{data:n,content:i}=S(h(e,`utf-8`));n.images=t.length>0?t:[null],v(e,S.stringify(i,n),`utf-8`),r.success(`Updated ${e} with ${t.length} image(s)`)}catch(e){r.error(`Error updating frontmatter: ${e}`)}}async function fe(t){if(t.length===0)return r.warn(`No dated folders with linkedin.md found`),null;let i=8;for(;;){let a=await o({message:`Select a folder to link images:`,options:pe(t,i)});if(n(a)&&(r.error(`Operation cancelled.`),e.exit(0)),a===W){i+=8;continue}return t.find(e=>e.path===a)||null}}function pe(e,t){let n=e.slice(0,t).map(e=>({label:e.displayPath,value:e.path,hint:`Link images in ${e.displayPath}`}));if(t>=e.length)return n;let r=Math.min(t+8,e.length);return[...n,{label:`Show ${r-t} more folders (${t+1}-${r} of ${e.length})`,value:W,hint:`Extend the list to ${e[r-1]?.displayPath}`}]}async function me(t=e.cwd()){let n=ue(t);if(n.length===0){r.warn(`No dated folders with linkedin.md found`);return}let i=await fe(n);if(!i)return;let a=de(i.path);if(a.length===0){r.info(`No images found in ${i.displayPath}`),G(b(i.path,`linkedin.md`),[]);return}r.info(`Found ${a.length} image(s): ${a.join(`, `)}`),G(b(i.path,`linkedin.md`),a)}function he(e){let t=new Date;t.setHours(0,0,0,0);let n=F.flatMap(t=>L(e,t).map(e=>({...e,contentType:t}))).filter(e=>e.date.getTime()>=t.getTime()).sort((e,t)=>{let n=e.date.getTime()-t.date.getTime();return n===0?F.indexOf(e.contentType)-F.indexOf(t.contentType):n});if(n.length===0){r.info(`No upcoming content found from today onward.`);return}r.info(`Found ${n.length} upcoming content item${n.length===1?``:`s`}:`),console.log(``);for(let e of n){let t=u(e.date,`EEEE, MMMM d, yyyy`),n=u(e.date,`yyyy-MM-dd`);console.log(`${t} (${n}) · ${I[e.contentType]} · ${e.title}`),console.log(e.path),console.log(``)}}function ge(t){try{let{data:e,content:n}=S(h(t,`utf-8`));if(e.ready===!0){r.warn(`This publication is already marked as ready.`);return}let i={...e,ready:!0};v(t,S.stringify(n,i),`utf-8`),r.success(`Marked publication as ready: ${t}`)}catch(t){r.error(`Failed to mark publication as ready: ${t instanceof Error?t.message:String(t)}`),e.exit(1)}}const K=E(new URL(`../../`,import.meta.url)),q=`content-creation.ics`;function J(e){return e.replace(/\\/g,`\\\\`).replace(/\r\n|\r|\n/g,`\\n`).replace(/;/g,`\\;`).replace(/,/g,`\\,`)}function _e(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
3
- `)}function ve(e){return e.toISOString().replace(/[-:]/g,``).replace(/\.\d{3}Z$/,`Z`)}function Y(e){return u(e,`yyyyMMdd`)}function X(e){return e.split(`/`).map(e=>encodeURIComponent(e)).join(`/`)}function ye(e){return`https://github.com/barbapapazes/content-creation/blob/main/${X(e)}`}function be(e){let t=X(e);return`vscode://file${t.startsWith(`/`)?``:`/`}${t}`}function xe(e){let t=[`GitHub: ${ye(e.relativePath)}`,`VS Code: ${be(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(`
4
- `)}function Se(e){return`${e.ready?`Ready`:`Not ready`} · ${e.title} · ${I.linkedin}`}function Ce(e){return`linkedin-${T(`sha1`).update(e).digest(`hex`)}@barbapapazes`}function we(e){let t=[`BEGIN:VCALENDAR`,`VERSION:2.0`,`PRODID:-//Barbapapazes//Content Creation LinkedIn Calendar//EN`,`CALSCALE:GREGORIAN`,`METHOD:PUBLISH`,`X-WR-CALNAME:${J(`content-creation.ics`)}`];for(let n of e){let e=ve(new Date(Date.UTC(n.date.getFullYear(),n.date.getMonth(),n.date.getDate()))),r=l(n.date,1);t.push(`BEGIN:VEVENT`,`UID:${Ce(n.relativePath)}`,`DTSTAMP:${e}`,`SUMMARY:${J(Se(n))}`,`DTSTART;VALUE=DATE:${Y(n.date)}`,`DTEND;VALUE=DATE:${Y(r)}`,`TRANSP:TRANSPARENT`,`DESCRIPTION:${J(xe(n))}`,`END:VEVENT`)}return t.push(`END:VCALENDAR`),`${t.map(_e).join(`\r
5
- `)}\r\n`}function Te(){let t=b(K,`node_modules`,`.bin`,e.platform===`win32`?`wrangler.cmd`:`wrangler`);return p(t)?t:`wrangler`}function Ee(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(q,n);return i.searchParams.set(`token`,t.calendar.token),i.toString()}function De(e){return L(e,`linkedin`).map(e=>{let t={title:e.title};try{t=S(h(e.path,`utf-8`)).data}catch(t){r.warn(`Failed to read LinkedIn metadata from ${e.path}: ${t}`)}return{...e,title:t.title||e.title,ready:t.ready===!0,theme:t.theme,video:t.video,imageCount:t.images?.filter(Boolean).length??0}}).sort((e,t)=>e.date.getTime()-t.date.getTime())}function Oe(t){let n=Te();try{w(n,[`r2`,`object`,`put`,`content-creation/${q}`,`--pipe`,`--content-type`,`text/calendar; charset=utf-8`,`--remote`],{cwd:K,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 ke(e,t){let n=De(e);if(n.length===0)return r.info(`No LinkedIn publications found. Skipping calendar upload.`),null;let i=Ee(t);return Oe(we(n)),r.success(`✓ LinkedIn calendar uploaded to R2 as ${q}`),r.info(`Subscription URL: ${i}`),i}async function Z(t,n){let i=b(t,`x.md`),{data:a,content:o}=S(h(i,`utf-8`));a.scheduled===!0&&(r.warn(`This content is already scheduled. Skipping.`),e.exit(0)),(!n.scheduling.automationEndpoint||!n.scheduling.cfAccessClientId||!n.scheduling.cfAccessClientSecret)&&(r.error(`Scheduling configuration is missing. Please set AUTOMATION_ENDPOINT, CF_ACCESS_CLIENT_ID, and CF_ACCESS_CLIENT_SECRET in your .env file or config.`),e.exit(1));let s=t.split(`/`),c=s[s.length-1],l=s[s.length-2],f=d(`${s[s.length-3]}-${l}-${c}`,`yyyy-MM-dd`,new Date);f=ee(f,11),f=te(f,0),f=ne(f,0);let p=f.getTime(),m={content:o.trim(),scheduleAt:p};try{let t=await fetch(n.scheduling.automationEndpoint,{method:`POST`,headers:{"Content-Type":`application/json`,Accept:`application/json`,"CF-Access-Client-Id":n.scheduling.cfAccessClientId,"CF-Access-Client-Secret":n.scheduling.cfAccessClientSecret},body:JSON.stringify(m)});if(!t.ok){let n=await t.text();r.error(`Failed to schedule reminder: ${t.status} ${t.statusText}\n${n}`),e.exit(1)}let s=await t.json(),c={...a,scheduled:!0};v(i,S.stringify(o,c),`utf-8`);let l=new Date(s.scheduledAt);r.success(`✓ Reminder scheduled successfully!`),r.info(`Scheduled for: ${u(l,`yyyy-MM-dd HH:mm:ss`)} UTC`)}catch(t){r.error(`Failed to schedule reminder: ${t instanceof Error?t.message:String(t)}`),e.exit(1)}}async function Ae(){let t=new Date,i=7;for(;;){let a=await o({message:`When do you want to create content?`,options:je(t,i)});if(n(a)&&(r.error(`Operation cancelled.`),e.exit(0)),a===`load-more`){i+=7;continue}return f(a)}}function je(e,t){let n=[];for(let r=0;r<t;r++){let t=l(e,r),i=u(t,`yyyy-MM-dd`),a=u(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:`load-more`,hint:`Extend the list up to ${u(l(e,t+6),`yyyy-MM-dd`)}`}),n}async function Q(){let t=await s({message:`What is the title of your content?`,placeholder:`Enter content title`,validate:e=>{if(!e||e.trim().length===0)return`Title is required`}});return n(t)&&(r.error(`Operation cancelled.`),e.exit(0)),t.trim()}async function Me(){let t=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 n(t)&&(r.error(`Operation cancelled.`),e.exit(0)),t}async function Ne(t){if(t.length===0)return;let i=await o({message:`What is the content theme? (optional)`,options:[{label:`No theme`,value:``,hint:`Leave theme empty`},...t.map(e=>({label:e,value:e,hint:`Use theme: ${e}`}))]});n(i)&&(r.error(`Operation cancelled.`),e.exit(0));let a=i;return a.length>0?a:void 0}async function Pe(){let t=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 n(t)&&(r.error(`Operation cancelled.`),e.exit(0)),t}async function Fe(){let t=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 n(t)&&(r.error(`Operation cancelled.`),e.exit(0)),t}async function Ie(t){let i=[];p(t)||(r.error(`Base path does not exist: ${t}`),e.exit(1));let a=g(t).filter(e=>_(b(t,e)).isDirectory()&&/^\d{4}$/.test(e));for(let e of a){let n=b(t,e),r=g(n).filter(e=>_(b(n,e)).isDirectory()&&/^\d{2}$/.test(e));for(let t of r){let r=b(n,t),a=g(r).filter(e=>_(b(r,e)).isDirectory()&&/^\d{2}$/.test(e));for(let n of a){let a=b(r,n);if(p(b(a,`x.md`))){let r=`${e}-${t}-${n}`;i.push({path:a,date:r,title:``})}}}}i.length===0&&(r.error(`No x.md files found in dated folders`),e.exit(1)),i.sort((e,t)=>t.date.localeCompare(e.date));let s=await o({message:`Which content do you want to schedule a reminder for?`,options:i.map(e=>({label:e.date,value:e.path,hint:`Schedule reminder for ${e.date}`}))});return n(s)&&(r.error(`Operation cancelled.`),e.exit(0)),s}async function Le(t){let i=F.flatMap(e=>L(t,e).map(t=>({...t,contentType:e}))).sort((e,t)=>{let n=t.date.getTime()-e.date.getTime();return n===0?F.indexOf(e.contentType)-F.indexOf(t.contentType):n});i.length===0&&(r.error(`No publications found in dated folders`),e.exit(1));let a=await o({message:`Which publication do you want to mark as ready?`,options:i.map(e=>Re(e))});return n(a)&&(r.error(`Operation cancelled.`),e.exit(0)),a}function Re(e){return{label:`${u(e.date,`yyyy-MM-dd`)} · ${I[e.contentType]} · ${e.title}`,value:e.path,hint:e.relativePath}}const $=c(`content-creation`);$.command(`[path]`,`Create a dated content directory with content files`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).action(async(n,r)=>{t(`Content Creation - Create dated content directory`);let i=await A(),o=await Q(),s=await Fe(),c={title:o};s.includes(`linkedin`)&&(c.hasVideo=await Me(),c.hasImages=!c.hasVideo,i.thematic.length>0&&(c.theme=await Ne(i.thematic)));let l=await Ae();se(l,s,c,i,n||r?.path||e.cwd());let d=s.join(`, `);a(`✓ Content directory created: ${u(l,`yyyy/MM/dd`)} (${d})`)}),$.command(`link-images [path]`,`Link images to LinkedIn frontmatter in recent folders`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).action(async(n,r)=>{t(`Content Creation - Link Images to LinkedIn`),await me(n||r?.path||e.cwd()),a(`✓ Images linked successfully`)}),$.command(`resource [path]`,`Create a resource with article/video/audio markdown file`).option(`--path <path>`,`Base path for resource directory (defaults to current directory)`).action(async(n,r)=>{t(`Content Creation - Create Resource`);let i=await Q(),o=await Pe();U(i,o,n||r?.path||e.cwd()),a(`✓ Resource created: resources/${H(i)}/${o}.md`)}),$.command(`create-index [path]`,`Create index files for each content type`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).action(async(n,r)=>{t(`Content Creation - Create Index Files`),V(n||r?.path||e.cwd()),a(`✓ Index files generated successfully`)}),$.command(`list-upcoming [path]`,`List content scheduled for today or later`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).action(async(n,r)=>{t(`Content Creation - List Upcoming Content`),he(n||r?.path||e.cwd()),a(`✓ Upcoming content listed successfully`)}),$.command(`publish-linkedin-calendar [path]`,`Create and upload a LinkedIn publication calendar`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).action(async(n,r)=>{t(`Content Creation - Publish LinkedIn Calendar`);let i=await A();if(await ke(n||r?.path||e.cwd(),i)){a(`✓ LinkedIn calendar published successfully`);return}a(`✓ No LinkedIn content found, calendar was not updated`)}),$.command(`schedule-reminder [path]`,`Schedule a reminder for X content`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).action(async(n,r)=>{t(`Content Creation - Schedule X Reminder`);let i=await A();await Z(await Ie(n||r?.path||e.cwd()),i),a(`✓ Reminder scheduled successfully`)}),$.command(`ready [path]`,`Mark a publication as ready`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).action(async(n,r)=>{t(`Content Creation - Mark Publication as Ready`),ge(await Le(n||r?.path||e.cwd())),a(`✓ Publication marked as ready`)}),$.help(),$.version(D),$.parse();export{};
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{cac as c}from"cac";import{addDays as l,format as u,parseISO as d,setHours as f,setMinutes as ee,setSeconds as p}from"date-fns";import{existsSync as m,mkdirSync as h,readFileSync as g,readdirSync as _,statSync as v,writeFileSync as y}from"node:fs";import{dirname as b,isAbsolute as te,join as x,resolve as S}from"node:path";import C from"gray-matter";import{loadConfig as ne}from"c12";import{execFileSync as w}from"node:child_process";import{createHash as T}from"node:crypto";import{fileURLToPath as E}from"node:url";var D=`0.18.6`;const O={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`]}},re={thematic:[],templatesDir:void 0,templates:{},scheduling:{},calendar:{}};function ie(t,n){return t?te(t)?t:S(n?b(n):e.cwd(),t):n?b(n):e.cwd()}function ae(e,t){let n={};if(e?.footerPath){let r=S(t,e.footerPath);m(r)&&(n.footerPath=r)}return n}function k(e,t){let n={};if(e?.templatePath){let r=S(t,e.templatePath);m(r)&&(n.templatePath=r)}return n}async function A(){let{config:t,configFile:n}=await ne({name:`content-creation`,defaults:re,globalRc:!0,dotenv:!0}),r=ie(t.templatesDir,n),i={linkedin:ae(t.templates?.linkedin,r),youtube:k(t.templates?.youtube,r),instagram:k(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};return{thematic:t.thematic||[],templatesDir:t.templatesDir||``,templates:i,scheduling:a,calendar:o}}function j(e){return m(e)?g(e,`utf-8`):``}function oe(t,n,i,a,o=e.cwd()){let s=S(x(o,u(t,`yyyy`),u(t,`MM`),u(t,`dd`)));m(s)||(h(s,{recursive:!0}),r.success(`Created directory: ${s}`));let c=se(i);for(let e of n)ce(e,s,c,a);return s}function se(e){return e.hasVideo?{...e,hasImages:!1}:{...e,hasImages:!0}}function ce(e,t,n,i){let a=O[e],o=x(t,a.mainFile);if(m(o)?r.info(`${a.mainFile} already exists, skipping`):le(e,o,n,i),a.additionalFiles)for(let o of a.additionalFiles){let a=x(t,o);M(o,e,n)&&(m(a)?r.info(`${o} already exists, skipping`):ue(e,a,o,i))}}function le(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=``;if(e===`linkedin`){let e=i.templates.linkedin?.footerPath;o=e?j(e):``}y(t,C.stringify(o,a),`utf-8`),r.success(`Created ${t}`)}function ue(e,t,n,i){let a=``;if(n.endsWith(`-description.md`)){let t=i.templates[e]?.templatePath;t&&(a=j(t))}y(t,a,`utf-8`),r.success(`Created ${t}`)}function M(e,t,n){return t===`linkedin`&&e===`linkedin-script.md`?n.hasVideo||!1:!0}const N=[`linkedin`,`x`,`youtube`,`instagram`],P={linkedin:`LinkedIn`,x:`X`,youtube:`YouTube`,instagram:`Instagram`};function F(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=N.indexOf(e.contentType)-N.indexOf(t.contentType);return i===0?e.relativePath.localeCompare(t.relativePath):i}function I(e,t={}){let n=[],i=t.contentTypes?.length?t.contentTypes:[...N],a=t.fromDate,o=t.sort||`date-desc`;try{let t=_(e).filter(t=>v(x(e,t)).isDirectory()&&/^\d{4}$/.test(t));for(let o of t){let t=x(e,o),s=_(t).filter(e=>v(x(t,e)).isDirectory()&&/^\d{2}$/.test(e));for(let e of s){let s=x(t,e),c=_(s).filter(e=>v(x(s,e)).isDirectory()&&/^\d{2}$/.test(e));for(let t of c){let c=x(s,t),l=`${o}-${e}-${t}`,u=new Date(Number(o),Number(e)-1,Number(t));if(!(a&&u.getTime()<a.getTime()))for(let a of i){let i=O[a].mainFile,s=S(c,i);if(m(s))try{let{data:r}=C(g(s,`utf-8`)),d=r,f=d.title||`Untitled (${l})`;n.push({contentType:a,path:s,folderPath:c,date:u,title:f,relativePath:`${o}/${e}/${t}/${i}`,frontmatter:d})}catch(e){r.warn(`Error reading ${s}: ${e}`)}}}}}}catch(e){return r.error(`Error reading directories: ${e}`),[]}return n.sort((e,t)=>F(e,t,o))}function L(e){switch(e){case`linkedin`:return`linkedin-posts.md`;case`x`:return`x-posts.md`;case`youtube`:return`youtube-videos.md`;case`instagram`:return`instagram-posts.md`}}function R(e){switch(e){case`linkedin`:return`LinkedIn Posts`;case`x`:return`X Posts`;case`youtube`:return`YouTube Videos`;case`instagram`:return`Instagram Posts`}}function z(e,t){let n=I(e,{contentTypes:[t],sort:`date-desc`});if(n.length===0){r.info(`No ${t} posts found`);return}let i=R(t),a=L(t),o=`# ${i}\n\n`;o+=`_Generated on ${new Date().toLocaleDateString()}_\n\n`,o+=`Total posts: ${n.length}\n\n`;for(let e of n){let t=e.date.toLocaleDateString(`en-US`,{year:`numeric`,month:`long`,day:`numeric`});o+=`- [${e.title}](${e.relativePath}) - _${t}_\n`}y(x(e,a),o,`utf-8`),r.success(`Created ${a} with ${n.length} posts`)}function B(t=e.cwd(),n){let r=n||[...N];for(let e of r)z(t,e)}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 H(t,n,i=e.cwd()){let a=V(t),o=x(S(x(i,`resources`)),a),s=`${n}.md`,c=x(o,s);if(m(c))return r.info(`${s} already exists at: ${c}`),o;h(o,{recursive:!0}),r.success(`Created directory: ${o}`);let l={title:t,url:``,date:``};return y(c,C.stringify(``,l),`utf-8`),r.success(`Created ${s} at: ${c}`),o}const U=`load-more`;async function de(){let t=new Date,i=7;for(;;){let a=await o({message:`When do you want to create content?`,options:fe(t,i)});if(n(a)&&(r.error(`Operation cancelled.`),e.exit(0)),a===`load-more`){i+=7;continue}return d(a)}}function fe(e,t){let n=[];for(let r=0;r<t;r++){let t=l(e,r),i=u(t,`yyyy-MM-dd`),a=u(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:`load-more`,hint:`Extend the list up to ${u(l(e,t+6),`yyyy-MM-dd`)}`}),n}async function W(){let t=await s({message:`What is the title of your content?`,placeholder:`Enter content title`,validate:e=>{if(!e||e.trim().length===0)return`Title is required`}});return n(t)&&(r.error(`Operation cancelled.`),e.exit(0)),t.trim()}async function pe(){let t=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 n(t)&&(r.error(`Operation cancelled.`),e.exit(0)),t}async function me(t){if(t.length===0)return;let i=await o({message:`What is the content theme? (optional)`,options:[{label:`No theme`,value:``,hint:`Leave theme empty`},...t.map(e=>({label:e,value:e,hint:`Use theme: ${e}`}))]});n(i)&&(r.error(`Operation cancelled.`),e.exit(0));let a=i;return a.length>0?a:void 0}async function he(){let t=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 n(t)&&(r.error(`Operation cancelled.`),e.exit(0)),t}async function ge(){let t=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 n(t)&&(r.error(`Operation cancelled.`),e.exit(0)),t}async function _e(e){return G({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 ve(e){return G({basePath:e,contentTypes:[`linkedin`],message:`Which LinkedIn publication do you want to link images for?`,emptyMessage:`No LinkedIn publications found in dated folders`,sort:`date-desc`})}async function ye(e){return G({basePath:e,contentTypes:[...N],message:`Which publication do you want to mark as ready?`,emptyMessage:`No publications found in dated folders`,sort:`date-desc`})}async function G(t){let i=I(t.basePath,{contentTypes:t.contentTypes,sort:t.sort});i.length===0&&(r.error(t.emptyMessage),e.exit(1));let a=20;for(;;){let s=await o({message:t.message,options:be(i,a)});if(n(s)&&(r.error(`Operation cancelled.`),e.exit(0)),s===U){a+=20;continue}let c=i.find(e=>e.path===s);return c||(r.error(`Unable to resolve selected publication: ${s}`),e.exit(1)),c}}function be(e,t){let n=e.slice(0,t).map(e=>xe(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:U,hint:`Extend the list to ${e[r-1]?.relativePath}`}]}function xe(e){return{label:`${u(e.date,`yyyy-MM-dd`)} · ${P[e.contentType]} · ${e.title}`,value:e.path,hint:e.relativePath}}function Se(e){let t=[`.jpg`,`.jpeg`,`.png`],n=[];try{let r=_(e);for(let i of r)if(v(x(e,i)).isFile()){let e=i.toLowerCase().substring(i.lastIndexOf(`.`));t.includes(e)&&n.push(i)}}catch(e){r.error(`Error reading directory: ${e}`)}return n.sort()}function K(e,t){try{let{data:n,content:i}=C(g(e.path,`utf-8`));n.images=t.length>0?t:[null];let a=C.stringify(i,n);y(e.path,a,`utf-8`),r.success(`LinkedIn publication updated: ${e.relativePath} (${t.length} image(s))`)}catch(e){r.error(`Error updating frontmatter: ${e}`)}}async function Ce(t=e.cwd()){let n=await ve(t),i=Se(n.folderPath);if(i.length===0){r.info(`No images found for LinkedIn publication: ${n.relativePath}`),K(n,[]);return}r.info(`Found ${i.length} image(s) for ${n.relativePath}: ${i.join(`, `)}`),K(n,i)}function we(e){let t=new Date;t.setHours(0,0,0,0);let n=I(e,{fromDate:t,sort:`date-asc`});if(n.length===0){r.info(`No upcoming publications found from today onward.`);return}r.info(`Found ${n.length} upcoming publication${n.length===1?``:`s`}:`),console.log(``);for(let e of n){let t=u(e.date,`EEEE, MMMM d, yyyy`),n=u(e.date,`yyyy-MM-dd`);console.log(`${t} (${n}) · ${P[e.contentType]} · ${e.title}`),console.log(e.path),console.log(``)}}function Te(t){let n=t.path;try{let{data:e,content:i}=C(g(n,`utf-8`));if(e.ready===!0){r.warn(`This publication is already marked as ready.`);return}let a={...e,ready:!0};y(n,C.stringify(i,a),`utf-8`),r.success(`Publication marked as ready: ${t.relativePath}`)}catch(t){r.error(`Failed to mark publication as ready: ${t instanceof Error?t.message:String(t)}`),e.exit(1)}}const q=E(new URL(`../../`,import.meta.url)),J=`content-creation.ics`;function Y(e){return e.replace(/\\/g,`\\\\`).replace(/\r\n|\r|\n/g,`\\n`).replace(/;/g,`\\;`).replace(/,/g,`\\,`)}function Ee(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
3
+ `)}function De(e){return e.toISOString().replace(/[-:]/g,``).replace(/\.\d{3}Z$/,`Z`)}function X(e){return u(e,`yyyyMMdd`)}function Z(e){return e.split(`/`).map(e=>encodeURIComponent(e)).join(`/`)}function Oe(e){return`https://github.com/barbapapazes/content-creation/blob/main/${Z(e)}`}function Q(e){let t=Z(e);return`vscode://file${t.startsWith(`/`)?``:`/`}${t}`}function ke(e){let t=[`GitHub: ${Oe(e.relativePath)}`,`VS Code: ${Q(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(`
4
+ `)}function Ae(e){return`${e.ready?`Ready`:`Not ready`} · ${e.title} · ${P.linkedin}`}function je(e){return`linkedin-${T(`sha1`).update(e).digest(`hex`)}@barbapapazes`}function Me(e){let t=[`BEGIN:VCALENDAR`,`VERSION:2.0`,`PRODID:-//Barbapapazes//Content Creation LinkedIn Calendar//EN`,`CALSCALE:GREGORIAN`,`METHOD:PUBLISH`,`X-WR-CALNAME:${Y(`content-creation.ics`)}`];for(let n of e){let e=De(new Date(Date.UTC(n.date.getFullYear(),n.date.getMonth(),n.date.getDate()))),r=l(n.date,1);t.push(`BEGIN:VEVENT`,`UID:${je(n.relativePath)}`,`DTSTAMP:${e}`,`SUMMARY:${Y(Ae(n))}`,`DTSTART;VALUE=DATE:${X(n.date)}`,`DTEND;VALUE=DATE:${X(r)}`,`TRANSP:TRANSPARENT`,`DESCRIPTION:${Y(ke(n))}`,`END:VEVENT`)}return t.push(`END:VCALENDAR`),`${t.map(Ee).join(`\r
5
+ `)}\r\n`}function Ne(){let t=x(q,`node_modules`,`.bin`,e.platform===`win32`?`wrangler.cmd`:`wrangler`);return m(t)?t:`wrangler`}function Pe(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(J,n);return i.searchParams.set(`token`,t.calendar.token),i.toString()}function Fe(e){return I(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 Ie(t){let n=Ne();try{w(n,[`r2`,`object`,`put`,`content-creation/${J}`,`--pipe`,`--content-type`,`text/calendar; charset=utf-8`,`--remote`],{cwd:q,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 Le(e,t){let n=Fe(e);if(n.length===0)return r.info(`No LinkedIn publications found. Skipping calendar upload.`),null;let i=Pe(t);return Ie(Me(n)),r.success(`✓ LinkedIn calendar uploaded to R2 as ${J}`),r.info(`Subscription URL: ${i}`),i}async function Re(t,n){let i=t.path,{data:a,content:o}=C(g(i,`utf-8`));a.scheduled===!0&&(r.warn(`This X publication is already scheduled. Skipping.`),e.exit(0)),(!n.scheduling.automationEndpoint||!n.scheduling.cfAccessClientId||!n.scheduling.cfAccessClientSecret)&&(r.error(`Scheduling configuration is missing. Please set AUTOMATION_ENDPOINT, CF_ACCESS_CLIENT_ID, and CF_ACCESS_CLIENT_SECRET in your .env file or config.`),e.exit(1));let s=new Date(t.date);s=f(s,11),s=ee(s,0),s=p(s,0);let c=s.getTime(),l={content:o.trim(),scheduleAt:c};try{let t=await fetch(n.scheduling.automationEndpoint,{method:`POST`,headers:{"Content-Type":`application/json`,Accept:`application/json`,"CF-Access-Client-Id":n.scheduling.cfAccessClientId,"CF-Access-Client-Secret":n.scheduling.cfAccessClientSecret},body:JSON.stringify(l)});if(!t.ok){let n=await t.text();r.error(`Failed to schedule reminder: ${t.status} ${t.statusText}\n${n}`),e.exit(1)}let s=await t.json(),c={...a,scheduled:!0};y(i,C.stringify(o,c),`utf-8`);let d=new Date(s.scheduledAt);r.success(`✓ X publication reminder scheduled`),r.info(`Reminder scheduled for: ${u(d,`yyyy-MM-dd HH:mm:ss`)} UTC`)}catch(t){r.error(`Failed to schedule X publication reminder: ${t instanceof Error?t.message:String(t)}`),e.exit(1)}}const $=c(`content-creation`);$.command(`[path]`,`Create a dated content directory with content files`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).action(async(n,r)=>{t(`Content Creation - Create dated content directory`);let i=await A(),o=await W(),s=await ge(),c={title:o};s.includes(`linkedin`)&&(c.hasVideo=await pe(),c.hasImages=!c.hasVideo,i.thematic.length>0&&(c.theme=await me(i.thematic)));let l=await de();oe(l,s,c,i,n||r?.path||e.cwd());let d=s.join(`, `);a(`✓ Content directory created: ${u(l,`yyyy/MM/dd`)} (${d})`)}),$.command(`link-images [path]`,`Link image files to a LinkedIn publication`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).action(async(n,r)=>{t(`Content Creation - Link Images to LinkedIn Publication`),await Ce(n||r?.path||e.cwd()),a(`✓ LinkedIn publication images linked`)}),$.command(`resource [path]`,`Create a resource with article/video/audio markdown file`).option(`--path <path>`,`Base path for resource directory (defaults to current directory)`).action(async(n,r)=>{t(`Content Creation - Create Resource`);let i=await W(),o=await he();H(i,o,n||r?.path||e.cwd()),a(`✓ Resource created: resources/${V(i)}/${o}.md`)}),$.command(`create-index [path]`,`Create index files for each content type`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).action(async(n,r)=>{t(`Content Creation - Create Index Files`),B(n||r?.path||e.cwd()),a(`✓ Index files generated successfully`)}),$.command(`list-upcoming [path]`,`List publications scheduled for today or later`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).action(async(n,r)=>{t(`Content Creation - List Upcoming Publications`),we(n||r?.path||e.cwd()),a(`✓ Upcoming publications listed`)}),$.command(`publish-linkedin-calendar [path]`,`Create and upload a LinkedIn publication calendar`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).action(async(n,r)=>{t(`Content Creation - Publish LinkedIn Calendar`);let i=await A();if(await Le(n||r?.path||e.cwd(),i)){a(`✓ LinkedIn calendar published successfully`);return}a(`✓ No LinkedIn publications found, calendar was not updated`)}),$.command(`schedule-reminder [path]`,`Schedule a reminder for an X publication`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).action(async(n,r)=>{t(`Content Creation - Schedule X Publication Reminder`);let i=await A();await Re(await _e(n||r?.path||e.cwd()),i),a(`✓ X publication reminder scheduled`)}),$.command(`ready [path]`,`Mark a publication as ready`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).action(async(n,r)=>{t(`Content Creation - Mark Publication Ready`),Te(await ye(n||r?.path||e.cwd())),a(`✓ Publication marked as ready`)}),$.help(),$.version(D),$.parse();export{};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@barbapapazes/content-creation",
3
3
  "type": "module",
4
- "version": "0.18.4",
4
+ "version": "0.18.6",
5
5
  "author": "Estéban Soubiran <esteban@soubiran.dev>",
6
6
  "license": "MIT",
7
7
  "funding": "https://github.com/sponsors/Barbapapazes",