@barbapapazes/content-creation 0.18.6 → 0.18.8

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
@@ -11,6 +11,8 @@ A CLI tool to streamline multi-platform content creation by generating dated dir
11
11
  - Config-driven templates for each content type
12
12
  - Never overwrites existing files (skip if exists behavior)
13
13
  - Generate separate index files per content type
14
+ - Manage article series with generated README indexes and ordered inserts
15
+ - Estimate series article reading time and write it to frontmatter
14
16
  - List upcoming content from today onward with absolute file paths
15
17
  - Generate and upload a LinkedIn publication calendar for Google Calendar subscriptions
16
18
  - Link images to LinkedIn post frontmatter
@@ -25,6 +27,15 @@ npm install -g @barbapapazes/content-creation
25
27
 
26
28
  ## Usage
27
29
 
30
+ Several workflows are now grouped under domain-oriented commands for better discoverability:
31
+
32
+ - `content-creation index ...`
33
+ - `content-creation series ...`
34
+ - `content-creation linkedin ...`
35
+ - `content-creation x ...`
36
+ - `content-creation publication ...`
37
+ - `content-creation resource ...`
38
+
28
39
  ### Create Content Directory
29
40
 
30
41
  Create a dated directory with content files for selected platforms:
@@ -65,10 +76,10 @@ Generate index files for each content type found in your dated folders:
65
76
 
66
77
  ```bash
67
78
  # Generate index files in current directory
68
- content-creation create-index
79
+ content-creation index create
69
80
 
70
81
  # Generate index files at a specific path
71
- content-creation create-index --path /path/to/content
82
+ content-creation index create --path /path/to/content
72
83
  ```
73
84
 
74
85
  This command scans your `YYYY/MM/DD` directory structure and creates:
@@ -97,11 +108,10 @@ Total posts: 15
97
108
  List all content scheduled for today or later:
98
109
 
99
110
  ```bash
100
- # List upcoming content in current directory
101
- content-creation list-upcoming
111
+ content-creation publication list-upcoming
102
112
 
103
113
  # List upcoming content at a specific path
104
- content-creation list-upcoming --path /path/to/content
114
+ content-creation publication list-upcoming --path /path/to/content
105
115
  ```
106
116
 
107
117
  This command scans your `YYYY/MM/DD` directory structure, reads the content title from frontmatter, and prints each matching content item with its absolute file path so it is easy to click from the terminal.
@@ -116,16 +126,88 @@ Example output:
116
126
  /absolute/path/to/content/2026/04/20/x.md
117
127
  ```
118
128
 
129
+ ### Manage Series
130
+
131
+ Work with article series stored under `series/<series-slug>/`.
132
+
133
+ Each series directory can contain:
134
+
135
+ - a `README.md` file with the editorial context for the series;
136
+ - numbered markdown files such as `1.introduction.md`, `2.my-next-article.md`, etc.
137
+
138
+ #### Generate the series index
139
+
140
+ Refresh the generated `## Index` section in each series `README.md` based on the numbered article files:
141
+
142
+ ```bash
143
+ # Generate indexes for all series in the current directory
144
+ content-creation series create-index
145
+
146
+ # Generate the index for a specific series only
147
+ content-creation series create-index --series my-series-slug
148
+
149
+ # Generate series indexes from another base path
150
+ content-creation series create-index --path /path/to/content
151
+ ```
152
+
153
+ The command preserves the existing `README.md` content and only manages the generated `## Index` section.
154
+
155
+ #### Introduce a new article in a series
156
+
157
+ Insert a new article at a specific position in the series:
158
+
159
+ ```bash
160
+ # Interactive mode
161
+ content-creation series introduce
162
+
163
+ # Fully scripted mode
164
+ content-creation series introduce \
165
+ --series my-series-slug \
166
+ --title "What tool calling really changes" \
167
+ --position 6
168
+ ```
169
+
170
+ This command will:
171
+
172
+ 1. create a new markdown file with frontmatter containing the title;
173
+ 2. rename subsequent numbered files to make room for the new article;
174
+ 3. regenerate the series `README.md` index automatically.
175
+
176
+ Generated article files follow the pattern `<index>.<slug>.md`.
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
+
119
202
  ### Publish LinkedIn Calendar
120
203
 
121
204
  Generate a Google Calendar-compatible `.ics` file for LinkedIn publications, upload it to Cloudflare R2 using Wrangler, and print the subscription URL:
122
205
 
