@life-and-dev/mdsite 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/README.md +228 -0
  2. package/bin/mdsite.js +3 -0
  3. package/dist/commands/commands.test.d.ts +1 -0
  4. package/dist/commands/commands.test.js +302 -0
  5. package/dist/commands/commands.test.js.map +1 -0
  6. package/dist/commands/generate.d.ts +1 -0
  7. package/dist/commands/generate.js +20 -0
  8. package/dist/commands/generate.js.map +1 -0
  9. package/dist/commands/init.d.ts +1 -0
  10. package/dist/commands/init.js +16 -0
  11. package/dist/commands/init.js.map +1 -0
  12. package/dist/commands/prepare.d.ts +1 -0
  13. package/dist/commands/prepare.js +111 -0
  14. package/dist/commands/prepare.js.map +1 -0
  15. package/dist/commands/prepare.test.d.ts +1 -0
  16. package/dist/commands/prepare.test.js +131 -0
  17. package/dist/commands/prepare.test.js.map +1 -0
  18. package/dist/commands/preview.d.ts +5 -0
  19. package/dist/commands/preview.js +62 -0
  20. package/dist/commands/preview.js.map +1 -0
  21. package/dist/commands/start.d.ts +5 -0
  22. package/dist/commands/start.js +50 -0
  23. package/dist/commands/start.js.map +1 -0
  24. package/dist/commands/stop.d.ts +1 -0
  25. package/dist/commands/stop.js +26 -0
  26. package/dist/commands/stop.js.map +1 -0
  27. package/dist/commands/workflows.test.d.ts +1 -0
  28. package/dist/commands/workflows.test.js +183 -0
  29. package/dist/commands/workflows.test.js.map +1 -0
  30. package/dist/config/default-mdsite-config.d.ts +2 -0
  31. package/dist/config/default-mdsite-config.js +82 -0
  32. package/dist/config/default-mdsite-config.js.map +1 -0
  33. package/dist/config/default-mdsite-config.test.d.ts +1 -0
  34. package/dist/config/default-mdsite-config.test.js +42 -0
  35. package/dist/config/default-mdsite-config.test.js.map +1 -0
  36. package/dist/config/mdsite-config.d.ts +42 -0
  37. package/dist/config/mdsite-config.js +74 -0
  38. package/dist/config/mdsite-config.js.map +1 -0
  39. package/dist/config/mdsite-config.test.d.ts +1 -0
  40. package/dist/config/mdsite-config.test.js +115 -0
  41. package/dist/config/mdsite-config.test.js.map +1 -0
  42. package/dist/config/menu.d.ts +3 -0
  43. package/dist/config/menu.js +58 -0
  44. package/dist/config/menu.js.map +1 -0
  45. package/dist/config/menu.test.d.ts +1 -0
  46. package/dist/config/menu.test.js +53 -0
  47. package/dist/config/menu.test.js.map +1 -0
  48. package/dist/index.d.ts +2 -0
  49. package/dist/index.js +99 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/index.test.d.ts +1 -0
  52. package/dist/index.test.js +133 -0
  53. package/dist/index.test.js.map +1 -0
  54. package/dist/process/child-process.d.ts +9 -0
  55. package/dist/process/child-process.js +142 -0
  56. package/dist/process/child-process.js.map +1 -0
  57. package/dist/process/child-process.test.d.ts +1 -0
  58. package/dist/process/child-process.test.js +131 -0
  59. package/dist/process/child-process.test.js.map +1 -0
  60. package/dist/process/runtime-state.d.ts +16 -0
  61. package/dist/process/runtime-state.js +42 -0
  62. package/dist/process/runtime-state.js.map +1 -0
  63. package/dist/process/runtime-state.test.d.ts +1 -0
  64. package/dist/process/runtime-state.test.js +57 -0
  65. package/dist/process/runtime-state.test.js.map +1 -0
  66. package/dist/renderer/mdsite-nuxt.d.ts +22 -0
  67. package/dist/renderer/mdsite-nuxt.js +194 -0
  68. package/dist/renderer/mdsite-nuxt.js.map +1 -0
  69. package/dist/renderer/mdsite-nuxt.test.d.ts +1 -0
  70. package/dist/renderer/mdsite-nuxt.test.js +226 -0
  71. package/dist/renderer/mdsite-nuxt.test.js.map +1 -0
  72. package/mdsite-nuxt/app/app.vue +7 -0
  73. package/mdsite-nuxt/app/assets/css/bible-tooltips.css +87 -0
  74. package/mdsite-nuxt/app/assets/css/markdown.css +195 -0
  75. package/mdsite-nuxt/app/assets/css/print.css +133 -0
  76. package/mdsite-nuxt/app/components/AppBar.vue +138 -0
  77. package/mdsite-nuxt/app/components/AppFooter.vue +88 -0
  78. package/mdsite-nuxt/app/components/AppNavigation.vue +162 -0
  79. package/mdsite-nuxt/app/components/AppTableOfContents.vue +149 -0
  80. package/mdsite-nuxt/app/components/BreadcrumbNav.vue +139 -0
  81. package/mdsite-nuxt/app/components/NavigationTree.vue +169 -0
  82. package/mdsite-nuxt/app/components/SearchBox.vue +212 -0
  83. package/mdsite-nuxt/app/components/TocItem.vue +75 -0
  84. package/mdsite-nuxt/app/components/TreeNode.vue +264 -0
  85. package/mdsite-nuxt/app/components/content/MarkdownAlert.vue +42 -0
  86. package/mdsite-nuxt/app/components/content/Mermaid.vue +103 -0
  87. package/mdsite-nuxt/app/components/content/ProseA.vue +57 -0
  88. package/mdsite-nuxt/app/components/content/ProseBlockquote.vue +14 -0
  89. package/mdsite-nuxt/app/components/content/ProsePre.vue +49 -0
  90. package/mdsite-nuxt/app/components/content/ProseTable.vue +143 -0
  91. package/mdsite-nuxt/app/composables/useAppTheme.ts +89 -0
  92. package/mdsite-nuxt/app/composables/useBibleTooltips.ts +21 -0
  93. package/mdsite-nuxt/app/composables/useBreadcrumbs.ts +70 -0
  94. package/mdsite-nuxt/app/composables/useContentPostProcessing.ts +89 -0
  95. package/mdsite-nuxt/app/composables/useNavigationTree.ts +174 -0
  96. package/mdsite-nuxt/app/composables/useSearchIndex.ts +47 -0
  97. package/mdsite-nuxt/app/composables/useSearchRelevance.test.ts +255 -0
  98. package/mdsite-nuxt/app/composables/useSearchRelevance.ts +207 -0
  99. package/mdsite-nuxt/app/composables/useSidebarState.ts +62 -0
  100. package/mdsite-nuxt/app/composables/useSiteConfig.ts +37 -0
  101. package/mdsite-nuxt/app/composables/useSourceEdit.ts +69 -0
  102. package/mdsite-nuxt/app/composables/useTableOfContents.ts +141 -0
  103. package/mdsite-nuxt/app/composables/useTableParser.ts +106 -0
  104. package/mdsite-nuxt/app/composables/useTooltipConfig.ts +20 -0
  105. package/mdsite-nuxt/app/config/themes.ts +17 -0
  106. package/mdsite-nuxt/app/layouts/default.vue +376 -0
  107. package/mdsite-nuxt/app/pages/[...slug].vue +56 -0
  108. package/mdsite-nuxt/app/pages/index.vue +54 -0
  109. package/mdsite-nuxt/app/plugins/bible-tooltips.client.ts +401 -0
  110. package/mdsite-nuxt/app/plugins/bible-tooltips.test.ts +284 -0
  111. package/mdsite-nuxt/app/types/content.d.ts +10 -0
  112. package/mdsite-nuxt/app/types/table.ts +57 -0
  113. package/mdsite-nuxt/app/utils/bible-book-names.ts +37 -0
  114. package/mdsite-nuxt/app/utils/bible-verse-utils.test.ts +148 -0
  115. package/mdsite-nuxt/app/utils/bible-verse-utils.ts +236 -0
  116. package/mdsite-nuxt/content.config.ts +18 -0
  117. package/mdsite-nuxt/example.config.yml +67 -0
  118. package/mdsite-nuxt/nuxt.config.ts +247 -0
  119. package/mdsite-nuxt/package-lock.json +15922 -0
  120. package/mdsite-nuxt/package.json +50 -0
  121. package/mdsite-nuxt/playwright.config.js +47 -0
  122. package/mdsite-nuxt/public/robots.txt +2 -0
  123. package/mdsite-nuxt/scripts/generate-favicons.ts +192 -0
  124. package/mdsite-nuxt/scripts/generate-indices.ts +508 -0
  125. package/mdsite-nuxt/scripts/renderer-hooks.test.ts +277 -0
  126. package/mdsite-nuxt/scripts/renderer-hooks.ts +141 -0
  127. package/mdsite-nuxt/scripts/start.test.ts +164 -0
  128. package/mdsite-nuxt/scripts/start.ts +55 -0
  129. package/mdsite-nuxt/scripts/sync-content.ts +504 -0
  130. package/mdsite-nuxt/tsconfig.json +18 -0
  131. package/mdsite-nuxt/utils/mdsite-config.ts +240 -0
  132. package/mdsite-nuxt/vitest.config.ts +7 -0
  133. package/package.json +36 -0
