@barbapapazes/content-creation 0.13.0 β†’ 0.15.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.
Files changed (3) hide show
  1. package/README.md +179 -29
  2. package/dist/cli.mjs +2 -4
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,15 +1,18 @@
1
1
  # @barbapapazes/content-creation
2
2
 
3
- A CLI tool to streamline content creation by generating dated directories with content files.
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
5
  ## Features
6
6
 
7
7
  - Create dated directory structure (YYYY/MM/DD format)
8
+ - Support for multiple content types: LinkedIn, X, YouTube, Instagram
8
9
  - User-friendly date selection (up to 7 days ahead or custom date)
9
- - Automatic `linkedin.md` file creation in the day directory
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
10
14
  - Link images to LinkedIn post frontmatter
11
15
  - Create resource directories (articles, videos, audio)
12
- - Generate a markdown list of all LinkedIn posts ordered by date
13
16
 
14
17
  ## Installation
15
18
 
@@ -21,16 +24,69 @@ npm install -g @barbapapazes/content-creation
21
24
 
22
25
  ### Create Content Directory
23
26
 
24
- Create a dated directory with a LinkedIn post file:
27
+ Create a dated directory with content files for selected platforms:
25
28
 
26
29
  ```bash
27
30
  # Create content directory in current working directory
28
31
  content-creation
29
- # Result: 2026/01/01/linkedin.md
32
+ # Prompts you to select: LinkedIn, X, YouTube, and/or Instagram
30
33
 
31
34
  # Create content directory at a specific path
32
35
  content-creation --path /path/to/content
33
- # Result: /path/to/content/2026/01/01/linkedin.md
36
+ ```
37
+
38
+ The tool will prompt you to:
39
+ 1. Enter a title (shared across all selected types)
40
+ 2. Select one or more content types
41
+ 3. Answer type-specific questions:
42
+ - **LinkedIn**: Whether to include video and images
43
+ - **YouTube/Instagram**: Whether to create script and description files
44
+ 4. Choose a date
45
+
46
+ #### Content Files by Type
47
+
48
+ Each content type creates specific files:
49
+
50
+ - **LinkedIn**: `linkedin.md`, `linkedin-script.md` (if video planned)
51
+ - **X**: `x.md`
52
+ - **YouTube**: `youtube.md`, `youtube-script.md`, `youtube-description.md`
53
+ - **Instagram**: `instagram.md`, `instagram-script.md`, `instagram-description.md`
54
+
55
+ All main files (`*.md`) include frontmatter with at least a `title` field for indexing.
56
+
57
+ **Important**: Files are never overwritten. If a file already exists, it will be skipped.
58
+
59
+ ### Create Index Files
60
+
61
+ Generate index files for each content type found in your dated folders:
62
+
63
+ ```bash
64
+ # Generate index files in current directory
65
+ content-creation create-index
66
+
67
+ # Generate index files at a specific path
68
+ content-creation create-index --path /path/to/content
69
+ ```
70
+
71
+ This command scans your `YYYY/MM/DD` directory structure and creates:
72
+
73
+ - `linkedin-posts.md` - Index of all LinkedIn posts
74
+ - `x-posts.md` - Index of all X posts
75
+ - `youtube-videos.md` - Index of all YouTube videos
76
+ - `instagram-posts.md` - Index of all Instagram posts
77
+
78
+ Each index file contains:
79
+
80
+ ```markdown
81
+ # LinkedIn Posts
82
+
83
+ _Generated on 1/31/2026_
84
+
85
+ Total posts: 15
86
+
87
+ - [Understanding AI Agents](2026/01/20/linkedin.md) - _January 20, 2026_
88
+ - [Deep Dive into RAG](2026/01/15/linkedin.md) - _January 15, 2026_
89
+ ...
34
90
  ```
35
91
 
36
92
  ### Link Images
@@ -57,32 +113,38 @@ content-creation resource
57
113
  content-creation resource --path /path/to/resources
58
114
  ```
59
115
 
60
- ### List Posts
116
+ ### Schedule Reminder
61
117
 
62
- Generate a markdown file listing all LinkedIn posts ordered by date (most recent first):
118
+ Schedule a reminder for X (Twitter) content to be posted at 11:00 AM UTC on the date specified in the folder path:
63
119
 
64
120
  ```bash
