@barbapapazes/content-creation 0.14.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.
- package/README.md +179 -29
- package/dist/cli.mjs +2 -4
- 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
|
-
-
|
|
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
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
###
|
|
116
|
+
### Schedule Reminder
|
|
61
117
|
|
|
62
|
-
|
|
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
|
-
#
|
|
66
|
-
content-creation
|
|
121
|
+
# Schedule reminder for X content
|
|
122
|
+
content-creation schedule-reminder
|
|
67
123
|
|
|
68
|
-
#
|
|
69
|
-
content-creation
|
|
124
|
+
# Schedule reminder at a specific path
|
|
125
|
+
content-creation schedule-reminder --path /path/to/content
|
|
70
126
|
```
|
|
71
127
|
|
|
72
|
-
|
|
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
|
-
|
|
75
|
-
# LinkedIn Posts
|
|
138
|
+
**Required Configuration**: You must set up the following environment variables in a `.env` file:
|
|
76
139
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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 (
|
|
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
|
-
###
|
|
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
|
-
|
|
215
|
+
templates: {
|
|
216
|
+
linkedin: {
|
|
217
|
+
footer: '\n\n---\n\nCustom signature here! π',
|
|
218
|
+
},
|
|
219
|
+
},
|
|
111
220
|
})
|
|
112
221
|
```
|
|
113
222
|
|
|
114
|
-
Or
|
|
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
|
-
|
|
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
|
|
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,
|
|
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=c(t,e),r=l(n,`yyyy-MM-dd`),a=l(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 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