uniweb 0.13.16 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +1 -1
  2. package/package.json +7 -7
  3. package/partials/agents.md +51 -0
  4. package/src/backend/client.js +173 -40
  5. package/src/backend/data-bundle.js +15 -3
  6. package/src/backend/foundation-bring-along.js +76 -21
  7. package/src/backend/payment-handoff.js +28 -9
  8. package/src/backend/site-media.js +147 -19
  9. package/src/backend/site-sync.js +362 -40
  10. package/src/commands/add.js +575 -273
  11. package/src/commands/build.js +160 -57
  12. package/src/commands/clone.js +79 -26
  13. package/src/commands/content.js +11 -7
  14. package/src/commands/deploy.js +80 -32
  15. package/src/commands/dev.js +39 -17
  16. package/src/commands/docs.js +44 -16
  17. package/src/commands/doctor.js +338 -82
  18. package/src/commands/export.js +15 -7
  19. package/src/commands/handoff.js +6 -2
  20. package/src/commands/i18n.js +248 -99
  21. package/src/commands/inspect.js +48 -19
  22. package/src/commands/invite.js +9 -3
  23. package/src/commands/org.js +19 -5
  24. package/src/commands/publish.js +229 -60
  25. package/src/commands/pull.js +157 -48
  26. package/src/commands/push.js +144 -42
  27. package/src/commands/refresh.js +37 -10
  28. package/src/commands/register.js +235 -64
  29. package/src/commands/rename.js +126 -46
  30. package/src/commands/runtime.js +74 -24
  31. package/src/commands/status.js +48 -15
  32. package/src/commands/sync.js +7 -2
  33. package/src/commands/template.js +12 -4
  34. package/src/commands/update.js +263 -87
  35. package/src/commands/validate.js +115 -32
  36. package/src/framework-index.json +16 -14
  37. package/src/index.js +298 -145
  38. package/src/templates/fetchers/github.js +7 -7
  39. package/src/templates/fetchers/npm.js +3 -3
  40. package/src/templates/fetchers/release.js +21 -18
  41. package/src/templates/index.js +34 -12
  42. package/src/templates/processor.js +44 -12
  43. package/src/templates/resolver.js +42 -15
  44. package/src/templates/validator.js +14 -7
  45. package/src/utils/asset-upload.js +90 -22
  46. package/src/utils/code-upload.js +62 -37
  47. package/src/utils/config.js +31 -10
  48. package/src/utils/dep-survey.js +33 -7
  49. package/src/utils/destination-prompt.js +27 -17
  50. package/src/utils/git.js +66 -22
  51. package/src/utils/host-prompt.js +4 -3
  52. package/src/utils/install-integrity.js +8 -4
  53. package/src/utils/interactive.js +3 -3
  54. package/src/utils/json-file.js +6 -2
  55. package/src/utils/names.js +15 -6
  56. package/src/utils/placement.js +16 -8
  57. package/src/utils/registry-auth.js +185 -57
  58. package/src/utils/registry-orgs.js +142 -53
  59. package/src/utils/runtime-upload.js +52 -14
  60. package/src/utils/scaffold.js +55 -14
  61. package/src/utils/site-content-refs.js +3 -1
  62. package/src/utils/update-check.js +43 -17
  63. package/src/utils/workspace.js +13 -7
  64. package/src/versions.js +18 -7
  65. package/templates/site/_gitignore +5 -0
@@ -17,14 +17,18 @@
17
17
 
18
18
  import { resolve, join, dirname, basename, relative } from 'path'
19
19
  import { existsSync } from 'fs'
20
- import { readFile, writeFile, mkdir, readdir, unlink, rename } from 'fs/promises'
20
+ import {
21
+ readFile,
22
+ writeFile,
23
+ mkdir,
24
+ readdir,
25
+ unlink,
26
+ rename
27
+ } from 'fs/promises'
21
28
  import yaml from 'js-yaml'
22
29
  import { resolveDefaultLocale } from '@uniweb/core/locale-config'
23
- import {
24
- isWorkspaceRoot,
25
- findSites,
26
- promptSelect,
27
- } from '../utils/workspace.js'
30
+ import { DATA_DIR } from '@uniweb/core/data-paths'
31
+ import { isWorkspaceRoot, findSites, promptSelect } from '../utils/workspace.js'
28
32
 
29
33
  // Colors for terminal output
