@barbapapazes/content-creation 0.18.9 → 0.20.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 +31 -429
- package/dist/cli.mjs +1 -6
- package/dist/index.d.mts +190 -64
- package/dist/index.mjs +1 -1
- package/dist/x-dKYi_aTZ.mjs +9 -0
- package/package.json +8 -6
package/README.md
CHANGED
|
@@ -2,454 +2,56 @@
|
|
|
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
|
|
179
|
-
|
|
180
|
-
Compute the reading duration of a numbered series article and store it in the `time` frontmatter field.
|
|
181
|
-
|
|
182
|
-
The estimate ignores frontmatter and HTML comments, assumes 5 characters per word, and uses `100` words per minute by default.
|
|
183
|
-
|
|
184
|
-
```bash
|
|
185
|
-
# Interactive mode
|
|
186
|
-
content-creation series estimate-time
|
|
187
|
-
|
|
188
|
-
# Fully scripted mode
|
|
189
|
-
content-creation series estimate-time \
|
|
190
|
-
--series my-series-slug \
|
|
191
|
-
--file 6.what-tool-calling-really-changes.md \
|
|
192
|
-
--wpm 120
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
This command will:
|
|
196
|
-
|
|
197
|
-
1. prompt you for the series and the article file when needed;
|
|
198
|
-
2. estimate the reading time in whole minutes from the markdown body only;
|
|
199
|
-
3. ignore frontmatter and HTML comments during the calculation;
|
|
200
|
-
4. write the resulting value into `time` in the article frontmatter.
|
|
201
|
-
|
|
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
|
-
```
|
|
212
|
-
|
|
213
|
-
This command will:
|
|
214
|
-
1. Scan your `YYYY/MM/DD` directory structure for `linkedin.md` files only
|
|
215
|
-
2. Read the LinkedIn frontmatter and generate all-day calendar events
|
|
216
|
-
3. Stream the generated calendar directly to Cloudflare R2 using Wrangler
|
|
217
|
-
4. Upload `content-creation.ics` to the `content-creation` R2 bucket
|
|
218
|
-
5. Print the final subscription URL for Google Calendar
|
|
219
|
-
|
|
220
|
-
**Required Configuration**: You must set up the following environment variables in a `.env` file:
|
|
221
|
-
|
|
222
|
-
```bash
|
|
223
|
-
CALENDAR_PUBLIC_URL=https://calendar.soubiran.dev
|
|
224
|
-
CALENDAR_TOKEN=your-calendar-token-here
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
See `.env.example` for a template. The printed URL uses the format:
|
|
228
|
-
|
|
229
|
-
```text
|
|
230
|
-
https://calendar.soubiran.dev/content-creation.ics?token=<your-calendar-token>
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
**Current Scope**: This command publishes LinkedIn entries only for now.
|
|
234
|
-
|
|
235
|
-
### Link Images
|
|
236
|
-
|
|
237
|
-
Scan recent LinkedIn posts and link images to their frontmatter:
|
|
238
|
-
|
|
239
|
-
```bash
|
|
240
|
-
content-creation linkedin link-images
|
|
241
|
-
|
|
242
|
-
# Link images at a specific path
|
|
243
|
-
content-creation linkedin link-images --path /path/to/content
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
### Create Resource
|
|
247
|
-
|
|
248
|
-
Create a resource directory with article, video, or audio content:
|
|
249
|
-
|
|
250
|
-
```bash
|
|
251
|
-
# Preferred grouped command
|
|
252
|
-
content-creation resource create
|
|
253
|
-
|
|
254
|
-
# Create resource at a specific path
|
|
255
|
-
content-creation resource create --path /path/to/resources
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
### Schedule Reminder
|
|
11
|
+
## CLI help
|
|
259
12
|
|
|
260
|
-
|
|
13
|
+
Use the built-in help to discover the available commands and subcommands:
|
|
261
14
|
|
|
262
15
|
```bash
|
|
263
|
-
content-creation
|
|
264
|
-
|
|
265
|
-
# Schedule reminder at a specific path
|
|
266
|
-
content-creation x schedule-reminder --path /path/to/content
|
|
16
|
+
content-creation --help
|
|
17
|
+
content-creation <command> --help
|
|
267
18
|
```
|
|
268
19
|
|
|
269
|
-
|
|
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:
|
|
20
|
+
## Commands
|
|
280
21
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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 video process` — process a series video into subtitles and optional thumbnails
|
|
27
|
+
- `series article new` — insert a new article in a series
|
|
28
|
+
- `series article ready` — mark a series article as ready and refresh reading time
|
|
29
|
+
- `series article estimate-time` — compute reading time for a series article
|
|
30
|
+
- `linkedin ready` — link images, mark the post ready, and mark the script ready when present
|
|
31
|
+
- `linkedin publish-calendar` — publish the LinkedIn calendar to Cloudflare R2
|
|
32
|
+
- `x schedule-reminder` — schedule an X reminder through the automation endpoint
|
|
33
|
+
- `resource new` — create a new resource directory
|
|
304
34
|
|
|
305
35
|
## Configuration
|
|
306
36
|
|
|
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
|
-
},
|
|
37
|
+
The package reads configuration from:
|
|
346
38
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
cfAccessClientId: 'your-client-id',
|
|
351
|
-
cfAccessClientSecret: 'your-client-secret',
|
|
352
|
-
},
|
|
39
|
+
- `content-creation.config.{ts,js,mjs,json}` in your project
|
|
40
|
+
- `~/.content-creationrc`
|
|
41
|
+
- environment variables loaded from `.env`
|
|
353
42
|
|
|
354
|
-
|
|
355
|
-
calendar: {
|
|
356
|
-
publicUrl: 'https://calendar.soubiran.dev',
|
|
357
|
-
token: 'your-calendar-token',
|
|
358
|
-
},
|
|
43
|
+
Environment variables used by advanced workflows:
|
|
359
44
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
```
|
|
45
|
+
- `CALENDAR_PUBLIC_URL`
|
|
46
|
+
- `CALENDAR_TOKEN`
|
|
47
|
+
- `AUTOMATION_ENDPOINT`
|
|
48
|
+
- `CF_ACCESS_CLIENT_ID`
|
|
49
|
+
- `CF_ACCESS_CLIENT_SECRET`
|
|
50
|
+
- `OPENAI_API_KEY`
|
|
51
|
+
- `OPENAI_TRANSCRIPTION_LANGUAGE`
|
|
52
|
+
- `OPENAI_TRANSCRIPTION_MODEL`
|
|
451
53
|
|
|
452
|
-
|
|
54
|
+
For series video processing, the command scans the selected series directory recursively for `.mp4` files, generates an `.srt` transcription next to the chosen video, and can optionally render thumbnails into a `thumbnails/` subdirectory.
|
|
453
55
|
|
|
454
56
|
## License
|
|
455
57
|
|
package/dist/cli.mjs
CHANGED
|
@@ -1,7 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{
|
|
3
|
-
`)}function be(e,t){let n=b(e,`README.md`);if(p(n)){let e=h(n,`utf-8`).split(`
|
|
4
|
-
`).map(e=>e.trim()).find(e=>e.startsWith(`# `));if(e)return e.slice(2).trim()}return B(t)}function xe(e,t){try{let{data:t}=C(h(e,`utf-8`)),n=typeof t.title==`string`?t.title.trim():``;if(n.length>0)return n}catch(t){r.warn(`Unable to read article title from ${e}: ${t}`)}return B(t)}function Se(e){let t=e.match(/^(\d+)\.([^.]+(?:\.[^.]+)*)\.md$/);return t?{index:Number(t[1]),slug:t[2],fileName:e}:null}function B(e){return e.split(`-`).filter(Boolean).map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join(` `)}function V(e){return e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`)}const H=`load-more`;async function Ce(){let e=new Date,t=7;for(;;){let i=await o({message:`When do you want to create content?`,options:we(e,t)});if(n(i)&&(r.error(`Operation cancelled.`),S.exit(0)),i===`load-more`){t+=7;continue}return u(i)}}function we(e,t){let n=[];for(let r=0;r<t;r++){let t=c(e,r),i=l(t,`yyyy-MM-dd`),a=l(t,`EEEE`),o=i,s=`Create content for ${a}, ${i}`;r===0?(o=`Today - ${a} (${i})`,s=`Create content for today (${a})`):r===1?(o=`Tomorrow - ${a} (${i})`,s=`Create content for tomorrow (${a})`):(o=`In ${r} days - ${a} (${i})`,s=`Create content for ${a}, ${i}`),n.push({label:o,value:i,hint:s})}return n.push({label:`Show 7 more dates (${t+1}-${t+7} days ahead)`,value:`load-more`,hint:`Extend the list up to ${l(c(e,t+6),`yyyy-MM-dd`)}`}),n}async function U(){let e=await s({message:`What is the title of your content?`,placeholder:`Enter content title`,validate:e=>{if(!e||e.trim().length===0)return`Title is required`}});return n(e)&&(r.error(`Operation cancelled.`),S.exit(0)),e.trim()}async function Te(){let e=await o({message:`Is a LinkedIn video planned?`,options:[{label:`Yes`,value:!0,hint:`Add video: true to frontmatter`},{label:`No`,value:!1,hint:`No video metadata`}]});return n(e)&&(r.error(`Operation cancelled.`),S.exit(0)),e}async function Ee(e){if(e.length===0)return;let t=await o({message:`What is the content theme? (optional)`,options:[{label:`No theme`,value:``,hint:`Leave theme empty`},...e.map(e=>({label:e,value:e,hint:`Use theme: ${e}`}))]});n(t)&&(r.error(`Operation cancelled.`),S.exit(0));let i=t;return i.length>0?i:void 0}async function De(){let e=await o({message:`What type of resource is this?`,options:[{label:`Article`,value:`article`,hint:`Create article.md`},{label:`Video`,value:`video`,hint:`Create video.md`},{label:`Audio`,value:`audio`,hint:`Create audio.md`},{label:`Tweet`,value:`tweet`,hint:`Create tweet.md`}]});return n(e)&&(r.error(`Operation cancelled.`),S.exit(0)),e}async function Oe(){let e=await i({message:`Which content types do you want to create?`,options:[{label:`LinkedIn`,value:`linkedin`,hint:`Create LinkedIn post and optional script`},{label:`X (Twitter)`,value:`x`,hint:`Create X post`},{label:`YouTube`,value:`youtube`,hint:`Create YouTube script and description`},{label:`Instagram`,value:`instagram`,hint:`Create Instagram script and description`}],required:!0});return n(e)&&(r.error(`Operation cancelled.`),S.exit(0)),e}async function ke(e){return W({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 Ae(e){return W({basePath:e,contentTypes:[`linkedin`],message:`Which LinkedIn publication do you want to link images for?`,emptyMessage:`No LinkedIn publications found in dated folders`,sort:`date-desc`})}async function je(e){return W({basePath:e,contentTypes:[...k],message:`Which publication do you want to mark as ready?`,emptyMessage:`No publications found in dated folders`,sort:`date-desc`})}async function W(e){let t=j(e.basePath,{contentTypes:e.contentTypes,sort:e.sort});t.length===0&&(r.error(e.emptyMessage),S.exit(1));let i=20;for(;;){let a=await o({message:e.message,options:Me(t,i)});if(n(a)&&(r.error(`Operation cancelled.`),S.exit(0)),a===H){i+=20;continue}let s=t.find(e=>e.path===a);return s||(r.error(`Unable to resolve selected publication: ${a}`),S.exit(1)),s}}function Me(e,t){let n=e.slice(0,t).map(e=>Ne(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:H,hint:`Extend the list to ${e[r-1]?.relativePath}`}]}function Ne(e){return{label:`${l(e.date,`yyyy-MM-dd`)} · ${A[e.contentType]} · ${e.title}`,value:e.path,hint:e.relativePath}}async function G(e){let t=F(e);if(t.length===0&&(r.error(`No series directories found`),S.exit(1)),t.length===1)return t[0];let i=await o({message:`Which series do you want to work on?`,options:t.map(e=>({label:e.title,value:e.path,hint:`series/${e.name}`}))});n(i)&&(r.error(`Operation cancelled.`),S.exit(0));let a=t.find(e=>e.path===i);return a||(r.error(`Unable to resolve selected series: ${i}`),S.exit(1)),a}async function Pe(e){let t=L(e),i=t.map(e=>({label:`Insert at #${e.index} · before ${e.title}`,value:String(e.index),hint:e.fileName}));i.push({label:`Append as #${t.length+1}`,value:String(t.length+1),hint:`Add the new article at the end of the series`});let a=await o({message:`Where should the new article be inserted?`,options:i});return n(a)&&(r.error(`Operation cancelled.`),S.exit(0)),Number(a)}async function K(e){let t=L(e);if(t.length===0&&(r.error(`No series articles found`),S.exit(1)),t.length===1)return t[0];let i=await o({message:`Which series article do you want to update?`,options:t.map(e=>({label:`#${e.index} · ${e.title}`,value:e.path,hint:e.fileName}))});n(i)&&(r.error(`Operation cancelled.`),S.exit(0));let a=t.find(e=>e.path===i);return a||(r.error(`Unable to resolve selected series article: ${i}`),S.exit(1)),a}function q(e,t){return e||t?.path||S.cwd()}async function Fe(e,n){t(`Content Creation - Create dated content directory`);let r=await D(),i=await U(),o=await Oe(),s={title:i};o.includes(`linkedin`)&&(s.hasVideo=await Te(),s.hasImages=!s.hasVideo,r.thematic.length>0&&(s.theme=await Ee(r.thematic)));let c=await Ce();ue(c,o,s,r,q(e,n));let u=o.join(`, `);a(`✓ Content directory created: ${l(c,`yyyy/MM/dd`)} (${u})`)}function Ie(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 Le(e){switch(e){case`linkedin`:return`LinkedIn Posts`;case`x`:return`X Posts`;case`youtube`:return`YouTube Videos`;case`instagram`:return`Instagram Posts`}}function Re(e,t){let n=j(e,{contentTypes:[t],sort:`date-desc`});if(n.length===0){r.info(`No ${t} posts found`);return}let i=Le(t),a=Ie(t),o=`# ${i}\n\n`;o+=`_Generated on ${new Date().toLocaleDateString()}_\n\n`,o+=`Total posts: ${n.length}\n\n`;for(let e of n){let t=e.date.toLocaleDateString(`en-US`,{year:`numeric`,month:`long`,day:`numeric`});o+=`- [${e.title}](${e.relativePath}) - _${t}_\n`}v(b(e,a),o,`utf-8`),r.success(`Created ${a} with ${n.length} posts`)}function ze(e=S.cwd(),t){let n=t||[...k];for(let t of n)Re(e,t)}function Be(e,n){t(`Content Creation - Create Index Files`),ze(q(e,n)),a(`✓ Index files generated successfully`)}function Ve(e,t,n){if(e===`create`){Be(t,n);return}throw Error(`Unknown index action: ${e}. Supported actions are: create`)}function He(e){let t=[`.jpg`,`.jpeg`,`.png`],n=[];try{let r=g(e);for(let i of r)if(_(b(e,i)).isFile()){let e=i.toLowerCase().substring(i.lastIndexOf(`.`));t.includes(e)&&n.push(i)}}catch(e){r.error(`Error reading directory: ${e}`)}return n.sort()}function J(e,t){try{let{data:n,content:i}=C(h(e.path,`utf-8`));n.images=t.length>0?t:[null];let a=C.stringify(i,n);v(e.path,a,`utf-8`),r.success(`LinkedIn publication updated: ${e.relativePath} (${t.length} image(s))`)}catch(e){r.error(`Error updating frontmatter: ${e}`)}}async function Ue(e=S.cwd()){let t=await Ae(e),n=He(t.folderPath);if(n.length===0){r.info(`No images found for LinkedIn publication: ${t.relativePath}`),J(t,[]);return}r.info(`Found ${n.length} image(s) for ${t.relativePath}: ${n.join(`, `)}`),J(t,n)}const We=oe(new URL(`../../`,import.meta.url)),Y=`content-creation.ics`;function X(e){return e.replace(/\\/g,`\\\\`).replace(/\r\n|\r|\n/g,`\\n`).replace(/;/g,`\\;`).replace(/,/g,`\\,`)}function Ge(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 Ke(e){return e.toISOString().replace(/[-:]/g,``).replace(/\.\d{3}Z$/,`Z`)}function qe(e){return l(e,`yyyyMMdd`)}function Je(e){return e.split(`/`).map(e=>encodeURIComponent(e)).join(`/`)}function Ye(e){return`https://github.com/barbapapazes/content-creation/blob/main/${Je(e)}`}function Xe(e){let t=Je(e);return`vscode://file${t.startsWith(`/`)?``:`/`}${t}`}function Ze(e){let t=[`GitHub: ${Ye(e.relativePath)}`,`VS Code: ${Xe(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 Qe(e){return`${e.ready?`Ready`:`Not ready`} · ${e.title} · ${A.linkedin}`}function $e(e){return`linkedin-${ae(`sha1`).update(e).digest(`hex`)}@barbapapazes`}function et(e){let t=[`BEGIN:VCALENDAR`,`VERSION:2.0`,`PRODID:-//Barbapapazes//Content Creation LinkedIn Calendar//EN`,`CALSCALE:GREGORIAN`,`METHOD:PUBLISH`,`X-WR-CALNAME:${X(`content-creation.ics`)}`];for(let n of e){let e=Ke(new Date(Date.UTC(n.date.getFullYear(),n.date.getMonth(),n.date.getDate()))),r=c(n.date,1);t.push(`BEGIN:VEVENT`,`UID:${$e(n.relativePath)}`,`DTSTAMP:${e}`,`SUMMARY:${X(Qe(n))}`,`DTSTART;VALUE=DATE:${qe(n.date)}`,`DTEND;VALUE=DATE:${qe(r)}`,`TRANSP:TRANSPARENT`,`DESCRIPTION:${X(Ze(n))}`,`END:VEVENT`)}return t.push(`END:VCALENDAR`),`${t.map(Ge).join(`\r
|
|
7
|
-
`)}\r\n`}function tt(){let e=b(We,`node_modules`,`.bin`,S.platform===`win32`?`wrangler.cmd`:`wrangler`);return p(e)?e:`wrangler`}function nt(e){(!e.calendar.publicUrl||!e.calendar.token)&&(r.error(`Calendar publishing configuration is missing. Please set CALENDAR_PUBLIC_URL and CALENDAR_TOKEN in your .env file or config.`),S.exit(1));let t=e.calendar.publicUrl.endsWith(`/`)?e.calendar.publicUrl:`${e.calendar.publicUrl}/`,n=new URL(Y,t);return n.searchParams.set(`token`,e.calendar.token),n.toString()}function rt(e){return j(e,{contentTypes:[`linkedin`],sort:`date-asc`}).map(e=>{let t=e.frontmatter;return{...e,title:t.title||e.title,ready:t.ready===!0,theme:t.theme,video:t.video,imageCount:t.images?.filter(Boolean).length??0}})}function it(e){let t=tt();try{ie(t,[`r2`,`object`,`put`,`content-creation/${Y}`,`--pipe`,`--content-type`,`text/calendar; charset=utf-8`,`--remote`],{cwd:We,stdio:[`pipe`,`inherit`,`inherit`],env:S.env,input:e})}catch(e){r.error(`Failed to upload LinkedIn calendar: ${e instanceof Error?e.message:String(e)}`),S.exit(1)}}async function at(e,t){let n=rt(e);if(n.length===0)return r.info(`No LinkedIn publications found. Skipping calendar upload.`),null;let i=nt(t);return it(et(n)),r.success(`✓ LinkedIn calendar uploaded to R2 as ${Y}`),r.info(`Subscription URL: ${i}`),i}async function ot(e,n){t(`Content Creation - Link Images to LinkedIn Publication`),await Ue(q(e,n)),a(`✓ LinkedIn publication images linked`)}async function st(e,n){t(`Content Creation - Publish LinkedIn Calendar`);let r=await D();if(await at(q(e,n),r)){a(`✓ LinkedIn calendar published successfully`);return}a(`✓ No LinkedIn publications found, calendar was not updated`)}async function ct(e,t,n){if(e===`link-images`){await ot(t,n);return}if(e===`publish-calendar`){await st(t,n);return}throw Error(`Unknown linkedin action: ${e}. Supported actions are: link-images, publish-calendar`)}function lt(e){let t=new Date;t.setHours(0,0,0,0);let n=j(e,{fromDate:t,sort:`date-asc`});if(n.length===0){r.info(`No upcoming publications found from today onward.`);return}r.info(`Found ${n.length} upcoming publication${n.length===1?``:`s`}:`),console.log(``);for(let e of n){let t=l(e.date,`EEEE, MMMM d, yyyy`),n=l(e.date,`yyyy-MM-dd`);console.log(`${t} (${n}) · ${A[e.contentType]} · ${e.title}`),console.log(e.path),console.log(``)}}const ut=/<!--([\s\S]*?)-->/g;function dt(e,t){try{let{data:n,content:i}=C(h(e,`utf-8`)),a=ft(i,t);return v(e,C.stringify(i,{...n,time:a}),`utf-8`),r.success(`Estimated time set to ${a} min: ${e}`),a}catch(e){r.error(`Failed to estimate series article time: ${e instanceof Error?e.message:String(e)}`),S.exit(1)}}function ft(e,t){if(!Number.isFinite(t)||t<=0)throw RangeError(`Words per minute must be a positive number`);let n=e.replace(ut,``).replace(/\s+/g,``);if(n.length===0)return 0;let r=n.length/5;return Math.ceil(r/t)}function pt(e){ht({path:e.path,relativePath:e.relativePath},`publication`)}function mt(e,t){return ht({path:e.path,relativePath:e.fileName},`series article`),dt(e.path,t)}function ht(e,t){let n=e.path;try{let{data:i,content:a}=C(h(n,`utf-8`));if(i.ready===!0){r.warn(`This ${t} is already marked as ready.`);return}let o={...i,ready:!0};v(n,C.stringify(a,o),`utf-8`),r.success(`${gt(t)} marked as ready: ${e.relativePath}`)}catch(e){r.error(`Failed to mark ${t} as ready: ${e instanceof Error?e.message:String(e)}`),S.exit(1)}}function gt(e){return e.charAt(0).toUpperCase()+e.slice(1)}function _t(e,n){t(`Content Creation - List Upcoming Publications`),lt(q(e,n)),a(`✓ Upcoming publications listed`)}async function vt(e,n){t(`Content Creation - Mark Publication Ready`),pt(await je(q(e,n))),a(`✓ Publication marked as ready`)}async function yt(e,t,n){if(e===`list-upcoming`){_t(t,n);return}if(e===`ready`){await vt(t,n);return}throw Error(`Unknown publication action: ${e}. Supported actions are: list-upcoming, ready`)}function Z(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 bt(e,t,n=S.cwd()){let i=Z(e),a=b(x(b(n,`resources`)),i),o=`${t}.md`,s=b(a,o);if(p(s))return r.info(`${o} already exists at: ${s}`),a;m(a,{recursive:!0}),r.success(`Created directory: ${a}`);let c={title:e,url:``,date:``};return v(s,C.stringify(``,c),`utf-8`),r.success(`Created ${o} at: ${s}`),a}async function xt(e,n){t(`Content Creation - Create Resource`);let r=await U(),i=await De();bt(r,i,q(e,n)),a(`✓ Resource created: resources/${Z(r)}/${i}.md`)}async function St(e,t,n){if(e===`create`){await xt(t,n);return}throw Error(`Unknown resource action: ${e}. Supported actions are: create`)}function Ct(e,t){let n=F(e);if(n.length===0)return;let r=t?n.filter(e=>e.name===t):n;if(r.length===0)throw Error(`Series not found: ${t}`);for(let e of r)z(e.path)}function wt(e,t,n){let i=L(e),a=Et(n,i.length),o=b(e,`${a}.${Z(t)}.md`);if(p(o))throw Error(`Article already exists at ${o}`);return Tt(e,i,a),v(o,C.stringify(``,{title:t}),`utf-8`),r.success(`Created article: ${o}`),z(e),o}function Tt(e,t,n){let i=t.filter(e=>e.index>=n).sort((e,t)=>t.index-e.index);for(let t of i){let n=`${t.index+1}.${t.slug}.md`,i=b(e,n);te(t.path,i),r.info(`Renamed ${t.fileName} → ${n}`)}}function Et(e,t){if(!Number.isInteger(e))throw TypeError(`Article index must be an integer`);if(e<1||e>t+1)throw RangeError(`Article index must be between 1 and ${t+1}`);return e}async function Dt(e,n,r){let i=q(n,r);if(e===`create-index`){t(`Content Creation - Create Series Index`),Ct(i,r?.series),a(r?.series?`✓ Series index generated for ${r.series}`:`✓ Series indexes generated successfully`);return}if(e===`introduce`){t(`Content Creation - Introduce Series Article`);let e=r?.title||await U(),n=r?.series?I(i,r.series):await G(i);if(!n)throw Error(`Series not found: ${r?.series}`);let o=r?.position?Number(r.position):await Pe(n.path);wt(n.path,e,o),a(`✓ Series article created: ${n.name}/${o}.${Z(e)}.md`);return}if(e===`estimate-time`){t(`Content Creation - Estimate Series Article Time`);let e=await D(),n=r?.series?I(i,r.series):await G(i);if(!n)throw Error(`Series not found: ${r?.series}`);let o=r?.file?R(n.path,r.file):await K(n.path);if(!o)throw Error(`Series article not found: ${r?.file}`);let s=Q(r?.wpm,e.reading.wordsPerMinute),c=dt(o.path,s);a(`✓ Estimated time updated for ${n.name}/${o.fileName}: ${c} min`);return}if(e===`ready`){t(`Content Creation - Mark Series Article Ready`);let e=await D(),n=r?.series?I(i,r.series):await G(i);if(!n)throw Error(`Series not found: ${r?.series}`);let o=r?.file?R(n.path,r.file):await K(n.path);if(!o)throw Error(`Series article not found: ${r?.file}`);let s=mt(o,Q(r?.wpm,e.reading.wordsPerMinute));a(`✓ Series article marked as ready and time updated for ${n.name}/${o.fileName}: ${s} min`);return}throw Error(`Unknown series action: ${e}. Supported actions are: create-index, introduce, estimate-time, ready`)}function Q(e,t){let n=e?Number(e):t??100;if(!Number.isFinite(n)||n<=0)throw RangeError(`Words per minute must be a positive number`);return n}async function Ot(e,t){let n=e.path,{data:i,content:a}=C(h(n,`utf-8`));i.scheduled===!0&&(r.warn(`This X publication is already scheduled. Skipping.`),S.exit(0)),(!t.scheduling.automationEndpoint||!t.scheduling.cfAccessClientId||!t.scheduling.cfAccessClientSecret)&&(r.error(`Scheduling configuration is missing. Please set AUTOMATION_ENDPOINT, CF_ACCESS_CLIENT_ID, and CF_ACCESS_CLIENT_SECRET in your .env file or config.`),S.exit(1));let o=new Date(e.date);o=d(o,11),o=f(o,0),o=ee(o,0);let s=o.getTime(),c={content:a.trim(),scheduleAt:s};try{let e=await fetch(t.scheduling.automationEndpoint,{method:`POST`,headers:{"Content-Type":`application/json`,Accept:`application/json`,"CF-Access-Client-Id":t.scheduling.cfAccessClientId,"CF-Access-Client-Secret":t.scheduling.cfAccessClientSecret},body:JSON.stringify(c)});if(!e.ok){let t=await e.text();r.error(`Failed to schedule reminder: ${e.status} ${e.statusText}\n${t}`),S.exit(1)}let o=await e.json(),s={...i,scheduled:!0};v(n,C.stringify(a,s),`utf-8`);let u=new Date(o.scheduledAt);r.success(`✓ X publication reminder scheduled`),r.info(`Reminder scheduled for: ${l(u,`yyyy-MM-dd HH:mm:ss`)} UTC`)}catch(e){r.error(`Failed to schedule X publication reminder: ${e instanceof Error?e.message:String(e)}`),S.exit(1)}}async function kt(e,n){t(`Content Creation - Schedule X Publication Reminder`);let r=await D();await Ot(await ke(q(e,n)),r),a(`✓ X publication reminder scheduled`)}async function At(e,t,n){if(e===`schedule-reminder`){await kt(t,n);return}throw Error(`Unknown x action: ${e}. Supported actions are: schedule-reminder`)}const $=e(`content-creation`);$.command(`[path]`,`Create a dated content directory with content files`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).action(Fe),$.command(`resource <action> [path]`,`Manage resource markdown files`).option(`--path <path>`,`Base path for resource directory (defaults to current directory)`).action(St),$.command(`series <action> [path]`,`Manage article series and generated indexes`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).option(`--file <file>`,`Series article file name inside the selected series`).option(`--series <series>`,`Series folder name inside the series directory`).option(`--title <title>`,`Title of the new series article`).option(`--position <position>`,`Insert position in the series index (1-based)`).option(`--wpm <wpm>`,`Words per minute used to estimate reading time`).action(Dt),$.command(`index <action> [path]`,`Manage generated indexes`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).action(Ve),$.command(`linkedin <action> [path]`,`Manage LinkedIn-specific workflows`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).action(ct),$.command(`x <action> [path]`,`Manage X-specific workflows`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).action(At),$.command(`publication <action> [path]`,`Manage publication workflows`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).action(yt),$.help(),$.version(se),$.parse();export{};
|
|
2
|
+
import{C as e,S as t,a as n,g as r,i,o as a,r as o,s,t as c,v as l,w as u,y as d}from"./x-dKYi_aTZ.mjs";import f from"node:process";import{log as p}from"@clack/prompts";import{Command as m}from"commander";var h=`@barbapapazes/content-creation`,g=`0.20.0`,_=`CLI tool for content creation and management`;const v=new m;v.name(h).description(_).version(g).showHelpAfterError();const y=v.command(`content`);y.description(`Manage dated content`),y.command(`new`).description(`Create a dated content directory`).action(()=>u(f.cwd())),y.command(`upcoming`).description(`List upcoming publications`).action(()=>e(f.cwd())),y.command(`rebuild-indexes`).description(`Rebuild all content index files`).action(()=>t(f.cwd()));const b=v.command(`series`);b.description(`Manage article series`),b.command(`new`).description(`Create a new series`).action(()=>a(f.cwd())),b.command(`video`).description(`Manage series videos`).command(`process`).description(`Process a video from a series directory`).action(()=>i(f.cwd()));const x=b.command(`article`).description(`Manage series articles`);x.command(`new`).description(`Create a new article in a series`).action(()=>n(f.cwd())),x.command(`ready`).description(`Mark a series article as ready`).action(()=>o(f.cwd())),x.command(`estimate-time`).description(`Estimate the reading time of a series article`).action(()=>s(f.cwd()));const S=v.command(`linkedin`);S.description(`Manage LinkedIn publications`),S.command(`ready`).description(`Prepare a LinkedIn publication and mark it as ready`).action(()=>l(f.cwd())),S.command(`publish-calendar`).description(`Publish the LinkedIn calendar`).action(()=>d(f.cwd()));const C=v.command(`x`);C.description(`Manage X publications`),C.command(`schedule-reminder`).description(`Schedule an X publication reminder`).action(()=>c(f.cwd()));const w=v.command(`resource`);w.description(`Manage resources`),w.command(`new`).description(`Create a new resource`).action(()=>r(f.cwd())),v.parseAsync().catch(e=>{p.error(e instanceof Error?e.message:String(e)),f.exit(1)});export{};
|
package/dist/index.d.mts
CHANGED
|
@@ -1,99 +1,225 @@
|
|
|
1
|
+
//#region src/constants.d.ts
|
|
2
|
+
declare const CONTENT_TYPES: readonly ["linkedin", "x", "youtube", "instagram"];
|
|
3
|
+
//#endregion
|
|
1
4
|
//#region src/types.d.ts
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Template configuration for LinkedIn
|
|
5
|
-
*/
|
|
5
|
+
type ContentType = (typeof CONTENT_TYPES)[number];
|
|
6
6
|
interface LinkedInTemplateConfig {
|
|
7
|
-
/**
|
|
8
|
-
* Path to a template file for the footer (resolved relative to templatesDir or config file location)
|
|
9
|
-
*/
|
|
10
7
|
footerPath?: string;
|
|
11
8
|
}
|
|
12
|
-
/**
|
|
13
|
-
* Template configuration for YouTube/Instagram
|
|
14
|
-
*/
|
|
15
9
|
interface VideoTemplateConfig {
|
|
16
|
-
/**
|
|
17
|
-
* Path to a template file for the description blueprint (resolved relative to templatesDir or config file location)
|
|
18
|
-
*/
|
|
19
10
|
templatePath?: string;
|
|
20
11
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
12
|
+
interface VideoProcessingConfig {
|
|
13
|
+
openaiApiKey?: string;
|
|
14
|
+
language?: string;
|
|
15
|
+
model?: string;
|
|
16
|
+
}
|
|
17
|
+
interface ResolvedVideoProcessingConfig {
|
|
18
|
+
openaiApiKey: string;
|
|
19
|
+
language: string;
|
|
20
|
+
model: string;
|
|
21
|
+
}
|
|
24
22
|
interface SchedulingConfig {
|
|
25
|
-
/**
|
|
26
|
-
* Automation endpoint URL
|
|
27
|
-
*/
|
|
28
23
|
automationEndpoint?: string;
|
|
29
|
-
/**
|
|
30
|
-
* Cloudflare Access Client ID
|
|
31
|
-
*/
|
|
32
24
|
cfAccessClientId?: string;
|
|
33
|
-
/**
|
|
34
|
-
* Cloudflare Access Client Secret
|
|
35
|
-
*/
|
|
36
25
|
cfAccessClientSecret?: string;
|
|
37
26
|
}
|
|
38
|
-
/**
|
|
39
|
-
* Calendar publishing configuration
|
|
40
|
-
*/
|
|
41
27
|
interface CalendarPublishingConfig {
|
|
42
|
-
/**
|
|
43
|
-
* Public base URL used to build the Google Calendar subscription link
|
|
44
|
-
*/
|
|
45
28
|
publicUrl?: string;
|
|
46
|
-
/**
|
|
47
|
-
* Token appended to the public calendar subscription URL
|
|
48
|
-
*/
|
|
49
29
|
token?: string;
|
|
50
30
|
}
|
|
51
|
-
/**
|
|
52
|
-
* Reading-time estimation configuration.
|
|
53
|
-
*/
|
|
54
31
|
interface ReadingConfig {
|
|
55
|
-
/**
|
|
56
|
-
* Reading speed used when estimating content duration.
|
|
57
|
-
* @default 100
|
|
58
|
-
*/
|
|
59
32
|
wordsPerMinute?: number;
|
|
60
33
|
}
|
|
61
|
-
/**
|
|
62
|
-
* Configuration for content-creation
|
|
63
|
-
*/
|
|
64
34
|
interface Config {
|
|
65
|
-
/**
|
|
66
|
-
* List of thematic areas
|
|
67
|
-
* @default []
|
|
68
|
-
*/
|
|
69
35
|
thematic?: string[];
|
|
70
|
-
/**
|
|
71
|
-
* Base directory for external template files
|
|
72
|
-
*/
|
|
73
36
|
templatesDir?: string;
|
|
74
|
-
/**
|
|
75
|
-
* Templates configuration per content type
|
|
76
|
-
*/
|
|
77
37
|
templates?: {
|
|
78
38
|
linkedin?: LinkedInTemplateConfig;
|
|
79
39
|
youtube?: VideoTemplateConfig;
|
|
80
40
|
instagram?: VideoTemplateConfig;
|
|
81
41
|
};
|
|
82
|
-
|
|
83
|
-
* Scheduling configuration
|
|
84
|
-
*/
|
|
42
|
+
videoProcessing?: VideoProcessingConfig;
|
|
85
43
|
scheduling?: SchedulingConfig;
|
|
86
|
-
/**
|
|
87
|
-
* Calendar publishing configuration
|
|
88
|
-
*/
|
|
89
44
|
calendar?: CalendarPublishingConfig;
|
|
90
|
-
/**
|
|
91
|
-
* Reading-time configuration for content metadata helpers.
|
|
92
|
-
*/
|
|
93
45
|
reading?: ReadingConfig;
|
|
94
46
|
}
|
|
47
|
+
interface ResolvedConfig extends Omit<Required<Config>, 'reading' | 'videoProcessing'> {
|
|
48
|
+
reading: {
|
|
49
|
+
wordsPerMinute: number;
|
|
50
|
+
};
|
|
51
|
+
videoProcessing: ResolvedVideoProcessingConfig;
|
|
52
|
+
}
|
|
53
|
+
interface ContentFrontmatter {
|
|
54
|
+
title: string;
|
|
55
|
+
theme?: string;
|
|
56
|
+
video?: boolean;
|
|
57
|
+
images?: (string | null)[];
|
|
58
|
+
scheduled?: boolean;
|
|
59
|
+
ready?: boolean;
|
|
60
|
+
}
|
|
61
|
+
interface ContentEntry {
|
|
62
|
+
contentType: ContentType;
|
|
63
|
+
path: string;
|
|
64
|
+
folderPath: string;
|
|
65
|
+
date: Date;
|
|
66
|
+
title: string;
|
|
67
|
+
relativePath: string;
|
|
68
|
+
frontmatter: ContentFrontmatter;
|
|
69
|
+
}
|
|
70
|
+
//#endregion
|
|
71
|
+
//#region src/common/frontmatter.d.ts
|
|
72
|
+
interface MarkdownDocument<TFrontmatter extends object = Record<string, unknown>> {
|
|
73
|
+
content: string;
|
|
74
|
+
frontmatter: TFrontmatter;
|
|
75
|
+
}
|
|
76
|
+
declare function readMarkdownDocument<TFrontmatter extends object = Record<string, unknown>>(filePath: string): MarkdownDocument<TFrontmatter>;
|
|
77
|
+
declare function writeMarkdownDocument<TFrontmatter extends object>(filePath: string, document: MarkdownDocument<TFrontmatter>): void;
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region src/common/prompts.d.ts
|
|
80
|
+
interface PromptForRequiredTextOptions {
|
|
81
|
+
message: string;
|
|
82
|
+
placeholder: string;
|
|
83
|
+
requiredMessage?: string;
|
|
84
|
+
}
|
|
85
|
+
declare function promptForRequiredText(options: PromptForRequiredTextOptions): Promise<string>;
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/common/reading.d.ts
|
|
88
|
+
declare function estimateReadingMinutesFromMarkdown(markdownContent: string, wordsPerMinute: number): number;
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/common/ready.d.ts
|
|
91
|
+
interface ReadyEntry {
|
|
92
|
+
path: string;
|
|
93
|
+
relativePath: string;
|
|
94
|
+
}
|
|
95
|
+
declare function markEntryAsReady(entry: ReadyEntry, entryLabel: string): void;
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region src/common/slugify.d.ts
|
|
98
|
+
declare function slugify(title: string): string;
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/common/strings.d.ts
|
|
101
|
+
declare function capitalize(value: string): string;
|
|
102
|
+
declare function humanizeSlug(value: string): string;
|
|
103
|
+
declare function escapeRegExp(value: string): string;
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region ../shared/src/cli.d.ts
|
|
106
|
+
interface BasePathOption {
|
|
107
|
+
path?: string;
|
|
108
|
+
}
|
|
109
|
+
declare function exitAsCancelled(message?: string): never;
|
|
110
|
+
declare function exitWithError(message: string): never;
|
|
111
|
+
declare function formatErrorMessage(error: unknown): string;
|
|
112
|
+
declare function unwrapPromptValue<T>(value: T, cancellationMessage?: string): Exclude<T, symbol>;
|
|
113
|
+
declare function resolveBasePath(basePath?: string): string;
|
|
114
|
+
//#endregion
|
|
115
|
+
//#region src/config.d.ts
|
|
116
|
+
declare function loadContentCreationConfig(): Promise<ResolvedConfig>;
|
|
117
|
+
//#endregion
|
|
118
|
+
//#region src/modules/content/steps/create-content-directory.d.ts
|
|
119
|
+
interface ContentCreationOptions {
|
|
120
|
+
title: string;
|
|
121
|
+
hasVideo?: boolean;
|
|
122
|
+
hasImages?: boolean;
|
|
123
|
+
theme?: string;
|
|
124
|
+
}
|
|
125
|
+
declare function createContentDirectory(date: Date, selectedTypes: ContentType[], options: ContentCreationOptions, config: ResolvedConfig, basePath: string): string;
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/modules/content/steps/list-upcoming-content.d.ts
|
|
128
|
+
declare function listUpcomingContent(basePath: string): void;
|
|
129
|
+
//#endregion
|
|
130
|
+
//#region src/modules/content/steps/rebuild-content-indexes.d.ts
|
|
131
|
+
declare function rebuildContentIndexes(basePath: string): void;
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region src/modules/content/workflows/content-new.d.ts
|
|
134
|
+
declare function contentNewWorkflow(basePath: string): Promise<void>;
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/modules/content/workflows/content-upcoming.d.ts
|
|
137
|
+
declare function contentUpcomingWorkflow(basePath: string): void;
|
|
138
|
+
//#endregion
|
|
139
|
+
//#region src/modules/content/workflows/rebuild-content-indexes.d.ts
|
|
140
|
+
declare function rebuildContentIndexesWorkflow(basePath: string): void;
|
|
141
|
+
//#endregion
|
|
142
|
+
//#region src/modules/linkedin/steps/publish-linkedin-calendar.d.ts
|
|
143
|
+
declare function publishLinkedInCalendar(basePath: string, config: ResolvedConfig): Promise<string | null>;
|
|
144
|
+
//#endregion
|
|
145
|
+
//#region src/modules/linkedin/steps/ready-linkedin-publication.d.ts
|
|
146
|
+
interface LinkedInReadyResult {
|
|
147
|
+
imageCount: number;
|
|
148
|
+
scriptMarked: boolean;
|
|
149
|
+
}
|
|
150
|
+
declare function readyLinkedInPublication(entry: ContentEntry): LinkedInReadyResult;
|
|
151
|
+
//#endregion
|
|
152
|
+
//#region src/modules/linkedin/workflows/linkedin-publish-calendar.d.ts
|
|
153
|
+
declare function linkedinPublishCalendarWorkflow(basePath: string): Promise<void>;
|
|
154
|
+
//#endregion
|
|
155
|
+
//#region src/modules/linkedin/workflows/linkedin-ready.d.ts
|
|
156
|
+
declare function linkedinReadyWorkflow(basePath: string): Promise<void>;
|
|
157
|
+
//#endregion
|
|
158
|
+
//#region src/modules/resource/steps/create-resource-directory.d.ts
|
|
159
|
+
declare function createResourceDirectory(title: string, resourceType: 'article' | 'video' | 'audio' | 'tweet', basePath: string): string;
|
|
160
|
+
//#endregion
|
|
161
|
+
//#region src/modules/resource/workflows/resource-new.d.ts
|
|
162
|
+
declare function resourceNewWorkflow(basePath: string): Promise<void>;
|
|
163
|
+
//#endregion
|
|
164
|
+
//#region src/modules/series/steps/create-series-article.d.ts
|
|
165
|
+
declare function createSeriesArticle(seriesPath: string, title: string, insertAtIndex: number): string;
|
|
166
|
+
//#endregion
|
|
167
|
+
//#region src/modules/series/types.d.ts
|
|
168
|
+
interface SeriesDirectoryEntry {
|
|
169
|
+
name: string;
|
|
170
|
+
path: string;
|
|
171
|
+
title: string;
|
|
172
|
+
}
|
|
173
|
+
interface SeriesArticleEntry {
|
|
174
|
+
index: number;
|
|
175
|
+
slug: string;
|
|
176
|
+
title: string;
|
|
177
|
+
fileName: string;
|
|
178
|
+
path: string;
|
|
179
|
+
}
|
|
180
|
+
interface SeriesVideoEntry {
|
|
181
|
+
name: string;
|
|
182
|
+
path: string;
|
|
183
|
+
relativePath: string;
|
|
184
|
+
}
|
|
185
|
+
//#endregion
|
|
186
|
+
//#region src/modules/series/steps/create-series-directory.d.ts
|
|
187
|
+
declare function createSeriesDirectory(basePath: string, title: string): SeriesDirectoryEntry;
|
|
188
|
+
//#endregion
|
|
189
|
+
//#region src/modules/series/steps/estimate-series-article-time.d.ts
|
|
190
|
+
declare function estimateSeriesArticleTime(filePath: string, wordsPerMinute: number): number;
|
|
191
|
+
//#endregion
|
|
192
|
+
//#region src/modules/series/steps/mark-series-article-ready.d.ts
|
|
193
|
+
declare function markSeriesArticleReady(entry: SeriesArticleEntry): void;
|
|
194
|
+
//#endregion
|
|
195
|
+
//#region src/modules/series/steps/series.d.ts
|
|
196
|
+
declare function getSeriesRootPath(basePath: string): string;
|
|
197
|
+
declare function getSeriesDirectories(basePath: string): SeriesDirectoryEntry[];
|
|
198
|
+
declare function getSeriesArticles(seriesPath: string): SeriesArticleEntry[];
|
|
199
|
+
declare function createSeriesIndex(seriesPath: string): void;
|
|
200
|
+
//#endregion
|
|
201
|
+
//#region src/modules/series/workflows/estimate-series-article-time.d.ts
|
|
202
|
+
declare function seriesArticleEstimateTimeWorkflow(basePath: string): Promise<void>;
|
|
203
|
+
//#endregion
|
|
204
|
+
//#region src/modules/series/workflows/new-series.d.ts
|
|
205
|
+
declare function seriesNewWorkflow(basePath: string): Promise<void>;
|
|
206
|
+
//#endregion
|
|
207
|
+
//#region src/modules/series/workflows/new-series-article.d.ts
|
|
208
|
+
declare function seriesArticleNewWorkflow(basePath: string): Promise<void>;
|
|
209
|
+
//#endregion
|
|
210
|
+
//#region src/modules/series/workflows/process-series-video.d.ts
|
|
211
|
+
declare function seriesVideoProcessWorkflow(basePath: string): Promise<void>;
|
|
212
|
+
//#endregion
|
|
213
|
+
//#region src/modules/series/workflows/ready-series-article.d.ts
|
|
214
|
+
declare function seriesArticleReadyWorkflow(basePath: string): Promise<void>;
|
|
215
|
+
//#endregion
|
|
216
|
+
//#region src/modules/x/steps/schedule-reminder.d.ts
|
|
217
|
+
declare function scheduleReminder(entry: ContentEntry, config: ResolvedConfig): Promise<boolean>;
|
|
218
|
+
//#endregion
|
|
219
|
+
//#region src/modules/x/workflows/x-schedule-reminder.d.ts
|
|
220
|
+
declare function xScheduleReminderWorkflow(basePath: string): Promise<void>;
|
|
95
221
|
//#endregion
|
|
96
222
|
//#region src/index.d.ts
|
|
97
223
|
declare function defineConfig(config: Config): Config;
|
|
98
224
|
//#endregion
|
|
99
|
-
export { defineConfig };
|
|
225
|
+
export { BasePathOption, type Config, ContentCreationOptions, LinkedInReadyResult, MarkdownDocument, type ResolvedConfig, SeriesArticleEntry, SeriesDirectoryEntry, SeriesVideoEntry, capitalize, contentNewWorkflow, contentUpcomingWorkflow, createContentDirectory, createResourceDirectory, createSeriesArticle, createSeriesDirectory, createSeriesIndex, defineConfig, escapeRegExp, estimateReadingMinutesFromMarkdown, estimateSeriesArticleTime, exitAsCancelled, exitWithError, formatErrorMessage, getSeriesArticles, getSeriesDirectories, getSeriesRootPath, humanizeSlug, linkedinPublishCalendarWorkflow, linkedinReadyWorkflow, listUpcomingContent, loadContentCreationConfig, markEntryAsReady, markSeriesArticleReady, promptForRequiredText, publishLinkedInCalendar, readMarkdownDocument, readyLinkedInPublication, rebuildContentIndexes, rebuildContentIndexesWorkflow, resolveBasePath, resourceNewWorkflow, scheduleReminder, seriesArticleEstimateTimeWorkflow, seriesArticleNewWorkflow, seriesArticleReadyWorkflow, seriesNewWorkflow, seriesVideoProcessWorkflow, slugify, unwrapPromptValue, writeMarkdownDocument, xScheduleReminderWorkflow };
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
function
|
|
1
|
+
import{A as e,B as t,C as n,D as r,E as i,F as a,H as o,I as s,L as c,M as l,N as u,O as d,P as f,R as p,S as m,T as h,V as g,_,a as v,b as y,c as b,d as x,f as S,g as C,h as w,i as T,j as E,k as D,l as O,m as k,n as A,o as j,p as M,r as N,s as P,t as F,u as I,v as L,w as R,x as z,y as B,z as V}from"./x-dKYi_aTZ.mjs";function H(e){return e}export{r as capitalize,R as contentNewWorkflow,n as contentUpcomingWorkflow,g as createContentDirectory,_ as createResourceDirectory,x as createSeriesArticle,I as createSeriesDirectory,S as createSeriesIndex,H as defineConfig,d as escapeRegExp,e as estimateReadingMinutesFromMarkdown,O as estimateSeriesArticleTime,f as exitAsCancelled,a as exitWithError,s as formatErrorMessage,M as getSeriesArticles,k as getSeriesDirectories,w as getSeriesRootPath,D as humanizeSlug,B as linkedinPublishCalendarWorkflow,L as linkedinReadyWorkflow,t as listUpcomingContent,o as loadContentCreationConfig,i as markEntryAsReady,b as markSeriesArticleReady,E as promptForRequiredText,z as publishLinkedInCalendar,l as readMarkdownDocument,y as readyLinkedInPublication,V as rebuildContentIndexes,m as rebuildContentIndexesWorkflow,c as resolveBasePath,C as resourceNewWorkflow,A as scheduleReminder,P as seriesArticleEstimateTimeWorkflow,v as seriesArticleNewWorkflow,N as seriesArticleReadyWorkflow,j as seriesNewWorkflow,T as seriesVideoProcessWorkflow,h as slugify,p as unwrapPromptValue,u as writeMarkdownDocument,F as xScheduleReminderWorkflow};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
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{existsSync as c,mkdirSync as l,readFileSync as u,readdirSync as d,renameSync as f,statSync as p,writeFileSync as m}from"node:fs";import{basename as h,dirname as ee,extname as g,isAbsolute as te,join as _,relative as ne,resolve as v}from"node:path";import{addDays as y,format as b,parseISO as re,setHours as ie,setMinutes as ae,setSeconds as oe}from"date-fns";import x from"gray-matter";import{loadConfig as se}from"c12";import{execFileSync as ce}from"node:child_process";import{createHash as le}from"node:crypto";import{fileURLToPath as ue}from"node:url";import{cleanupFile as de,extractAudio as fe,generateThumbnail as pe,generateTranscription as me}from"@barbapapazes/video-toolkit";const S={thematic:[],templatesDir:void 0,templates:{},videoProcessing:{openaiApiKey:``,language:`fr`,model:`whisper-1`},scheduling:{},calendar:{},reading:{wordsPerMinute:100}};function he(t,n){return t?te(t)?t:v(n?ee(n):e.cwd(),t):n?ee(n):e.cwd()}function ge(e,t,n){if(!t)return;let i=v(e,t);if(!c(i)){r.warn(`Configured ${n} template was not found: ${i}`);return}return i}function _e(e,t){return{footerPath:ge(t,e?.footerPath,`LinkedIn footer`)}}function ve(e,t){return{templatePath:ge(t,e?.templatePath,`video description`)}}async function C(){let{config:t,configFile:n}=await se({name:`content-creation`,defaults:S,globalRc:!0,dotenv:!0}),r=he(t.templatesDir,n),i={linkedin:_e(t.templates?.linkedin,r),youtube:ve(t.templates?.youtube,r),instagram:ve(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={openaiApiKey:t.videoProcessing?.openaiApiKey??e.env.OPENAI_API_KEY??``,language:t.videoProcessing?.language??e.env.OPENAI_TRANSCRIPTION_LANGUAGE??S.videoProcessing?.language??`fr`,model:t.videoProcessing?.model??e.env.OPENAI_TRANSCRIPTION_MODEL??S.videoProcessing?.model??`whisper-1`},s={publicUrl:t.calendar?.publicUrl??e.env.CALENDAR_PUBLIC_URL,token:t.calendar?.token??e.env.CALENDAR_TOKEN},c=t.reading?.wordsPerMinute??S.reading?.wordsPerMinute??100;if(!Number.isFinite(c)||c<=0)throw RangeError(`Reading wordsPerMinute must be a positive number`);return{thematic:t.thematic??[],templatesDir:t.templatesDir??``,templates:i,videoProcessing:o,scheduling:a,calendar:s,reading:{wordsPerMinute:c}}}function w(e){return e?c(e)?u(e,`utf-8`):(r.warn(`Template file not found: ${e}`),``):``}const T=[`linkedin`,`x`,`youtube`,`instagram`],E={linkedin:`LinkedIn`,x:`X`,youtube:`YouTube`,instagram:`Instagram`},ye={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`]}},be={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`}},xe=`__load-more__`,Se=[`.jpg`,`.jpeg`,`.png`],D={hours:11,minutes:0,seconds:0};function Ce(e,t,n,i,a){let o=v(_(a,b(e,`yyyy`),b(e,`MM`),b(e,`dd`)));c(o)||(l(o,{recursive:!0}),r.success(`Created directory: ${o}`));let s=we(n);for(let e of t)Te(e,o,s,i);return o}function we(e){return e.hasVideo?{...e,hasImages:!1}:{...e,hasImages:!0}}function Te(e,t,n,i){let a=ye[e],o=_(t,a.mainFile),s=`additionalFiles`in a?a.additionalFiles:void 0;if(c(o)?r.info(`${a.mainFile} already exists, skipping`):Ee(e,o,n,i),s)for(let a of s){let o=_(t,a);Oe(a,e,n)&&(c(o)?r.info(`${a} already exists, skipping`):De(e,o,a,i))}}function Ee(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=w(i.templates.linkedin?.footerPath)),m(t,x.stringify(o,a),`utf-8`),r.success(`Created ${t}`)}function De(e,t,n,i){let a=``;n.endsWith(`-description.md`)&&(a=w(i.templates[e]?.templatePath)),m(t,a,`utf-8`),r.success(`Created ${t}`)}function Oe(e,t,n){return t===`linkedin`&&e===`linkedin-script.md`?n.hasVideo||!1:!0}function ke(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=T.indexOf(e.contentType)-T.indexOf(t.contentType);return i===0?e.relativePath.localeCompare(t.relativePath):i}function O(e,t={}){let n=[],i=t.contentTypes?.length?t.contentTypes:[...T],a=t.fromDate,o=t.sort||`date-desc`;try{let t=d(e).filter(t=>p(_(e,t)).isDirectory()&&/^\d{4}$/.test(t));for(let o of t){let t=_(e,o),s=d(t).filter(e=>p(_(t,e)).isDirectory()&&/^\d{2}$/.test(e));for(let e of s){let s=_(t,e),l=d(s).filter(e=>p(_(s,e)).isDirectory()&&/^\d{2}$/.test(e));for(let t of l){let l=_(s,t),d=`${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=ye[a].mainFile,s=v(l,i);if(c(s))try{let{data:r}=x(u(s,`utf-8`)),c=r,p=c.title||`Untitled (${d})`;n.push({contentType:a,path:s,folderPath:l,date:f,title:p,relativePath:`${o}/${e}/${t}/${i}`,frontmatter:c})}catch(e){r.warn(`Error reading ${s}: ${e}`)}}}}}}catch(e){return r.error(`Error reading directories: ${e}`),[]}return n.sort((e,t)=>ke(e,t,o))}function k(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(`
|
|
2
|
+
`);for(let t of i){let n=b(t.date,`EEEE, MMMM d, yyyy`),r=b(t.date,`yyyy-MM-dd`);e.stdout.write(`${n} (${r}) · ${E[t.contentType]} · ${t.title}\n`),e.stdout.write(`${t.path}\n\n`)}}function Ae(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}=be[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(_(e,i),`${o.join(`
|
|
3
|
+
`)}\n`,`utf-8`),r.success(`Created ${i} with ${n.length} posts`)}function A(e){for(let t of T)Ae(e,t)}function j(t=`Operation cancelled.`){r.error(t),e.exit(0)}function M(t){r.error(t),e.exit(1)}function je(e){return e instanceof Error?e.message:String(e)}function N(e,t){return n(e)&&j(t),e}function Me(t){return v(t??e.cwd())}async function P(e){let t=O(e.basePath,{contentTypes:e.contentTypes,sort:e.sort});t.length===0&&M(e.emptyMessage);let n=20;for(;;){let r=N(await o({message:e.message,options:Ne(t,n)}));if(r===`__load-more__`){n+=20;continue}typeof r!=`string`&&M(`Unable to resolve selected publication: ${String(r)}`);let i=t.find(e=>e.path===r);return i||M(`Unable to resolve selected publication: ${r}`),i}}function Ne(e,t){let n=e.slice(0,t).map(e=>Pe(e));if(t>=e.length)return n;let r=Math.min(t+20,e.length);return[...n,{label:`Show ${r-t} more publications (${t+1}-${r} of ${e.length})`,value:xe,hint:`Extend the list to ${e[r-1]?.relativePath}`}]}function Pe(e){return{label:`${b(e.date,`yyyy-MM-dd`)} · ${E[e.contentType]} · ${e.title}`,value:e.path,hint:e.relativePath}}function F(e){let{content:t,data:n}=x(u(e,`utf-8`));return{content:t,frontmatter:n}}function I(e,t){m(e,x.stringify(t.content,t.frontmatter),`utf-8`)}async function L(e){return N(await s({message:e.message,placeholder:e.placeholder,validate:t=>{if(!t||t.trim().length===0)return e.requiredMessage||`Value is required`}})).trim()}const Fe=/<!--([\s\S]*?)-->/g;function R(e,t){let n=e.replace(Fe,``).replace(/\s+/g,``);if(n.length===0)return 0;let r=n.length/5;return Math.ceil(r/t)}function z(e){return e.charAt(0).toUpperCase()+e.slice(1)}function B(e){return e.split(`-`).filter(Boolean).map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join(` `)}function V(e){return e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`)}function H(e,t){try{let{content:n,frontmatter:i}=F(e.path);if(i.ready===!0){r.warn(`This ${t} is already marked as ready.`);return}let a={...i,ready:!0};I(e.path,{content:n,frontmatter:a}),r.success(`${z(t)} marked as ready: ${e.relativePath}`)}catch(e){M(`Failed to mark ${t} as ready: ${e instanceof Error?e.message:String(e)}`)}}function U(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`}async function Ie(){let e=N(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=>!T.includes(e)))&&M(`Unable to resolve selected content types.`),e}async function Le(){let e=new Date,t=7;for(;;){let n=N(await o({message:`When do you want to create content?`,options:Re(e,t)}));if(n===`__load-more__`){t+=7;continue}return typeof n!=`string`&&M(`Unable to resolve selected date: ${String(n)}`),re(n)}}function Re(e,t){let n=[];for(let r=0;r<t;r++){let t=y(e,r),i=b(t,`yyyy-MM-dd`),a=b(t,`EEEE`),o=i;o=r===0?`${a.padEnd(10)} ${i} - Today`:r===1?`${a.padEnd(10)} ${i} - Tomorrow`:`${a.padEnd(10)} ${i} - ${r} days ahead`,n.push({label:o,value:i})}return n.push({label:`Show 7 more dates (${t+1}-${t+7} days ahead)`,value:xe,hint:`Extend the list up to ${b(y(e,t+7-1),`yyyy-MM-dd`)}`}),n}async function ze(e){if(e.length===0)return;let t=N(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`&&M(`Unable to resolve selected theme: ${String(t)}`);let n=t;return n.length>0?n:void 0}async function Be(){return L({message:`What is the title of your content?`,placeholder:`Enter content title`,requiredMessage:`Title is required`})}async function Ve(){let e=N(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`&&M(`Unable to resolve LinkedIn video choice: ${String(e)}`),e}async function He(e){t(`Content Creation - Create dated content directory`);let n=await C(),r=await Be(),i=await Ie(),o={title:r};i.includes(`linkedin`)&&(o.hasVideo=await Ve(),o.hasImages=!o.hasVideo,n.thematic.length>0&&(o.theme=await ze(n.thematic)));let s=await Le();Ce(s,i,o,n,e);let c=i.join(`, `);a(`✓ Content directory created: ${b(s,`yyyy/MM/dd`)} (${c})`)}function Ue(e){t(`Content Creation - List Upcoming Publications`),k(e),a(`✓ Upcoming publications listed`)}function We(e){t(`Content Creation - Rebuild Content Indexes`),A(e),a(`✓ Content indexes rebuilt successfully`)}const Ge=ue(new URL(`../../../../`,import.meta.url)),W=`content-creation.ics`;function G(e){return e.replace(/\\/g,`\\\\`).replace(/\r\n|\r|\n/g,`\\n`).replace(/;/g,`\\;`).replace(/,/g,`\\,`)}function Ke(e){if(e.length<=75)return e;let t=[],n=e;for(;n.length>75;)t.push(n.slice(0,75)),n=` ${n.slice(75)}`;return t.push(n),t.join(`\r
|
|
4
|
+
`)}function qe(e){return e.toISOString().replace(/[-:]/g,``).replace(/\.\d{3}Z$/,`Z`)}function Je(e){return b(e,`yyyyMMdd`)}function Ye(e){return e.split(`/`).map(e=>encodeURIComponent(e)).join(`/`)}function Xe(e){return`https://github.com/barbapapazes/content-creation/blob/main/${Ye(e)}`}function Ze(e){let t=Ye(e);return`vscode://file${t.startsWith(`/`)?``:`/`}${t}`}function Qe(e){let t=[`GitHub: ${Xe(e.relativePath)}`,`VS Code: ${Ze(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(`
|
|
5
|
+
`)}function $e(e){return`${e.ready?`Ready`:`Not ready`} · ${e.title} · ${E.linkedin}`}function et(e){return`linkedin-${le(`sha1`).update(e).digest(`hex`)}@barbapapazes`}function tt(e){let t=[`BEGIN:VCALENDAR`,`VERSION:2.0`,`PRODID:-//Barbapapazes//Content Creation LinkedIn Calendar//EN`,`CALSCALE:GREGORIAN`,`METHOD:PUBLISH`,`X-WR-CALNAME:${G(`content-creation.ics`)}`];for(let n of e){let e=qe(new Date(Date.UTC(n.date.getFullYear(),n.date.getMonth(),n.date.getDate()))),r=y(n.date,1);t.push(`BEGIN:VEVENT`,`UID:${et(n.relativePath)}`,`DTSTAMP:${e}`,`SUMMARY:${G($e(n))}`,`DTSTART;VALUE=DATE:${Je(n.date)}`,`DTEND;VALUE=DATE:${Je(r)}`,`TRANSP:TRANSPARENT`,`DESCRIPTION:${G(Qe(n))}`,`END:VEVENT`)}return t.push(`END:VCALENDAR`),`${t.map(Ke).join(`\r
|
|
6
|
+
`)}\r\n`}function nt(){let t=_(Ge,`node_modules`,`.bin`,e.platform===`win32`?`wrangler.cmd`:`wrangler`);return c(t)?t:`wrangler`}function rt(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(W,n);return i.searchParams.set(`token`,t.calendar.token),i.toString()}function it(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 at(t){let n=nt();try{ce(n,[`r2`,`object`,`put`,`content-creation/${W}`,`--pipe`,`--content-type`,`text/calendar; charset=utf-8`,`--remote`],{cwd:Ge,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 ot(e,t){let n=it(e);if(n.length===0)return r.info(`No LinkedIn publications found. Skipping calendar upload.`),null;let i=rt(t);return at(tt(n)),r.success(`✓ LinkedIn calendar uploaded to R2 as ${W}`),r.info(`Subscription URL: ${i}`),i}function st(e){let t=[];try{let n=d(e);for(let r of n){if(!p(_(e,r)).isFile())continue;let n=r.toLowerCase().substring(r.lastIndexOf(`.`));Se.includes(n)&&t.push(r)}}catch(e){M(`Error reading directory: ${e instanceof Error?e.message:String(e)}`)}return t.sort()}function ct(e,t){try{let{content:n,frontmatter:i}=F(e.path);I(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){M(`Failed to update LinkedIn publication: ${e instanceof Error?e.message:String(e)}`)}}function lt(e){let t=st(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(`, `)}`),ct(e,t),H({path:e.path,relativePath:e.relativePath},`linkedin publication`);let n=_(e.folderPath,`linkedin-script.md`),i=e.relativePath.slice(0,e.relativePath.lastIndexOf(`/`)),a=c(n);return a&&H({path:n,relativePath:`${i}/linkedin-script.md`},`linkedin script`),{imageCount:t.length,scriptMarked:a}}async function ut(e){if(t(`Content Creation - Publish LinkedIn Calendar`),await ot(e,await C())){a(`✓ LinkedIn calendar published successfully`);return}a(`✓ No LinkedIn publications found, calendar was not updated`)}async function dt(e){return P({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 ft(e){t(`Content Creation - Mark LinkedIn Publication Ready`);let n=await dt(e),r=lt(n),i=r.scriptMarked?`script marked ready`:`no script found`;a(`✓ LinkedIn publication ready: ${n.relativePath} (${r.imageCount} image(s), ${i})`)}function pt(e,t,n){let i=U(e),a=_(v(_(n,`resources`)),i),o=`${t}.md`,s=_(a,o);if(c(s))return r.info(`${o} already exists at: ${s}`),a;l(a,{recursive:!0}),r.success(`Created directory: ${a}`);let u={title:e,url:``,date:``};return m(s,x.stringify(``,u),`utf-8`),r.success(`Created ${o} at: ${s}`),a}async function mt(){let e=N(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))||M(`Unable to resolve resource type: ${String(e)}`),e}async function ht(){return L({message:`What is the title of your content?`,placeholder:`Enter content title`,requiredMessage:`Title is required`})}async function gt(e){t(`Content Creation - Create Resource`);let n=await ht(),r=await mt();pt(n,r,e),a(`✓ Resource created: resources/${U(n)}/${r}.md`)}const _t=`## Index`,K=`<!-- content-creation:series-index:start -->`,q=`<!-- content-creation:series-index:end -->`,vt=/^(\d+)\.([^.]+(?:\.[^.]+)*)\.md$/;function J(e){return v(_(e,`series`))}function yt(e){let t=J(e);return c(t)?d(t).filter(e=>Ct(_(t,e))).map(e=>wt(t,e)).sort((e,t)=>e.title.localeCompare(t.title,`fr`)):[]}function Y(e){return d(e).map(Ot).filter(e=>e!==null).map(t=>Tt(e,t)).sort((e,t)=>e.index-t.index)}function X(e){let t=Y(e),n=_(e,`README.md`);m(n,bt(c(n)?u(n,`utf-8`):``,t,e),`utf-8`),r.success(`Generated series index: ${n}`)}function bt(e,t,n){let r=xt(t);if(e.includes(K)&&e.includes(q))return e.replace(St(),r);let i=e.trimEnd();return i.length>0?`${i}\n\n${r}\n`:`# ${B(n.split(`/`).pop()||`series`)}\n\n${r}\n`}function xt(e){let t=[_t,``,K];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(``,q),t.join(`
|
|
7
|
+
`)}function St(){return RegExp(`${V(_t)}\n\n${V(K)}[\\s\\S]*?${V(q)}`)}function Ct(e){return p(e).isDirectory()}function wt(e,t){let n=_(e,t);return{name:t,path:n,title:Et(n,t)}}function Tt(e,t){let n=_(e,t.fileName);return{...t,path:n,title:Dt(n,t.slug)}}function Et(e,t){let n=_(e,`README.md`);if(c(n)){let e=u(n,`utf-8`).split(`
|
|
8
|
+
`).map(e=>e.trim()).find(e=>e.startsWith(`# `));if(e)return e.slice(2).trim()}return B(t)}function Dt(e,t){try{let{data:t}=x(u(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 B(t)}function Ot(e){let t=e.match(vt);return t?{index:Number(t[1]),slug:t[2],fileName:e}:null}function kt(e,t,n){let i=Y(e),a=jt(n,i.length),o=_(e,`${a}.${U(t)}.md`);if(c(o))throw Error(`Article already exists at ${o}`);return At(e,i,a),m(o,x.stringify(``,{title:t}),`utf-8`),r.success(`Created article: ${o}`),X(e),o}function At(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=_(e,n);f(t.path,i),r.info(`Renamed ${t.fileName} → ${n}`)}}function jt(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 Mt(e,t){let n=t.trim();if(n.length===0)throw Error(`Series title is required`);let i=U(n),a=J(e),o=_(a,i);if(l(a,{recursive:!0}),c(o))throw Error(`Series already exists: series/${i}`);return l(o),m(_(o,`README.md`),`# ${n}\n`,`utf-8`),X(o),r.success(`Created series: ${o}`),{name:i,path:o,title:n}}function Z(e,t){let{content:n,frontmatter:i}=F(e),a=R(n,t);return I(e,{content:n,frontmatter:{...i,time:a}}),r.success(`Estimated time set to ${a} min: ${e}`),a}function Nt(e){H({path:e.path,relativePath:e.fileName},`series article`)}async function Pt(e){let t=Y(e);if(t.length===0&&M(`No series articles found`),t.length===1)return t[0];let n=N(await o({message:`Which series article do you want to update?`,options:t.map(e=>({label:`#${e.index} · ${e.title}`,value:e.path,hint:e.fileName}))}));typeof n!=`string`&&M(`Unable to resolve selected series article: ${String(n)}`);let r=t.find(e=>e.path===n);return r||M(`Unable to resolve selected series article: ${n}`),r}async function Ft(){return L({message:`What is the title of your article?`,placeholder:`Enter article title`,requiredMessage:`Title is required`})}async function Q(e){let t=yt(e);if(t.length===0&&M(`No series directories found`),t.length===1)return t[0];let n=N(await o({message:`Which series do you want to work on?`,options:t.map(e=>({label:e.title,value:e.path,hint:`series/${e.name}`}))}));typeof n!=`string`&&M(`Unable to resolve selected series: ${String(n)}`);let r=t.find(e=>e.path===n);return r||M(`Unable to resolve selected series: ${n}`),r}async function It(e){let t=Y(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=N(await o({message:`Where should the new article be inserted?`,options:n}));return typeof r!=`string`&&M(`Unable to resolve selected insert position: ${String(r)}`),Number(r)}async function Lt(){return L({message:`What is the title of your series?`,placeholder:`Enter series title`,requiredMessage:`Title is required`})}function Rt(e){return zt(e,e).sort((e,t)=>e.relativePath.localeCompare(t.relativePath))}function zt(e,t){let n=d(t,{withFileTypes:!0}),r=[];for(let i of n){let n=_(t,i.name);if(i.isDirectory()){r.push(...zt(e,n));continue}!i.isFile()||g(i.name).toLowerCase()!==`.mp4`||r.push({name:i.name,path:n,relativePath:ne(e,n)})}return r}async function Bt(e){let t=Rt(e);if(t.length===0&&M(`No .mp4 files found in series directory: ${e}`),t.length===1)return t[0];let n=N(await o({message:`Which series video do you want to process?`,options:t.map(e=>({label:e.relativePath,value:e.path,hint:e.name}))}));typeof n!=`string`&&M(`Unable to resolve selected series video: ${String(n)}`);let r=t.find(e=>e.path===n);return r||M(`Unable to resolve selected series video: ${n}`),r}async function Vt(){let e=await s({message:`Enter text for the thumbnail (or press Enter to skip):`,placeholder:`Optional thumbnail text`});if(typeof e!=`symbol`)return e.trim()||void 0}async function Ht(e){let t=Ut(e);if(t.length===0){r.error(`No templates found in ${e||`the configured templates directory`}`),r.info(`Please add SVG templates before generating thumbnails.`);return}let n=await o({message:`Choose a template:`,options:t.map(e=>({label:e,value:e}))});if(typeof n!=`symbol`)return n}function Ut(e){if(!e||!c(e))return[];try{return d(e,{withFileTypes:!0}).filter(e=>e.isFile()&&g(e.name).toLowerCase()===`.svg`).map(e=>e.name.replace(/\.svg$/i,``)).sort((e,t)=>e.localeCompare(t))}catch{return[]}}async function Wt(e){t(`Content Creation - Estimate Series Article Time`);let n=await C(),r=await Q(e),i=await Pt(r.path),o=Z(i.path,n.reading.wordsPerMinute);a(`✓ Estimated time updated for ${r.name}/${i.fileName}: ${o} min`)}async function Gt(e){t(`Content Creation - Create Series`),a(`✓ Series created: series/${Mt(e,await Lt()).name}`)}async function Kt(e){t(`Content Creation - Create Series Article`);let n=await Q(e),r=await Ft(),i=await It(n.path),o=kt(n.path,r,i);a(`✓ Series article created: ${n.name}/${h(o)}`)}async function qt(e){let n=await C();n.videoProcessing.openaiApiKey||M(`OpenAI API key is required. Set OPENAI_API_KEY in .env or configure videoProcessing.openaiApiKey in content-creation.config.{ts,js,mjs,json}.`);let i=await Q(e),o=await Bt(i.path);t(`Content Creation - Process Series Video: ${h(o.path)}`);let{videoPath:s,audioPath:c}=await fe(o.path),{srtPath:l}=await me(s,c,{openaiApiKey:n.videoProcessing.openaiApiKey,language:n.videoProcessing.language,model:n.videoProcessing.model,templatesDir:n.templatesDir});de(c,`audio file`);let u=await Vt();if(!u){a(`✓ Transcription saved to: ${l}`);return}let d=await Ht(n.templatesDir);if(!d){r.warn(`No template selected. Skipping thumbnail generation.`),a(`✓ Transcription saved to: ${l}`);return}let{thumbnailPaths:f}=await pe(s,u,d,n.templatesDir);a(`✓ Series video processed: series/${i.name}/${o.relativePath}\n✓ Transcription saved to: ${l}\n✓ Thumbnails saved:\n - ${f.join(`
|
|
9
|
+
- `)}`)}async function Jt(e){t(`Content Creation - Mark Series Article Ready`);let n=await C(),r=await Q(e),i=await Pt(r.path);Nt(i);let o=Z(i.path,n.reading.wordsPerMinute);a(`✓ Series article updated for ${r.name}/${i.fileName}: ready + ${o} min`)}async function $(e,t){let n=e.path,{content:i,frontmatter:a}=F(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)&&M(`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=ie(o,D.hours),o=ae(o,D.minutes),o=oe(o,D.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();M(`Failed to schedule reminder: ${e.status} ${e.statusText}\n${t}`)}let o=await e.json();I(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: ${b(c,`yyyy-MM-dd HH:mm:ss`)} UTC`),!0}catch(e){M(`Failed to schedule X publication reminder: ${e instanceof Error?e.message:String(e)}`)}}async function Yt(e){return P({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 Xt(e){t(`Content Creation - Schedule X Publication Reminder`);let n=await C();a(await $(await Yt(e),n)?`✓ X publication reminder scheduled`:`✓ X publication was already scheduled`)}export{R as A,k as B,Ue as C,z as D,H as E,M as F,C as H,je as I,Me as L,F as M,I as N,V as O,j as P,N as R,We as S,U as T,Ce as V,pt as _,Kt as a,lt as b,Nt as c,kt as d,X as f,gt as g,J as h,qt as i,L as j,B as k,Z as l,yt as m,$ as n,Gt as o,Y as p,Jt as r,Wt as s,Xt as t,Mt as u,ft as v,He as w,ot as x,ut as y,A as z};
|
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.20.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"
|
|
@@ -30,17 +31,18 @@
|
|
|
30
31
|
"node": ">=24"
|
|
31
32
|
},
|
|
32
33
|
"dependencies": {
|
|
33
|
-
"@clack/prompts": "^
|
|
34
|
+
"@clack/prompts": "^1.4.0",
|
|
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
|
-
"wrangler": "^4.61.1"
|
|
39
|
+
"wrangler": "^4.61.1",
|
|
40
|
+
"@barbapapazes/video-toolkit": "0.20.0"
|
|
39
41
|
},
|
|
40
42
|
"devDependencies": {
|
|
41
43
|
"@tsconfig/node24": "^24.0.4",
|
|
42
44
|
"@types/node": "^22.19.17",
|
|
43
|
-
"tsdown": "^0.
|
|
45
|
+
"tsdown": "^0.22.0",
|
|
44
46
|
"typescript": "^5.9.3"
|
|
45
47
|
},
|
|
46
48
|
"scripts": {
|