123
206
  ```bash
124
- # Publish the LinkedIn calendar from the current directory
125
- content-creation publish-linkedin-calendar
207
+ content-creation linkedin publish-calendar
126
208
 
127
209
  # Publish the LinkedIn calendar from a specific content directory
128
- content-creation publish-linkedin-calendar --path /path/to/content
210
+ content-creation linkedin publish-calendar --path /path/to/content
129
211
  ```
130
212
 
131
213
  This command will:
@@ -155,11 +237,10 @@ https://calendar.soubiran.dev/content-creation.ics?token=<your-calendar-token>
155
237
  Scan recent LinkedIn posts and link images to their frontmatter:
156
238
 
157
239
  ```bash
158
- # Link images in current directory
159
- content-creation link-images
240
+ content-creation linkedin link-images
160
241
 
161
242
  # Link images at a specific path
162
- content-creation link-images --path /path/to/content
243
+ content-creation linkedin link-images --path /path/to/content
163
244
  ```
164
245
 
165
246
  ### Create Resource
@@ -167,11 +248,11 @@ content-creation link-images --path /path/to/content
167
248
  Create a resource directory with article, video, or audio content:
168
249
 
169
250
  ```bash
170
- # Create resource in current directory
171
- content-creation resource
251
+ # Preferred grouped command
252
+ content-creation resource create
172
253
 
173
254
  # Create resource at a specific path
174
- content-creation resource --path /path/to/resources
255
+ content-creation resource create --path /path/to/resources
175
256
  ```
176
257
 
177
258
  ### Schedule Reminder
@@ -179,11 +260,10 @@ content-creation resource --path /path/to/resources
179
260
  Schedule a reminder for X (Twitter) content to be posted at 11:00 AM UTC on the date specified in the folder path:
180
261
 
181
262
  ```bash
182
- # Schedule reminder for X content
183
- content-creation schedule-reminder
263
+ content-creation x schedule-reminder
184
264
 
185
265
  # Schedule reminder at a specific path
186
- content-creation schedule-reminder --path /path/to/content
266
+ content-creation x schedule-reminder --path /path/to/content
187
267
  ```
188
268
 
189
269
  The command will:
@@ -211,11 +291,10 @@ See `.env.example` for a template.
211
291
  Mark a publication as ready by setting `ready: true` in the selected markdown file frontmatter:
212
292
 
213
293
  ```bash
214
- # Mark a publication as ready from the current directory
215
- content-creation ready
294
+ content-creation publication ready
216
295
 
217
296
  # Mark a publication as ready from a specific content directory
218
- content-creation ready --path /path/to/content
297
+ content-creation publication ready --path /path/to/content
219
298
  ```
220
299
 
221
300
  This command will:
@@ -277,6 +356,11 @@ export default {
277
356
  publicUrl: 'https://calendar.soubiran.dev',
278
357
  token: 'your-calendar-token',
279
358
  },
359
+
360
+ // Reading-time configuration (optional)
361
+ reading: {
362
+ wordsPerMinute: 100,
363
+ },
280
364
  }