65
- # Generate posts list in current directory
66
- content-creation list-posts
121
+ # Schedule reminder for X content
122
+ content-creation schedule-reminder
67
123
 
68
- # Generate posts list at a specific path
69
- content-creation list-posts --path /path/to/content
124
+ # Schedule reminder at a specific path
125
+ content-creation schedule-reminder --path /path/to/content
70
126
  ```
71
127
 
72
- This creates a `posts-list.md` file at the root with clickable links to each post:
128
+ The command will:
129
+ 1. Scan your directory for dated folders containing `x.md` files
130
+ 2. Prompt you to select which content to schedule
131
+ 3. Check if the content is already scheduled (exits early if `scheduled: true` in frontmatter)
132
+ 4. Extract the tweet content from `x.md`
133
+ 5. Calculate the scheduled time (11:00 AM UTC based on the folder date: YYYY/MM/DD)
134
+ 6. Send a POST request to your automation endpoint
135
+ 7. Update the frontmatter with `scheduled: true`
136
+ 8. Display a confirmation message with the scheduled date/time
73
137
 
74
- ```markdown
75
- # LinkedIn Posts
138
+ **Required Configuration**: You must set up the following environment variables in a `.env` file:
76
139
 
77
- _Generated on 1/21/2026_
78
-
79
- Total posts: 15
80
-
81
- - [Understanding AI Agents](2026/01/20/linkedin.md) - _January 20, 2026_
82
- - [Deep Dive into RAG](2026/01/15/linkedin.md) - _January 15, 2026_
83
- ...
140
+ ```bash
141
+ AUTOMATION_ENDPOINT=https://automation.soubiran.dev/trigger
142
+ CF_ACCESS_CLIENT_ID=your-client-id-here
143
+ CF_ACCESS_CLIENT_SECRET=your-client-secret-here
84
144
  ```
85
145
 
146
+ See `.env.example` for a template.
147
+
86
148
  ## Configuration
87
149
 
88
150
  You can configure the tool using a configuration file. Create one of the following files:
@@ -94,12 +156,55 @@ You can configure the tool using a configuration file. Create one of the followi
94
156
 
95
157
  ```typescript
96
158
  export default {
97
- // Array of thematic areas (currently unused, reserved for future features)
159
+ // Array of thematic areas (reserved for future features)
98
160
  thematic: ['JavaScript', 'TypeScript', 'Node.js'],
161
+
162
+ // Base directory for external template files (optional)
163
+ templatesDir: '~/.config/content-creation/templates',
164
+
165
+ // Templates configuration per content type
166
+ templates: {
167
+ linkedin: {
168
+ body: '', // Inline template for body content
169
+ bodyPath: 'linkedin-body.md', // Or path to template file
170
+ footer: '\n\n---\n\nCustom footer text', // Inline footer
171
+ footerPath: 'linkedin-footer.md', // Or path to footer file
172
+ },
173
+ x: {
174
+ body: '',
175
+ bodyPath: 'x-body.md',
176
+ },
177
+ youtube: {
178
+ body: '',
179
+ script: '',
180
+ description: '',
181
+ // Or use paths: bodyPath, scriptPath, descriptionPath
182
+ },
183
+ instagram: {
184
+ body: '',
185
+ script: '',
186
+ description: '',
187
+ },
188
+ },
189
+
190
+ // Scheduling configuration (optional, can also be set via environment variables)
191
+ scheduling: {
192
+ automationEndpoint: 'https://automation.soubiran.dev/trigger',
193
+ cfAccessClientId: 'your-client-id',
194
+ cfAccessClientSecret: 'your-client-secret',
195
+ },
99
196
  }
100
197
  ```
101
198
 
102
- ### Example Configuration
199
+ ### Template Resolution
200
+
201
+ For each template field (body, footer, script, description):
202
+
203
+ 1. If `*Path` is specified, it's resolved relative to `templatesDir` or the config file location
204
+ 2. Otherwise, the inline string value is used
205
+ 3. If neither is specified, defaults are used (LinkedIn has a default footer)
206
+
207
+ ### Example: Customizing LinkedIn Footer
103
208
 
