@barbapapazes/content-creation 0.18.8 → 0.19.0

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
@@ -2,454 +2,50 @@
2
2
 
3
3
  A CLI tool to streamline multi-platform content creation by generating dated directories with content files for LinkedIn, X (Twitter), YouTube, and Instagram.
4
4
 
5
- ## Features
6
-
7
- - Create dated directory structure (YYYY/MM/DD format)
8
- - Support for multiple content types: LinkedIn, X, YouTube, Instagram
9
- - User-friendly date selection that starts with 7 days and can keep loading more future dates
10
- - Multi-select content types to create multiple files in one run
11
- - Config-driven templates for each content type
12
- - Never overwrites existing files (skip if exists behavior)
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
16
- - List upcoming content from today onward with absolute file paths
17
- - Generate and upload a LinkedIn publication calendar for Google Calendar subscriptions
18
- - Link images to LinkedIn post frontmatter
19
- - Mark a publication as ready by setting `ready: true` in frontmatter
20
- - Create resource directories (articles, videos, audio)
21
-
22
5
  ## Installation
23
6
 
24
7
  ```bash
25
8
  npm install -g @barbapapazes/content-creation
26
9
  ```
27
10
 
28
- ## Usage
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
-
39
- ### Create Content Directory
40
-
41
- Create a dated directory with content files for selected platforms:
42
-
43
- ```bash
44
- # Create content directory in current working directory
45
- content-creation
46
- # Prompts you to select: LinkedIn, X, YouTube, and/or Instagram
47
-
48
- # Create content directory at a specific path
49
- content-creation --path /path/to/content
50
- ```
51
-
52
- The tool will prompt you to:
53
- 1. Enter a title (shared across all selected types)
54
- 2. Select one or more content types
55
- 3. Answer type-specific questions:
56
- - **LinkedIn**: Whether to include a video. If not, photos are automatically enabled.
57
- - **YouTube/Instagram**: Whether to create script and description files
58
- 4. Choose a date, with the option to keep loading more future dates
59
-
60
- #### Content Files by Type
61
-
62
- Each content type creates specific files:
63
-
64
- - **LinkedIn**: `linkedin.md` with `images: [null]` by default, plus `linkedin-script.md` when video is planned
65
- - **X**: `x.md`
66
- - **YouTube**: `youtube.md`, `youtube-script.md`, `youtube-description.md`
67
- - **Instagram**: `instagram.md`, `instagram-script.md`, `instagram-description.md`
68
-
69
- All main files (`*.md`) include frontmatter with at least a `title` field for indexing.
70
-
71
- **Important**: Files are never overwritten. If a file already exists, it will be skipped.
72
-
73
- ### Create Index Files
74
-
75
- Generate index files for each content type found in your dated folders:
76
-
77
- ```bash
78
- # Generate index files in current directory
79
- content-creation index create
80
-
81
- # Generate index files at a specific path
82
- content-creation index create --path /path/to/content
83
- ```
84
-
85
- This command scans your `YYYY/MM/DD` directory structure and creates:
86
-
87
- - `linkedin-posts.md` - Index of all LinkedIn posts
88
- - `x-posts.md` - Index of all X posts
89
- - `youtube-videos.md` - Index of all YouTube videos
90
- - `instagram-posts.md` - Index of all Instagram posts
91
-
92
- Each index file contains:
93
-
94
- ```markdown
95
- # LinkedIn Posts
96
-
97
- _Generated on 1/31/2026_
98
-
99
- Total posts: 15
100
-
101
- - [Understanding AI Agents](2026/01/20/linkedin.md) - _January 20, 2026_
102
- - [Deep Dive into RAG](2026/01/15/linkedin.md) - _January 15, 2026_
103
- ...
104
- ```
105
-
106
- ### List Upcoming Content
107
-
108
- List all content scheduled for today or later:
109
-
110
- ```bash
111
- content-creation publication list-upcoming
112
-
113
- # List upcoming content at a specific path
114
- content-creation publication list-upcoming --path /path/to/content
115
- ```
116
-
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.
118
-
119
- Example output:
120
-
121
- ```text
122
- 2026-04-18 · LinkedIn · Shipping a better content workflow
123
- /absolute/path/to/content/2026/04/18/linkedin.md
124
-
125
- 2026-04-20 · X · A tiny automation tip
126
- /absolute/path/to/content/2026/04/20/x.md
127
- ```
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
11
+ ## CLI help
179
12
 
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.
13
+ Use the built-in help to discover the available commands and subcommands:
183
14
 
184
15
  ```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
16
+ content-creation --help
17
+ content-creation <command> --help
193
18
  ```
194
19
 
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
-
202
- ### Publish LinkedIn Calendar
203
-
204
- Generate a Google Calendar-compatible `.ics` file for LinkedIn publications, upload it to Cloudflare R2 using Wrangler, and print the subscription URL:
205
-
206
- ```bash
207
- content-creation linkedin publish-calendar
208
-
209
- # Publish the LinkedIn calendar from a specific content directory
210
- content-creation linkedin publish-calendar --path /path/to/content
211
- ```
20
+ ## Commands
212
21
 
