uniweb 0.13.17 → 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 +15 -13
  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,21 +17,50 @@ import { join, relative, basename } from 'node:path'
17
17
  import prompts from 'prompts'
18
18
  import yaml from 'js-yaml'
19
19
  import { resolveFoundationSrcPath } from '@uniweb/build'
20
- import { scaffoldFoundation, scaffoldSite, applyContent, applyStarter, mergeTemplateDependencies } from '../utils/scaffold.js'
21
- import { resolvePlacement, SITE_KIND, FOUNDATION_KIND } from '../utils/placement.js'
20
+ import {
21
+ scaffoldFoundation,
22
+ scaffoldSite,
23
+ applyContent,
24
+ applyStarter,
25
+ mergeTemplateDependencies
26
+ } from '../utils/scaffold.js'
27
+ import {
28
+ resolvePlacement,
29
+ SITE_KIND,
30
+ FOUNDATION_KIND
31
+ } from '../utils/placement.js'
22
32
  import {
23
33
  readWorkspaceConfig,
24
34
  addWorkspaceGlob,
25
- updateRootScripts,
35
+ updateRootScripts
26
36
  } from '../utils/config.js'
27
37
  import { discoverFoundations, discoverSites } from '../utils/discover.js'
28
- import { validatePackageName, getExistingPackageNames, resolveUniqueName } from '../utils/names.js'
38
+ import {
39
+ validatePackageName,
40
+ getExistingPackageNames,
41
+ resolveUniqueName
42
+ } from '../utils/names.js'
29
43
  import { findWorkspaceRoot } from '../utils/workspace.js'
30
- import { detectPackageManager, detectWorkspacePm, detectInstalledPnpmVersion, filterCmd, installCmd } from '../utils/pm.js'
31
- import { isNonInteractive, getCliPrefix, stripNonInteractiveFlag, formatOptions } from '../utils/interactive.js'
44
+ import {
45
+ detectPackageManager,
46
+ detectWorkspacePm,
47
+ detectInstalledPnpmVersion,
48
+ filterCmd,
49
+ installCmd
50
+ } from '../utils/pm.js'
51
+ import {
52
+ isNonInteractive,
53
+ getCliPrefix,
54
+ stripNonInteractiveFlag,
55
+ formatOptions
56
+ } from '../utils/interactive.js'
32
57
  import { resolveTemplate } from '../templates/index.js'
33
58
  import { validateTemplate } from '../templates/validator.js'
34
- import { getVersionsForTemplates, resolveCiPnpmVersion, resolveCiNodeVersion } from '../versions.js'
59
+ import {
60
+ getVersionsForTemplates,
61
+ resolveCiPnpmVersion,
62
+ resolveCiNodeVersion
63
+ } from '../versions.js'
35
64
 
36
65
  // Colors for terminal output