30
34
  const colors = {
@@ -34,7 +38,7 @@ const colors = {
34
38
  cyan: '\x1b[36m',
35
39
  green: '\x1b[32m',
36
40
  yellow: '\x1b[33m',
37
- red: '\x1b[31m',
41
+ red: '\x1b[31m'
38
42
  }
39
43
 
40
44
  function log(message) {
@@ -61,7 +65,7 @@ function parseTargetOption(args) {
61
65
  if (args[i] === '--target' || args[i] === '-t') {
62
66
  return {
63
67
  target: args[i + 1],
64
- remainingArgs: [...args.slice(0, i), ...args.slice(i + 2)],
68
+ remainingArgs: [...args.slice(0, i), ...args.slice(i + 2)]
65
69
  }
66
70
  }
67
71
  }
@@ -85,13 +89,19 @@ export async function i18n(args) {
85
89
 
86
90
  // Default to 'extract' if no subcommand (or if first arg is an option)
87
91
  const firstArg = remainingArgs[0]
88
- const effectiveSubcommand = !firstArg || firstArg.startsWith('-') ? 'extract' : firstArg
89
- const effectiveArgs = !firstArg || firstArg.startsWith('-') ? remainingArgs : remainingArgs.slice(1)
92
+ const effectiveSubcommand =
93
+ !firstArg || firstArg.startsWith('-') ? 'extract' : firstArg
94
+ const effectiveArgs =
95
+ !firstArg || firstArg.startsWith('-')
96
+ ? remainingArgs
97
+ : remainingArgs.slice(1)
90
98
 
91
99
  // Find site root
92
100
  const siteRoot = await findSiteRoot(target)
93
101
  if (!siteRoot) {
94
- error('Could not find site root. Make sure you are in a Uniweb site directory.')
102
+ error(
103
+ 'Could not find site root. Make sure you are in a Uniweb site directory.'
104
+ )
95
105
  process.exit(1)
96
106
  }
97
107
 
@@ -162,7 +172,9 @@ async function findSiteRoot(target) {
162
172
 
163
173
  if (sites.length === 0) {
164
174
  error('No sites found in this workspace.')
165
- log(`${colors.dim}Sites have @uniweb/runtime in dependencies.${colors.reset}`)
175
+ log(
176
+ `${colors.dim}Sites have @uniweb/runtime in dependencies.${colors.reset}`
177
+ )
166
178
  process.exit(1)
167
179
  }
168
180
 
@@ -220,7 +232,7 @@ async function loadSiteConfig(siteRoot) {
220
232
  defaultLocale: resolveDefaultLocale(config),
221
233
  locales,
222
234
  localesDir,
223
- ...config.i18n,
235
+ ...config.i18n
224
236
  }
225
237
  }
226
238
 
@@ -230,18 +242,22 @@ async function loadSiteConfig(siteRoot) {
230
242
  async function runExtract(siteRoot, config, args) {
231
243
  const verbose = args.includes('--verbose') || args.includes('-v')
232
244
  const dryRun = args.includes('--dry-run')
233
- const collectionsOnly = args.includes('--collections-only') || args.includes('--collections')
245
+ const collectionsOnly =
246
+ args.includes('--collections-only') || args.includes('--collections')
234
247
  const noCollections = args.includes('--no-collections')
235
248
  // --with-collections is now a no-op (collections are included by default)
236
249
 
237
250
  // Extract page content (unless --collections-only)
238
251
  if (!collectionsOnly) {
239
- log(`\n${colors.cyan}Extracting translatable content${dryRun ? ' (dry run)' : ''}...${colors.reset}\n`)
252
+ log(
253
+ `\n${colors.cyan}Extracting translatable content${dryRun ? ' (dry run)' : ''}...${colors.reset}\n`
254
+ )
240
255
 
241
256
  try {
242
257
  // Collect site content directly from source files (no build required)
243
258
  const { collectSiteContent } = await import('@uniweb/build/site')
244
- const { extractManifest, formatSyncReport } = await import('@uniweb/build/i18n')
259
+ const { extractManifest, formatSyncReport } =
260
+ await import('@uniweb/build/i18n')
245
261
 
246
262
  log(`${colors.dim}Collecting site content...${colors.reset}`)
247
263
  const siteContent = await collectSiteContent(siteRoot)
@@ -250,11 +266,15 @@ async function runExtract(siteRoot, config, args) {
250
266
  const manifestPath = join(siteRoot, config.localesDir, 'manifest.json')
251
267
  const isUpdate = existsSync(manifestPath)
252
268
 
253
- const { manifest, report } = await extractManifest(siteRoot, siteContent, {
254
- localesDir: config.localesDir,
255
- verbose,
256
- dryRun,
257
- })
269
+ const { manifest, report } = await extractManifest(
270
+ siteRoot,
271
+ siteContent,
272
+ {
273
+ localesDir: config.localesDir,
274
+ verbose,
275
+ dryRun
276
+ }
277
+ )
258
278
 
259
279
  // Show results
260
280
  const unitCount = Object.keys(manifest.units).length
@@ -269,12 +289,18 @@ async function runExtract(siteRoot, config, args) {
269
289
  if (dryRun) {
270
290
  log(`\n${colors.dim}Dry run — no files were modified.${colors.reset}`)
271
291
  } else {
272
- log(`\nManifest written to: ${colors.dim}${config.localesDir}/manifest.json${colors.reset}`)
292
+ log(
293
+ `\nManifest written to: ${colors.dim}${config.localesDir}/manifest.json${colors.reset}`
294
+ )
273
295
  }
274
296
 
275
297
  if (config.locales.length === 0) {
276
- log(`\n${colors.dim}No translation files found in ${config.localesDir}/.`)
277
- log(`After translating, create locale files like ${config.localesDir}/es.json${colors.reset}`)
298
+ log(
299
+ `\n${colors.dim}No translation files found in ${config.localesDir}/.`
300
+ )
301
+ log(
302
+ `After translating, create locale files like ${config.localesDir}/es.json${colors.reset}`
303
+ )
278
304
  }
279
305
  } catch (err) {
280
306
  error(`Extraction failed: ${err.message}`)
@@ -285,10 +311,12 @@ async function runExtract(siteRoot, config, args) {
285
311
 
286
312
  // Extract collection content (by default, skip with --no-collections)
287
313
  if (!noCollections) {
288
- log(`\n${colors.cyan}Extracting collection content${dryRun ? ' (dry run)' : ''}...${colors.reset}\n`)
314
+ log(
315
+ `\n${colors.cyan}Extracting collection content${dryRun ? ' (dry run)' : ''}...${colors.reset}\n`
316
+ )
289
317
 
290
318
  // Check if collections exist
291
- const dataDir = join(siteRoot, 'public', 'data')
319
+ const dataDir = join(siteRoot, 'public', DATA_DIR)
292
320
  if (!existsSync(dataDir)) {
293
321
  if (collectionsOnly) {
294
322
  error('No collections found. Create collection data in public/data/.')
@@ -299,14 +327,20 @@ async function runExtract(siteRoot, config, args) {
299
327
  }
300
328
 
301
329
  try {
302
- const { extractCollectionManifest, formatSyncReport } = await import('@uniweb/build/i18n')
303
-
304
- const collectionsManifestPath = join(siteRoot, config.localesDir, 'collections', 'manifest.json')
330
+ const { extractCollectionManifest, formatSyncReport } =
331
+ await import('@uniweb/build/i18n')
332
+
333
+ const collectionsManifestPath = join(
334
+ siteRoot,
335
+ config.localesDir,
336
+ 'collections',
337
+ 'manifest.json'
338
+ )
305
339
  const isUpdate = existsSync(collectionsManifestPath)
306
340
 
307
341
  const { manifest, report } = await extractCollectionManifest(siteRoot, {
308
342
  localesDir: config.localesDir,
309
- dryRun,
343
+ dryRun
310
344
  })
311
345
 
312
346
  const unitCount = Object.keys(manifest.units).length
@@ -321,10 +355,14 @@ async function runExtract(siteRoot, config, args) {
321
355
  if (dryRun) {
322
356
  log(`\n${colors.dim}Dry run — no files were modified.${colors.reset}`)
323
357
  } else {
324
- log(`\nManifest written to: ${colors.dim}${config.localesDir}/collections/manifest.json${colors.reset}`)
358
+ log(
359
+ `\nManifest written to: ${colors.dim}${config.localesDir}/collections/manifest.json${colors.reset}`
360
+ )
325
361
  }
326
362
  } else {
327
- log(`${colors.dim}No translatable content found in collections.${colors.reset}`)
363
+ log(
364
+ `${colors.dim}No translatable content found in collections.${colors.reset}`
365
+ )
328
366
  }
329
367
  } catch (err) {
330
368
  error(`Collection extraction failed: ${err.message}`)
@@ -348,7 +386,7 @@ async function runInit(siteRoot, config, args) {
348
386
  const force = args.includes('--force')
349
387
 
350
388
  // Collect locale codes from positional args (skip flags)
351
- const positionalLocales = args.filter(a => !a.startsWith('-'))
389
+ const positionalLocales = args.filter((a) => !a.startsWith('-'))
352
390
 
353
391
  // Read manifest
354
392
  const localesPath = join(siteRoot, config.localesDir)
@@ -370,13 +408,14 @@ async function runInit(siteRoot, config, args) {
370
408
  }
371
409
 
372
410
  // Determine target locales
373
- let targetLocales = positionalLocales.length > 0
374
- ? positionalLocales
375
- : config.locales
411
+ let targetLocales =
412
+ positionalLocales.length > 0 ? positionalLocales : config.locales
376
413
 
377
414
  if (!targetLocales || targetLocales.length === 0) {
378
415
  error('No target locales specified.')
379
- log(`${colors.dim}Specify locales as arguments (e.g., "uniweb i18n generate es fr")`)
416
+ log(
417
+ `${colors.dim}Specify locales as arguments (e.g., "uniweb i18n generate es fr")`
418
+ )
380
419
  log(`or configure them in site.yml under languages.${colors.reset}`)
381
420
  process.exit(1)
382
421
  }
@@ -401,7 +440,9 @@ async function runInit(siteRoot, config, args) {
401
440
  try {
402
441
  existing = JSON.parse(existingRaw)
403
442
  } catch {
404
- warn(`${locale}.json has invalid JSON, skipping (use --force to overwrite)`)
443
+ warn(
444
+ `${locale}.json has invalid JSON, skipping (use --force to overwrite)`
445
+ )
405
446
  continue
406
447
  }
407
448
 
@@ -418,7 +459,9 @@ async function runInit(siteRoot, config, args) {
418
459
  if (added > 0) {
419
460
  await writeFile(localePath, JSON.stringify(existing, null, 2) + '\n')
420
461
  const alreadyCount = existingKeys.size
421
- success(`Updated ${locale}.json (${added} new string${added !== 1 ? 's' : ''} added, ${alreadyCount} already translated)`)
462
+ success(
463
+ `Updated ${locale}.json (${added} new string${added !== 1 ? 's' : ''} added, ${alreadyCount} already translated)`
464
+ )
422
465
  } else {
423
466
  success(`${locale}.json already has all ${unitCount} strings`)
424
467
  }
@@ -431,7 +474,9 @@ async function runInit(siteRoot, config, args) {
431
474
  }
432
475
 
433
476
  await writeFile(localePath, JSON.stringify(localeData, null, 2) + '\n')
434
- success(`Created ${locale}.json (${unitCount} string${unitCount !== 1 ? 's' : ''})`)
477
+ success(
478
+ `Created ${locale}.json (${unitCount} string${unitCount !== 1 ? 's' : ''})`
479
+ )
435
480
  }
436
481
  }
437
482
 
@@ -445,7 +490,7 @@ async function runInit(siteRoot, config, args) {
445
490
  * Status command - show translation coverage
446
491
  */
447
492
  async function runStatus(siteRoot, config, args) {
448
- const locale = args.find(a => !a.startsWith('-'))
493
+ const locale = args.find((a) => !a.startsWith('-'))
449
494
  const showMissing = args.includes('--missing')
450
495
  const showFreeform = args.includes('--freeform')
451
496
  const outputJson = args.includes('--json')
@@ -478,22 +523,31 @@ async function runStatus(siteRoot, config, args) {
478
523
 
479
524
  if (config.locales.length === 0) {
480
525
  if (outputJson) {
481
- log(JSON.stringify({ error: 'No translation files found', locales: [] }, null, 2))
526
+ log(
527
+ JSON.stringify(
528
+ { error: 'No translation files found', locales: [] },
529
+ null,
530
+ 2
531
+ )
532
+ )
482
533
  } else {
483
534
  log(`${colors.dim}No translation files found in ${config.localesDir}/.`)
484
- log(`Create locale files like ${config.localesDir}/es.json to add translations.${colors.reset}`)
535
+ log(
536
+ `Create locale files like ${config.localesDir}/es.json to add translations.${colors.reset}`
537
+ )
485
538
  }
486
539
  return
487
540
  }
488
541
 
489
542
  try {
490
- const { getTranslationStatus, formatTranslationStatus } = await import('@uniweb/build/i18n')
543
+ const { getTranslationStatus, formatTranslationStatus } =
544
+ await import('@uniweb/build/i18n')
491
545
 
492
546
  const localesToCheck = locale ? [locale] : config.locales
493
547
 
494
548
  const status = await getTranslationStatus(siteRoot, {
495
549
  localesDir: config.localesDir,
496
- locales: localesToCheck,
550
+ locales: localesToCheck
497
551
  })
498
552
 
499
553
  if (outputJson) {
@@ -504,10 +558,14 @@ async function runStatus(siteRoot, config, args) {
504
558
  log(formatTranslationStatus(status))
505
559
 
506
560
  // Show next steps if there are missing translations
507
- const hasMissing = Object.values(status.locales).some(l => l.missing > 0)
561
+ const hasMissing = Object.values(status.locales).some((l) => l.missing > 0)
508
562
  if (hasMissing) {
509
- log(`\n${colors.dim}To translate missing strings, edit the locale files in ${config.localesDir}/`)
510
- log(`Or use: uniweb i18n status --missing --json > missing.json${colors.reset}`)
563
+ log(
564
+ `\n${colors.dim}To translate missing strings, edit the locale files in ${config.localesDir}/`
565
+ )
566
+ log(
567
+ `Or use: uniweb i18n status --missing --json > missing.json${colors.reset}`
568
+ )
511
569
  }
512
570
  } catch (err) {
513
571
  error(`Status check failed: ${err.message}`)
@@ -525,9 +583,17 @@ async function runStatusFreeform(siteRoot, config, locale, options = {}) {
525
583
 
526
584
  if (!existsSync(freeformPath)) {
527
585
  if (outputJson) {
528
- log(JSON.stringify({ error: 'No free-form translations found', locales: {} }, null, 2))
586
+ log(
587
+ JSON.stringify(
588
+ { error: 'No free-form translations found', locales: {} },
589
+ null,
590
+ 2
591
+ )
592
+ )
529
593
  } else {
530
- log(`${colors.dim}No free-form translations found in ${config.localesDir}/freeform/.${colors.reset}`)
594
+ log(
595
+ `${colors.dim}No free-form translations found in ${config.localesDir}/freeform/.${colors.reset}`
596
+ )
531
597
  }
532
598
  return
533
599
  }
@@ -572,7 +638,7 @@ async function runStatusFreeform(siteRoot, config, locale, options = {}) {
572
638
 
573
639
  // Find all locales
574
640
  const entries = await readdir(freeformPath, { withFileTypes: true })
575
- const locales = entries.filter(e => e.isDirectory()).map(e => e.name)
641
+ const locales = entries.filter((e) => e.isDirectory()).map((e) => e.name)
576
642
  const localesToCheck = locale ? [locale] : locales
577
643
 
578
644
  const results = {}
@@ -583,22 +649,33 @@ async function runStatusFreeform(siteRoot, config, locale, options = {}) {
583
649
 
584
650
  // Discover translations
585
651
  const discovered = await discoverFreeformTranslations(loc, localesPath)
586
- const allPaths = [...discovered.pages, ...discovered.pageIds, ...discovered.collections]
652
+ const allPaths = [
653
+ ...discovered.pages,
654
+ ...discovered.pageIds,
655
+ ...discovered.collections
656
+ ]
587
657
 
588
658
  // Check staleness
589
659
  const stale = await getStaleTranslations(localeDir, sourceHashes)
590
660
  const orphaned = await getOrphanedTranslations(localeDir, validPaths)
591
661
 
592
- const upToDate = allPaths.filter(p =>
593
- !stale.some(s => s.path === p) &&
594
- !orphaned.some(o => o.path === p)
662
+ const upToDate = allPaths.filter(
663
+ (p) =>
664
+ !stale.some((s) => s.path === p) &&
665
+ !orphaned.some((o) => o.path === p)
595
666
  )
596
667
 
597
668
  results[loc] = {
598
669
  total: allPaths.length,
599
670
  upToDate: upToDate.length,
600
- stale: stale.map(s => ({ path: s.path, recordedDate: s.recordedDate })),
601
- orphaned: orphaned.map(o => ({ path: o.path, recordedDate: o.recordedDate }))
671
+ stale: stale.map((s) => ({
672
+ path: s.path,
673
+ recordedDate: s.recordedDate
674
+ })),
675
+ orphaned: orphaned.map((o) => ({
676
+ path: o.path,
677
+ recordedDate: o.recordedDate
678
+ }))
602
679
  }
603
680
  }
604
681
 
@@ -621,7 +698,9 @@ async function runStatusFreeform(siteRoot, config, locale, options = {}) {
621
698
  if (info.stale.length > 0) {
622
699
  log(` ${colors.yellow}Stale (source changed):${colors.reset}`)
623
700
  for (const item of info.stale) {
624
- log(` ${colors.yellow}⚠${colors.reset} ${item.path} ${colors.dim}(${item.recordedDate})${colors.reset}`)
701
+ log(
702
+ ` ${colors.yellow}⚠${colors.reset} ${item.path} ${colors.dim}(${item.recordedDate})${colors.reset}`
703
+ )
625
704
  }
626
705
  }
627
706
 
@@ -632,19 +711,27 @@ async function runStatusFreeform(siteRoot, config, locale, options = {}) {
632
711
  }
633
712
  }
634
713
 
635
- log(` ${colors.dim}Summary: ${info.upToDate} up to date, ${info.stale.length} stale, ${info.orphaned.length} orphaned${colors.reset}`)
714
+ log(
715
+ ` ${colors.dim}Summary: ${info.upToDate} up to date, ${info.stale.length} stale, ${info.orphaned.length} orphaned${colors.reset}`
716
+ )
636
717
  log('')
637
718
  }
638
719
 
639
720
  // Show next steps
640
- const hasStale = Object.values(results).some(r => r.stale.length > 0)
641
- const hasOrphaned = Object.values(results).some(r => r.orphaned.length > 0)
721
+ const hasStale = Object.values(results).some((r) => r.stale.length > 0)
722
+ const hasOrphaned = Object.values(results).some(
723
+ (r) => r.orphaned.length > 0
724
+ )
642
725
 
643
726
  if (hasStale) {
644
- log(`${colors.dim}Run 'uniweb i18n update-hash <locale> --all-stale' to update hashes after reviewing.${colors.reset}`)
727
+ log(
728
+ `${colors.dim}Run 'uniweb i18n update-hash <locale> --all-stale' to update hashes after reviewing.${colors.reset}`
729
+ )
645
730
  }
646
731
  if (hasOrphaned) {
647
- log(`${colors.dim}Run 'uniweb i18n prune --freeform' to remove orphaned translations.${colors.reset}`)
732
+ log(
733
+ `${colors.dim}Run 'uniweb i18n prune --freeform' to remove orphaned translations.${colors.reset}`
734
+ )
648
735
  }
649
736
  } catch (err) {
650
737
  error(`Status check failed: ${err.message}`)
@@ -661,9 +748,17 @@ async function runStatusMissing(siteRoot, config, locale, options = {}) {
661
748
 
662
749
  if (config.locales.length === 0) {
663
750
  if (outputJson) {
664
- log(JSON.stringify({ error: 'No translation files found', missing: [] }, null, 2))
751
+ log(
752
+ JSON.stringify(
753
+ { error: 'No translation files found', missing: [] },
754
+ null,
755
+ 2
756
+ )
757
+ )
665
758
  } else {
666
- log(`${colors.dim}No translation files found in ${config.localesDir}/.${colors.reset}`)
759
+ log(
760
+ `${colors.dim}No translation files found in ${config.localesDir}/.${colors.reset}`
761
+ )
667
762
  }
668
763
  return
669
764
  }
@@ -683,9 +778,10 @@ async function runStatusMissing(siteRoot, config, locale, options = {}) {
683
778
  total: result.total,
684
779
  translated: result.valid.length,
685
780
  missing: result.missing.length,
686
- coverage: result.total > 0
687
- ? Math.round((result.valid.length / result.total) * 100)
688
- : 100
781
+ coverage:
782
+ result.total > 0
783
+ ? Math.round((result.valid.length / result.total) * 100)
784
+ : 100
689
785
  }
690
786
 
691
787
  // Add locale info to each missing entry
@@ -746,11 +842,15 @@ async function runStatusMissing(siteRoot, config, locale, options = {}) {
746
842
  }
747
843
 
748
844
  if (allMissing.length > 20) {
749
- log(`\n ${colors.dim}... and ${allMissing.length - 20} more${colors.reset}`)
845
+ log(
846
+ `\n ${colors.dim}... and ${allMissing.length - 20} more${colors.reset}`
847
+ )
750
848
  }
751
849
  }
752
850
 
753
- log(`\n${colors.dim}Use --json to export for translation tools.${colors.reset}`)
851
+ log(
852
+ `\n${colors.dim}Use --json to export for translation tools.${colors.reset}`
853
+ )
754
854
  } catch (err) {
755
855
  error(`Status check failed: ${err.message}`)
756
856
  process.exit(1)
@@ -782,7 +882,7 @@ function truncateString(str, maxLen) {
782
882
  * Audit command - find stale and missing translations
783
883
  */
784
884
  async function runAudit(siteRoot, config, args) {
785
- const locale = args.find(a => !a.startsWith('-'))
885
+ const locale = args.find((a) => !a.startsWith('-'))
786
886
  const clean = args.includes('--clean')
787
887
  const verbose = args.includes('--verbose') || args.includes('-v')
788
888
 
@@ -798,12 +898,15 @@ async function runAudit(siteRoot, config, args) {
798
898
 
799
899
  if (config.locales.length === 0) {
800
900
  log(`${colors.dim}No translation files found in ${config.localesDir}/.`)
801
- log(`Create locale files like ${config.localesDir}/es.json to add translations.${colors.reset}`)
901
+ log(
902
+ `Create locale files like ${config.localesDir}/es.json to add translations.${colors.reset}`
903
+ )
802
904
  return
803
905
  }
804
906
 
805
907
  try {
806
- const { auditLocale, cleanLocale, formatAuditReport } = await import('@uniweb/build/i18n')
908
+ const { auditLocale, cleanLocale, formatAuditReport } =
909
+ await import('@uniweb/build/i18n')
807
910
 
808
911
  const localesToAudit = locale ? [locale] : config.locales
809
912
  const results = []
@@ -823,10 +926,16 @@ async function runAudit(siteRoot, config, args) {
823
926
 
824
927
  for (const result of results) {
825
928
  if (result.stale.length > 0) {
826
- const staleHashes = result.stale.map(s => s.hash)
827
- const removed = await cleanLocale(localesPath, result.locale, staleHashes)
929
+ const staleHashes = result.stale.map((s) => s.hash)
930
+ const removed = await cleanLocale(
931
+ localesPath,
932
+ result.locale,
933
+ staleHashes
934
+ )
828
935
  if (removed > 0) {
829
- success(`Removed ${removed} stale entries from ${result.locale}.json`)
936
+ success(
937
+ `Removed ${removed} stale entries from ${result.locale}.json`
938
+ )
830
939
  totalRemoved += removed
831
940
  }
832
941
  }
@@ -837,19 +946,30 @@ async function runAudit(siteRoot, config, args) {
837
946
  }
838
947
  } else {
839
948
  // Suggest --clean if there are stale entries
840
- const hasStale = results.some(r => r.stale.length > 0)
949
+ const hasStale = results.some((r) => r.stale.length > 0)
841
950
  if (hasStale) {
842
- log(`\n${colors.dim}Run with --clean to remove stale entries.${colors.reset}`)
951
+ log(
952
+ `\n${colors.dim}Run with --clean to remove stale entries.${colors.reset}`
953
+ )
843
954
  }
844
955
  }
845
956
 
846
957
  // Report entries that need inline tag updates
847
- const needsTagsTotal = results.reduce((sum, r) => sum + (r.needsTags?.length || 0), 0)
958
+ const needsTagsTotal = results.reduce(
959
+ (sum, r) => sum + (r.needsTags?.length || 0),
960
+ 0
961
+ )
848
962
  if (needsTagsTotal > 0) {
849
- log(`\n${colors.yellow}${needsTagsTotal} translation(s) have inline marks in the source but not in the translation.`)
850
- log(`These translations won't preserve accent/span styling.${colors.reset}`)
963
+ log(
964
+ `\n${colors.yellow}${needsTagsTotal} translation(s) have inline marks in the source but not in the translation.`
965
+ )
966
+ log(
967
+ `These translations won't preserve accent/span styling.${colors.reset}`
968
+ )
851
969
  if (!verbose) {
852
- log(`${colors.dim}Run with --verbose to see which entries are affected.${colors.reset}`)
970
+ log(
971
+ `${colors.dim}Run with --verbose to see which entries are affected.${colors.reset}`
972
+ )
853
973
  }
854
974
  }
855
975
  } catch (err) {
@@ -880,7 +1000,9 @@ async function runInitFreeform(siteRoot, config, args) {
880
1000
  log(`${colors.dim}Examples:`)
881
1001
  log(' uniweb i18n init-freeform es pages/about hero')
882
1002
  log(' uniweb i18n init-freeform es page-ids/installation intro')
883
- log(` uniweb i18n init-freeform es collections/articles getting-started${colors.reset}`)
1003
+ log(
1004
+ ` uniweb i18n init-freeform es collections/articles getting-started${colors.reset}`
1005
+ )
884
1006
  process.exit(1)
885
1007
  }
886
1008
 
@@ -894,7 +1016,9 @@ async function runInitFreeform(siteRoot, config, args) {
894
1016
  // Check if already exists
895
1017
  if (existsSync(targetPath)) {
896
1018
  error(`Translation already exists: ${relativePath}`)
897
- log(`${colors.dim}Edit it directly or use 'update-hash' after changes.${colors.reset}`)
1019
+ log(
1020
+ `${colors.dim}Edit it directly or use 'update-hash' after changes.${colors.reset}`
1021
+ )
898
1022
  process.exit(1)
899
1023
  }
900
1024
 
@@ -936,7 +1060,12 @@ async function runInitFreeform(siteRoot, config, args) {
936
1060
  } else if (pathType.startsWith('collections/')) {
937
1061
  // Find item in collection data
938
1062
  const collectionName = pathType.replace('collections/', '')
939
- const dataPath = join(siteRoot, 'public', 'data', `${collectionName}.json`)
1063
+ const dataPath = join(
1064
+ siteRoot,
1065
+ 'public',
1066
+ 'data',
1067
+ `${collectionName}.json`
1068
+ )
940
1069
 
941
1070
  if (existsSync(dataPath)) {
942
1071
  const dataRaw = await readFile(dataPath, 'utf-8')
@@ -971,7 +1100,9 @@ async function runInitFreeform(siteRoot, config, args) {
971
1100
  await recordHash(freeformDir, relativePath, sourceHash)
972
1101
 
973
1102
  success(`Created free-form translation: ${relativePath}`)
974
- log(`${colors.dim}Edit the file, then run 'update-hash' when source changes.${colors.reset}`)
1103
+ log(
1104
+ `${colors.dim}Edit the file, then run 'update-hash' when source changes.${colors.reset}`
1105
+ )
975
1106
  } catch (err) {
976
1107
  error(`Failed to initialize free-form translation: ${err.message}`)
977
1108
  process.exit(1)
@@ -1058,7 +1189,9 @@ async function runUpdateHash(siteRoot, config, args) {
1058
1189
 
1059
1190
  if (!locale) {
1060
1191
  error('Usage: uniweb i18n update-hash <locale> [path] [section-id]')
1061
- log(`${colors.dim}Or: uniweb i18n update-hash <locale> --all-stale${colors.reset}`)
1192
+ log(
1193
+ `${colors.dim}Or: uniweb i18n update-hash <locale> --all-stale${colors.reset}`
1194
+ )
1062
1195
  process.exit(1)
1063
1196
  }
1064
1197
 
@@ -1089,7 +1222,11 @@ async function runUpdateHash(siteRoot, config, args) {
1089
1222
  } = await import('@uniweb/build/i18n')
1090
1223
 
1091
1224
  // Build source hashes map
1092
- const sourceHashes = buildSourceHashMap(siteContent, buildFreeformPath, computeSourceHash)
1225
+ const sourceHashes = buildSourceHashMap(
1226
+ siteContent,
1227
+ buildFreeformPath,
1228
+ computeSourceHash
1229
+ )
1093
1230
 
1094
1231
  if (allStale) {
1095
1232
  // Update all stale translations
@@ -1165,7 +1302,9 @@ async function runMove(siteRoot, config, args) {
1165
1302
 
1166
1303
  if (!oldPath || !newPath) {
1167
1304
  error('Usage: uniweb i18n move <old-path> <new-path>')
1168
- log(`${colors.dim}Example: uniweb i18n move pages/docs/setup pages/getting-started${colors.reset}`)
1305
+ log(
1306
+ `${colors.dim}Example: uniweb i18n move pages/docs/setup pages/getting-started${colors.reset}`
1307
+ )
1169
1308
  process.exit(1)
1170
1309
  }
1171
1310
 
@@ -1182,7 +1321,7 @@ async function runMove(siteRoot, config, args) {
1182
1321
 
1183
1322
  // Find all locales with free-form translations
1184
1323
  const entries = await readdir(freeformPath, { withFileTypes: true })
1185
- const locales = entries.filter(e => e.isDirectory()).map(e => e.name)
1324
+ const locales = entries.filter((e) => e.isDirectory()).map((e) => e.name)
1186
1325
 
1187
1326
  let totalMoved = 0
1188
1327
 
@@ -1220,7 +1359,9 @@ async function runMove(siteRoot, config, args) {
1220
1359
  }
1221
1360
 
1222
1361
  if (totalMoved > 0) {
1223
- success(`Moved ${totalMoved} translation file(s) across ${locales.length} locale(s)`)
1362
+ success(
1363
+ `Moved ${totalMoved} translation file(s) across ${locales.length} locale(s)`
1364
+ )
1224
1365
  } else {
1225
1366
  log(`${colors.dim}No translations found at: ${oldPath}${colors.reset}`)
1226
1367
  }
@@ -1243,7 +1384,9 @@ async function runRename(siteRoot, config, args) {
1243
1384
 
1244
1385
  if (!path || !oldName || !newName) {
1245
1386
  error('Usage: uniweb i18n rename <path> <old-name> <new-name>')
1246
- log(`${colors.dim}Example: uniweb i18n rename pages/about hero welcome${colors.reset}`)
1387
+ log(
1388
+ `${colors.dim}Example: uniweb i18n rename pages/about hero welcome${colors.reset}`
1389
+ )
1247
1390
  process.exit(1)
1248
1391
  }
1249
1392
 
@@ -1260,7 +1403,7 @@ async function runRename(siteRoot, config, args) {
1260
1403
 
1261
1404
  // Find all locales with free-form translations
1262
1405
  const entries = await readdir(freeformPath, { withFileTypes: true })
1263
- const locales = entries.filter(e => e.isDirectory()).map(e => e.name)
1406
+ const locales = entries.filter((e) => e.isDirectory()).map((e) => e.name)
1264
1407
 
1265
1408
  let totalRenamed = 0
1266
1409
 
@@ -1285,7 +1428,9 @@ async function runRename(siteRoot, config, args) {
1285
1428
  if (totalRenamed > 0) {
1286
1429
  success(`Renamed translation in ${totalRenamed} locale(s)`)
1287
1430
  } else {
1288
- log(`${colors.dim}No translations found: ${path}/${oldName}.md${colors.reset}`)
1431
+ log(
1432
+ `${colors.dim}No translations found: ${path}/${oldName}.md${colors.reset}`
1433
+ )
1289
1434
  }
1290
1435
  } catch (err) {
1291
1436
  error(`Failed to rename translation: ${err.message}`)
@@ -1316,7 +1461,9 @@ async function runPrune(siteRoot, config, args) {
1316
1461
  return
1317
1462
  }
1318
1463
 
1319
- log(`\n${colors.cyan}Pruning orphaned free-form translations${dryRun ? ' (dry run)' : ''}...${colors.reset}\n`)
1464
+ log(
1465
+ `\n${colors.cyan}Pruning orphaned free-form translations${dryRun ? ' (dry run)' : ''}...${colors.reset}\n`
1466
+ )
1320
1467
 
1321
1468
  try {
1322
1469
  // Load site content
@@ -1349,7 +1496,7 @@ async function runPrune(siteRoot, config, args) {
1349
1496
 
1350
1497
  // Find all locales
1351
1498
  const entries = await readdir(freeformPath, { withFileTypes: true })
1352
- const locales = entries.filter(e => e.isDirectory()).map(e => e.name)
1499
+ const locales = entries.filter((e) => e.isDirectory()).map((e) => e.name)
1353
1500
 
1354
1501
  let totalPruned = 0
1355
1502
 
@@ -1379,14 +1526,16 @@ async function runPrune(siteRoot, config, args) {
1379
1526
 
1380
1527
  // Update manifest
1381
1528
  if (!dryRun && orphaned.length > 0) {
1382
- const paths = orphaned.map(o => o.path)
1529
+ const paths = orphaned.map((o) => o.path)
1383
1530
  await removeManifestEntries(localeDir, paths)
1384
1531
  }
1385
1532
  }
1386
1533
 
1387
1534
  if (totalPruned > 0) {
1388
1535
  if (dryRun) {
1389
- log(`\n${colors.dim}Would remove ${totalPruned} orphaned translation(s). Run without --dry-run to delete.${colors.reset}`)
1536
+ log(
1537
+ `\n${colors.dim}Would remove ${totalPruned} orphaned translation(s). Run without --dry-run to delete.${colors.reset}`
1538
+ )
1390
1539
  } else {
1391
1540
  success(`\nRemoved ${totalPruned} orphaned translation(s)`)
1392
1541
  }
@@ -1412,7 +1561,7 @@ async function discoverFiles(dir) {
1412
1561
  for (const entry of entries) {
1413
1562
  const fullPath = join(dir, entry.name)
1414
1563
  if (entry.isDirectory()) {
1415
- files.push(...await discoverFiles(fullPath))
1564
+ files.push(...(await discoverFiles(fullPath)))
1416
1565
  } else {
1417
1566
  files.push(fullPath)
1418
1567
  }