213
- This command will:
214
- 1. Scan your `YYYY/MM/DD` directory structure for `linkedin.md` files only
215
- 2. Read the LinkedIn frontmatter and generate all-day calendar events
216
- 3. Stream the generated calendar directly to Cloudflare R2 using Wrangler
217
- 4. Upload `content-creation.ics` to the `content-creation` R2 bucket
218
- 5. Print the final subscription URL for Google Calendar
219
-
220
- **Required Configuration**: You must set up the following environment variables in a `.env` file:
221
-
222
- ```bash
223
- CALENDAR_PUBLIC_URL=https://calendar.soubiran.dev
224
- CALENDAR_TOKEN=your-calendar-token-here
225
- ```
226
-
227
- See `.env.example` for a template. The printed URL uses the format:
228
-
229
- ```text
230
- https://calendar.soubiran.dev/content-creation.ics?token=<your-calendar-token>
231
- ```
232
-
233
- **Current Scope**: This command publishes LinkedIn entries only for now.
234
-
235
- ### Link Images
236
-
237
- Scan recent LinkedIn posts and link images to their frontmatter:
238
-
239
- ```bash
240
- content-creation linkedin link-images
241
-
242
- # Link images at a specific path
243
- content-creation linkedin link-images --path /path/to/content
244
- ```
245
-
246
- ### Create Resource
247
-
248
- Create a resource directory with article, video, or audio content:
249
-
250
- ```bash
251
- # Preferred grouped command
252
- content-creation resource create
253
-
254
- # Create resource at a specific path
255
- content-creation resource create --path /path/to/resources
256
- ```
257
-
258
- ### Schedule Reminder
259
-
260
- Schedule a reminder for X (Twitter) content to be posted at 11:00 AM UTC on the date specified in the folder path:
261
-
262
- ```bash
263
- content-creation x schedule-reminder
264
-
265
- # Schedule reminder at a specific path
266
- content-creation x schedule-reminder --path /path/to/content
267
- ```
268
-
269
- The command will:
270
- 1. Scan your directory for dated folders containing `x.md` files
271
- 2. Prompt you to select which content to schedule
272
- 3. Check if the content is already scheduled (exits early if `scheduled: true` in frontmatter)
273
- 4. Extract the tweet content from `x.md`
274
- 5. Calculate the scheduled time (11:00 AM UTC based on the folder date: YYYY/MM/DD)
275
- 6. Send a POST request to your automation endpoint
276
- 7. Update the frontmatter with `scheduled: true`
277
- 8. Display a confirmation message with the scheduled date/time
278
-
279
- **Required Configuration**: You must set up the following environment variables in a `.env` file:
280
-
281
- ```bash
282
- AUTOMATION_ENDPOINT=https://automation.soubiran.dev/trigger
283
- CF_ACCESS_CLIENT_ID=your-client-id-here
284
- CF_ACCESS_CLIENT_SECRET=your-client-secret-here
285
- ```
286
-
287
- See `.env.example` for a template.
288
-
289
- ### Mark Publication as Ready
290
-
291
- Mark a publication as ready by setting `ready: true` in the selected markdown file frontmatter:
292
-
293
- ```bash
294
- content-creation publication ready
295
-
296
- # Mark a publication as ready from a specific content directory
297
- content-creation publication ready --path /path/to/content
298
- ```
299
-
300
- This command will:
301
- 1. Scan your `YYYY/MM/DD` directory structure for publication files
302
- 2. Prompt you to select which publication to update
303
- 3. Add `ready: true` to the selected file frontmatter
22
+ - `content new` — create a dated content directory
23
+ - `content upcoming` list upcoming publications
24
+ - `content rebuild-indexes` regenerate content indexes
25
+ - `series new` create a new series directory
26
+ - `series article new` insert a new article in a series
27
+ - `series article ready` mark a series article as ready and refresh reading time
28
+ - `series article estimate-time` — compute reading time for a series article
29
+ - `linkedin ready` link images, mark the post ready, and mark the script ready when present
30
+ - `linkedin publish-calendar` — publish the LinkedIn calendar to Cloudflare R2
31
+ - `x schedule-reminder` — schedule an X reminder through the automation endpoint
32
+ - `resource new` — create a new resource directory
304
33
 
305
34
  ## Configuration
306
35
 
307
- You can configure the tool using a configuration file. Create one of the following files:
308
-
309
- - Local config: `content-creation.config.{ts,js,mjs,json}` in your project root
310
- - Global config: `~/.content-creationrc` (JSON format)
311
-
312
- ### Configuration Options
313
-
314
- ```typescript
315
- export default {
316
- // Array of thematic areas (reserved for future features)
317
- thematic: ['JavaScript', 'TypeScript', 'Node.js'],
318
-
319
- // Base directory for external template files (optional)
320
- templatesDir: '~/.config/content-creation/templates',
321
-
322
- // Templates configuration per content type
323
- templates: {
324
- linkedin: {
325
- body: '', // Inline template for body content
326
- bodyPath: 'linkedin-body.md', // Or path to template file
327
- footer: '\n\n---\n\nCustom footer text', // Inline footer
328
- footerPath: 'linkedin-footer.md', // Or path to footer file
329
- },
330
- x: {
331
- body: '',
332
- bodyPath: 'x-body.md',
333
- },
334
- youtube: {
335
- body: '',
336
- script: '',
337
- description: '',
338
- // Or use paths: bodyPath, scriptPath, descriptionPath
339
- },
340
- instagram: {
341
- body: '',
342
- script: '',
343
- description: '',
344
- },
345
- },
346
-
347
- // Scheduling configuration (optional, can also be set via environment variables)
348
- scheduling: {
349
- automationEndpoint: 'https://automation.soubiran.dev/trigger',
350
- cfAccessClientId: 'your-client-id',
351
- cfAccessClientSecret: 'your-client-secret',
352
- },
36
+ The package reads configuration from:
353
37
 
354
- // Calendar publishing configuration (optional, can also be set via environment variables)
355
- calendar: {
356
- publicUrl: 'https://calendar.soubiran.dev',
357
- token: 'your-calendar-token',
358
- },
38
+ - `content-creation.config.{ts,js,mjs,json}` in your project
39
+ - `~/.content-creationrc`
40
+ - environment variables loaded from `.env`
359
41
 
360
- // Reading-time configuration (optional)
361
- reading: {
362
- wordsPerMinute: 100,
363
- },
364
- }
365
- ```
366
-
367
- ### Template Resolution
368
-
369
- For each template field (body, footer, script, description):
370
-
371
- 1. If `*Path` is specified, it's resolved relative to `templatesDir` or the config file location
372
- 2. Otherwise, the inline string value is used
373
- 3. If neither is specified, defaults are used (LinkedIn has a default footer)
374
-
375
- ### Example: Customizing LinkedIn Footer
376
-
377
- Create a `content-creation.config.ts` file:
378
-
379
- ```typescript
380
- import { defineConfig } from '@barbapapazes/content-creation'
381
-
382
- export default defineConfig({
383
- templates: {
384
- linkedin: {
385
- footer: '\n\n---\n\nCustom signature here! 🚀',
386
- },
387
- },
388
- })
389
- ```
390
-
391
- Or store templates in external files:
392
-
393
- ```typescript
394
- import { defineConfig } from '@barbapapazes/content-creation'
395
-
396
- export default defineConfig({
397
- templatesDir: '~/.config/content-creation/templates',
398
- templates: {
399
- linkedin: {
400
- footerPath: 'linkedin-footer.md',
401
- },
402
- youtube: {
403
- descriptionPath: 'youtube-description.md',
404
- },
405
- },
406
- })
407
- ```
408
-
409
- Then create `~/.config/content-creation/templates/linkedin-footer.md`:
410
-
411
- ```markdown
412
- ---
413
-
414
- Subscribe for more content! 🎯
415
- ```
416
-
417
- ### JSON Configuration
418
-
419
- For a JSON config file (`~/.content-creationrc`):
420
-
421
- ```json
422
- {
423
- "thematic": ["JavaScript", "TypeScript", "Node.js"],
424
- "templates": {
425
- "linkedin": {
426
- "footer": "\n\n---\n\nFollow for more! 🚀"
427
- }
428
- }
429
- }
430
- ```
431
-
432
- ## Directory Structure
433
-
434
- The CLI creates a nested directory structure organized by date:
435
-
436
- ```
437
- base-path/
438
- └── YYYY/ # Year folder
439
- └── MM/ # Month folder
440
- └── DD/ # Day folder
441
- ├── linkedin.md
442
- ├── linkedin-script.md
443
- ├── x.md
444
- ├── youtube.md
445
- ├── youtube-script.md
446
- ├── youtube-description.md
447
- ├── instagram.md
448
- ├── instagram-script.md
449
- └── instagram-description.md
450
- ```
42
+ Environment variables used by advanced workflows:
451
43
 