281
365
  ```
282
366
 
package/dist/cli.mjs CHANGED
@@ -1,5 +1,7 @@
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,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{};
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.8`;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:xe(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=>Ce(e)).filter(e=>e!==null).map(t=>({...t,path:b(e,t.fileName),title:Se(b(e,t.fileName),t.slug)})).sort((e,t)=>e.index-t.index)}function ve(e,t){return L(e).find(e=>e.fileName===t)}function R(e){let t=L(e),n=b(e,`README.md`);v(n,ye(p(n)?h(n,`utf-8`):``,t,e),`utf-8`),r.success(`Generated series index: ${n}`)}function ye(e,t,n){let r=be(t);if(e.includes(N)&&e.includes(P))return e.replace(RegExp(`${B(M)}\\n\\n${B(N)}[\\s\\S]*?${B(P)}`),r);let i=e.trimEnd();return i.length>0?`${i}\n\n${r}\n`:`# ${z(n.split(`/`).pop()||`series`)}\n\n${r}\n`}function be(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
+ `)}function xe(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 z(t)}function Se(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 z(t)}function Ce(e){let t=e.match(/^(\d+)\.([^.]+(?:\.[^.]+)*)\.md$/);return t?{index:Number(t[1]),slug:t[2],fileName:e}:null}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,`\\$&`)}const V=`load-more`;async function we(){let e=new Date,t=7;for(;;){let i=await o({message:`When do you want to create content?`,options:Te(e,t)});if(n(i)&&(r.error(`Operation cancelled.`),S.exit(0)),i===`load-more`){t+=7;continue}return u(i)}}function Te(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 H(){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 Ee(){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 De(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 Oe(){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 ke(){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 Ae(e){return U({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 je(e){return U({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 Me(e){return U({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 U(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:Ne(t,i)});if(n(a)&&(r.error(`Operation cancelled.`),S.exit(0)),a===V){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 Ne(e,t){let n=e.slice(0,t).map(e=>Pe(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:V,hint:`Extend the list to ${e[r-1]?.relativePath}`}]}function Pe(e){return{label:`${l(e.date,`yyyy-MM-dd`)} · ${A[e.contentType]} · ${e.title}`,value:e.path,hint:e.relativePath}}async function W(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 Fe(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 Ie(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 G(e,t){return e||t?.path||S.cwd()}async function Le(e,n){t(`Content Creation - Create dated content directory`);let r=await D(),i=await H(),o=await ke(),s={title:i};o.includes(`linkedin`)&&(s.hasVideo=await Ee(),s.hasImages=!s.hasVideo,r.thematic.length>0&&(s.theme=await De(r.thematic)));let c=await we();ue(c,o,s,r,G(e,n));let u=o.join(`, `);a(`✓ Content directory created: ${l(c,`yyyy/MM/dd`)} (${u})`)}function Re(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 ze(e){switch(e){case`linkedin`:return`LinkedIn Posts`;case`x`:return`X Posts`;case`youtube`:return`YouTube Videos`;case`instagram`:return`Instagram Posts`}}function Be(e,t){let n=j(e,{contentTypes:[t],sort:`date-desc`});if(n.length===0){r.info(`No ${t} posts found`);return}let i=ze(t),a=Re(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 Ve(e=S.cwd(),t){let n=t||[...k];for(let t of n)Be(e,t)}function He(e,n){t(`Content Creation - Create Index Files`),Ve(G(e,n)),a(`✓ Index files generated successfully`)}function Ue(e,t,n){if(e===`create`){He(t,n);return}throw Error(`Unknown index action: ${e}. Supported actions are: create`)}function We(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 K(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 Ge(e=S.cwd()){let t=await je(e),n=We(t.folderPath);if(n.length===0){r.info(`No images found for LinkedIn publication: ${t.relativePath}`),K(t,[]);return}r.info(`Found ${n.length} image(s) for ${t.relativePath}: ${n.join(`, `)}`),K(t,n)}const q=oe(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 Ke(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 qe(e){return e.toISOString().replace(/[-:]/g,``).replace(/\.\d{3}Z$/,`Z`)}function X(e){return l(e,`yyyyMMdd`)}function Z(e){return e.split(`/`).map(e=>encodeURIComponent(e)).join(`/`)}function Je(e){return`https://github.com/barbapapazes/content-creation/blob/main/${Z(e)}`}function Ye(e){let t=Z(e);return`vscode://file${t.startsWith(`/`)?``:`/`}${t}`}function Xe(e){let t=[`GitHub: ${Je(e.relativePath)}`,`VS Code: ${Ye(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 Ze(e){return`${e.ready?`Ready`:`Not ready`} · ${e.title} · ${A.linkedin}`}function Qe(e){return`linkedin-${ae(`sha1`).update(e).digest(`hex`)}@barbapapazes`}function $e(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=qe(new Date(Date.UTC(n.date.getFullYear(),n.date.getMonth(),n.date.getDate()))),r=c(n.date,1);t.push(`BEGIN:VEVENT`,`UID:${Qe(n.relativePath)}`,`DTSTAMP:${e}`,`SUMMARY:${Y(Ze(n))}`,`DTSTART;VALUE=DATE:${X(n.date)}`,`DTEND;VALUE=DATE:${X(r)}`,`TRANSP:TRANSPARENT`,`DESCRIPTION:${Y(Xe(n))}`,`END:VEVENT`)}return t.push(`END:VCALENDAR`),`${t.map(Ke).join(`\r
7
+ `)}\r\n`}function et(){let e=b(q,`node_modules`,`.bin`,S.platform===`win32`?`wrangler.cmd`:`wrangler`);return p(e)?e:`wrangler`}function tt(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(J,t);return n.searchParams.set(`token`,e.calendar.token),n.toString()}function nt(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 rt(e){let t=et();try{ie(t,[`r2`,`object`,`put`,`content-creation/${J}`,`--pipe`,`--content-type`,`text/calendar; charset=utf-8`,`--remote`],{cwd:q,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 it(e,t){let n=nt(e);if(n.length===0)return r.info(`No LinkedIn publications found. Skipping calendar upload.`),null;let i=tt(t);return rt($e(n)),r.success(`✓ LinkedIn calendar uploaded to R2 as ${J}`),r.info(`Subscription URL: ${i}`),i}async function at(e,n){t(`Content Creation - Link Images to LinkedIn Publication`),await Ge(G(e,n)),a(`✓ LinkedIn publication images linked`)}async function ot(e,n){t(`Content Creation - Publish LinkedIn Calendar`);let r=await D();if(await it(G(e,n),r)){a(`✓ LinkedIn calendar published successfully`);return}a(`✓ No LinkedIn publications found, calendar was not updated`)}async function st(e,t,n){if(e===`link-images`){await at(t,n);return}if(e===`publish-calendar`){await ot(t,n);return}throw Error(`Unknown linkedin action: ${e}. Supported actions are: link-images, publish-calendar`)}function ct(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 lt(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 ut(e,n){t(`Content Creation - List Upcoming Publications`),ct(G(e,n)),a(`✓ Upcoming publications listed`)}async function dt(e,n){t(`Content Creation - Mark Publication Ready`),lt(await Me(G(e,n))),a(`✓ Publication marked as ready`)}async function ft(e,t,n){if(e===`list-upcoming`){ut(t,n);return}if(e===`ready`){await dt(t,n);return}throw Error(`Unknown publication action: ${e}. Supported actions are: list-upcoming, ready`)}function Q(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 pt(e,t,n=S.cwd()){let i=Q(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 mt(e,n){t(`Content Creation - Create Resource`);let r=await H(),i=await Oe();pt(r,i,G(e,n)),a(`✓ Resource created: resources/${Q(r)}/${i}.md`)}async function ht(e,t,n){if(e===`create`){await mt(t,n);return}throw Error(`Unknown resource action: ${e}. Supported actions are: create`)}function gt(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)}const _t=/<!--([\s\S]*?)-->/g;function vt(e,t){try{let{data:n,content:i}=C(h(e,`utf-8`)),a=yt(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 yt(e,t){if(!Number.isFinite(t)||t<=0)throw RangeError(`Words per minute must be a positive number`);let n=e.replace(_t,``).replace(/\s+/g,``);if(n.length===0)return 0;let r=n.length/5;return Math.ceil(r/t)}function bt(e,t,n){let i=L(e),a=St(n,i.length),o=b(e,`${a}.${Q(t)}.md`);if(p(o))throw Error(`Article already exists at ${o}`);return xt(e,i,a),v(o,C.stringify(``,{title:t}),`utf-8`),r.success(`Created article: ${o}`),R(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=b(e,n);te(t.path,i),r.info(`Renamed ${t.fileName} → ${n}`)}}function St(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 Ct(e,n,r){let i=G(n,r);if(e===`create-index`){t(`Content Creation - Create Series Index`),gt(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 W(i);if(!n)throw Error(`Series not found: ${r?.series}`);let o=r?.position?Number(r.position):await Fe(n.path);bt(n.path,e,o),a(`✓ Series article created: ${n.name}/${o}.${Q(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 W(i);if(!n)throw Error(`Series not found: ${r?.series}`);let o=r?.file?ve(n.path,r.file):await Ie(n.path);if(!o)throw Error(`Series article not found: ${r?.file}`);let s=wt(r?.wpm,e.reading.wordsPerMinute),c=vt(o.path,s);a(`✓ Estimated time updated for ${n.name}/${o.fileName}: ${c} min`);return}throw Error(`Unknown series action: ${e}. Supported actions are: create-index, introduce, estimate-time`)}function wt(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 Tt(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 Et(e,n){t(`Content Creation - Schedule X Publication Reminder`);let r=await D();await Tt(await Ae(G(e,n)),r),a(`✓ X publication reminder scheduled`)}async function Dt(e,t,n){if(e===`schedule-reminder`){await Et(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(Le),$.command(`resource <action> [path]`,`Manage resource markdown files`).option(`--path <path>`,`Base path for resource directory (defaults to current directory)`).action(ht),$.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(Ct),$.command(`index <action> [path]`,`Manage generated indexes`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).action(Ue),$.command(`linkedin <action> [path]`,`Manage LinkedIn-specific workflows`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).action(st),$.command(`x <action> [path]`,`Manage X-specific workflows`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).action(Dt),$.command(`publication <action> [path]`,`Manage publication workflows`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).action(ft),$.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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@barbapapazes/content-creation",
3
3
  "type": "module",
4
- "version": "0.18.6",
4
+ "version": "0.18.8",
5
5
  "author": "Estéban Soubiran <esteban@soubiran.dev>",
6
6
  "license": "MIT",
7
7
  "funding": "https://github.com/sponsors/Barbapapazes",