104
209
  Create a `content-creation.config.ts` file:
105
210
 
@@ -107,15 +212,52 @@ Create a `content-creation.config.ts` file:
107
212
  import { defineConfig } from '@barbapapazes/content-creation'
108
213
 
109
214
  export default defineConfig({
110
- thematic: ['JavaScript', 'TypeScript', 'Node.js'],
215
+ templates: {
216
+ linkedin: {
217
+ footer: '\n\n---\n\nCustom signature here! πŸš€',
218
+ },
219
+ },
111
220
  })
112
221
  ```
113
222
 
114
- Or a JSON config file (`~/.content-creationrc`):
223
+ Or store templates in external files:
224
+
225
+ ```typescript
226
+ import { defineConfig } from '@barbapapazes/content-creation'
227
+
228
+ export default defineConfig({
229
+ templatesDir: '~/.config/content-creation/templates',
230
+ templates: {
231
+ linkedin: {
232
+ footerPath: 'linkedin-footer.md',
233
+ },
234
+ youtube: {
235
+ descriptionPath: 'youtube-description.md',
236
+ },
237
+ },
238
+ })
239
+ ```
240
+
241
+ Then create `~/.config/content-creation/templates/linkedin-footer.md`:
242
+
243
+ ```markdown
244
+ ---
245
+
246
+ Subscribe for more content! 🎯
247
+ ```
248
+
249
+ ### JSON Configuration
250
+
251
+ For a JSON config file (`~/.content-creationrc`):
115
252
 
116
253
  ```json
117
254
  {
118
- "thematic": ["JavaScript", "TypeScript", "Node.js"]
255
+ "thematic": ["JavaScript", "TypeScript", "Node.js"],
256
+ "templates": {
257
+ "linkedin": {
258
+ "footer": "\n\n---\n\nFollow for more! πŸš€"
259
+ }
260
+ }
119
261
  }
120
262
  ```
121
263
 
@@ -128,10 +270,18 @@ base-path/
128
270
  └── YYYY/ # Year folder
129
271
  └── MM/ # Month folder
130
272
  └── DD/ # Day folder
131
- └── linkedin.md
273
+ β”œβ”€β”€ linkedin.md
274
+ β”œβ”€β”€ linkedin-script.md
275
+ β”œβ”€β”€ x.md
276
+ β”œβ”€β”€ youtube.md
277
+ β”œβ”€β”€ youtube-script.md
278
+ β”œβ”€β”€ youtube-description.md
279
+ β”œβ”€β”€ instagram.md
280
+ β”œβ”€β”€ instagram-script.md
281
+ └── instagram-description.md
132
282
  ```
133
283
 
134
- Example: Running the CLI on January 1, 2026 will create `2026/01/01/linkedin.md`
284
+ Example: Running the CLI on January 31, 2026 and selecting all types will create a `2026/01/31/` directory with all the relevant files.
135
285
 
136
286
  ## License
137
287
 