452
- Example: Running the CLI on January 31, 2026 and selecting all types will create a `2026/01/31/` directory with all the relevant files.
44
+ - `CALENDAR_PUBLIC_URL`
45
+ - `CALENDAR_TOKEN`
46
+ - `AUTOMATION_ENDPOINT`
47
+ - `CF_ACCESS_CLIENT_ID`
48
+ - `CF_ACCESS_CLIENT_SECRET`
453
49
 
454
50
  ## License
455
51
 
package/dist/cli.mjs CHANGED
@@ -1,7 +1,9 @@
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.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{};
2
+ import e from"node:process";import{intro as t,isCancel as n,log as r,multiselect as i,outro as a,select as o,text as s}from"@clack/prompts";import{Command as c}from"commander";import{existsSync as l,mkdirSync as u,readFileSync as d,readdirSync as f,renameSync as ee,statSync as p,writeFileSync as m}from"node:fs";import{basename as te,dirname as ne,isAbsolute as re,join as h,resolve as g}from"node:path";import{addDays as _,format as v,parseISO as ie,setHours as ae,setMinutes as oe,setSeconds as se}from"date-fns";import y from"gray-matter";import{loadConfig as ce}from"c12";import{execFileSync as le}from"node:child_process";import{createHash as ue}from"node:crypto";import{fileURLToPath as de}from"node:url";var fe=`@barbapapazes/content-creation`,pe=`0.19.0`,me=`CLI tool for content creation and management`;const b=[`linkedin`,`x`,`youtube`,`instagram`],x={linkedin:`LinkedIn`,x:`X`,youtube:`YouTube`,instagram:`Instagram`},S={linkedin:{mainFile:`linkedin.md`,additionalFiles:[`linkedin-script.md`]},x:{mainFile:`x.md`},youtube:{mainFile:`youtube-script.md`,additionalFiles:[`youtube-description.md`]},instagram:{mainFile:`instagram-script.md`,additionalFiles:[`instagram-description.md`]}},he={linkedin:{fileName:`linkedin-posts.md`,heading:`LinkedIn Posts`},x:{fileName:`x-posts.md`,heading:`X Posts`},youtube:{fileName:`youtube-videos.md`,heading:`YouTube Videos`},instagram:{fileName:`instagram-posts.md`,heading:`Instagram Posts`}},C=`__load-more__`,ge=[`.jpg`,`.jpeg`,`.png`],w={hours:11,minutes:0,seconds:0},_e={thematic:[],templatesDir:void 0,templates:{},scheduling:{},calendar:{},reading:{wordsPerMinute:100}};function ve(t,n){return t?re(t)?t:g(n?ne(n):e.cwd(),t):n?ne(n):e.cwd()}function T(e,t,n){if(!t)return;let i=g(e,t);if(!l(i)){r.warn(`Configured ${n} template was not found: ${i}`);return}return i}function ye(e,t){return{footerPath:T(t,e?.footerPath,`LinkedIn footer`)}}function be(e,t){return{templatePath:T(t,e?.templatePath,`video description`)}}async function E(){let{config:t,configFile:n}=await ce({name:`content-creation`,defaults:_e,globalRc:!0,dotenv:!0}),r=ve(t.templatesDir,n),i={linkedin:ye(t.templates?.linkedin,r),youtube:be(t.templates?.youtube,r),instagram:be(t.templates?.instagram,r)},a={automationEndpoint:t.scheduling?.automationEndpoint??e.env.AUTOMATION_ENDPOINT,cfAccessClientId:t.scheduling?.cfAccessClientId??e.env.CF_ACCESS_CLIENT_ID,cfAccessClientSecret:t.scheduling?.cfAccessClientSecret??e.env.CF_ACCESS_CLIENT_SECRET},o={publicUrl:t.calendar?.publicUrl??e.env.CALENDAR_PUBLIC_URL,token:t.calendar?.token??e.env.CALENDAR_TOKEN},s=t.reading?.wordsPerMinute??_e.reading?.wordsPerMinute??100;if(!Number.isFinite(s)||s<=0)throw RangeError(`Reading wordsPerMinute must be a positive number`);return{thematic:t.thematic??[],templatesDir:t.templatesDir??``,templates:i,scheduling:a,calendar:o,reading:{wordsPerMinute:s}}}function D(e){return e?l(e)?d(e,`utf-8`):(r.warn(`Template file not found: ${e}`),``):``}function xe(e,t,n,i,a){let o=g(h(a,v(e,`yyyy`),v(e,`MM`),v(e,`dd`)));l(o)||(u(o,{recursive:!0}),r.success(`Created directory: ${o}`));let s=Se(n);for(let e of t)Ce(e,o,s,i);return o}function Se(e){return e.hasVideo?{...e,hasImages:!1}:{...e,hasImages:!0}}function Ce(e,t,n,i){let a=S[e],o=h(t,a.mainFile),s=`additionalFiles`in a?a.additionalFiles:void 0;if(l(o)?r.info(`${a.mainFile} already exists, skipping`):we(e,o,n,i),s)for(let a of s){let o=h(t,a);Ee(a,e,n)&&(l(o)?r.info(`${a} already exists, skipping`):Te(e,o,a,i))}}function we(e,t,n,i){let a={title:n.title};e===`linkedin`&&(n.theme&&(a.theme=n.theme),n.hasVideo&&(a.video=!0),n.hasImages&&(a.images=[null]));let o=``;e===`linkedin`&&(o=D(i.templates.linkedin?.footerPath)),m(t,y.stringify(o,a),`utf-8`),r.success(`Created ${t}`)}function Te(e,t,n,i){let a=``;n.endsWith(`-description.md`)&&(a=D(i.templates[e]?.templatePath)),m(t,a,`utf-8`),r.success(`Created ${t}`)}function Ee(e,t,n){return t===`linkedin`&&e===`linkedin-script.md`?n.hasVideo||!1:!0}function De(e,t,n){let r=n===`date-asc`?e.date.getTime()-t.date.getTime():t.date.getTime()-e.date.getTime();if(r!==0)return r;let i=b.indexOf(e.contentType)-b.indexOf(t.contentType);return i===0?e.relativePath.localeCompare(t.relativePath):i}function O(e,t={}){let n=[],i=t.contentTypes?.length?t.contentTypes:[...b],a=t.fromDate,o=t.sort||`date-desc`;try{let t=f(e).filter(t=>p(h(e,t)).isDirectory()&&/^\d{4}$/.test(t));for(let o of t){let t=h(e,o),s=f(t).filter(e=>p(h(t,e)).isDirectory()&&/^\d{2}$/.test(e));for(let e of s){let s=h(t,e),c=f(s).filter(e=>p(h(s,e)).isDirectory()&&/^\d{2}$/.test(e));for(let t of c){let c=h(s,t),u=`${o}-${e}-${t}`,f=new Date(Number(o),Number(e)-1,Number(t));if(!(a&&f.getTime()<a.getTime()))for(let a of i){let i=S[a].mainFile,s=g(c,i);if(l(s))try{let{data:r}=y(d(s,`utf-8`)),l=r,ee=l.title||`Untitled (${u})`;n.push({contentType:a,path:s,folderPath:c,date:f,title:ee,relativePath:`${o}/${e}/${t}/${i}`,frontmatter:l})}catch(e){r.warn(`Error reading ${s}: ${e}`)}}}}}}catch(e){return r.error(`Error reading directories: ${e}`),[]}return n.sort((e,t)=>De(e,t,o))}function Oe(t){let n=new Date;n.setHours(0,0,0,0);let i=O(t,{fromDate:n,sort:`date-asc`});if(i.length===0){r.info(`No upcoming publications found from today onward.`);return}r.info(`Found ${i.length} upcoming publication${i.length===1?``:`s`}:`),e.stdout.write(`
3
+ `);for(let t of i){let n=v(t.date,`EEEE, MMMM d, yyyy`),r=v(t.date,`yyyy-MM-dd`);e.stdout.write(`${n} (${r}) · ${x[t.contentType]} · ${t.title}\n`),e.stdout.write(`${t.path}\n\n`)}}function ke(e,t){let n=O(e,{contentTypes:[t],sort:`date-desc`});if(n.length===0){r.info(`No ${t} posts found`);return}let{fileName:i,heading:a}=he[t],o=[`# ${a}`,``,`_Generated on ${new Date().toLocaleDateString()}_`,``,`Total posts: ${n.length}`,``];for(let e of n){let t=e.date.toLocaleDateString(`en-US`,{year:`numeric`,month:`long`,day:`numeric`});o.push(`- [${e.title}](${e.relativePath}) - _${t}_`)}m(h(e,i),`${o.join(`
4
+ `)}\n`,`utf-8`),r.success(`Created ${i} with ${n.length} posts`)}function Ae(e){for(let t of b)ke(e,t)}function je(t=`Operation cancelled.`){r.error(t),e.exit(0)}function k(t){r.error(t),e.exit(1)}function A(e,t){return n(e)&&je(t),e}function j(e){let{content:t,data:n}=y(d(e,`utf-8`));return{content:t,frontmatter:n}}function M(e,t){m(e,y.stringify(t.content,t.frontmatter),`utf-8`)}async function N(e){return A(await s({message:e.message,placeholder:e.placeholder,validate:t=>{if(!t||t.trim().length===0)return e.requiredMessage||`Value is required`}})).trim()}const Me=/<!--([\s\S]*?)-->/g;function Ne(e,t){let n=e.replace(Me,``).replace(/\s+/g,``);if(n.length===0)return 0;let r=n.length/5;return Math.ceil(r/t)}function Pe(e){return e.charAt(0).toUpperCase()+e.slice(1)}function P(e){return e.split(`-`).filter(Boolean).map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join(` `)}function F(e){return e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`)}function I(e,t){try{let{content:n,frontmatter:i}=j(e.path);if(i.ready===!0){r.warn(`This ${t} is already marked as ready.`);return}let a={...i,ready:!0};M(e.path,{content:n,frontmatter:a}),r.success(`${Pe(t)} marked as ready: ${e.relativePath}`)}catch(e){k(`Failed to mark ${t} as ready: ${e instanceof Error?e.message:String(e)}`)}}async function Fe(){let e=new Date,t=7;for(;;){let n=A(await o({message:`When do you want to create content?`,options:Ie(e,t)}));if(n===C){t+=7;continue}return typeof n!=`string`&&k(`Unable to resolve selected date: ${String(n)}`),ie(n)}}function Ie(e,t){let n=[];for(let r=0;r<t;r++){let t=_(e,r),i=v(t,`yyyy-MM-dd`),a=v(t,`EEEE`),o=i,s=`Create content for ${a}, ${i}`;r===0?(o=`Today - ${a} (${i})`,s=`Create content for today (${a})`):r===1?(o=`Tomorrow - ${a} (${i})`,s=`Create content for tomorrow (${a})`):(o=`In ${r} days - ${a} (${i})`,s=`Create content for ${a}, ${i}`),n.push({label:o,value:i,hint:s})}return n.push({label:`Show 7 more dates (${t+1}-${t+7} days ahead)`,value:C,hint:`Extend the list up to ${v(_(e,t+7-1),`yyyy-MM-dd`)}`}),n}async function Le(){return N({message:`What is the title of your content?`,placeholder:`Enter content title`,requiredMessage:`Title is required`})}async function Re(){let e=A(await o({message:`Is a LinkedIn video planned?`,options:[{label:`Yes`,value:!0,hint:`Add video: true to frontmatter`},{label:`No`,value:!1,hint:`No video metadata`}]}));return typeof e!=`boolean`&&k(`Unable to resolve LinkedIn video choice: ${String(e)}`),e}async function ze(e){if(e.length===0)return;let t=A(await o({message:`What is the content theme? (optional)`,options:[{label:`No theme`,value:``,hint:`Leave theme empty`},...e.map(e=>({label:e,value:e,hint:`Use theme: ${e}`}))]}));typeof t!=`string`&&k(`Unable to resolve selected theme: ${String(t)}`);let n=t;return n.length>0?n:void 0}async function Be(){let e=A(await o({message:`What type of resource is this?`,options:[{label:`Article`,value:`article`,hint:`Create article.md`},{label:`Video`,value:`video`,hint:`Create video.md`},{label:`Audio`,value:`audio`,hint:`Create audio.md`},{label:`Tweet`,value:`tweet`,hint:`Create tweet.md`}]}));return[`article`,`video`,`audio`,`tweet`].includes(String(e))||k(`Unable to resolve resource type: ${String(e)}`),e}async function Ve(){let e=A(await i({message:`Which content types do you want to create?`,options:[{label:`LinkedIn`,value:`linkedin`,hint:`Create LinkedIn post and optional script`},{label:`X (Twitter)`,value:`x`,hint:`Create X post`},{label:`YouTube`,value:`youtube`,hint:`Create YouTube script and description`},{label:`Instagram`,value:`instagram`,hint:`Create Instagram script and description`}],required:!0}));return(!Array.isArray(e)||e.some(e=>!b.includes(e)))&&k(`Unable to resolve selected content types.`),e}async function He(e){return L({basePath:e,contentTypes:[`x`],message:`Which X publication do you want to schedule a reminder for?`,emptyMessage:`No X publications found in dated folders`,sort:`date-desc`})}async function Ue(e){return L({basePath:e,contentTypes:[`linkedin`],message:`Which LinkedIn publication do you want to mark as ready?`,emptyMessage:`No LinkedIn publications found in dated folders`,sort:`date-desc`})}async function L(e){let t=O(e.basePath,{contentTypes:e.contentTypes,sort:e.sort});t.length===0&&k(e.emptyMessage);let n=20;for(;;){let r=A(await o({message:e.message,options:We(t,n)}));if(r===C){n+=20;continue}typeof r!=`string`&&k(`Unable to resolve selected publication: ${String(r)}`);let i=t.find(e=>e.path===r);return i||k(`Unable to resolve selected publication: ${r}`),i}}function We(e,t){let n=e.slice(0,t).map(e=>Ge(e));if(t>=e.length)return n;let r=Math.min(t+20,e.length);return[...n,{label:`Show ${r-t} more publications (${t+1}-${r} of ${e.length})`,value:C,hint:`Extend the list to ${e[r-1]?.relativePath}`}]}function Ge(e){return{label:`${v(e.date,`yyyy-MM-dd`)} · ${x[e.contentType]} · ${e.title}`,value:e.path,hint:e.relativePath}}async function Ke(e){t(`Content Creation - Create dated content directory`);let n=await E(),r=await Le(),i=await Ve(),o={title:r};i.includes(`linkedin`)&&(o.hasVideo=await Re(),o.hasImages=!o.hasVideo,n.thematic.length>0&&(o.theme=await ze(n.thematic)));let s=await Fe();xe(s,i,o,n,e);let c=i.join(`, `);a(`✓ Content directory created: ${v(s,`yyyy/MM/dd`)} (${c})`)}function qe(e){t(`Content Creation - List Upcoming Publications`),Oe(e),a(`✓ Upcoming publications listed`)}function Je(e){t(`Content Creation - Rebuild Content Indexes`),Ae(e),a(`✓ Content indexes rebuilt successfully`)}const R=de(new URL(`../../../../`,import.meta.url)),z=`content-creation.ics`;function B(e){return e.replace(/\\/g,`\\\\`).replace(/\r\n|\r|\n/g,`\\n`).replace(/;/g,`\\;`).replace(/,/g,`\\,`)}function Ye(e){if(e.length<=75)return e;let t=[],n=e;for(;n.length>75;)t.push(n.slice(0,75)),n=` ${n.slice(75)}`;return t.push(n),t.join(`\r
5
+ `)}function Xe(e){return e.toISOString().replace(/[-:]/g,``).replace(/\.\d{3}Z$/,`Z`)}function Ze(e){return v(e,`yyyyMMdd`)}function Qe(e){return e.split(`/`).map(e=>encodeURIComponent(e)).join(`/`)}function $e(e){return`https://github.com/barbapapazes/content-creation/blob/main/${Qe(e)}`}function et(e){let t=Qe(e);return`vscode://file${t.startsWith(`/`)?``:`/`}${t}`}function tt(e){let t=[`GitHub: ${$e(e.relativePath)}`,`VS Code: ${et(e.path)}`,`Status: ${e.ready?`ready`:`not ready`}`];return e.theme&&t.push(`Theme: ${e.theme}`),typeof e.video==`boolean`&&t.push(`Video planned: ${e.video?`yes`:`no`}`),e.imageCount>0&&t.push(`Images planned: ${e.imageCount}`),t.join(`
6
+ `)}function nt(e){return`${e.ready?`Ready`:`Not ready`} · ${e.title} · ${x.linkedin}`}function rt(e){return`linkedin-${ue(`sha1`).update(e).digest(`hex`)}@barbapapazes`}function it(e){let t=[`BEGIN:VCALENDAR`,`VERSION:2.0`,`PRODID:-//Barbapapazes//Content Creation LinkedIn Calendar//EN`,`CALSCALE:GREGORIAN`,`METHOD:PUBLISH`,`X-WR-CALNAME:${B(`content-creation.ics`)}`];for(let n of e){let e=Xe(new Date(Date.UTC(n.date.getFullYear(),n.date.getMonth(),n.date.getDate()))),r=_(n.date,1);t.push(`BEGIN:VEVENT`,`UID:${rt(n.relativePath)}`,`DTSTAMP:${e}`,`SUMMARY:${B(nt(n))}`,`DTSTART;VALUE=DATE:${Ze(n.date)}`,`DTEND;VALUE=DATE:${Ze(r)}`,`TRANSP:TRANSPARENT`,`DESCRIPTION:${B(tt(n))}`,`END:VEVENT`)}return t.push(`END:VCALENDAR`),`${t.map(Ye).join(`\r
7
+ `)}\r\n`}function at(){let t=h(R,`node_modules`,`.bin`,e.platform===`win32`?`wrangler.cmd`:`wrangler`);return l(t)?t:`wrangler`}function ot(t){(!t.calendar.publicUrl||!t.calendar.token)&&(r.error(`Calendar publishing configuration is missing. Please set CALENDAR_PUBLIC_URL and CALENDAR_TOKEN in your .env file or config.`),e.exit(1));let n=t.calendar.publicUrl.endsWith(`/`)?t.calendar.publicUrl:`${t.calendar.publicUrl}/`,i=new URL(z,n);return i.searchParams.set(`token`,t.calendar.token),i.toString()}function st(e){return O(e,{contentTypes:[`linkedin`],sort:`date-asc`}).map(e=>{let t=e.frontmatter;return{...e,title:t.title||e.title,ready:t.ready===!0,theme:t.theme,video:t.video,imageCount:t.images?.filter(Boolean).length??0}})}function ct(t){let n=at();try{le(n,[`r2`,`object`,`put`,`content-creation/${z}`,`--pipe`,`--content-type`,`text/calendar; charset=utf-8`,`--remote`],{cwd:R,stdio:[`pipe`,`inherit`,`inherit`],env:e.env,input:t})}catch(t){r.error(`Failed to upload LinkedIn calendar: ${t instanceof Error?t.message:String(t)}`),e.exit(1)}}async function lt(e,t){let n=st(e);if(n.length===0)return r.info(`No LinkedIn publications found. Skipping calendar upload.`),null;let i=ot(t);return ct(it(n)),r.success(`✓ LinkedIn calendar uploaded to R2 as ${z}`),r.info(`Subscription URL: ${i}`),i}function ut(e){let t=[];try{let n=f(e);for(let r of n){if(!p(h(e,r)).isFile())continue;let n=r.toLowerCase().substring(r.lastIndexOf(`.`));ge.includes(n)&&t.push(r)}}catch(e){k(`Error reading directory: ${e instanceof Error?e.message:String(e)}`)}return t.sort()}function dt(e,t){try{let{content:n,frontmatter:i}=j(e.path);M(e.path,{content:n,frontmatter:{...i,images:t.length>0?t:[null]}}),r.success(`LinkedIn publication updated: ${e.relativePath} (${t.length} image(s))`)}catch(e){k(`Failed to update LinkedIn publication: ${e instanceof Error?e.message:String(e)}`)}}function ft(e){let t=ut(e.folderPath);t.length===0?r.info(`No images found for LinkedIn publication: ${e.relativePath}`):r.info(`Found ${t.length} image(s) for ${e.relativePath}: ${t.join(`, `)}`),dt(e,t),I({path:e.path,relativePath:e.relativePath},`linkedin publication`);let n=h(e.folderPath,`linkedin-script.md`),i=e.relativePath.slice(0,e.relativePath.lastIndexOf(`/`)),a=l(n);return a&&I({path:n,relativePath:`${i}/linkedin-script.md`},`linkedin script`),{imageCount:t.length,scriptMarked:a}}async function pt(e){if(t(`Content Creation - Publish LinkedIn Calendar`),await lt(e,await E())){a(`✓ LinkedIn calendar published successfully`);return}a(`✓ No LinkedIn publications found, calendar was not updated`)}async function mt(e){t(`Content Creation - Mark LinkedIn Publication Ready`);let n=await Ue(e),r=ft(n),i=r.scriptMarked?`script marked ready`:`no script found`;a(`✓ LinkedIn publication ready: ${n.relativePath} (${r.imageCount} image(s), ${i})`)}function V(e){return e.toLowerCase().trim().normalize(`NFD`).replace(/[\u0300-\u036F]/g,``).replace(/[^a-z0-9\s-]/g,``).replace(/\s+/g,`-`).replace(/-+/g,`-`).replace(/^-+|-+$/g,``)||`untitled`}function ht(e,t,n){let i=V(e),a=h(g(h(n,`resources`)),i),o=`${t}.md`,s=h(a,o);if(l(s))return r.info(`${o} already exists at: ${s}`),a;u(a,{recursive:!0}),r.success(`Created directory: ${a}`);let c={title:e,url:``,date:``};return m(s,y.stringify(``,c),`utf-8`),r.success(`Created ${o} at: ${s}`),a}async function gt(e){t(`Content Creation - Create Resource`);let n=await Le(),r=await Be();ht(n,r,e),a(`✓ Resource created: resources/${V(n)}/${r}.md`)}const H=`## Index`,U=`<!-- content-creation:series-index:start -->`,W=`<!-- content-creation:series-index:end -->`,_t=/^(\d+)\.([^.]+(?:\.[^.]+)*)\.md$/;function G(e){return g(h(e,`series`))}function vt(e){let t=G(e);return l(t)?f(t).filter(e=>St(h(t,e))).map(e=>Ct(t,e)).sort((e,t)=>e.title.localeCompare(t.title,`fr`)):[]}function K(e){return f(e).map(Dt).filter(e=>e!==null).map(t=>wt(e,t)).sort((e,t)=>e.index-t.index)}function q(e){let t=K(e),n=h(e,`README.md`);m(n,yt(l(n)?d(n,`utf-8`):``,t,e),`utf-8`),r.success(`Generated series index: ${n}`)}function yt(e,t,n){let r=bt(t);if(e.includes(U)&&e.includes(W))return e.replace(xt(),r);let i=e.trimEnd();return i.length>0?`${i}\n\n${r}\n`:`# ${P(n.split(`/`).pop()||`series`)}\n\n${r}\n`}function bt(e){let t=[H,``,U];if(e.length===0)t.push(``,`_No articles yet._`);else{t.push(``);for(let n of e)t.push(`${n.index}. [${n.title}](./${n.fileName})`)}return t.push(``,W),t.join(`
8
+ `)}function xt(){return RegExp(`${F(H)}\n\n${F(U)}[\\s\\S]*?${F(W)}`)}function St(e){return p(e).isDirectory()}function Ct(e,t){let n=h(e,t);return{name:t,path:n,title:Tt(n,t)}}function wt(e,t){let n=h(e,t.fileName);return{...t,path:n,title:Et(n,t.slug)}}function Tt(e,t){let n=h(e,`README.md`);if(l(n)){let e=d(n,`utf-8`).split(`
9
+ `).map(e=>e.trim()).find(e=>e.startsWith(`# `));if(e)return e.slice(2).trim()}return P(t)}function Et(e,t){try{let{data:t}=y(d(e,`utf-8`)),n=typeof t.title==`string`?t.title.trim():``;if(n.length>0)return n}catch(t){r.warn(`Unable to read article title from ${e}: ${t}`)}return P(t)}function Dt(e){let t=e.match(_t);return t?{index:Number(t[1]),slug:t[2],fileName:e}:null}function Ot(e,t,n){let i=K(e),a=At(n,i.length),o=h(e,`${a}.${V(t)}.md`);if(l(o))throw Error(`Article already exists at ${o}`);return kt(e,i,a),m(o,y.stringify(``,{title:t}),`utf-8`),r.success(`Created article: ${o}`),q(e),o}function kt(e,t,n){let i=t.filter(e=>e.index>=n).sort((e,t)=>t.index-e.index);for(let t of i){let n=`${t.index+1}.${t.slug}.md`,i=h(e,n);ee(t.path,i),r.info(`Renamed ${t.fileName} → ${n}`)}}function At(e,t){if(!Number.isInteger(e))throw TypeError(`Article index must be an integer`);if(e<1||e>t+1)throw RangeError(`Article index must be between 1 and ${t+1}`);return e}function jt(e,t){let n=t.trim();if(n.length===0)throw Error(`Series title is required`);let i=V(n),a=G(e),o=h(a,i);if(u(a,{recursive:!0}),l(o))throw Error(`Series already exists: series/${i}`);return u(o),m(h(o,`README.md`),`# ${n}\n`,`utf-8`),q(o),r.success(`Created series: ${o}`),{name:i,path:o,title:n}}function Mt(e,t){let{content:n,frontmatter:i}=j(e),a=Ne(n,t);return M(e,{content:n,frontmatter:{...i,time:a}}),r.success(`Estimated time set to ${a} min: ${e}`),a}function Nt(e){I({path:e.path,relativePath:e.fileName},`series article`)}async function Pt(){return N({message:`What is the title of your series?`,placeholder:`Enter series title`,requiredMessage:`Title is required`})}async function Ft(){return N({message:`What is the title of your article?`,placeholder:`Enter article title`,requiredMessage:`Title is required`})}async function J(e){return Rt({entries:vt(e),message:`Which series do you want to work on?`,emptyMessage:`No series directories found`,resolveMessage:`Unable to resolve selected series`,buildOption:e=>({label:e.title,value:e.path,hint:`series/${e.name}`})})}async function It(e){let t=K(e),n=t.map(e=>({label:`Insert at #${e.index} · before ${e.title}`,value:String(e.index),hint:e.fileName}));n.push({label:`Append as #${t.length+1}`,value:String(t.length+1),hint:`Add the new article at the end of the series`});let r=A(await o({message:`Where should the new article be inserted?`,options:n}));return typeof r!=`string`&&k(`Unable to resolve selected insert position: ${String(r)}`),Number(r)}async function Lt(e){return Rt({entries:K(e),message:`Which series article do you want to update?`,emptyMessage:`No series articles found`,resolveMessage:`Unable to resolve selected series article`,buildOption:e=>({label:`#${e.index} · ${e.title}`,value:e.path,hint:e.fileName})})}async function Rt({entries:e,message:t,emptyMessage:n,resolveMessage:r,buildOption:i}){if(e.length===0&&k(n),e.length===1)return e[0];let a=A(await o({message:t,options:e.map(i)}));typeof a!=`string`&&k(`${r}: ${String(a)}`);let s=e.find(e=>e.path===a);return s||k(`${r}: ${a}`),s}async function zt(e){t(`Content Creation - Estimate Series Article Time`);let n=await E(),r=await J(e),i=await Lt(r.path),o=Mt(i.path,n.reading.wordsPerMinute);a(`✓ Estimated time updated for ${r.name}/${i.fileName}: ${o} min`)}async function Bt(e){t(`Content Creation - Create Series`),a(`✓ Series created: series/${jt(e,await Pt()).name}`)}async function Vt(e){t(`Content Creation - Create Series Article`);let n=await J(e),r=await Ft(),i=await It(n.path),o=Ot(n.path,r,i);a(`✓ Series article created: ${n.name}/${te(o)}`)}async function Ht(e){t(`Content Creation - Mark Series Article Ready`);let n=await E(),r=await J(e),i=await Lt(r.path);Nt(i);let o=Mt(i.path,n.reading.wordsPerMinute);a(`✓ Series article updated for ${r.name}/${i.fileName}: ready + ${o} min`)}async function Ut(e,t){let n=e.path,{content:i,frontmatter:a}=j(n);if(a.scheduled===!0)return r.warn(`This X publication is already scheduled. Skipping.`),!1;(!t.scheduling.automationEndpoint||!t.scheduling.cfAccessClientId||!t.scheduling.cfAccessClientSecret)&&k(`Scheduling configuration is missing. Please set AUTOMATION_ENDPOINT, CF_ACCESS_CLIENT_ID, and CF_ACCESS_CLIENT_SECRET in your .env file or config.`);let o=new Date(e.date);o=ae(o,w.hours),o=oe(o,w.minutes),o=se(o,w.seconds);let s={content:i.trim(),scheduleAt:o.getTime()};try{let e=await fetch(t.scheduling.automationEndpoint,{method:`POST`,headers:{"Content-Type":`application/json`,Accept:`application/json`,"CF-Access-Client-Id":t.scheduling.cfAccessClientId,"CF-Access-Client-Secret":t.scheduling.cfAccessClientSecret},body:JSON.stringify(s)});if(!e.ok){let t=await e.text();k(`Failed to schedule reminder: ${e.status} ${e.statusText}\n${t}`)}let o=await e.json();M(n,{content:i,frontmatter:{...a,scheduled:!0}});let c=new Date(o.scheduledAt);return r.success(`✓ X publication reminder scheduled`),r.info(`Reminder scheduled for: ${v(c,`yyyy-MM-dd HH:mm:ss`)} UTC`),!0}catch(e){k(`Failed to schedule X publication reminder: ${e instanceof Error?e.message:String(e)}`)}}async function Wt(e){t(`Content Creation - Schedule X Publication Reminder`);let n=await E();a(await Ut(await He(e),n)?`✓ X publication reminder scheduled`:`✓ X publication was already scheduled`)}const Y=new c;Y.name(fe).description(me).version(pe).showHelpAfterError();const X=Y.command(`content`);X.description(`Manage dated content`),X.command(`new`).description(`Create a dated content directory`).action(()=>Ke(e.cwd())),X.command(`upcoming`).description(`List upcoming publications`).action(()=>qe(e.cwd())),X.command(`rebuild-indexes`).description(`Rebuild all content index files`).action(()=>Je(e.cwd()));const Z=Y.command(`series`);Z.description(`Manage article series`),Z.command(`new`).description(`Create a new series`).action(()=>Bt(e.cwd()));const Q=Z.command(`article`).description(`Manage series articles`);Q.command(`new`).description(`Create a new article in a series`).action(()=>Vt(e.cwd())),Q.command(`ready`).description(`Mark a series article as ready`).action(()=>Ht(e.cwd())),Q.command(`estimate-time`).description(`Estimate the reading time of a series article`).action(()=>zt(e.cwd()));const $=Y.command(`linkedin`);$.description(`Manage LinkedIn publications`),$.command(`ready`).description(`Prepare a LinkedIn publication and mark it as ready`).action(()=>mt(e.cwd())),$.command(`publish-calendar`).description(`Publish the LinkedIn calendar`).action(()=>pt(e.cwd()));const Gt=Y.command(`x`);Gt.description(`Manage X publications`),Gt.command(`schedule-reminder`).description(`Schedule an X publication reminder`).action(()=>Wt(e.cwd()));const Kt=Y.command(`resource`);Kt.description(`Manage resources`),Kt.command(`new`).description(`Create a new resource`).action(()=>gt(e.cwd())),Y.parseAsync().catch(t=>{r.error(t instanceof Error?t.message:String(t)),e.exit(1)});export{};
package/dist/index.d.mts CHANGED
@@ -1,95 +1,33 @@
1
1
  //#region src/types.d.ts
