@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 +26 -430
- package/dist/cli.mjs +8 -6
- package/dist/index.d.mts +0 -62
- package/package.json +4 -3
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
186
|
-
content-creation
|
|
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
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3
|
-
`)}function
|
|
4
|
-
`).
|
|
5
|
-
`)}function
|
|
6
|
-
`)}function
|
|
7
|
-
`)}\r\n`}function
|
|
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.
|
|
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": "
|
|
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
|
-
"
|
|
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"
|