37
66
  const colors = {
@@ -41,20 +70,28 @@ const colors = {
41
70
  cyan: '\x1b[36m',
42
71
  green: '\x1b[32m',
43
72
  yellow: '\x1b[33m',
44
- red: '\x1b[31m',
73
+ red: '\x1b[31m'
45
74
  }
46
75
 
47
- function log(message) { console.log(message) }
48
- function success(message) { console.log(`${colors.green}✓${colors.reset} ${message}`) }
49
- function error(message) { console.error(`${colors.red}✗${colors.reset} ${message}`) }
50
- function info(message) { console.log(`${colors.dim}${message}${colors.reset}`) }
76
+ function log(message) {
77
+ console.log(message)
78
+ }
79
+ function success(message) {
80
+ console.log(`${colors.green}✓${colors.reset} ${message}`)
81
+ }
82
+ function error(message) {
83
+ console.error(`${colors.red}✗${colors.reset} ${message}`)
84
+ }
85
+ function info(message) {
86
+ console.log(`${colors.dim}${message}${colors.reset}`)
87
+ }
51
88
 
52
89
  /**
53
90
  * Parse add command arguments
54
91
  */
55
92
  function parseArgs(args) {
56
93
  const result = {
57
- subcommand: args[0], // foundation, site, extension, ci, …
94
+ subcommand: args[0], // foundation, site, extension, ci, …
58
95
  name: null,
59
96
  path: null,
60
97
  project: null,
@@ -69,7 +106,7 @@ function parseArgs(args) {
69
106
  // scaffold PR-preview workflows.
70
107
  target: null,
71
108
  projectName: null,
72
- previews: true,
109
+ previews: true
73
110
  }
74
111
 
75
112
  // Booleans (no value) consumed up-front so the value-flag loop below
@@ -94,7 +131,7 @@ function parseArgs(args) {
94
131
  '--host': 'host',
95
132
  '--domain': 'domain',
96
133
  '--target': 'target',
97
- '--project-name': 'projectName',
134
+ '--project-name': 'projectName'
98
135
  }
99
136
 
100
137
  /** Split `--flag=value` into [flag, value]; `--flag` into [flag, null]. */
@@ -156,7 +193,9 @@ export async function add(rawArgs) {
156
193
  // Find workspace root
157
194
  const rootDir = findWorkspaceRoot()
158
195
  if (!rootDir) {
159
- error('Not in a Uniweb workspace. Run this command from a project directory.')
196
+ error(
197
+ 'Not in a Uniweb workspace. Run this command from a project directory.'
198
+ )
160
199
  error('Use "uniweb create" to create a new project first.')
161
200
  process.exit(1)
162
201
  }
@@ -166,37 +205,73 @@ export async function add(rawArgs) {
166
205
  if (!args.length || (args[0] && args[0].startsWith('--'))) {
167
206
  if (nonInteractive) {
168
207
  error(`Missing subcommand.\n`)
169
- log(formatOptions([
170
- { label: 'project', description: 'Co-located foundation + site pair' },
171
- { label: 'foundation', description: 'Component system for content authors' },
172
- { label: 'site', description: 'Content site' },
173
- { label: 'extension', description: 'Additional component package' },
174
- { label: 'section', description: 'Section type in a foundation' },
175
- { label: 'ci', description: 'CI deploy workflow for a host (e.g., GitHub Pages)' },
176
- ]))
208
+ log(
209
+ formatOptions([
210
+ {
211
+ label: 'project',
212
+ description: 'Co-located foundation + site pair'
213
+ },
214
+ {
215
+ label: 'foundation',
216
+ description: 'Component system for content authors'
217
+ },
218
+ { label: 'site', description: 'Content site' },
219
+ { label: 'extension', description: 'Additional component package' },
220
+ { label: 'section', description: 'Section type in a foundation' },
221
+ {
222
+ label: 'ci',
223
+ description: 'CI deploy workflow for a host (e.g., GitHub Pages)'
224
+ }
225
+ ])
226
+ )
177
227
  log('')
178
- log(`Usage: ${prefix} add <project|foundation|site|extension|section|ci> [name]`)
228
+ log(
229
+ `Usage: ${prefix} add <project|foundation|site|extension|section|ci> [name]`
230
+ )
179
231
  process.exit(1)
180
232
  }
181
233
 
182
- const response = await prompts({
183
- type: 'select',
184
- name: 'subcommand',
185
- message: 'What would you like to add?',
186
- choices: [
187
- { title: 'Project', value: 'project', description: 'Co-located foundation + site pair' },
188
- { title: 'Foundation', value: 'foundation', description: 'Component system for content authors' },
189
- { title: 'Site', value: 'site', description: 'Content site' },
190
- { title: 'Extension', value: 'extension', description: 'Additional component package' },
191
- { title: 'Section', value: 'section', description: 'Section type in a foundation' },
192
- { title: 'CI workflow', value: 'ci', description: 'Deploy workflow for a host (e.g., GitHub Pages)' },
193
- ],
194
- }, {
195
- onCancel: () => {
196
- log('\nCancelled.')
197
- process.exit(0)
234
+ const response = await prompts(
235
+ {
236
+ type: 'select',
237
+ name: 'subcommand',
238
+ message: 'What would you like to add?',
239
+ choices: [
240
+ {
241
+ title: 'Project',
242
+ value: 'project',
243
+ description: 'Co-located foundation + site pair'
244
+ },
245
+ {
246
+ title: 'Foundation',
247
+ value: 'foundation',
248
+ description: 'Component system for content authors'
249
+ },
250
+ { title: 'Site', value: 'site', description: 'Content site' },
251
+ {
252
+ title: 'Extension',
253
+ value: 'extension',
254
+ description: 'Additional component package'
255
+ },
256
+ {
257
+ title: 'Section',
258
+ value: 'section',
259
+ description: 'Section type in a foundation'
260
+ },
261
+ {
262
+ title: 'CI workflow',
263
+ value: 'ci',
264
+ description: 'Deploy workflow for a host (e.g., GitHub Pages)'
265
+ }
266
+ ]
198
267
  },
199
- })
268
+ {
269
+ onCancel: () => {
270
+ log('\nCancelled.')
271
+ process.exit(0)
272
+ }
273
+ }
274
+ )
200
275
  parsed = parseArgs([response.subcommand, ...args])
201
276
  } else {
202
277
  parsed = parseArgs(args)
@@ -238,7 +313,9 @@ export async function add(rawArgs) {
238
313
  break
239
314
  default:
240
315
  error(`Unknown subcommand: ${parsed.subcommand}`)
241
- log(`Valid subcommands: project, foundation, site, extension, section, ci`)
316
+ log(
317
+ `Valid subcommands: project, foundation, site, extension, section, ci`
318
+ )
242
319
  process.exit(1)
243
320
  }
244
321
  }
@@ -273,11 +350,17 @@ async function addFoundation(rootDir, projectName, opts, pm = 'pnpm') {
273
350
 
274
351
  // Collision check 1: target folder already exists.
275
352
  if (existsSync(fullPath)) {
276
- error(`Cannot create foundation: ${colors.bright}${relativePath}/${colors.reset} already exists.`)
353
+ error(
354
+ `Cannot create foundation: ${colors.bright}${relativePath}/${colors.reset} already exists.`
355
+ )
277
356
  log('')
278
357
  log(`Pick a different name, or pass --path to choose a different folder:`)
279
- log(` ${colors.cyan}${getCliPrefix()} add foundation <name>${colors.reset}`)
280
- log(` ${colors.cyan}${getCliPrefix()} add foundation <name> --path <parent-dir>${colors.reset}`)
358
+ log(
359
+ ` ${colors.cyan}${getCliPrefix()} add foundation <name>${colors.reset}`
360
+ )
361
+ log(
362
+ ` ${colors.cyan}${getCliPrefix()} add foundation <name> --path <parent-dir>${colors.reset}`
363
+ )
281
364
  process.exit(1)
282
365
  }
283
366
 
@@ -293,33 +376,47 @@ async function addFoundation(rootDir, projectName, opts, pm = 'pnpm') {
293
376
  // situation, not a disambiguation case.
294
377
  if (existingNames.has(packageName)) {
295
378
  const sites = await discoverSites(rootDir)
296
- const isSiteCollision = sites.some(s => s.name === packageName)
379
+ const isSiteCollision = sites.some((s) => s.name === packageName)
297
380
  if (isSiteCollision) {
298
381
  const suffixed = `${packageName}-src`
299
382
  if (existingNames.has(suffixed)) {
300
- error(`Cannot create foundation: both ${colors.bright}${packageName}${colors.reset} and ${colors.bright}${suffixed}${colors.reset} are taken in this workspace.`)
383
+ error(
384
+ `Cannot create foundation: both ${colors.bright}${packageName}${colors.reset} and ${colors.bright}${suffixed}${colors.reset} are taken in this workspace.`
385
+ )
301
386
  log(`Pick a different name:`)
302
- log(` ${colors.cyan}${getCliPrefix()} add foundation <other-name>${colors.reset}`)
387
+ log(
388
+ ` ${colors.cyan}${getCliPrefix()} add foundation <other-name>${colors.reset}`
389
+ )
303
390
  process.exit(1)
304
391
  }
305
- info(`Package "${packageName}" is taken by a site; using "${suffixed}" for this foundation.`)
392
+ info(
393
+ `Package "${packageName}" is taken by a site; using "${suffixed}" for this foundation.`
394
+ )
306
395
  packageName = suffixed
307
396
  } else {
308
- error(`Cannot create foundation: a foundation named ${colors.bright}${packageName}${colors.reset} already exists in this workspace.`)
397
+ error(
398
+ `Cannot create foundation: a foundation named ${colors.bright}${packageName}${colors.reset} already exists in this workspace.`
399
+ )
309
400
  log(`Pick a different name:`)
310
- log(` ${colors.cyan}${getCliPrefix()} add foundation <other-name>${colors.reset}`)
401
+ log(
402
+ ` ${colors.cyan}${getCliPrefix()} add foundation <other-name>${colors.reset}`
403
+ )
311
404
  process.exit(1)
312
405
  }
313
406
  }
314
407
 
315
408
  // Scaffold
316
- await scaffoldFoundation(fullPath, {
317
- name: packageName,
318
- projectName,
319
- isExtension: false,
320
- }, {
321
- onProgress: (msg) => info(` ${msg}`),
322
- })
409
+ await scaffoldFoundation(
410
+ fullPath,
411
+ {
412
+ name: packageName,
413
+ projectName,
414
+ isExtension: false
415
+ },
416
+ {
417
+ onProgress: (msg) => info(` ${msg}`)
418
+ }
419
+ )
323
420
 
324
421
  // Apply template content if --from specified
325
422
  if (opts.from) {
@@ -335,7 +432,9 @@ async function addFoundation(rootDir, projectName, opts, pm = 'pnpm') {
335
432
  const sites = await discoverSites(rootDir)
336
433
  await updateRootScripts(rootDir, sites, pm)
337
434
 
338
- success(`Created foundation ${colors.bright}${packageName}${colors.reset} at ${relativePath}/`)
435
+ success(
436
+ `Created foundation ${colors.bright}${packageName}${colors.reset} at ${relativePath}/`
437
+ )
339
438
  log('')
340
439
  log(`Next: ${colors.cyan}${installCmd(pm)}${colors.reset}`)
341
440
  }
@@ -364,11 +463,15 @@ async function addSite(rootDir, projectName, opts, pm = 'pnpm') {
364
463
 
365
464
  // Collision check 1: target folder exists.
366
465
  if (existsSync(fullPath)) {
367
- error(`Cannot create site: ${colors.bright}${relativePath}/${colors.reset} already exists.`)
466
+ error(
467
+ `Cannot create site: ${colors.bright}${relativePath}/${colors.reset} already exists.`
468
+ )
368
469
  log('')
369
470
  log(`Pick a different name, or pass --path to choose a different folder:`)
370
471
  log(` ${colors.cyan}${getCliPrefix()} add site <name>${colors.reset}`)
371
- log(` ${colors.cyan}${getCliPrefix()} add site <name> --path <parent-dir>${colors.reset}`)
472
+ log(
473
+ ` ${colors.cyan}${getCliPrefix()} add site <name> --path <parent-dir>${colors.reset}`
474
+ )
372
475
  process.exit(1)
373
476
  }
374
477
 
@@ -377,21 +480,31 @@ async function addSite(rootDir, projectName, opts, pm = 'pnpm') {
377
480
  // addFoundation for the rationale.
378
481
  if (existingNames.has(siteName)) {
379
482
  const foundations = await discoverFoundations(rootDir)
380
- const isFoundationCollision = foundations.some(f => f.name === siteName)
483
+ const isFoundationCollision = foundations.some((f) => f.name === siteName)
381
484
  if (isFoundationCollision) {
382
485
  const suffixed = `${siteName}-site`
383
486
  if (existingNames.has(suffixed)) {
384
- error(`Cannot create site: both ${colors.bright}${siteName}${colors.reset} and ${colors.bright}${suffixed}${colors.reset} are taken in this workspace.`)
487
+ error(
488
+ `Cannot create site: both ${colors.bright}${siteName}${colors.reset} and ${colors.bright}${suffixed}${colors.reset} are taken in this workspace.`
489
+ )
385
490
  log(`Pick a different name:`)
386
- log(` ${colors.cyan}${getCliPrefix()} add site <other-name>${colors.reset}`)
491
+ log(
492
+ ` ${colors.cyan}${getCliPrefix()} add site <other-name>${colors.reset}`
493
+ )
387
494
  process.exit(1)
388
495
  }
389
- info(`Package "${siteName}" is taken by a foundation; using "${suffixed}" for this site.`)
496
+ info(
497
+ `Package "${siteName}" is taken by a foundation; using "${suffixed}" for this site.`
498
+ )
390
499
  siteName = suffixed
391
500
  } else {
392
- error(`Cannot create site: a site named ${colors.bright}${siteName}${colors.reset} already exists in this workspace.`)
501
+ error(
502
+ `Cannot create site: a site named ${colors.bright}${siteName}${colors.reset} already exists in this workspace.`
503
+ )
393
504
  log(`Pick a different name:`)
394
- log(` ${colors.cyan}${getCliPrefix()} add site <other-name>${colors.reset}`)
505
+ log(
506
+ ` ${colors.cyan}${getCliPrefix()} add site <other-name>${colors.reset}`
507
+ )
395
508
  process.exit(1)
396
509
  }
397
510
  }
@@ -404,26 +517,36 @@ async function addSite(rootDir, projectName, opts, pm = 'pnpm') {
404
517
  const foundationPath = computeFoundationPath(relativePath, foundation.path)
405
518
 
406
519
  // Scaffold
407
- await scaffoldSite(fullPath, {
408
- name: siteName,
409
- projectName,
410
- foundationName: foundation.name,
411
- foundationPath,
412
- foundationRef: foundation.name,
413
- }, {
414
- onProgress: (msg) => info(` ${msg}`),
415
- })
520
+ await scaffoldSite(
521
+ fullPath,
522
+ {
523
+ name: siteName,
524
+ projectName,
525
+ foundationName: foundation.name,
526
+ foundationPath,
527
+ foundationRef: foundation.name
528
+ },
529
+ {
530
+ onProgress: (msg) => info(` ${msg}`)
531
+ }
532
+ )
416
533
  } else {
417
534
  // No foundation — scaffold without wiring
418
- await scaffoldSite(fullPath, {
419
- name: siteName,
420
- projectName,
421
- foundationName: '',
422
- foundationPath: '',
423
- }, {
424
- onProgress: (msg) => info(` ${msg}`),
425
- })
426
- log(` ${colors.yellow}⚠ No foundation wired. Add one later with: uniweb add foundation${colors.reset}`)
535
+ await scaffoldSite(
536
+ fullPath,
537
+ {
538
+ name: siteName,
539
+ projectName,
540
+ foundationName: '',
541
+ foundationPath: ''
542
+ },
543
+ {
544
+ onProgress: (msg) => info(` ${msg}`)
545
+ }
546
+ )
547
+ log(
548
+ ` ${colors.yellow}⚠ No foundation wired. Add one later with: uniweb add foundation${colors.reset}`
549
+ )
427
550
  }
428
551
 
429
552
  // Apply template content if --from specified
@@ -436,22 +559,32 @@ async function addSite(rootDir, projectName, opts, pm = 'pnpm') {
436
559
 
437
560
  // Update root scripts (discover sites after registration — includes the new one)
438
561
  const sites = await discoverSites(rootDir)
439
- if (!sites.find(s => s.path === relativePath)) {
562
+ if (!sites.find((s) => s.path === relativePath)) {
440
563
  sites.push({ name: siteName, path: relativePath })
441
564
  }
442
565
  await updateRootScripts(rootDir, sites, pm)
443
566
 
444
567
  if (foundation) {
445
- success(`Created site ${colors.bright}${siteName}${colors.reset} at ${relativePath}/ → foundation '${foundation.name}'`)
568
+ success(
569
+ `Created site ${colors.bright}${siteName}${colors.reset} at ${relativePath}/ → foundation '${foundation.name}'`
570
+ )
446
571
  } else {
447
- success(`Created site ${colors.bright}${siteName}${colors.reset} at ${relativePath}/`)
572
+ success(
573
+ `Created site ${colors.bright}${siteName}${colors.reset} at ${relativePath}/`
574
+ )
448
575
  }
449
576
  log('')
450
- log(`Next: ${colors.cyan}${installCmd(pm)} && uniweb dev ${siteName}${colors.reset}`)
577
+ log(
578
+ `Next: ${colors.cyan}${installCmd(pm)} && uniweb dev ${siteName}${colors.reset}`
579
+ )
451
580
  if (!opts.from) {
452
581
  log('')
453
- log(`${colors.dim}To add your first page, create ${relativePath}/pages/home/page.yml and a .md file.${colors.reset}`)
454
- log(`${colors.dim}Or use --from to start with template content: uniweb add site --from starter${colors.reset}`)
582
+ log(
583
+ `${colors.dim}To add your first page, create ${relativePath}/pages/home/page.yml and a .md file.${colors.reset}`
584
+ )
585
+ log(
586
+ `${colors.dim}Or use --from to start with template content: uniweb add site --from starter${colors.reset}`
587
+ )
455
588
  }
456
589
  }
457
590
 
@@ -479,17 +612,20 @@ async function addExtension(rootDir, projectName, opts, pm = 'pnpm') {
479
612
  process.exit(1)
480
613
  }
481
614
 
482
- const response = await prompts({
483
- type: 'text',
484
- name: 'name',
485
- message: 'Extension name:',
486
- validate: (value) => validatePackageName(value),
487
- }, {
488
- onCancel: () => {
489
- log('\nCancelled.')
490
- process.exit(0)
615
+ const response = await prompts(
616
+ {
617
+ type: 'text',
618
+ name: 'name',
619
+ message: 'Extension name:',
620
+ validate: (value) => validatePackageName(value)
491
621
  },
492
- })
622
+ {
623
+ onCancel: () => {
624
+ log('\nCancelled.')
625
+ process.exit(0)
626
+ }
627
+ }
628
+ )
493
629
  name = response.name
494
630
  }
495
631
 
@@ -514,13 +650,17 @@ async function addExtension(rootDir, projectName, opts, pm = 'pnpm') {
514
650
  }
515
651
 
516
652
  // Scaffold foundation with extension flag
517
- await scaffoldFoundation(fullPath, {
518
- name: extensionPackageName,
519
- projectName,
520
- isExtension: true,
521
- }, {
522
- onProgress: (msg) => info(` ${msg}`),
523
- })
653
+ await scaffoldFoundation(
654
+ fullPath,
655
+ {
656
+ name: extensionPackageName,
657
+ projectName,
658
+ isExtension: true
659
+ },
660
+ {
661
+ onProgress: (msg) => info(` ${msg}`)
662
+ }
663
+ )
524
664
 
525
665
  // Apply template content if --from specified
526
666
  if (opts.from) {
@@ -546,25 +686,44 @@ async function addExtension(rootDir, projectName, opts, pm = 'pnpm') {
546
686
  } else {
547
687
  const sites = await discoverSites(rootDir)
548
688
  if (sites.length === 1) {
549
- wiredSite = await wireExtensionToSite(rootDir, sites[0].name, name, target)
689
+ wiredSite = await wireExtensionToSite(
690
+ rootDir,
691
+ sites[0].name,
692
+ name,
693
+ target
694
+ )
550
695
  } else if (sites.length > 1) {
551
696
  if (isNonInteractive(process.argv)) {
552
697
  unwiredReason = `Multiple sites in workspace; extension not wired. Re-run with --site <name>, or edit <site>/site.yml::extensions: manually.`
553
698
  } else {
554
- const sortedSites = [...sites].sort((a, b) => a.name.localeCompare(b.name))
555
- const response = await prompts({
556
- type: 'select',
557
- name: 'site',
558
- message: 'Which site is this extension for?',
559
- choices: sortedSites.map(s => ({ title: s.name, description: s.path, value: s.name })),
560
- }, {
561
- onCancel: () => {
562
- log('\nCancelled.')
563
- process.exit(0)
699
+ const sortedSites = [...sites].sort((a, b) =>
700
+ a.name.localeCompare(b.name)
701
+ )
702
+ const response = await prompts(
703
+ {
704
+ type: 'select',
705
+ name: 'site',
706
+ message: 'Which site is this extension for?',
707
+ choices: sortedSites.map((s) => ({
708
+ title: s.name,
709
+ description: s.path,
710
+ value: s.name
711
+ }))
564
712
  },
565
- })
713
+ {
714
+ onCancel: () => {
715
+ log('\nCancelled.')
716
+ process.exit(0)
717
+ }
718
+ }
719
+ )
566
720
  if (response.site) {
567
- wiredSite = await wireExtensionToSite(rootDir, response.site, name, target)
721
+ wiredSite = await wireExtensionToSite(
722
+ rootDir,
723
+ response.site,
724
+ name,
725
+ target
726
+ )
568
727
  }
569
728
  }
570
729
  } else {
@@ -612,17 +771,20 @@ async function addProject(rootDir, projectName, opts, pm = 'pnpm') {
612
771
  process.exit(1)
613
772
  }
614
773
 
615
- const response = await prompts({
616
- type: 'text',
617
- name: 'name',
618
- message: 'Project name:',
619
- validate: (value) => validatePackageName(value),
620
- }, {
621
- onCancel: () => {
622
- log('\nCancelled.')
623
- process.exit(0)
774
+ const response = await prompts(
775
+ {
776
+ type: 'text',
777
+ name: 'name',
778
+ message: 'Project name:',
779
+ validate: (value) => validatePackageName(value)
624
780
  },
625
- })
781
+ {
782
+ onCancel: () => {
783
+ log('\nCancelled.')
784
+ process.exit(0)
785
+ }
786
+ }
787
+ )
626
788
  name = response.name
627
789
  }
628
790
 
@@ -652,26 +814,44 @@ async function addProject(rootDir, projectName, opts, pm = 'pnpm') {
652
814
 
653
815
  // Scaffold foundation (folder: src/, package name: <project>-src)
654
816
  info(`Creating foundation: ${foundationPkgName}...`)
655
- await scaffoldFoundation(join(projectDir, 'src'), {
656
- name: foundationPkgName,
657
- projectName,
658
- isExtension: false,
659
- }, { onProgress: progressCb })
817
+ await scaffoldFoundation(
818
+ join(projectDir, 'src'),
819
+ {
820
+ name: foundationPkgName,
821
+ projectName,
822
+ isExtension: false
823
+ },
824
+ { onProgress: progressCb }
825
+ )
660
826
 
661
827
  // Scaffold site
662
828
  info(`Creating site: ${sitePkgName}...`)
663
- await scaffoldSite(join(projectDir, 'site'), {
664
- name: sitePkgName,
665
- projectName,
666
- foundationName: foundationPkgName,
667
- foundationPath: 'file:../src',
668
- foundationRef: foundationPkgName,
669
- }, { onProgress: progressCb })
829
+ await scaffoldSite(
830
+ join(projectDir, 'site'),
831
+ {
832
+ name: sitePkgName,
833
+ projectName,
834
+ foundationName: foundationPkgName,
835
+ foundationPath: 'file:../src',
836
+ foundationRef: foundationPkgName
837
+ },
838
+ { onProgress: progressCb }
839
+ )
670
840
 
671
841
  // Apply template content if --from specified
672
842
  if (opts.from) {
673
- await applyFromTemplate(opts.from, 'foundation', join(projectDir, 'src'), projectName)
674
- await applyFromTemplate(opts.from, 'site', join(projectDir, 'site'), projectName)
843
+ await applyFromTemplate(
844
+ opts.from,
845
+ 'foundation',
846
+ join(projectDir, 'src'),
847
+ projectName
848
+ )
849
+ await applyFromTemplate(
850
+ opts.from,
851
+ 'site',
852
+ join(projectDir, 'site'),
853
+ projectName
854
+ )
675
855
  }
676
856
 
677
857
  // Update workspace globs for co-located layout
@@ -680,19 +860,22 @@ async function addProject(rootDir, projectName, opts, pm = 'pnpm') {
680
860
 
681
861
  // Update root scripts
682
862
  const sites = await discoverSites(rootDir)
683
- if (!sites.find(s => s.path === `${name}/site`)) {
863
+ if (!sites.find((s) => s.path === `${name}/site`)) {
684
864
  sites.push({ name: sitePkgName, path: `${name}/site` })
685
865
  }
686
866
  await updateRootScripts(rootDir, sites, pm)
687
867
 
688
868
  success(`Created project '${name}' at ${name}/`)
689
- log(` ${colors.dim}Foundation: ${name}/src/ (${foundationPkgName})${colors.reset}`)
869
+ log(
870
+ ` ${colors.dim}Foundation: ${name}/src/ (${foundationPkgName})${colors.reset}`
871
+ )
690
872
  log(` ${colors.dim}Site: ${name}/site/ (${sitePkgName})${colors.reset}`)
691
873
  log('')
692
- log(`Next: ${colors.cyan}${installCmd(pm)} && uniweb dev ${sitePkgName}${colors.reset}`)
874
+ log(
875
+ `Next: ${colors.cyan}${installCmd(pm)} && uniweb dev ${sitePkgName}${colors.reset}`
876
+ )
693
877
  }
694
878
 
695
-
696
879
  /**
697
880
  * Resolve which foundation to wire a site to
698
881
  */
@@ -701,12 +884,14 @@ async function resolveFoundation(rootDir, foundationFlag) {
701
884
 
702
885
  if (foundationFlag) {
703
886
  // Find by name
704
- const found = foundations.find(f => f.name === foundationFlag)
887
+ const found = foundations.find((f) => f.name === foundationFlag)
705
888
  if (found) return found
706
889
 
707
890
  // Not found — could be a URL or new foundation
708
891
  error(`Foundation '${foundationFlag}' not found in workspace.`)
709
- log(`Available foundations: ${foundations.map(f => f.name).join(', ') || 'none'}`)
892
+ log(
893
+ `Available foundations: ${foundations.map((f) => f.name).join(', ') || 'none'}`
894
+ )
710
895
  process.exit(1)
711
896
  }
712
897
 
@@ -722,30 +907,37 @@ async function resolveFoundation(rootDir, foundationFlag) {
722
907
  // Multiple foundations — prompt (or fail in non-interactive mode)
723
908
  if (isNonInteractive(process.argv)) {
724
909
  error(`Multiple foundations found. Specify which to use:\n`)
725
- log(formatOptions(foundations.map(f => ({
726
- label: f.name,
727
- description: f.path,
728
- }))))
910
+ log(
911
+ formatOptions(
912
+ foundations.map((f) => ({
913
+ label: f.name,
914
+ description: f.path
915
+ }))
916
+ )
917
+ )
729
918
  log('')
730
919
  log(`Usage: ${getCliPrefix()} add site <name> --foundation <name>`)
731
920
  process.exit(1)
732
921
  }
733
922
 
734
- const response = await prompts({
735
- type: 'select',
736
- name: 'foundation',
737
- message: 'Which foundation should this site use?',
738
- choices: foundations.map(f => ({
739
- title: f.name,
740
- description: f.path,
741
- value: f,
742
- })),
743
- }, {
744
- onCancel: () => {
745
- log('\nCancelled.')
746
- process.exit(0)
923
+ const response = await prompts(
924
+ {
925
+ type: 'select',
926
+ name: 'foundation',
927
+ message: 'Which foundation should this site use?',
928
+ choices: foundations.map((f) => ({
929
+ title: f.name,
930
+ description: f.path,
931
+ value: f
932
+ }))
747
933
  },
748
- })
934
+ {
935
+ onCancel: () => {
936
+ log('\nCancelled.')
937
+ process.exit(0)
938
+ }
939
+ }
940
+ )
749
941
 
750
942
  return response.foundation
751
943
  }
@@ -759,7 +951,6 @@ function computeFoundationPath(sitePath, foundationPath) {
759
951
  return `file:${rel}`
760
952
  }
761
953
 
762
-
763
954
  /**
764
955
  * Apply content from a template to a scaffolded package
765
956
  *
@@ -768,34 +959,47 @@ function computeFoundationPath(sitePath, foundationPath) {
768
959
  * @param {string} targetDir - Absolute path to the scaffolded package
769
960
  * @param {string} projectName - Project name for template context
770
961
  */
771
- async function applyFromTemplate(templateId, packageType, targetDir, projectName) {
962
+ async function applyFromTemplate(
963
+ templateId,
964
+ packageType,
965
+ targetDir,
966
+ projectName
967
+ ) {
772
968
  info(`Resolving template: ${templateId}...`)
773
969
 
774
970
  const resolved = await resolveTemplate(templateId, {
775
- onProgress: (msg) => info(` ${msg}`),
971
+ onProgress: (msg) => info(` ${msg}`)
776
972
  })
777
973
 
778
974
  try {
779
975
  const metadata = await validateTemplate(resolved.path, {})
780
976
 
781
977
  // Look in contentDirs for matching package type
782
- const match = metadata.contentDirs.find(d => d.type === packageType) ||
783
- metadata.contentDirs.find(d => d.name === packageType)
978
+ const match =
979
+ metadata.contentDirs.find((d) => d.type === packageType) ||
980
+ metadata.contentDirs.find((d) => d.name === packageType)
784
981
  const contentDir = match ? match.dir : null
785
982
 
786
983
  if (contentDir) {
787
984
  info(`Applying ${metadata.name} content...`)
788
- await applyContent(contentDir, targetDir, {
789
- projectName,
790
- versions: getVersionsForTemplates(),
791
- }, {
792
- onProgress: (msg) => info(` ${msg}`),
793
- renames: match.renames,
794
- })
985
+ await applyContent(
986
+ contentDir,
987
+ targetDir,
988
+ {
989
+ projectName,
990
+ versions: getVersionsForTemplates()
991
+ },
992
+ {
993
+ onProgress: (msg) => info(` ${msg}`),
994
+ renames: match.renames
995
+ }
996
+ )
795
997
 
796
998
  // Merge template dependencies
797
999
  if (metadata.dependencies) {
798
- const deps = metadata.dependencies[packageType] || metadata.dependencies[match?.name]
1000
+ const deps =
1001
+ metadata.dependencies[packageType] ||
1002
+ metadata.dependencies[match?.name]
799
1003
  if (deps) {
800
1004
  await mergeTemplateDependencies(join(targetDir, 'package.json'), deps)
801
1005
  }
@@ -804,11 +1008,15 @@ async function applyFromTemplate(templateId, packageType, targetDir, projectName
804
1008
  // If site content applied, inform about expected section types
805
1009
  if (packageType === 'site' && metadata.components) {
806
1010
  log('')
807
- info(`This template expects section types: ${metadata.components.join(', ')}`)
1011
+ info(
1012
+ `This template expects section types: ${metadata.components.join(', ')}`
1013
+ )
808
1014
  info(`Make sure your foundation provides them.`)
809
1015
  }
810
1016
  } else {
811
- info(`Template '${metadata.name}' has no ${packageType} content to apply.`)
1017
+ info(
1018
+ `Template '${metadata.name}' has no ${packageType} content to apply.`
1019
+ )
812
1020
  }
813
1021
  } finally {
814
1022
  if (resolved.cleanup) await resolved.cleanup()
@@ -818,10 +1026,15 @@ async function applyFromTemplate(templateId, packageType, targetDir, projectName
818
1026
  /**
819
1027
  * Wire an extension URL to a site's site.yml
820
1028
  */
821
- async function wireExtensionToSite(rootDir, siteName, extensionName, extensionPath) {
1029
+ async function wireExtensionToSite(
1030
+ rootDir,
1031
+ siteName,
1032
+ extensionName,
1033
+ extensionPath
1034
+ ) {
822
1035
  // Find the site directory
823
1036
  const sites = await discoverSites(rootDir)
824
- const site = sites.find(s => s.name === siteName)
1037
+ const site = sites.find((s) => s.name === siteName)
825
1038
  if (!site) {
826
1039
  info(`Could not find site '${siteName}' to wire extension.`)
827
1040
  return null
@@ -846,7 +1059,10 @@ async function wireExtensionToSite(rootDir, siteName, extensionName, extensionPa
846
1059
  config.extensions.push(extensionUrl)
847
1060
  }
848
1061
 
849
- await writeFile(siteYmlPath, yaml.dump(config, { flowLevel: -1, quotingType: "'" }))
1062
+ await writeFile(
1063
+ siteYmlPath,
1064
+ yaml.dump(config, { flowLevel: -1, quotingType: "'" })
1065
+ )
850
1066
  return siteName
851
1067
  } catch (err) {
852
1068
  info(`Warning: Could not update site.yml: ${err.message}`)
@@ -869,23 +1085,30 @@ async function addSection(rootDir, opts) {
869
1085
  process.exit(1)
870
1086
  }
871
1087
 
872
- const response = await prompts({
873
- type: 'text',
874
- name: 'name',
875
- message: 'Section name (PascalCase):',
876
- validate: (value) => /^[A-Z][a-zA-Z0-9]*$/.test(value) || 'Use PascalCase: Hero, FeatureGrid, CallToAction',
877
- }, {
878
- onCancel: () => {
879
- log('\nCancelled.')
880
- process.exit(0)
1088
+ const response = await prompts(
1089
+ {
1090
+ type: 'text',
1091
+ name: 'name',
1092
+ message: 'Section name (PascalCase):',
1093
+ validate: (value) =>
1094
+ /^[A-Z][a-zA-Z0-9]*$/.test(value) ||
1095
+ 'Use PascalCase: Hero, FeatureGrid, CallToAction'
881
1096
  },
882
- })
1097
+ {
1098
+ onCancel: () => {
1099
+ log('\nCancelled.')
1100
+ process.exit(0)
1101
+ }
1102
+ }
1103
+ )
883
1104
  name = response.name
884
1105
  }
885
1106
 
886
1107
  // Validate PascalCase
887
1108
  if (!/^[A-Z][a-zA-Z0-9]*$/.test(name)) {
888
- error(`Section name must be PascalCase (e.g., Hero, FeatureGrid, CallToAction).`)
1109
+ error(
1110
+ `Section name must be PascalCase (e.g., Hero, FeatureGrid, CallToAction).`
1111
+ )
889
1112
  process.exit(1)
890
1113
  }
891
1114
 
@@ -900,30 +1123,41 @@ async function addSection(rootDir, opts) {
900
1123
  } else if (foundations.length === 1) {
901
1124
  foundation = foundations[0]
902
1125
  } else if (opts.foundation) {
903
- foundation = foundations.find(f => f.name === opts.foundation)
1126
+ foundation = foundations.find((f) => f.name === opts.foundation)
904
1127
  if (!foundation) {
905
1128
  error(`Foundation '${opts.foundation}' not found.`)
906
- log(`Available: ${foundations.map(f => f.name).join(', ')}`)
1129
+ log(`Available: ${foundations.map((f) => f.name).join(', ')}`)
907
1130
  process.exit(1)
908
1131
  }
909
1132
  } else if (isNonInteractive(process.argv)) {
910
1133
  error(`Multiple foundations found. Specify which to use:\n`)
911
- log(formatOptions(foundations.map(f => ({ label: f.name, description: f.path }))))
1134
+ log(
1135
+ formatOptions(
1136
+ foundations.map((f) => ({ label: f.name, description: f.path }))
1137
+ )
1138
+ )
912
1139
  log('')
913
1140
  log(`Usage: ${getCliPrefix()} add section ${name} --foundation <name>`)
914
1141
  process.exit(1)
915
1142
  } else {
916
- const response = await prompts({
917
- type: 'select',
918
- name: 'foundation',
919
- message: 'Which foundation?',
920
- choices: foundations.map(f => ({ title: f.name, description: f.path, value: f })),
921
- }, {
922
- onCancel: () => {
923
- log('\nCancelled.')
924
- process.exit(0)
1143
+ const response = await prompts(
1144
+ {
1145
+ type: 'select',
1146
+ name: 'foundation',
1147
+ message: 'Which foundation?',
1148
+ choices: foundations.map((f) => ({
1149
+ title: f.name,
1150
+ description: f.path,
1151
+ value: f
1152
+ }))
925
1153
  },
926
- })
1154
+ {
1155
+ onCancel: () => {
1156
+ log('\nCancelled.')
1157
+ process.exit(0)
1158
+ }
1159
+ }
1160
+ )
927
1161
  foundation = response.foundation
928
1162
  }
929
1163
 
@@ -936,7 +1170,9 @@ async function addSection(rootDir, opts) {
936
1170
  const relSectionPath = relative(foundationDir, sectionDir)
937
1171
 
938
1172
  if (existsSync(sectionDir)) {
939
- error(`Section '${name}' already exists at ${foundation.path}/${relSectionPath}/`)
1173
+ error(
1174
+ `Section '${name}' already exists at ${foundation.path}/${relSectionPath}/`
1175
+ )
940
1176
  process.exit(1)
941
1177
  }
942
1178
 
@@ -982,11 +1218,17 @@ export default function ${name}({ content, params }) {
982
1218
  await writeFile(join(sectionDir, 'meta.js'), metaContent)
983
1219
 
984
1220
  success(`Created section '${name}' at ${foundation.path}/${relSectionPath}/`)
985
- log(` ${colors.dim}index.jsx${colors.reset} — component (customize the JSX)`)
986
- log(` ${colors.dim}meta.js${colors.reset} metadata (add content expectations, params, presets)`)
1221
+ log(
1222
+ ` ${colors.dim}index.jsx${colors.reset} component (customize the JSX)`
1223
+ )
1224
+ log(
1225
+ ` ${colors.dim}meta.js${colors.reset} — metadata (add content expectations, params, presets)`
1226
+ )
987
1227
  if (foundations.length === 1) {
988
1228
  log('')
989
- log(`${colors.dim}The dev server will pick it up automatically.${colors.reset}`)
1229
+ log(
1230
+ `${colors.dim}The dev server will pick it up automatically.${colors.reset}`
1231
+ )
990
1232
  }
991
1233
  }
992
1234
 
@@ -1006,7 +1248,7 @@ async function addCi(rootDir, opts, pm = 'pnpm') {
1006
1248
  // every add invocation.
1007
1249
  let getAdapter, listAdapters
1008
1250
  try {
1009
- ({ getAdapter, listAdapters } = await import('@uniweb/build/hosts'))
1251
+ ;({ getAdapter, listAdapters } = await import('@uniweb/build/hosts'))
1010
1252
  } catch {
1011
1253
  error('Failed to load host adapter registry from @uniweb/build/hosts.')
1012
1254
  process.exit(1)
@@ -1026,9 +1268,11 @@ async function addCi(rootDir, opts, pm = 'pnpm') {
1026
1268
  // capability, never a hardcoded list — an adapter that grows an initCi
1027
1269
  // shows up here with no edit to this file.
1028
1270
  const ciHosts = listAdapters()
1029
- .map(name => getAdapter(name))
1030
- .filter(a => typeof a.initCi === 'function')
1031
- .filter(a => (target === 'foundation' ? a.display?.foundationCi === true : true))
1271
+ .map((name) => getAdapter(name))
1272
+ .filter((a) => typeof a.initCi === 'function')
1273
+ .filter((a) =>
1274
+ target === 'foundation' ? a.display?.foundationCi === true : true
1275
+ )
1032
1276
  .sort((a, b) => (a.display?.order ?? 999) - (b.display?.order ?? 999))
1033
1277
 
1034
1278
  if (ciHosts.length === 0) {
@@ -1042,24 +1286,30 @@ async function addCi(rootDir, opts, pm = 'pnpm') {
1042
1286
  host = ciHosts[0].name
1043
1287
  } else if (isNonInteractive(process.argv)) {
1044
1288
  error('`uniweb add ci` needs a host.')
1045
- log(`Available: ${ciHosts.map(a => a.name).join(', ')}`)
1289
+ log(`Available: ${ciHosts.map((a) => a.name).join(', ')}`)
1046
1290
  process.exit(1)
1047
1291
  } else {
1048
- const choice = await prompts({
1049
- type: 'select',
1050
- name: 'host',
1051
- message: 'Which host should the workflow deploy to?',
1052
- choices: ciHosts.map(a => ({
1053
- title: `${a.display?.title || a.name} · ${a.display?.qualifier || ''}`.trim().replace(/ ·\s*$/, ''),
1054
- description: a.display?.summary,
1055
- value: a.name,
1056
- })),
1057
- }, {
1058
- onCancel: () => {
1059
- log('\nCancelled.')
1060
- process.exit(0)
1292
+ const choice = await prompts(
1293
+ {
1294
+ type: 'select',
1295
+ name: 'host',
1296
+ message: 'Which host should the workflow deploy to?',
1297
+ choices: ciHosts.map((a) => ({
1298
+ title:
1299
+ `${a.display?.title || a.name} · ${a.display?.qualifier || ''}`
1300
+ .trim()
1301
+ .replace(/ ·\s*$/, ''),
1302
+ description: a.display?.summary,
1303
+ value: a.name
1304
+ }))
1061
1305
  },
1062
- })
1306
+ {
1307
+ onCancel: () => {
1308
+ log('\nCancelled.')
1309
+ process.exit(0)
1310
+ }
1311
+ }
1312
+ )
1063
1313
  host = choice.host
1064
1314
  }
1065
1315
  }
@@ -1074,8 +1324,10 @@ async function addCi(rootDir, opts, pm = 'pnpm') {
1074
1324
 
1075
1325
  if (typeof adapter.initCi !== 'function') {
1076
1326
  error(`Host '${host}' does not provide a CI workflow.`)
1077
- log(`Hosts that do: ${ciHosts.map(a => a.name).join(', ')}`)
1078
- log(`Others are dashboard-driven — connect the repo in the host's UI instead.`)
1327
+ log(`Hosts that do: ${ciHosts.map((a) => a.name).join(', ')}`)
1328
+ log(
1329
+ `Others are dashboard-driven — connect the repo in the host's UI instead.`
1330
+ )
1079
1331
  process.exit(1)
1080
1332
  }
1081
1333
 
@@ -1084,7 +1336,7 @@ async function addCi(rootDir, opts, pm = 'pnpm') {
1084
1336
  // scaffold a SITE workflow — the wrong artifact, with no error.
1085
1337
  if (target === 'foundation' && adapter.display?.foundationCi !== true) {
1086
1338
  error(`Host '${host}' cannot publish a foundation.`)
1087
- log(`Hosts that can: ${ciHosts.map(a => a.name).join(', ')}`)
1339
+ log(`Hosts that can: ${ciHosts.map((a) => a.name).join(', ')}`)
1088
1340
  log('')
1089
1341
  log('Foundation publishing needs permanent versioned URLs, which the')
1090
1342
  log('gh-pages branch layout provides. Other hosts overwrite on deploy.')
@@ -1100,7 +1352,9 @@ async function addCi(rootDir, opts, pm = 'pnpm') {
1100
1352
  // it (writes a CNAME, switches UNIWEB_BASE to root).
1101
1353
  if (opts.domain && !isLikelyDomain(opts.domain)) {
1102
1354
  error(`Invalid --domain value: '${opts.domain}'`)
1103
- log(`Expected a bare hostname (e.g., 'mysite.com' or 'docs.mysite.com'). No scheme, no path.`)
1355
+ log(
1356
+ `Expected a bare hostname (e.g., 'mysite.com' or 'docs.mysite.com'). No scheme, no path.`
1357
+ )
1104
1358
  process.exit(1)
1105
1359
  }
1106
1360
 
@@ -1114,37 +1368,46 @@ async function addCi(rootDir, opts, pm = 'pnpm') {
1114
1368
  // Resolve site: --site flag, single site auto, prompt, or error.
1115
1369
  const sites = await discoverSites(rootDir)
1116
1370
  if (sites.length === 0) {
1117
- error('No site found in this workspace. Add one with `uniweb add site` first.')
1371
+ error(
1372
+ 'No site found in this workspace. Add one with `uniweb add site` first.'
1373
+ )
1118
1374
  process.exit(1)
1119
1375
  }
1120
1376
 
1121
1377
  let site
1122
1378
  if (opts.site) {
1123
- site = sites.find(s => s.name === opts.site)
1379
+ site = sites.find((s) => s.name === opts.site)
1124
1380
  if (!site) {
1125
1381
  error(`Site '${opts.site}' not found.`)
1126
- log(`Available sites: ${sites.map(s => s.name).join(', ')}`)
1382
+ log(`Available sites: ${sites.map((s) => s.name).join(', ')}`)
1127
1383
  process.exit(1)
1128
1384
  }
1129
1385
  } else if (sites.length === 1) {
1130
1386
  site = sites[0]
1131
1387
  } else if (isNonInteractive(process.argv)) {
1132
1388
  error(`Multiple sites in workspace. Specify --site <name>.`)
1133
- log(`Available sites: ${sites.map(s => s.name).join(', ')}`)
1389
+ log(`Available sites: ${sites.map((s) => s.name).join(', ')}`)
1134
1390
  process.exit(1)
1135
1391
  } else {
1136
1392
  const sortedSites = [...sites].sort((a, b) => a.name.localeCompare(b.name))
1137
- const response = await prompts({
1138
- type: 'select',
1139
- name: 'site',
1140
- message: 'Which site should the workflow build?',
1141
- choices: sortedSites.map(s => ({ title: s.name, description: s.path, value: s })),
1142
- }, {
1143
- onCancel: () => {
1144
- log('\nCancelled.')
1145
- process.exit(0)
1393
+ const response = await prompts(
1394
+ {
1395
+ type: 'select',
1396
+ name: 'site',
1397
+ message: 'Which site should the workflow build?',
1398
+ choices: sortedSites.map((s) => ({
1399
+ title: s.name,
1400
+ description: s.path,
1401
+ value: s
1402
+ }))
1146
1403
  },
1147
- })
1404
+ {
1405
+ onCancel: () => {
1406
+ log('\nCancelled.')
1407
+ process.exit(0)
1408
+ }
1409
+ }
1410
+ )
1148
1411
  site = response.site
1149
1412
  }
1150
1413
 
@@ -1158,10 +1421,20 @@ async function addCi(rootDir, opts, pm = 'pnpm') {
1158
1421
  // project's floor, raised if that pnpm needs more.
1159
1422
  const installedPnpm = detectInstalledPnpmVersion(rootDir)
1160
1423
  const pnpmVersion = resolveCiPnpmVersion(rootPkg, installedPnpm)
1161
- const nodeVersion = resolveCiNodeVersion(rootPkg.engines?.node, pm, pnpmVersion)
1424
+ const nodeVersion = resolveCiNodeVersion(
1425
+ rootPkg.engines?.node,
1426
+ pm,
1427
+ pnpmVersion
1428
+ )
1162
1429
  reportCiToolchain({
1163
- pm, pnpmVersion, nodeVersion,
1164
- source: rootPkg?.packageManager ? 'declared' : installedPnpm ? 'installed' : 'fallback',
1430
+ pm,
1431
+ pnpmVersion,
1432
+ nodeVersion,
1433
+ source: rootPkg?.packageManager
1434
+ ? 'declared'
1435
+ : installedPnpm
1436
+ ? 'installed'
1437
+ : 'fallback'
1165
1438
  })
1166
1439
 
1167
1440
  const siteDir = join(rootDir, site.path)
@@ -1187,7 +1460,13 @@ async function addCi(rootDir, opts, pm = 'pnpm') {
1187
1460
  pnpmVersion,
1188
1461
  domain: resolvedDomain,
1189
1462
  previews: opts.previews !== false,
1190
- projectName: resolveHostProjectName(rootDir, rootPkg, site, sites.length, opts),
1463
+ projectName: resolveHostProjectName(
1464
+ rootDir,
1465
+ rootPkg,
1466
+ site,
1467
+ sites.length,
1468
+ opts
1469
+ )
1191
1470
  })
1192
1471
 
1193
1472
  await writeCiFiles(rootDir, result.files, opts.force)
@@ -1206,7 +1485,7 @@ async function addCi(rootDir, opts, pm = 'pnpm') {
1206
1485
  const { recordTarget } = await import('@uniweb/build/site')
1207
1486
  const writeResult = await recordTarget(siteDir, {
1208
1487
  targetName: host,
1209
- targetConfig: result.targetConfig,
1488
+ targetConfig: result.targetConfig
1210
1489
  })
1211
1490
  success(
1212
1491
  writeResult.action === 'scaffold'
@@ -1252,7 +1531,12 @@ const GENERIC_SITE_NAMES = new Set(['site', 'sites', 'www', 'web', 'app'])
1252
1531
  * Prefer a meaningful package/directory name; fall back to the workspace
1253
1532
  * name; disambiguate only when several foundations would collide.
1254
1533
  */
1255
- const GENERIC_FOUNDATION_NAMES = new Set(['src', 'foundation', 'foundations', 'lib'])
1534
+ const GENERIC_FOUNDATION_NAMES = new Set([
1535
+ 'src',
1536
+ 'foundation',
1537
+ 'foundations',
1538
+ 'lib'
1539
+ ])
1256
1540
 
1257
1541
  function resolveFoundationPublicNames(rootDir, rootPkg, foundations) {
1258
1542
  const workspaceName = stripScope(rootPkg?.name) || basename(rootDir)
@@ -1302,14 +1586,17 @@ function reportCiToolchain({ pm, pnpmVersion, nodeVersion, source }) {
1302
1586
  info(`CI will use ${pm} + Node ${nodeVersion}`)
1303
1587
  return
1304
1588
  }
1305
- const origin = {
1306
- declared: '',
1307
- installed: ' (matching your install)',
1308
- fallback: ' (no pnpm detected — using the default)',
1309
- }[source] ?? ''
1589
+ const origin =
1590
+ {
1591
+ declared: '',
1592
+ installed: ' (matching your install)',
1593
+ fallback: ' (no pnpm detected — using the default)'
1594
+ }[source] ?? ''
1310
1595
  info(`CI will use pnpm ${pnpmVersion} + Node ${nodeVersion}${origin}`)
1311
1596
  if (source === 'fallback') {
1312
- info(`Pin with "packageManager": "pnpm@<version>" in package.json to choose a major.`)
1597
+ info(
1598
+ `Pin with "packageManager": "pnpm@<version>" in package.json to choose a major.`
1599
+ )
1313
1600
  }
1314
1601
  }
1315
1602
 
@@ -1356,10 +1643,12 @@ async function addFoundationCi(rootDir, opts, adapter, pm) {
1356
1643
  // its own versioned directory, so there's no collision).
1357
1644
  let foundations = all
1358
1645
  if (opts.foundation) {
1359
- const match = all.find(f => f.name === opts.foundation || basename(f.path) === opts.foundation)
1646
+ const match = all.find(
1647
+ (f) => f.name === opts.foundation || basename(f.path) === opts.foundation
1648
+ )
1360
1649
  if (!match) {
1361
1650
  error(`Foundation '${opts.foundation}' not found.`)
1362
- log(`Available: ${all.map(f => f.name).join(', ')}`)
1651
+ log(`Available: ${all.map((f) => f.name).join(', ')}`)
1363
1652
  process.exit(1)
1364
1653
  }
1365
1654
  foundations = [match]
@@ -1373,10 +1662,20 @@ async function addFoundationCi(rootDir, opts, adapter, pm) {
1373
1662
  // project's floor, raised if that pnpm needs more.
1374
1663
  const installedPnpm = detectInstalledPnpmVersion(rootDir)
1375
1664
  const pnpmVersion = resolveCiPnpmVersion(rootPkg, installedPnpm)
1376
- const nodeVersion = resolveCiNodeVersion(rootPkg.engines?.node, pm, pnpmVersion)
1665
+ const nodeVersion = resolveCiNodeVersion(
1666
+ rootPkg.engines?.node,
1667
+ pm,
1668
+ pnpmVersion
1669
+ )
1377
1670
  reportCiToolchain({
1378
- pm, pnpmVersion, nodeVersion,
1379
- source: rootPkg?.packageManager ? 'declared' : installedPnpm ? 'installed' : 'fallback',
1671
+ pm,
1672
+ pnpmVersion,
1673
+ nodeVersion,
1674
+ source: rootPkg?.packageManager
1675
+ ? 'declared'
1676
+ : installedPnpm
1677
+ ? 'installed'
1678
+ : 'fallback'
1380
1679
  })
1381
1680
 
1382
1681
  let result
@@ -1387,7 +1686,7 @@ async function addFoundationCi(rootDir, opts, adapter, pm) {
1387
1686
  target: 'foundation',
1388
1687
  packageManager: pm,
1389
1688
  nodeVersion,
1390
- pnpmVersion,
1689
+ pnpmVersion
1391
1690
  })
1392
1691
  } catch (err) {
1393
1692
  error(err.message)
@@ -1406,11 +1705,14 @@ async function addFoundationCi(rootDir, opts, adapter, pm) {
1406
1705
  }
1407
1706
 
1408
1707
  function isLikelyDomain(value) {
1409
- if (typeof value !== 'string' || value.length === 0 || value.length > 253) return false
1708
+ if (typeof value !== 'string' || value.length === 0 || value.length > 253)
1709
+ return false
1410
1710
  // Reject schemes, paths, ports, whitespace, leading/trailing dots/hyphens.
1411
1711
  // Each label is 1–63 chars of [a-z0-9-], no leading/trailing hyphen.
1412
1712
  // The TLD must be at least 2 characters of letters.
1413
- return /^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/i.test(value)
1713
+ return /^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/i.test(
1714
+ value
1715
+ )
1414
1716
  }
1415
1717
 
1416
1718
  function parseNodeMajor(engines) {