2
2
 
3
- /**
4
- * Template configuration for LinkedIn
5
- */
6
3
  interface LinkedInTemplateConfig {
7
- /**
8
- * Path to a template file for the footer (resolved relative to templatesDir or config file location)
9
- */
10
4
  footerPath?: string;
11
5
  }
12
- /**
13
- * Template configuration for YouTube/Instagram
14
- */
15
6
  interface VideoTemplateConfig {
16
- /**
17
- * Path to a template file for the description blueprint (resolved relative to templatesDir or config file location)
18
- */
19
7
  templatePath?: string;
20
8
  }
21
- /**
22
- * Scheduling configuration
23
- */
24
9
  interface SchedulingConfig {
25
- /**
26
- * Automation endpoint URL
27
- */
28
10
  automationEndpoint?: string;
29
- /**
30
- * Cloudflare Access Client ID
31
- */
32
11
  cfAccessClientId?: string;
33
- /**
34
- * Cloudflare Access Client Secret
35
- */
36
12
  cfAccessClientSecret?: string;
37
13
  }
38
- /**
39
- * Calendar publishing configuration
40
- */
41
14
  interface CalendarPublishingConfig {
42
- /**
43
- * Public base URL used to build the Google Calendar subscription link
44
- */
45
15
  publicUrl?: string;
46
- /**
47
- * Token appended to the public calendar subscription URL
48
- */
49
16
  token?: string;
50
17
  }