package/dist/cli.mjs CHANGED
@@ -1,9 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import e from"node:process";import{intro as t,isCancel as n,log as r,outro as i,select as a,text as o}from"@clack/prompts";import{cac as s}from"cac";import{addDays as c,format as l,parse as u}from"date-fns";import{existsSync as d,mkdirSync as f,readFileSync as p,readdirSync as m,statSync as h,writeFileSync as g}from"node:fs";import{join as _,resolve as v}from"node:path";import y from"gray-matter";var b=`0.13.0`;function x(t,n,i,a,o=e.cwd()){let s=v(_(o,l(t,`yyyy`),l(t,`MM`),l(t,`dd`))),c=_(s,`linkedin.md`);if(d(c))return r.info(`linkedin.md already exists at: ${c}`),s;f(s,{recursive:!0}),r.success(`Created directory: ${s}`);let u={title:n};if(i&&(u.video=!0),a&&(u.images=[null]),g(c,y.stringify(`
2
+ import e from"node:process";import{intro as t,isCancel as n,log as r,multiselect as i,outro as a,select as o,text as s}from"@clack/prompts";import{cac as c}from"cac";import{addDays as l,format as u,parse as d,setHours as f,setMinutes as p,setSeconds as m}from"date-fns";import{existsSync as h,mkdirSync as g,readFileSync as _,readdirSync as v,statSync as y,writeFileSync as b}from"node:fs";import{dirname as ee,isAbsolute as x,join as S,resolve as C}from"node:path";import w from"gray-matter";import{homedir as T}from"node:os";import{loadConfig as E}from"c12";var te=`0.15.0`;const D={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`]}},O={thematic:[],templatesDir:void 0,templates:{},scheduling:{}};function k(t,n){return t?x(t)?t:C(T(),t):n?ee(n):e.cwd()}function A(e,t){let n={};if(e?.footerPath){let r=C(t,e.footerPath);h(r)&&(n.footerPath=r)}return n}function j(e,t){let n={};if(e?.templatePath){let r=C(t,e.templatePath);h(r)&&(n.templatePath=r)}return n}async function M(){let{config:t,configFile:n}=await E({name:`content-creation`,defaults:O,globalRc:!0,dotenv:!0}),r=k(t.templatesDir,n),i={linkedin:A(t.templates?.linkedin,r),youtube:j(t.templates?.youtube,r),instagram:j(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};return{thematic:t.thematic||[],templatesDir:t.templatesDir||``,templates:i,scheduling:a}}function N(){return`
3
3
 
4
4
  ---
5
5
 
6
6
  Je m'appelle EstΓ©ban, et pour toi, je dΓ©code l'IA pour t'aider Γ  passer Γ  l'action. Abonne-toi ! πŸ› οΈ
7
- `,u),`utf-8`),r.success(`Created linkedin.md at: ${c}`),i){let e=_(s,`linkedin-script.md`);g(e,``,`utf-8`),r.success(`Created linkedin-script.md at: ${e}`)}return s}function S(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 C(t,n,i=e.cwd()){let a=S(t),o=_(v(_(i,`resources`)),a),s=`${n}.md`,c=_(o,s);if(d(c))return r.info(`${s} already exists at: ${c}`),o;f(o,{recursive:!0}),r.success(`Created directory: ${o}`);let l={title:t,url:``,date:``};return g(c,y.stringify(``,l),`utf-8`),r.success(`Created ${s} at: ${c}`),o}function w(t=e.cwd()){let n=[];try{let e=m(t).filter(e=>h(_(t,e)).isDirectory()&&/^\d{4}$/.test(e));for(let r of e){let e=_(t,r),i=m(e).filter(t=>h(_(e,t)).isDirectory()&&/^\d{2}$/.test(t));for(let t of i){let i=_(e,t),a=m(i).filter(e=>h(_(i,e)).isDirectory()&&/^\d{2}$/.test(e));for(let e of a){let a=_(i,e);if(d(_(a,`linkedin.md`))){let i=`${r}-${t}-${e}`;n.push({path:a,date:new Date(i),displayPath:`${r}/${t}/${e}`})}}}}}catch(e){return r.error(`Error reading directories: ${e}`),[]}return n.sort((e,t)=>t.date.getTime()-e.date.getTime()).slice(0,8)}function T(e){let t=[`.jpg`,`.jpeg`,`.png`],n=[];try{let r=m(e);for(let i of r)if(h(_(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 E(e,t){try{let{data:n,content:i}=y(p(e,`utf-8`));n.images=t.length>0?t:[null],g(e,y.stringify(i,n),`utf-8`),r.success(`Updated ${e} with ${t.length} image(s)`)}catch(e){r.error(`Error updating frontmatter: ${e}`)}}async function D(t){if(t.length===0)return r.warn(`No dated folders with linkedin.md found`),null;let i=await a({message:`Select a folder to link images:`,options:t.map(e=>({label:e.displayPath,value:e.path,hint:`Link images in ${e.displayPath}`}))});return n(i)&&(r.error(`Operation cancelled.`),e.exit(0)),t.find(e=>e.path===i)||null}async function O(t=e.cwd()){let n=w(t);if(n.length===0){r.warn(`No dated folders with linkedin.md found`);return}let i=await D(n);if(!i)return;let a=T(i.path);if(a.length===0){r.info(`No images found in ${i.displayPath}`),E(_(i.path,`linkedin.md`),[]);return}r.info(`Found ${a.length} image(s): ${a.join(`, `)}`),E(_(i.path,`linkedin.md`),a)}function k(t=e.cwd()){let n=[];try{let e=m(t).filter(e=>h(_(t,e)).isDirectory()&&/^\d{4}$/.test(e));for(let i of e){let e=_(t,i),a=m(e).filter(t=>h(_(e,t)).isDirectory()&&/^\d{2}$/.test(t));for(let t of a){let a=_(e,t),o=m(a).filter(e=>h(_(a,e)).isDirectory()&&/^\d{2}$/.test(e));for(let e of o){let o=_(_(a,e),`linkedin.md`);if(d(o)){let a=`${i}-${t}-${e}`,s=new Date(a);try{let{data:r}=y(p(o,`utf-8`)),a=r.title||`Untitled`;n.push({path:o,date:s,title:a,relativePath:`${i}/${t}/${e}/linkedin.md`})}catch(e){r.warn(`Error reading ${o}: ${e}`)}}}}}}catch(e){return r.error(`Error reading directories: ${e}`),[]}return n.sort((e,t)=>t.date.getTime()-e.date.getTime())}function A(t=e.cwd()){let n=k(t);if(n.length===0){r.warn(`No LinkedIn posts found in the directory structure`);return}let i=`# LinkedIn Posts
8
-
9
- `;i+=`_Generated on ${new Date().toLocaleDateString()}_\n\n`,i+=`Total posts: ${n.length}\n\n`;for(let e of n){let t=e.date.toLocaleDateString(`en-US`,{year:`numeric`,month:`long`,day:`numeric`});i+=`- [${e.title}](${e.relativePath}) - _${t}_\n`}let a=_(t,`posts-list.md`);g(a,i,`utf-8`),r.success(`Created posts list at: ${a}`),r.info(`Total posts listed: ${n.length}`)}async function j(){let t=new Date,i=[];for(let e=0;e<7;e++){let n=l(c(t,e),`yyyy-MM-dd`),r=n,a=`Create content for ${n}`;e===0?(r=`Today (${n})`,a=`Create content for today`):e===1?(r=`Tomorrow (${n})`,a=`Create content for tomorrow`):(r=`In ${e} days (${n})`,a=`Create content for ${n}`),i.push({label:r,value:`day-${e}`,hint:a})}i.push({label:`Custom date`,value:`custom`,hint:`Enter a specific date`});let s=await a({message:`When do you want to create content?`,options:i});if(n(s)&&(r.error(`Operation cancelled.`),e.exit(0)),s!==`custom`)return c(t,Number.parseInt(s.split(`-`)[1]));let d=await o({message:`Enter date (YYYY-MM-DD):`,placeholder:l(t,`yyyy-MM-dd`),validate:e=>{if(!e)return`Date is required`;if(!/^\d{4}-\d{2}-\d{2}$/.test(e))return`Invalid date format. Please use YYYY-MM-DD`;try{let t=u(e,`yyyy-MM-dd`,new Date);if(Number.isNaN(t.getTime()))return`Invalid date`}catch{return`Invalid date`}}});return n(d)&&(r.error(`Operation cancelled.`),e.exit(0)),u(d,`yyyy-MM-dd`,new Date)}async function M(){let t=await o({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 N(){let t=await a({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 P(){let t=await a({message:`Are there images?`,options:[{label:`Yes`,value:!0,hint:`Add images section to frontmatter`},{label:`No`,value:!1,hint:`No images metadata`}]});return n(t)&&(r.error(`Operation cancelled.`),e.exit(0)),t}async function F(){let t=await a({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}const I=s(`content-creation`);I.command(`[path]`,`Create a dated content directory with linkedin.md file`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).action(async(n,r)=>{t(`Content Creation - Create dated content directory`);let a=await M(),o=await N(),s=await P(),c=await j();x(c,a,o,s,n||r?.path||e.cwd()),i(`βœ“ Content directory created: ${l(c,`yyyy/MM/dd`)}/linkedin.md`)}),I.command(`link-images [path]`,`Link images to LinkedIn frontmatter in recent folders`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).action(async(n,r)=>{t(`Content Creation - Link Images to LinkedIn`),await O(n||r?.path||e.cwd()),i(`βœ“ Images linked successfully`)}),I.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 a=await M(),o=await F();C(a,o,n||r?.path||e.cwd()),i(`βœ“ Resource created: resources/${S(a)}/${o}.md`)}),I.command(`list-posts [path]`,`Create or update a markdown file listing all LinkedIn posts`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).action(async(n,r)=>{t(`Content Creation - List Posts`),A(n||r?.path||e.cwd()),i(`βœ“ Posts list generated successfully`)}),I.help(),I.version(b),I.parse();export{};
7
+ `}function P(e){return h(e)?_(e,`utf-8`):``}function F(t,n,i,a,o=e.cwd()){let s=C(S(o,u(t,`yyyy`),u(t,`MM`),u(t,`dd`)));h(s)||(g(s,{recursive:!0}),r.success(`Created directory: ${s}`));for(let e of n)I(e,s,i,a);return s}function I(e,t,n,i){let a=D[e],o=S(t,a.mainFile);if(h(o)?r.info(`${a.mainFile} already exists, skipping`):L(e,o,n,i),a.additionalFiles)for(let o of a.additionalFiles){let a=S(t,o);z(o,e,n)&&(h(a)?r.info(`${o} already exists, skipping`):R(e,a,o,i))}}function L(e,t,n,i){let a={title:n.title};e===`linkedin`&&(n.hasVideo&&(a.video=!0),n.hasImages&&(a.images=[null]));let o=``;if(e===`linkedin`){let e=i.templates.linkedin?.footerPath;o=e?P(e):N()}b(t,w.stringify(o,a),`utf-8`),r.success(`Created ${t}`)}function R(e,t,n,i){let a=``;if(n.endsWith(`-description.md`)){let t=i.templates[e]?.templatePath;t&&(a=P(t))}b(t,a,`utf-8`),r.success(`Created ${t}`)}function z(e,t,n){return t===`linkedin`&&e===`linkedin-script.md`?n.hasVideo||!1:!0}function B(e,t){let n=[],i=D[t].mainFile;try{let t=v(e).filter(t=>y(S(e,t)).isDirectory()&&/^\d{4}$/.test(t));for(let a of t){let t=S(e,a),o=v(t).filter(e=>y(S(t,e)).isDirectory()&&/^\d{2}$/.test(e));for(let e of o){let o=S(t,e),s=v(o).filter(e=>y(S(o,e)).isDirectory()&&/^\d{2}$/.test(e));for(let t of s){let s=S(S(o,t),i);if(h(s)){let o=`${a}-${e}-${t}`,c=new Date(o);try{let{data:r}=w(_(s,`utf-8`)),l=r.title||`Untitled (${o})`;n.push({path:s,date:c,title:l,relativePath:`${a}/${e}/${t}/${i}`})}catch(e){r.warn(`Error reading ${s}: ${e}`)}}}}}}catch(e){return r.error(`Error reading directories: ${e}`),[]}return n.sort((e,t)=>t.date.getTime()-e.date.getTime())}function V(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 H(e){switch(e){case`linkedin`:return`LinkedIn Posts`;case`x`:return`X Posts`;case`youtube`:return`YouTube Videos`;case`instagram`:return`Instagram Posts`}}function U(e,t){let n=B(e,t);if(n.length===0){r.info(`No ${t} posts found`);return}let i=H(t),a=V(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`}b(S(e,a),o,`utf-8`),r.success(`Created ${a} with ${n.length} posts`)}function ne(t=e.cwd(),n){let r=n||[`linkedin`,`x`,`youtube`,`instagram`];for(let e of r)U(t,e)}function W(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 G(t,n,i=e.cwd()){let a=W(t),o=S(C(S(i,`resources`)),a),s=`${n}.md`,c=S(o,s);if(h(c))return r.info(`${s} already exists at: ${c}`),o;g(o,{recursive:!0}),r.success(`Created directory: ${o}`);let l={title:t,url:``,date:``};return b(c,w.stringify(``,l),`utf-8`),r.success(`Created ${s} at: ${c}`),o}function K(t=e.cwd()){let n=[];try{let e=v(t).filter(e=>y(S(t,e)).isDirectory()&&/^\d{4}$/.test(e));for(let r of e){let e=S(t,r),i=v(e).filter(t=>y(S(e,t)).isDirectory()&&/^\d{2}$/.test(t));for(let t of i){let i=S(e,t),a=v(i).filter(e=>y(S(i,e)).isDirectory()&&/^\d{2}$/.test(e));for(let e of a){let a=S(i,e);if(h(S(a,`linkedin.md`))){let i=`${r}-${t}-${e}`;n.push({path:a,date:new Date(i),displayPath:`${r}/${t}/${e}`})}}}}}catch(e){return r.error(`Error reading directories: ${e}`),[]}return n.sort((e,t)=>t.date.getTime()-e.date.getTime()).slice(0,8)}function q(e){let t=[`.jpg`,`.jpeg`,`.png`],n=[];try{let r=v(e);for(let i of r)if(y(S(e,i)).isFile()){let e=i.toLowerCase().substring(i.lastIndexOf(`.`));t.includes(e)&&n.push(i)}}catch(e){r.error(`Error reading directory: ${e}`)}return n.sort()}function J(e,t){try{let{data:n,content:i}=w(_(e,`utf-8`));n.images=t.length>0?t:[null],b(e,w.stringify(i,n),`utf-8`),r.success(`Updated ${e} with ${t.length} image(s)`)}catch(e){r.error(`Error updating frontmatter: ${e}`)}}async function Y(t){if(t.length===0)return r.warn(`No dated folders with linkedin.md found`),null;let i=await o({message:`Select a folder to link images:`,options:t.map(e=>({label:e.displayPath,value:e.path,hint:`Link images in ${e.displayPath}`}))});return n(i)&&(r.error(`Operation cancelled.`),e.exit(0)),t.find(e=>e.path===i)||null}async function X(t=e.cwd()){let n=K(t);if(n.length===0){r.warn(`No dated folders with linkedin.md found`);return}let i=await Y(n);if(!i)return;let a=q(i.path);if(a.length===0){r.info(`No images found in ${i.displayPath}`),J(S(i.path,`linkedin.md`),[]);return}r.info(`Found ${a.length} image(s): ${a.join(`, `)}`),J(S(i.path,`linkedin.md`),a)}async function Z(t,n){let i=S(t,`x.md`),{data:a,content:o}=w(_(i,`utf-8`));a.scheduled===!0&&(r.warn(`This content is already scheduled. Skipping.`),e.exit(0)),(!n.scheduling.automationEndpoint||!n.scheduling.cfAccessClientId||!n.scheduling.cfAccessClientSecret)&&(r.error(`Scheduling configuration is missing. Please set AUTOMATION_ENDPOINT, CF_ACCESS_CLIENT_ID, and CF_ACCESS_CLIENT_SECRET in your .env file or config.`),e.exit(1));let s=t.split(`/`),c=s[s.length-1],l=s[s.length-2],h=d(`${s[s.length-3]}-${l}-${c}`,`yyyy-MM-dd`,new Date);h=f(h,11),h=p(h,0),h=m(h,0);let g=h.getTime(),v={content:o.trim(),scheduleAt:g};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(v)});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};b(i,w.stringify(o,c),`utf-8`);let l=new Date(s.scheduledAt);r.success(`βœ“ Reminder scheduled successfully!`),r.info(`Scheduled for: ${u(l,`yyyy-MM-dd HH:mm:ss`)} UTC`)}catch(t){r.error(`Failed to schedule reminder: ${t instanceof Error?t.message:String(t)}`),e.exit(1)}}async function re(){let t=new Date,i=[];for(let e=0;e<7;e++){let n=l(t,e),r=u(n,`yyyy-MM-dd`),a=u(n,`EEEE`),o=r,s=`Create content for ${a}, ${r}`;e===0?(o=`Today - ${a} (${r})`,s=`Create content for today (${a})`):e===1?(o=`Tomorrow - ${a} (${r})`,s=`Create content for tomorrow (${a})`):(o=`In ${e} days - ${a} (${r})`,s=`Create content for ${a}, ${r}`),i.push({label:o,value:`day-${e}`,hint:s})}i.push({label:`Custom date`,value:`custom`,hint:`Enter a specific date`});let a=await o({message:`When do you want to create content?`,options:i});if(n(a)&&(r.error(`Operation cancelled.`),e.exit(0)),a!==`custom`)return l(t,Number.parseInt(a.split(`-`)[1]));let c=await s({message:`Enter date (YYYY-MM-DD):`,placeholder:u(t,`yyyy-MM-dd`),validate:e=>{if(!e)return`Date is required`;if(!/^\d{4}-\d{2}-\d{2}$/.test(e))return`Invalid date format. Please use YYYY-MM-DD`;try{let t=d(e,`yyyy-MM-dd`,new Date);if(Number.isNaN(t.getTime()))return`Invalid date`}catch{return`Invalid date`}}});return n(c)&&(r.error(`Operation cancelled.`),e.exit(0)),d(c,`yyyy-MM-dd`,new Date)}async function Q(){let t=await s({message:`What is the title of your content?`,placeholder:`Enter content title`,validate:e=>{if(!e||e.trim().length===0)return`Title is required`}});return n(t)&&(r.error(`Operation cancelled.`),e.exit(0)),t.trim()}async function ie(){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 ae(){let t=await o({message:`Are there images?`,options:[{label:`Yes`,value:!0,hint:`Add images section to frontmatter`},{label:`No`,value:!1,hint:`No images metadata`}]});return n(t)&&(r.error(`Operation cancelled.`),e.exit(0)),t}async function oe(){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 se(){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 ce(t){let i=[];h(t)||(r.error(`Base path does not exist: ${t}`),e.exit(1));let a=v(t).filter(e=>y(S(t,e)).isDirectory()&&/^\d{4}$/.test(e));for(let e of a){let n=S(t,e),r=v(n).filter(e=>y(S(n,e)).isDirectory()&&/^\d{2}$/.test(e));for(let t of r){let r=S(n,t),a=v(r).filter(e=>y(S(r,e)).isDirectory()&&/^\d{2}$/.test(e));for(let n of a){let a=S(r,n);if(h(S(a,`x.md`))){let r=`${e}-${t}-${n}`;i.push({path:a,date:r,title:``})}}}}i.length===0&&(r.error(`No x.md files found in dated folders`),e.exit(1)),i.sort((e,t)=>t.date.localeCompare(e.date));let s=await o({message:`Which content do you want to schedule a reminder for?`,options:i.map(e=>({label:e.date,value:e.path,hint:`Schedule reminder for ${e.date}`}))});return n(s)&&(r.error(`Operation cancelled.`),e.exit(0)),s}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 M(),o=await Q(),s=await se(),c={title:o};s.includes(`linkedin`)&&(c.hasVideo=await ie(),c.hasImages=await ae());let l=await re();F(l,s,c,i,n||r?.path||e.cwd());let d=s.join(`, `);a(`βœ“ Content directory created: ${u(l,`yyyy/MM/dd`)} (${d})`)}),$.command(`link-images [path]`,`Link images to LinkedIn frontmatter in recent folders`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).action(async(n,r)=>{t(`Content Creation - Link Images to LinkedIn`),await X(n||r?.path||e.cwd()),a(`βœ“ Images linked successfully`)}),$.command(`resource [path]`,`Create a resource with article/video/audio markdown file`).option(`--path <path>`,`Base path for resource directory (defaults to current directory)`).action(async(n,r)=>{t(`Content Creation - Create Resource`);let i=await Q(),o=await oe();G(i,o,n||r?.path||e.cwd()),a(`βœ“ Resource created: resources/${W(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`),ne(n||r?.path||e.cwd()),a(`βœ“ Index files generated successfully`)}),$.command(`schedule-reminder [path]`,`Schedule a reminder for X content`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).action(async(n,r)=>{t(`Content Creation - Schedule X Reminder`);let i=await M();await Z(await ce(n||r?.path||e.cwd()),i),a(`βœ“ Reminder scheduled successfully`)}),$.help(),$.version(te),$.parse();export{};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@barbapapazes/content-creation",
3
3
  "type": "module",
4
- "version": "0.13.0",
4
+ "version": "0.15.0",
5
5
  "author": "EstΓ©ban Soubiran <esteban@soubiran.dev>",
6
6
  "license": "MIT",
7
7
  "funding": "https://github.com/sponsors/Barbapapazes",