@barbapapazes/content-creation 0.18.7 → 0.18.9
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 +30 -0
- package/dist/cli.mjs +5 -5
- package/dist/index.d.mts +14 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ A CLI tool to streamline multi-platform content creation by generating dated dir
|
|
|
12
12
|
- Never overwrites existing files (skip if exists behavior)
|
|
13
13
|
- Generate separate index files per content type
|
|
14
14
|
- Manage article series with generated README indexes and ordered inserts
|
|
15
|
+
- Estimate series article reading time and write it to frontmatter
|
|
15
16
|
- List upcoming content from today onward with absolute file paths
|
|
16
17
|
- Generate and upload a LinkedIn publication calendar for Google Calendar subscriptions
|
|
17
18
|
- Link images to LinkedIn post frontmatter
|
|
@@ -174,6 +175,30 @@ This command will:
|
|
|
174
175
|
|
|
175
176
|
Generated article files follow the pattern `<index>.<slug>.md`.
|
|
176
177
|
|
|
178
|
+
#### Estimate a series article reading time
|
|
179
|
+
|
|
180
|
+
Compute the reading duration of a numbered series article and store it in the `time` frontmatter field.
|
|
181
|
+
|
|
182
|
+
The estimate ignores frontmatter and HTML comments, assumes 5 characters per word, and uses `100` words per minute by default.
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
# Interactive mode
|
|
186
|
+
content-creation series estimate-time
|
|
187
|
+
|
|
188
|
+
# Fully scripted mode
|
|
189
|
+
content-creation series estimate-time \
|
|
190
|
+
--series my-series-slug \
|
|
191
|
+
--file 6.what-tool-calling-really-changes.md \
|
|
192
|
+
--wpm 120
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
This command will:
|
|
196
|
+
|
|
197
|
+
1. prompt you for the series and the article file when needed;
|
|
198
|
+
2. estimate the reading time in whole minutes from the markdown body only;
|
|
199
|
+
3. ignore frontmatter and HTML comments during the calculation;
|
|
200
|
+
4. write the resulting value into `time` in the article frontmatter.
|
|
201
|
+
|
|
177
202
|
### Publish LinkedIn Calendar
|
|
178
203
|
|
|
179
204
|
Generate a Google Calendar-compatible `.ics` file for LinkedIn publications, upload it to Cloudflare R2 using Wrangler, and print the subscription URL:
|
|
@@ -331,6 +356,11 @@ export default {
|
|
|
331
356
|
publicUrl: 'https://calendar.soubiran.dev',
|
|
332
357
|
token: 'your-calendar-token',
|
|
333
358
|
},
|
|
359
|
+
|
|
360
|
+
// Reading-time configuration (optional)
|
|
361
|
+
reading: {
|
|
362
|
+
wordsPerMinute: 100,
|
|
363
|
+
},
|
|
334
364
|
}
|
|
335
365
|
```
|
|
336
366
|
|
package/dist/cli.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{cac as e}from"cac";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{addDays as c,format as l,parseISO as u,setHours as d,setMinutes as f,setSeconds as ee}from"date-fns";import{existsSync as p,mkdirSync as m,readFileSync as h,readdirSync as g,renameSync as te,statSync as _,writeFileSync as v}from"node:fs";import{dirname as y,isAbsolute as ne,join as b,resolve as x}from"node:path";import S from"node:process";import C from"gray-matter";import{loadConfig as re}from"c12";import{execFileSync as ie}from"node:child_process";import{createHash as ae}from"node:crypto";import{fileURLToPath as oe}from"node:url";var se=`0.18.
|
|
2
|
+
import{cac as e}from"cac";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{addDays as c,format as l,parseISO as u,setHours as d,setMinutes as f,setSeconds as ee}from"date-fns";import{existsSync as p,mkdirSync as m,readFileSync as h,readdirSync as g,renameSync as te,statSync as _,writeFileSync as v}from"node:fs";import{dirname as y,isAbsolute as ne,join as b,resolve as x}from"node:path";import S from"node:process";import C from"gray-matter";import{loadConfig as re}from"c12";import{execFileSync as ie}from"node:child_process";import{createHash as ae}from"node:crypto";import{fileURLToPath as oe}from"node:url";var se=`0.18.9`;const w={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`]}},T={thematic:[],templatesDir:void 0,templates:{},scheduling:{},calendar:{},reading:{}};function ce(e,t){return e?ne(e)?e:x(t?y(t):S.cwd(),e):t?y(t):S.cwd()}function le(e,t){let n={};if(e?.footerPath){let r=x(t,e.footerPath);p(r)&&(n.footerPath=r)}return n}function E(e,t){let n={};if(e?.templatePath){let r=x(t,e.templatePath);p(r)&&(n.templatePath=r)}return n}async function D(){let{config:e,configFile:t}=await re({name:`content-creation`,defaults:T,globalRc:!0,dotenv:!0}),n=ce(e.templatesDir,t),r={linkedin:le(e.templates?.linkedin,n),youtube:E(e.templates?.youtube,n),instagram:E(e.templates?.instagram,n)},i={automationEndpoint:e.scheduling?.automationEndpoint||S.env.AUTOMATION_ENDPOINT,cfAccessClientId:e.scheduling?.cfAccessClientId||S.env.CF_ACCESS_CLIENT_ID,cfAccessClientSecret:e.scheduling?.cfAccessClientSecret||S.env.CF_ACCESS_CLIENT_SECRET},a={publicUrl:e.calendar?.publicUrl||S.env.CALENDAR_PUBLIC_URL,token:e.calendar?.token||S.env.CALENDAR_TOKEN},o={wordsPerMinute:e.reading?.wordsPerMinute||T.reading?.wordsPerMinute||100};return{thematic:e.thematic||[],templatesDir:e.templatesDir||``,templates:r,scheduling:i,calendar:a,reading:o}}function O(e){return p(e)?h(e,`utf-8`):``}function ue(e,t,n,i,a=S.cwd()){let o=x(b(a,l(e,`yyyy`),l(e,`MM`),l(e,`dd`)));p(o)||(m(o,{recursive:!0}),r.success(`Created directory: ${o}`));let s=de(n);for(let e of t)fe(e,o,s,i);return o}function de(e){return e.hasVideo?{...e,hasImages:!1}:{...e,hasImages:!0}}function fe(e,t,n,i){let a=w[e],o=b(t,a.mainFile);if(p(o)?r.info(`${a.mainFile} already exists, skipping`):pe(e,o,n,i),a.additionalFiles)for(let o of a.additionalFiles){let a=b(t,o);he(o,e,n)&&(p(a)?r.info(`${o} already exists, skipping`):me(e,a,o,i))}}function pe(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?O(e):``}v(t,C.stringify(o,a),`utf-8`),r.success(`Created ${t}`)}function me(e,t,n,i){let a=``;if(n.endsWith(`-description.md`)){let t=i.templates[e]?.templatePath;t&&(a=O(t))}v(t,a,`utf-8`),r.success(`Created ${t}`)}function he(e,t,n){return t===`linkedin`&&e===`linkedin-script.md`?n.hasVideo||!1:!0}const k=[`linkedin`,`x`,`youtube`,`instagram`],A={linkedin:`LinkedIn`,x:`X`,youtube:`YouTube`,instagram:`Instagram`};function ge(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=k.indexOf(e.contentType)-k.indexOf(t.contentType);return i===0?e.relativePath.localeCompare(t.relativePath):i}function j(e,t={}){let n=[],i=t.contentTypes?.length?t.contentTypes:[...k],a=t.fromDate,o=t.sort||`date-desc`;try{let t=g(e).filter(t=>_(b(e,t)).isDirectory()&&/^\d{4}$/.test(t));for(let o of t){let t=b(e,o),s=g(t).filter(e=>_(b(t,e)).isDirectory()&&/^\d{2}$/.test(e));for(let e of s){let s=b(t,e),c=g(s).filter(e=>_(b(s,e)).isDirectory()&&/^\d{2}$/.test(e));for(let t of c){let c=b(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=w[a].mainFile,s=x(c,i);if(p(s))try{let{data:r}=C(h(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)=>ge(e,t,o))}const M=`## Index`,N=`<!-- content-creation:series-index:start -->`,P=`<!-- content-creation:series-index:end -->`;function _e(e){return x(b(e,`series`))}function F(e){let t=_e(e);return p(t)?g(t).filter(e=>_(b(t,e)).isDirectory()).map(e=>{let n=b(t,e);return{name:e,path:n,title:be(n,e)}}).sort((e,t)=>e.title.localeCompare(t.title,`fr`)):(r.error(`Series directory not found: ${t}`),[])}function I(e,t){return F(e).find(e=>e.name===t)}function L(e){return g(e).map(e=>Se(e)).filter(e=>e!==null).map(t=>({...t,path:b(e,t.fileName),title:xe(b(e,t.fileName),t.slug)})).sort((e,t)=>e.index-t.index)}function R(e,t){return L(e).find(e=>e.fileName===t)}function z(e){let t=L(e),n=b(e,`README.md`);v(n,ve(p(n)?h(n,`utf-8`):``,t,e),`utf-8`),r.success(`Generated series index: ${n}`)}function ve(e,t,n){let r=ye(t);if(e.includes(N)&&e.includes(P))return e.replace(RegExp(`${V(M)}\\n\\n${V(N)}[\\s\\S]*?${V(P)}`),r);let i=e.trimEnd();return i.length>0?`${i}\n\n${r}\n`:`# ${B(n.split(`/`).pop()||`series`)}\n\n${r}\n`}function ye(e){let t=[M,``,N];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(``,P),t.join(`
|
|
3
3
|
`)}function be(e,t){let n=b(e,`README.md`);if(p(n)){let e=h(n,`utf-8`).split(`
|
|
4
|
-
`).map(e=>e.trim()).find(e=>e.startsWith(`# `));if(e)return e.slice(2).trim()}return
|
|
5
|
-
`)}function Ke(e){return e.toISOString().replace(/[-:]/g,``).replace(/\.\d{3}Z$/,`Z`)}function
|
|
6
|
-
`)}function
|
|
7
|
-
`)}\r\n`}function $e(){let e=b(K,`node_modules`,`.bin`,S.platform===`win32`?`wrangler.cmd`:`wrangler`);return p(e)?e:`wrangler`}function et(e){(!e.calendar.publicUrl||!e.calendar.token)&&(r.error(`Calendar publishing configuration is missing. Please set CALENDAR_PUBLIC_URL and CALENDAR_TOKEN in your .env file or config.`),S.exit(1));let t=e.calendar.publicUrl.endsWith(`/`)?e.calendar.publicUrl:`${e.calendar.publicUrl}/`,n=new URL(q,t);return n.searchParams.set(`token`,e.calendar.token),n.toString()}function tt(e){return j(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 nt(e){let t=$e();try{ie(t,[`r2`,`object`,`put`,`content-creation/${q}`,`--pipe`,`--content-type`,`text/calendar; charset=utf-8`,`--remote`],{cwd:K,stdio:[`pipe`,`inherit`,`inherit`],env:S.env,input:e})}catch(e){r.error(`Failed to upload LinkedIn calendar: ${e instanceof Error?e.message:String(e)}`),S.exit(1)}}async function rt(e,t){let n=tt(e);if(n.length===0)return r.info(`No LinkedIn publications found. Skipping calendar upload.`),null;let i=et(t);return nt(Qe(n)),r.success(`✓ LinkedIn calendar uploaded to R2 as ${q}`),r.info(`Subscription URL: ${i}`),i}async function it(e,n){t(`Content Creation - Link Images to LinkedIn Publication`),await We(W(e,n)),a(`✓ LinkedIn publication images linked`)}async function at(e,n){t(`Content Creation - Publish LinkedIn Calendar`);let r=await D();if(await rt(W(e,n),r)){a(`✓ LinkedIn calendar published successfully`);return}a(`✓ No LinkedIn publications found, calendar was not updated`)}async function ot(e,t,n){if(e===`link-images`){await it(t,n);return}if(e===`publish-calendar`){await at(t,n);return}throw Error(`Unknown linkedin action: ${e}. Supported actions are: link-images, publish-calendar`)}function st(e){let t=new Date;t.setHours(0,0,0,0);let n=j(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=l(e.date,`EEEE, MMMM d, yyyy`),n=l(e.date,`yyyy-MM-dd`);console.log(`${t} (${n}) · ${A[e.contentType]} · ${e.title}`),console.log(e.path),console.log(``)}}function ct(e){let t=e.path;try{let{data:n,content:i}=C(h(t,`utf-8`));if(n.ready===!0){r.warn(`This publication is already marked as ready.`);return}let a={...n,ready:!0};v(t,C.stringify(i,a),`utf-8`),r.success(`Publication marked as ready: ${e.relativePath}`)}catch(e){r.error(`Failed to mark publication as ready: ${e instanceof Error?e.message:String(e)}`),S.exit(1)}}function lt(e,n){t(`Content Creation - List Upcoming Publications`),st(W(e,n)),a(`✓ Upcoming publications listed`)}async function ut(e,n){t(`Content Creation - Mark Publication Ready`),ct(await je(W(e,n))),a(`✓ Publication marked as ready`)}async function dt(e,t,n){if(e===`list-upcoming`){lt(t,n);return}if(e===`ready`){await ut(t,n);return}throw Error(`Unknown publication action: ${e}. Supported actions are: list-upcoming, ready`)}function Z(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 ft(e,t,n=S.cwd()){let i=Z(e),a=b(x(b(n,`resources`)),i),o=`${t}.md`,s=b(a,o);if(p(s))return r.info(`${o} already exists at: ${s}`),a;m(a,{recursive:!0}),r.success(`Created directory: ${a}`);let c={title:e,url:``,date:``};return v(s,C.stringify(``,c),`utf-8`),r.success(`Created ${o} at: ${s}`),a}async function pt(e,n){t(`Content Creation - Create Resource`);let r=await H(),i=await De();ft(r,i,W(e,n)),a(`✓ Resource created: resources/${Z(r)}/${i}.md`)}async function Q(e,t,n){if(e===`create`){await pt(t,n);return}throw Error(`Unknown resource action: ${e}. Supported actions are: create`)}function mt(e,t){let n=F(e);if(n.length===0)return;let r=t?n.filter(e=>e.name===t):n;if(r.length===0)throw Error(`Series not found: ${t}`);for(let e of r)R(e.path)}function ht(e,t,n){let i=L(e),a=_t(n,i.length),o=b(e,`${a}.${Z(t)}.md`);if(p(o))throw Error(`Article already exists at ${o}`);return gt(e,i,a),v(o,C.stringify(``,{title:t}),`utf-8`),r.success(`Created article: ${o}`),R(e),o}function gt(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=b(e,n);te(t.path,i),r.info(`Renamed ${t.fileName} → ${n}`)}}function _t(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}async function vt(e,n,r){let i=W(n,r);if(e===`create-index`){t(`Content Creation - Create Series Index`),mt(i,r?.series),a(r?.series?`✓ Series index generated for ${r.series}`:`✓ Series indexes generated successfully`);return}if(e===`introduce`){t(`Content Creation - Introduce Series Article`);let e=r?.title||await H(),n=r?.series?I(i,r.series):await Pe(i);if(!n)throw Error(`Series not found: ${r?.series}`);let o=r?.position?Number(r.position):await Fe(n.path);ht(n.path,e,o),a(`✓ Series article created: ${n.name}/${o}.${Z(e)}.md`);return}throw Error(`Unknown series action: ${e}. Supported actions are: create-index, introduce`)}async function yt(e,t){let n=e.path,{data:i,content:a}=C(h(n,`utf-8`));i.scheduled===!0&&(r.warn(`This X publication is already scheduled. Skipping.`),S.exit(0)),(!t.scheduling.automationEndpoint||!t.scheduling.cfAccessClientId||!t.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.`),S.exit(1));let o=new Date(e.date);o=d(o,11),o=f(o,0),o=ee(o,0);let s=o.getTime(),c={content:a.trim(),scheduleAt:s};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(c)});if(!e.ok){let t=await e.text();r.error(`Failed to schedule reminder: ${e.status} ${e.statusText}\n${t}`),S.exit(1)}let o=await e.json(),s={...i,scheduled:!0};v(n,C.stringify(a,s),`utf-8`);let u=new Date(o.scheduledAt);r.success(`✓ X publication reminder scheduled`),r.info(`Reminder scheduled for: ${l(u,`yyyy-MM-dd HH:mm:ss`)} UTC`)}catch(e){r.error(`Failed to schedule X publication reminder: ${e instanceof Error?e.message:String(e)}`),S.exit(1)}}async function bt(e,n){t(`Content Creation - Schedule X Publication Reminder`);let r=await D();await yt(await ke(W(e,n)),r),a(`✓ X publication reminder scheduled`)}async function xt(e,t,n){if(e===`schedule-reminder`){await bt(t,n);return}throw Error(`Unknown x action: ${e}. Supported actions are: schedule-reminder`)}const $=e(`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(Ie),$.command(`resource <action> [path]`,`Manage resource markdown files`).option(`--path <path>`,`Base path for resource directory (defaults to current directory)`).action(Q),$.command(`series <action> [path]`,`Manage article series and generated indexes`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).option(`--series <series>`,`Series folder name inside the series directory`).option(`--title <title>`,`Title of the new series article`).option(`--position <position>`,`Insert position in the series index (1-based)`).action(vt),$.command(`index <action> [path]`,`Manage generated indexes`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).action(He),$.command(`linkedin <action> [path]`,`Manage LinkedIn-specific workflows`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).action(ot),$.command(`x <action> [path]`,`Manage X-specific workflows`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).action(xt),$.command(`publication <action> [path]`,`Manage publication workflows`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).action(dt),$.help(),$.version(se),$.parse();export{};
|
|
4
|
+
`).map(e=>e.trim()).find(e=>e.startsWith(`# `));if(e)return e.slice(2).trim()}return B(t)}function xe(e,t){try{let{data:t}=C(h(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 B(t)}function Se(e){let t=e.match(/^(\d+)\.([^.]+(?:\.[^.]+)*)\.md$/);return t?{index:Number(t[1]),slug:t[2],fileName:e}:null}function B(e){return e.split(`-`).filter(Boolean).map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join(` `)}function V(e){return e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`)}const H=`load-more`;async function Ce(){let e=new Date,t=7;for(;;){let i=await o({message:`When do you want to create content?`,options:we(e,t)});if(n(i)&&(r.error(`Operation cancelled.`),S.exit(0)),i===`load-more`){t+=7;continue}return u(i)}}function we(e,t){let n=[];for(let r=0;r<t;r++){let t=c(e,r),i=l(t,`yyyy-MM-dd`),a=l(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 ${l(c(e,t+6),`yyyy-MM-dd`)}`}),n}async function U(){let e=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(e)&&(r.error(`Operation cancelled.`),S.exit(0)),e.trim()}async function Te(){let e=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(e)&&(r.error(`Operation cancelled.`),S.exit(0)),e}async function Ee(e){if(e.length===0)return;let t=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}`}))]});n(t)&&(r.error(`Operation cancelled.`),S.exit(0));let i=t;return i.length>0?i:void 0}async function De(){let e=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(e)&&(r.error(`Operation cancelled.`),S.exit(0)),e}async function Oe(){let e=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(e)&&(r.error(`Operation cancelled.`),S.exit(0)),e}async function ke(e){return W({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 Ae(e){return W({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 je(e){return W({basePath:e,contentTypes:[...k],message:`Which publication do you want to mark as ready?`,emptyMessage:`No publications found in dated folders`,sort:`date-desc`})}async function W(e){let t=j(e.basePath,{contentTypes:e.contentTypes,sort:e.sort});t.length===0&&(r.error(e.emptyMessage),S.exit(1));let i=20;for(;;){let a=await o({message:e.message,options:Me(t,i)});if(n(a)&&(r.error(`Operation cancelled.`),S.exit(0)),a===H){i+=20;continue}let s=t.find(e=>e.path===a);return s||(r.error(`Unable to resolve selected publication: ${a}`),S.exit(1)),s}}function Me(e,t){let n=e.slice(0,t).map(e=>Ne(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:H,hint:`Extend the list to ${e[r-1]?.relativePath}`}]}function Ne(e){return{label:`${l(e.date,`yyyy-MM-dd`)} · ${A[e.contentType]} · ${e.title}`,value:e.path,hint:e.relativePath}}async function G(e){let t=F(e);if(t.length===0&&(r.error(`No series directories found`),S.exit(1)),t.length===1)return t[0];let i=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}`}))});n(i)&&(r.error(`Operation cancelled.`),S.exit(0));let a=t.find(e=>e.path===i);return a||(r.error(`Unable to resolve selected series: ${i}`),S.exit(1)),a}async function Pe(e){let t=L(e),i=t.map(e=>({label:`Insert at #${e.index} · before ${e.title}`,value:String(e.index),hint:e.fileName}));i.push({label:`Append as #${t.length+1}`,value:String(t.length+1),hint:`Add the new article at the end of the series`});let a=await o({message:`Where should the new article be inserted?`,options:i});return n(a)&&(r.error(`Operation cancelled.`),S.exit(0)),Number(a)}async function K(e){let t=L(e);if(t.length===0&&(r.error(`No series articles found`),S.exit(1)),t.length===1)return t[0];let i=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}))});n(i)&&(r.error(`Operation cancelled.`),S.exit(0));let a=t.find(e=>e.path===i);return a||(r.error(`Unable to resolve selected series article: ${i}`),S.exit(1)),a}function q(e,t){return e||t?.path||S.cwd()}async function Fe(e,n){t(`Content Creation - Create dated content directory`);let r=await D(),i=await U(),o=await Oe(),s={title:i};o.includes(`linkedin`)&&(s.hasVideo=await Te(),s.hasImages=!s.hasVideo,r.thematic.length>0&&(s.theme=await Ee(r.thematic)));let c=await Ce();ue(c,o,s,r,q(e,n));let u=o.join(`, `);a(`✓ Content directory created: ${l(c,`yyyy/MM/dd`)} (${u})`)}function Ie(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 Le(e){switch(e){case`linkedin`:return`LinkedIn Posts`;case`x`:return`X Posts`;case`youtube`:return`YouTube Videos`;case`instagram`:return`Instagram Posts`}}function Re(e,t){let n=j(e,{contentTypes:[t],sort:`date-desc`});if(n.length===0){r.info(`No ${t} posts found`);return}let i=Le(t),a=Ie(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 ze(e=S.cwd(),t){let n=t||[...k];for(let t of n)Re(e,t)}function Be(e,n){t(`Content Creation - Create Index Files`),ze(q(e,n)),a(`✓ Index files generated successfully`)}function Ve(e,t,n){if(e===`create`){Be(t,n);return}throw Error(`Unknown index action: ${e}. Supported actions are: create`)}function He(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 J(e,t){try{let{data:n,content:i}=C(h(e.path,`utf-8`));n.images=t.length>0?t:[null];let a=C.stringify(i,n);v(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 Ue(e=S.cwd()){let t=await Ae(e),n=He(t.folderPath);if(n.length===0){r.info(`No images found for LinkedIn publication: ${t.relativePath}`),J(t,[]);return}r.info(`Found ${n.length} image(s) for ${t.relativePath}: ${n.join(`, `)}`),J(t,n)}const We=oe(new URL(`../../`,import.meta.url)),Y=`content-creation.ics`;function X(e){return e.replace(/\\/g,`\\\\`).replace(/\r\n|\r|\n/g,`\\n`).replace(/;/g,`\\;`).replace(/,/g,`\\,`)}function Ge(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 Ke(e){return e.toISOString().replace(/[-:]/g,``).replace(/\.\d{3}Z$/,`Z`)}function qe(e){return l(e,`yyyyMMdd`)}function Je(e){return e.split(`/`).map(e=>encodeURIComponent(e)).join(`/`)}function Ye(e){return`https://github.com/barbapapazes/content-creation/blob/main/${Je(e)}`}function Xe(e){let t=Je(e);return`vscode://file${t.startsWith(`/`)?``:`/`}${t}`}function Ze(e){let t=[`GitHub: ${Ye(e.relativePath)}`,`VS Code: ${Xe(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 Qe(e){return`${e.ready?`Ready`:`Not ready`} · ${e.title} · ${A.linkedin}`}function $e(e){return`linkedin-${ae(`sha1`).update(e).digest(`hex`)}@barbapapazes`}function et(e){let t=[`BEGIN:VCALENDAR`,`VERSION:2.0`,`PRODID:-//Barbapapazes//Content Creation LinkedIn Calendar//EN`,`CALSCALE:GREGORIAN`,`METHOD:PUBLISH`,`X-WR-CALNAME:${X(`content-creation.ics`)}`];for(let n of e){let e=Ke(new Date(Date.UTC(n.date.getFullYear(),n.date.getMonth(),n.date.getDate()))),r=c(n.date,1);t.push(`BEGIN:VEVENT`,`UID:${$e(n.relativePath)}`,`DTSTAMP:${e}`,`SUMMARY:${X(Qe(n))}`,`DTSTART;VALUE=DATE:${qe(n.date)}`,`DTEND;VALUE=DATE:${qe(r)}`,`TRANSP:TRANSPARENT`,`DESCRIPTION:${X(Ze(n))}`,`END:VEVENT`)}return t.push(`END:VCALENDAR`),`${t.map(Ge).join(`\r
|
|
7
|
+
`)}\r\n`}function tt(){let e=b(We,`node_modules`,`.bin`,S.platform===`win32`?`wrangler.cmd`:`wrangler`);return p(e)?e:`wrangler`}function nt(e){(!e.calendar.publicUrl||!e.calendar.token)&&(r.error(`Calendar publishing configuration is missing. Please set CALENDAR_PUBLIC_URL and CALENDAR_TOKEN in your .env file or config.`),S.exit(1));let t=e.calendar.publicUrl.endsWith(`/`)?e.calendar.publicUrl:`${e.calendar.publicUrl}/`,n=new URL(Y,t);return n.searchParams.set(`token`,e.calendar.token),n.toString()}function rt(e){return j(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 it(e){let t=tt();try{ie(t,[`r2`,`object`,`put`,`content-creation/${Y}`,`--pipe`,`--content-type`,`text/calendar; charset=utf-8`,`--remote`],{cwd:We,stdio:[`pipe`,`inherit`,`inherit`],env:S.env,input:e})}catch(e){r.error(`Failed to upload LinkedIn calendar: ${e instanceof Error?e.message:String(e)}`),S.exit(1)}}async function at(e,t){let n=rt(e);if(n.length===0)return r.info(`No LinkedIn publications found. Skipping calendar upload.`),null;let i=nt(t);return it(et(n)),r.success(`✓ LinkedIn calendar uploaded to R2 as ${Y}`),r.info(`Subscription URL: ${i}`),i}async function ot(e,n){t(`Content Creation - Link Images to LinkedIn Publication`),await Ue(q(e,n)),a(`✓ LinkedIn publication images linked`)}async function st(e,n){t(`Content Creation - Publish LinkedIn Calendar`);let r=await D();if(await at(q(e,n),r)){a(`✓ LinkedIn calendar published successfully`);return}a(`✓ No LinkedIn publications found, calendar was not updated`)}async function ct(e,t,n){if(e===`link-images`){await ot(t,n);return}if(e===`publish-calendar`){await st(t,n);return}throw Error(`Unknown linkedin action: ${e}. Supported actions are: link-images, publish-calendar`)}function lt(e){let t=new Date;t.setHours(0,0,0,0);let n=j(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=l(e.date,`EEEE, MMMM d, yyyy`),n=l(e.date,`yyyy-MM-dd`);console.log(`${t} (${n}) · ${A[e.contentType]} · ${e.title}`),console.log(e.path),console.log(``)}}const ut=/<!--([\s\S]*?)-->/g;function dt(e,t){try{let{data:n,content:i}=C(h(e,`utf-8`)),a=ft(i,t);return v(e,C.stringify(i,{...n,time:a}),`utf-8`),r.success(`Estimated time set to ${a} min: ${e}`),a}catch(e){r.error(`Failed to estimate series article time: ${e instanceof Error?e.message:String(e)}`),S.exit(1)}}function ft(e,t){if(!Number.isFinite(t)||t<=0)throw RangeError(`Words per minute must be a positive number`);let n=e.replace(ut,``).replace(/\s+/g,``);if(n.length===0)return 0;let r=n.length/5;return Math.ceil(r/t)}function pt(e){ht({path:e.path,relativePath:e.relativePath},`publication`)}function mt(e,t){return ht({path:e.path,relativePath:e.fileName},`series article`),dt(e.path,t)}function ht(e,t){let n=e.path;try{let{data:i,content:a}=C(h(n,`utf-8`));if(i.ready===!0){r.warn(`This ${t} is already marked as ready.`);return}let o={...i,ready:!0};v(n,C.stringify(a,o),`utf-8`),r.success(`${gt(t)} marked as ready: ${e.relativePath}`)}catch(e){r.error(`Failed to mark ${t} as ready: ${e instanceof Error?e.message:String(e)}`),S.exit(1)}}function gt(e){return e.charAt(0).toUpperCase()+e.slice(1)}function _t(e,n){t(`Content Creation - List Upcoming Publications`),lt(q(e,n)),a(`✓ Upcoming publications listed`)}async function vt(e,n){t(`Content Creation - Mark Publication Ready`),pt(await je(q(e,n))),a(`✓ Publication marked as ready`)}async function yt(e,t,n){if(e===`list-upcoming`){_t(t,n);return}if(e===`ready`){await vt(t,n);return}throw Error(`Unknown publication action: ${e}. Supported actions are: list-upcoming, ready`)}function Z(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 bt(e,t,n=S.cwd()){let i=Z(e),a=b(x(b(n,`resources`)),i),o=`${t}.md`,s=b(a,o);if(p(s))return r.info(`${o} already exists at: ${s}`),a;m(a,{recursive:!0}),r.success(`Created directory: ${a}`);let c={title:e,url:``,date:``};return v(s,C.stringify(``,c),`utf-8`),r.success(`Created ${o} at: ${s}`),a}async function xt(e,n){t(`Content Creation - Create Resource`);let r=await U(),i=await De();bt(r,i,q(e,n)),a(`✓ Resource created: resources/${Z(r)}/${i}.md`)}async function St(e,t,n){if(e===`create`){await xt(t,n);return}throw Error(`Unknown resource action: ${e}. Supported actions are: create`)}function Ct(e,t){let n=F(e);if(n.length===0)return;let r=t?n.filter(e=>e.name===t):n;if(r.length===0)throw Error(`Series not found: ${t}`);for(let e of r)z(e.path)}function wt(e,t,n){let i=L(e),a=Et(n,i.length),o=b(e,`${a}.${Z(t)}.md`);if(p(o))throw Error(`Article already exists at ${o}`);return Tt(e,i,a),v(o,C.stringify(``,{title:t}),`utf-8`),r.success(`Created article: ${o}`),z(e),o}function Tt(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=b(e,n);te(t.path,i),r.info(`Renamed ${t.fileName} → ${n}`)}}function Et(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}async function Dt(e,n,r){let i=q(n,r);if(e===`create-index`){t(`Content Creation - Create Series Index`),Ct(i,r?.series),a(r?.series?`✓ Series index generated for ${r.series}`:`✓ Series indexes generated successfully`);return}if(e===`introduce`){t(`Content Creation - Introduce Series Article`);let e=r?.title||await U(),n=r?.series?I(i,r.series):await G(i);if(!n)throw Error(`Series not found: ${r?.series}`);let o=r?.position?Number(r.position):await Pe(n.path);wt(n.path,e,o),a(`✓ Series article created: ${n.name}/${o}.${Z(e)}.md`);return}if(e===`estimate-time`){t(`Content Creation - Estimate Series Article Time`);let e=await D(),n=r?.series?I(i,r.series):await G(i);if(!n)throw Error(`Series not found: ${r?.series}`);let o=r?.file?R(n.path,r.file):await K(n.path);if(!o)throw Error(`Series article not found: ${r?.file}`);let s=Q(r?.wpm,e.reading.wordsPerMinute),c=dt(o.path,s);a(`✓ Estimated time updated for ${n.name}/${o.fileName}: ${c} min`);return}if(e===`ready`){t(`Content Creation - Mark Series Article Ready`);let e=await D(),n=r?.series?I(i,r.series):await G(i);if(!n)throw Error(`Series not found: ${r?.series}`);let o=r?.file?R(n.path,r.file):await K(n.path);if(!o)throw Error(`Series article not found: ${r?.file}`);let s=mt(o,Q(r?.wpm,e.reading.wordsPerMinute));a(`✓ Series article marked as ready and time updated for ${n.name}/${o.fileName}: ${s} min`);return}throw Error(`Unknown series action: ${e}. Supported actions are: create-index, introduce, estimate-time, ready`)}function Q(e,t){let n=e?Number(e):t??100;if(!Number.isFinite(n)||n<=0)throw RangeError(`Words per minute must be a positive number`);return n}async function Ot(e,t){let n=e.path,{data:i,content:a}=C(h(n,`utf-8`));i.scheduled===!0&&(r.warn(`This X publication is already scheduled. Skipping.`),S.exit(0)),(!t.scheduling.automationEndpoint||!t.scheduling.cfAccessClientId||!t.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.`),S.exit(1));let o=new Date(e.date);o=d(o,11),o=f(o,0),o=ee(o,0);let s=o.getTime(),c={content:a.trim(),scheduleAt:s};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(c)});if(!e.ok){let t=await e.text();r.error(`Failed to schedule reminder: ${e.status} ${e.statusText}\n${t}`),S.exit(1)}let o=await e.json(),s={...i,scheduled:!0};v(n,C.stringify(a,s),`utf-8`);let u=new Date(o.scheduledAt);r.success(`✓ X publication reminder scheduled`),r.info(`Reminder scheduled for: ${l(u,`yyyy-MM-dd HH:mm:ss`)} UTC`)}catch(e){r.error(`Failed to schedule X publication reminder: ${e instanceof Error?e.message:String(e)}`),S.exit(1)}}async function kt(e,n){t(`Content Creation - Schedule X Publication Reminder`);let r=await D();await Ot(await ke(q(e,n)),r),a(`✓ X publication reminder scheduled`)}async function At(e,t,n){if(e===`schedule-reminder`){await kt(t,n);return}throw Error(`Unknown x action: ${e}. Supported actions are: schedule-reminder`)}const $=e(`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(Fe),$.command(`resource <action> [path]`,`Manage resource markdown files`).option(`--path <path>`,`Base path for resource directory (defaults to current directory)`).action(St),$.command(`series <action> [path]`,`Manage article series and generated indexes`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).option(`--file <file>`,`Series article file name inside the selected series`).option(`--series <series>`,`Series folder name inside the series directory`).option(`--title <title>`,`Title of the new series article`).option(`--position <position>`,`Insert position in the series index (1-based)`).option(`--wpm <wpm>`,`Words per minute used to estimate reading time`).action(Dt),$.command(`index <action> [path]`,`Manage generated indexes`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).action(Ve),$.command(`linkedin <action> [path]`,`Manage LinkedIn-specific workflows`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).action(ct),$.command(`x <action> [path]`,`Manage X-specific workflows`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).action(At),$.command(`publication <action> [path]`,`Manage publication workflows`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).action(yt),$.help(),$.version(se),$.parse();export{};
|
package/dist/index.d.mts
CHANGED
|
@@ -48,6 +48,16 @@ interface CalendarPublishingConfig {
|
|
|
48
48
|
*/
|
|
49
49
|
token?: string;
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Reading-time estimation configuration.
|
|
53
|
+
*/
|
|
54
|
+
interface ReadingConfig {
|
|
55
|
+
/**
|
|
56
|
+
* Reading speed used when estimating content duration.
|
|
57
|
+
* @default 100
|
|
58
|
+
*/
|
|
59
|
+
wordsPerMinute?: number;
|
|
60
|
+
}
|
|
51
61
|
/**
|
|
52
62
|
* Configuration for content-creation
|
|
53
63
|
*/
|
|
@@ -77,6 +87,10 @@ interface Config {
|
|
|
77
87
|
* Calendar publishing configuration
|
|
78
88
|
*/
|
|
79
89
|
calendar?: CalendarPublishingConfig;
|
|
90
|
+
/**
|
|
91
|
+
* Reading-time configuration for content metadata helpers.
|
|
92
|
+
*/
|
|
93
|
+
reading?: ReadingConfig;
|
|
80
94
|
}
|
|
81
95
|
//#endregion
|
|
82
96
|
//#region src/index.d.ts
|
package/package.json
CHANGED