51
- /**
52
- * Reading-time estimation configuration.
53
- */
54
18
  interface ReadingConfig {
55
- /**
56
- * Reading speed used when estimating content duration.
57
- * @default 100
58
- */
59
19
  wordsPerMinute?: number;
60
20
  }
61
- /**
62
- * Configuration for content-creation
63
- */
64
21
  interface Config {
65
- /**
66
- * List of thematic areas
67
- * @default []
68
- */
69
22
  thematic?: string[];
70
- /**
71
- * Base directory for external template files
72
- */
73
23
  templatesDir?: string;
74
- /**
75
- * Templates configuration per content type
76
- */
77
24
  templates?: {
78
25
  linkedin?: LinkedInTemplateConfig;
79
26
  youtube?: VideoTemplateConfig;
80
27
  instagram?: VideoTemplateConfig;
81
28
  };
82
- /**
83
- * Scheduling configuration
84
- */
85
29
  scheduling?: SchedulingConfig;
86
- /**
87
- * Calendar publishing configuration
88
- */
89
30
  calendar?: CalendarPublishingConfig;
90
- /**
91
- * Reading-time configuration for content metadata helpers.
92
- */
93
31
  reading?: ReadingConfig;
94
32
  }
95
33
  //#endregion
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@barbapapazes/content-creation",
3
3
  "type": "module",
4
- "version": "0.18.8",
4
+ "version": "0.19.0",
5
+ "description": "CLI tool for content creation and management",
5
6
  "author": "Estéban Soubiran <esteban@soubiran.dev>",
6
7
  "license": "MIT",
7
8
  "funding": "https://github.com/sponsors/Barbapapazes",
@@ -18,7 +19,7 @@
18
19
  "import": "./dist/index.mjs"
19
20
  }
20
21
  },
21
- "main": ".dist/index.mjs",
22
+ "main": "./dist/index.mjs",
22
23
  "types": "./dist/index.d.mts",
23
24
  "bin": {
24
25
  "content-creation": "./dist/cli.mjs"
@@ -32,7 +33,7 @@
32
33
  "dependencies": {
33
34
  "@clack/prompts": "^0.10.1",
34
35
  "c12": "^3.3.4",
35
- "cac": "^6.7.14",
36
+ "commander": "15.0.0-0",
36
37
  "date-fns": "^4.1.0",
37
38
  "gray-matter": "^4.0.3",
38
39
  "wrangler": "^4.61.1"