@@ -0,0 +1,504 @@
1
+ #!/usr/bin/env node
2
+
3
+ import chokidar from 'chokidar'
4
+ import fs from 'fs-extra'
5
+ import path from 'path'
6
+ import { fileURLToPath } from 'url'
7
+
8
+ const __filename = fileURLToPath(import.meta.url)
9
+ const __dirname = path.dirname(__filename)
10
+
11
+ const IMAGE_EXTS = ['jpg', 'jpeg', 'png', 'webp', 'gif', 'svg']
12
+ const STATIC_FILES = [
13
+ 'favicon.ico',
14
+ 'favicon.svg',
15
+ 'apple-touch-icon.png',
16
+ 'icon-192.png',
17
+ 'icon-512.png',
18
+ 'site.webmanifest',
19
+ 'robots.txt'
20
+ ]
21
+ const MENU_FILES = ['_menu.yml', '_menu.yaml']
22
+ const LOGO_FILE = 'logo.svg'
23
+ const FAVICON_DIR = 'favicon'
24
+ const FAVICON_FILES = [
25
+ 'favicon.svg',
26
+ 'favicon.ico',
27
+ 'apple-touch-icon.png',
28
+ 'icon-192.png',
29
+ 'icon-512.png'
30
+ ]
31
+
32
+ // Debounce timers for JSON regeneration (5 second delay)
33
+ let navigationDebounceTimer: NodeJS.Timeout | null = null
34
+ let searchDebounceTimer: NodeJS.Timeout | null = null
35
+
36
+ /**
37
+ * Get content domain from environment variable (read at runtime, not import time)
38
+ */
39
+ function getContentDomain(): string {
40
+ return process.env.CONTENT || 'cms'
41
+ }
42
+
43
+ /**
44
+ * Get source directory for content domain
45
+ */
46
+ function getSourceDir(): string {
47
+ if (process.env.CONTENT_DIR) {
48
+ return process.env.CONTENT_DIR
49
+ }
50
+ return path.resolve(__dirname, '..', '../../md-content', getContentDomain())
51
+ }
52
+
53
+ /**
54
+ * Get target public directory
55
+ */
56
+ function getTargetDir(): string {
57
+ return path.resolve(__dirname, '..', 'public')
58
+ }
59
+
60
+ /**
61
+ * Regenerate navigation JSON with debouncing (5 second delay)
62
+ */
63
+ function regenerateNavigation() {
64
+ if (navigationDebounceTimer) {
65
+ clearTimeout(navigationDebounceTimer)
66
+ }
67
+
68
+ navigationDebounceTimer = setTimeout(async () => {
69
+ console.log('🔄 Regenerating navigation tree...')
70
+ try {
71
+ const { generateNavigationJson } = await import('./generate-indices.js')
72
+ await generateNavigationJson()
73
+ } catch (error) {
74
+ console.error('❌ Failed to regenerate navigation:', error)
75
+ }
76
+ navigationDebounceTimer = null
77
+ }, 5000)
78
+ }
79
+
80
+ /**
81
+ * Regenerate search index JSON with debouncing (5 second delay)
82
+ */
83
+ function regenerateSearchIndex() {
84
+ if (searchDebounceTimer) {
85
+ clearTimeout(searchDebounceTimer)
86
+ }
87
+
88
+ searchDebounceTimer = setTimeout(async () => {
89
+ console.log('🔄 Regenerating search index...')
90
+ try {
91
+ const { generateSearchIndexJson } = await import('./generate-indices.js')
92
+ await generateSearchIndexJson()
93
+ } catch (error) {
94
+ console.error('❌ Failed to regenerate search index:', error)
95
+ }
96
+ searchDebounceTimer = null
97
+ }, 5000)
98
+ }
99
+
100
+ /**
101
+ * Generate navigation and search JSON files (one-time on startup)
102
+ */
103
+ export async function generateJsonFiles() {
104
+ console.log('🔨 Generating JSON files...')
105
+
106
+ try {
107
+ const { generateNavigationJson } = await import('./generate-indices.js')
108
+ await generateNavigationJson()
109
+ } catch (error) {
110
+ console.error('❌ Failed to generate navigation:', error)
111
+ }
112
+
113
+ try {
114
+ const { generateSearchIndexJson } = await import('./generate-indices.js')
115
+ await generateSearchIndexJson()
116
+ } catch (error) {
117
+ console.error('❌ Failed to generate search index:', error)
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Copy all images, menus, and favicons from content to public directory (one-time)
123
+ */
124
+ export async function copyAllImages() {
125
+ const sourceDir = getSourceDir()
126
+ const targetDir = getTargetDir()
127
+
128
+ console.log(`📦 Copying images, menus, and favicons from: ${sourceDir}`)
129
+ console.log(`📦 Target directory: ${targetDir}`)
130
+
131
+ let copiedCount = 0
132
+
133
+ // Copy images (excluding logo.svg and favicon directory - handled separately)
134
+ for (const ext of IMAGE_EXTS) {
135
+ const files = await getAllFiles(sourceDir, ext)
136
+
137
+ for (const sourcePath of files) {
138
+
139
+ // Skip files in favicon directory - handled separately
140
+ if (sourcePath.includes(`${path.sep}${FAVICON_DIR}${path.sep}`)) {
141
+ continue
142
+ }
143
+
144
+ await copyImage(sourcePath, false)
145
+ copiedCount++
146
+ }
147
+ }
148
+
149
+ // Copy root menu file (check all supported names)
150
+ for (const menuFile of MENU_FILES) {
151
+ const rootMenuPath = path.join(sourceDir, menuFile)
152
+ if (await fs.pathExists(rootMenuPath)) {
153
+ await copyMenuFile(rootMenuPath, false)
154
+ copiedCount++
155
+ }
156
+ }
157
+
158
+ // Copy favicon files
159
+ const faviconDir = path.join(sourceDir, FAVICON_DIR)
160
+ if (await fs.pathExists(faviconDir)) {
161
+ for (const faviconFile of FAVICON_FILES) {
162
+ const sourcePath = path.join(faviconDir, faviconFile)
163
+ if (await fs.pathExists(sourcePath)) {
164
+ await copyFaviconFile(sourcePath, false)
165
+ copiedCount++
166
+ }
167
+ }
168
+ }
169
+
170
+ console.log(`✓ Copied ${copiedCount} file(s)\n`)
171
+ }
172
+
173
+ /**
174
+ * Clean public directory except static files
175
+ */
176
+ export async function cleanPublicDirectory() {
177
+ const targetDir = getTargetDir()
178
+
179
+ console.log(`🧹 Cleaning public directory...`)
180
+
181
+ if (!await fs.pathExists(targetDir)) {
182
+ return
183
+ }
184
+
185
+ const items = await fs.readdir(targetDir)
186
+
187
+ for (const item of items) {
188
+ if (!STATIC_FILES.includes(item)) {
189
+ const itemPath = path.join(targetDir, item)
190
+ await fs.remove(itemPath)
191
+ console.log(` ✗ Removed: ${item}`)
192
+ }
193
+ }
194
+
195
+ console.log(`✓ Public directory cleaned\n`)
196
+ }
197
+
198
+ /**
199
+ * Watch images, markdown files, and menu files in content directory
200
+ */
201
+ export async function startWatcher() {
202
+ const sourceDir = getSourceDir()
203
+ const targetDir = getTargetDir()
204
+
205
+ // Clean public directory first (handles domain switching)
206
+ await cleanPublicDirectory()
207
+
208
+ // Copy all existing files BEFORE starting watcher (ensures files are ready)
209
+ await copyAllImages()
210
+
211
+ // Generate navigation and search JSON files
212
+ await generateJsonFiles()
213
+
214
+ console.log(`👀 Watching images, markdown, and menus in: ${sourceDir}`)
215
+ console.log(`👀 Target directory: ${targetDir}\n`)
216
+
217
+ const patterns = [
218
+ ...IMAGE_EXTS.map(ext => `${sourceDir}/**/*.${ext}`),
219
+ `${sourceDir}/**/*.md`, // Watch all markdown files
220
+ ...MENU_FILES.map(f => `${sourceDir}/${f}`) // Watch root menu files
221
+ ]
222
+
223
+ const watcher = chokidar.watch(patterns, {
224
+ persistent: true,
225
+ ignoreInitial: true, // Files already copied above
226
+ awaitWriteFinish: {
227
+ stabilityThreshold: 500,
228
+ pollInterval: 100
229
+ }
230
+ })
231
+
232
+ watcher
233
+ .on('add', (filePath) => {
234
+ const fileName = path.basename(filePath)
235
+ if (MENU_FILES.includes(fileName)) {
236
+ copyMenuFile(filePath, true, 'added')
237
+ regenerateNavigation() // Menu change affects navigation
238
+ } else if (fileName === LOGO_FILE) {
239
+ handleLogoChange(filePath, 'added')
240
+ } else if (fileName.endsWith('.md')) {
241
+ // Markdown file added - regenerate both navigation and search
242
+ console.log(`📝 Markdown added: ${fileName}`)
243
+ regenerateNavigation()
244
+ regenerateSearchIndex()
245
+ } else {
246
+ copyImage(filePath, true, 'added')
247
+ }
248
+ })
249
+ .on('change', (filePath) => {
250
+ const fileName = path.basename(filePath)
251
+ if (MENU_FILES.includes(fileName)) {
252
+ copyMenuFile(filePath, true, 'updated')
253
+ regenerateNavigation() // Menu change affects navigation
254
+ } else if (fileName === LOGO_FILE) {
255
+ handleLogoChange(filePath, 'updated')
256
+ } else if (fileName.endsWith('.md')) {
257
+ // Markdown file changed - regenerate both navigation and search
258
+ console.log(`📝 Markdown updated: ${fileName}`)
259
+ regenerateNavigation()
260
+ regenerateSearchIndex()
261
+ } else {
262
+ copyImage(filePath, true, 'updated')
263
+ }
264
+ })
265
+ .on('unlink', (filePath) => {
266
+ const fileName = path.basename(filePath)
267
+ if (MENU_FILES.includes(fileName)) {
268
+ deleteMenuFile(filePath)
269
+ regenerateNavigation() // Menu deletion affects navigation
270
+ } else if (fileName === LOGO_FILE) {
271
+ console.log(`🗑️ Logo removed: ${fileName}`)
272
+ } else if (fileName.endsWith('.md')) {
273
+ // Markdown file deleted - regenerate both navigation and search
274
+ console.log(`📝 Markdown deleted: ${fileName}`)
275
+ regenerateNavigation()
276
+ regenerateSearchIndex()
277
+ } else {
278
+ deleteImage(filePath)
279
+ }
280
+ })
281
+ .on('error', (error) => console.error(`❌ Watcher error: ${error}`))
282
+
283
+ return watcher
284
+ }
285
+
286
+ /**
287
+ * One-time sync for production builds
288
+ */
289
+ export async function syncContent() {
290
+ await cleanPublicDirectory()
291
+ await copyAllImages()
292
+ await generateJsonFiles()
293
+ }
294
+
295
+ /**
296
+ * Transform content path to public path (strips domain prefix)
297
+ * Example: /content/kingdom/church/image.jpg → /public/church/image.jpg
298
+ */
299
+ function getPublicPath(sourcePath: string): { relativePath: string, targetPath: string } {
300
+ const sourceDir = getSourceDir()
301
+ const targetDir = getTargetDir()
302
+ // With generic sourceDir, we just calculate relative path
303
+ const relativeFromSource = path.relative(sourceDir, sourcePath)
304
+ const targetPath = path.join(targetDir, relativeFromSource)
305
+
306
+ return { relativePath: relativeFromSource, targetPath }
307
+ }
308
+
309
+ /**
310
+ * Copy a single file from content to public (images or menus)
311
+ */
312
+ async function copyFile(
313
+ sourcePath: string,
314
+ fileType: 'Image' | 'Menu',
315
+ log: boolean = true,
316
+ action: string = 'copied'
317
+ ) {
318
+ try {
319
+ // Check if this is a draft-only image (skip menus, they don't have draft checks)
320
+ if (fileType === 'Image' && await isDraftOnlyImage(sourcePath)) {
321
+ if (log) {
322
+ const fileName = path.basename(sourcePath)
323
+ console.log(`⊗ Skipped draft image: ${fileName}`)
324
+ }
325
+ return
326
+ }
327
+
328
+ const { relativePath, targetPath } = getPublicPath(sourcePath)
329
+
330
+ await fs.ensureDir(path.dirname(targetPath))
331
+ await fs.copy(sourcePath, targetPath)
332
+
333
+ if (log) {
334
+ console.log(`✓ ${fileType} ${action}: ${relativePath}`)
335
+ }
336
+ } catch (error) {
337
+ console.error(`❌ Failed to copy ${sourcePath}:`, error)
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Delete a single file from public directory (images or menus)
343
+ */
344
+ async function deleteFile(sourcePath: string, fileType: 'Image' | 'Menu') {
345
+ try {
346
+ // Check if this was a draft-only image (skip menus)
347
+ if (fileType === 'Image' && await isDraftOnlyImage(sourcePath)) {
348
+ const fileName = path.basename(sourcePath)
349
+ console.log(`⊗ Draft image removed from content: ${fileName}`)
350
+ return
351
+ }
352
+
353
+ const { relativePath, targetPath } = getPublicPath(sourcePath)
354
+
355
+ if (await fs.pathExists(targetPath)) {
356
+ await fs.remove(targetPath)
357
+ console.log(`✗ ${fileType} deleted: ${relativePath}`)
358
+ }
359
+ } catch (error) {
360
+ console.error(`❌ Failed to delete ${sourcePath}:`, error)
361
+ }
362
+ }
363
+
364
+ /**
365
+ * Copy a single image from content to public
366
+ */
367
+ async function copyImage(sourcePath: string, log: boolean = true, action: string = 'copied') {
368
+ return copyFile(sourcePath, 'Image', log, action)
369
+ }
370
+
371
+ /**
372
+ * Delete image from public directory
373
+ */
374
+ async function deleteImage(sourcePath: string) {
375
+ return deleteFile(sourcePath, 'Image')
376
+ }
377
+
378
+ /**
379
+ * Copy a single _menu.yml file from content to public
380
+ */
381
+ async function copyMenuFile(sourcePath: string, log: boolean = true, action: string = 'copied') {
382
+ return copyFile(sourcePath, 'Menu', log, action)
383
+ }
384
+
385
+ /**
386
+ * Delete _menu.yml file from public directory
387
+ */
388
+ async function deleteMenuFile(sourcePath: string) {
389
+ return deleteFile(sourcePath, 'Menu')
390
+ }
391
+
392
+ /**
393
+ * Check if an image belongs to a draft-only page (no published .md exists)
394
+ */
395
+ async function isDraftOnlyImage(imagePath: string): Promise<boolean> {
396
+ const ext = path.extname(imagePath)
397
+ const fileName = path.basename(imagePath, ext)
398
+
399
+ // Extract page prefix from image name (e.g., "something.pic.jpg" → "something")
400
+ const parts = fileName.split('.')
401
+ if (parts.length < 2) {
402
+ // Single-part filename (no prefix) - assume it's a shared image, copy it
403
+ return false
404
+ }
405
+
406
+ const pagePrefix = parts[0]
407
+ const imageDir = path.dirname(imagePath)
408
+
409
+ // Check if corresponding published .md file exists
410
+ const publishedMdPath = path.join(imageDir, `${pagePrefix}.md`)
411
+ const hasPublishedVersion = await fs.pathExists(publishedMdPath)
412
+
413
+ // Check if only draft version exists
414
+ const draftMdPath = path.join(imageDir, `${pagePrefix}.draft.md`)
415
+ const hasDraftVersion = await fs.pathExists(draftMdPath)
416
+
417
+ // Skip copying if only draft exists (no published version)
418
+ return !hasPublishedVersion && hasDraftVersion
419
+ }
420
+
421
+ /**
422
+ * Copy a single favicon file from content to public
423
+ */
424
+ async function copyFaviconFile(sourcePath: string, log: boolean = true, action: string = 'copied') {
425
+ try {
426
+ const targetDir = getTargetDir()
427
+ const fileName = path.basename(sourcePath)
428
+ const targetPath = path.join(targetDir, fileName)
429
+
430
+ await fs.ensureDir(targetDir)
431
+ await fs.copy(sourcePath, targetPath)
432
+
433
+ if (log) {
434
+ console.log(`✓ Favicon ${action}: ${fileName}`)
435
+ }
436
+ } catch (error) {
437
+ console.error(`❌ Failed to copy favicon ${sourcePath}:`, error)
438
+ }
439
+ }
440
+
441
+ /**
442
+ * Handle logo.svg changes - regenerate favicons
443
+ */
444
+ async function handleLogoChange(logoPath: string, action: string) {
445
+ const domain = getContentDomain()
446
+ console.log(`🎨 Logo ${action}, regenerating favicons for ${domain}...`)
447
+
448
+ try {
449
+ const { generateFavicons, copyFaviconsToPublic } = await import('./generate-favicons.js')
450
+ const success = await generateFavicons(domain)
451
+
452
+ if (success) {
453
+ await copyFaviconsToPublic(domain)
454
+ console.log(`✓ Favicons regenerated for ${domain}\n`)
455
+ }
456
+ } catch (error) {
457
+ console.error(`❌ Failed to regenerate favicons:`, error)
458
+ }
459
+ }
460
+
461
+ /**
462
+ * Get all files with specific extension recursively
463
+ */
464
+ async function getAllFiles(dir: string, ext: string): Promise<string[]> {
465
+ const files: string[] = []
466
+
467
+ if (!await fs.pathExists(dir)) {
468
+ return files
469
+ }
470
+
471
+ const items = await fs.readdir(dir)
472
+
473
+ for (const item of items) {
474
+ const itemPath = path.join(dir, item)
475
+ const stat = await fs.stat(itemPath)
476
+
477
+ if (stat.isDirectory()) {
478
+ const subFiles = await getAllFiles(itemPath, ext)
479
+ files.push(...subFiles)
480
+ } else if (item.toLowerCase().endsWith(`.${ext}`)) {
481
+ files.push(itemPath)
482
+ }
483
+ }
484
+
485
+ return files
486
+ }
487
+
488
+ // CLI Entry Point
489
+ if (process.argv[1] === __filename) {
490
+ const args = process.argv.slice(2)
491
+
492
+ ; (async () => {
493
+ if (args.includes('--copy')) {
494
+ await copyAllImages()
495
+ } else if (args.includes('--sync')) {
496
+ await syncContent()
497
+ } else if (args.includes('--watch')) {
498
+ await startWatcher()
499
+ }
500
+ })().catch((error) => {
501
+ console.error(error)
502
+ process.exit(1)
503
+ })
504
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ // https://nuxt.com/docs/guide/concepts/typescript
3
+ "files": [],
4
+ "references": [
5
+ {
6
+ "path": "./.nuxt/tsconfig.app.json"
7
+ },
8
+ {
9
+ "path": "./.nuxt/tsconfig.server.json"
10
+ },
11
+ {
12
+ "path": "./.nuxt/tsconfig.shared.json"
13
+ },
14
+ {
15
+ "path": "./.nuxt/tsconfig.node.json"
16
+ }
17
+ ]
18
+ }