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
package/src/index.js CHANGED
@@ -41,12 +41,31 @@ import { template } from './commands/template.js'
41
41
  import {
42
42
  resolveTemplate,
43
43
  parseTemplateId,
44
- buildTemplateChoices,
44
+ buildTemplateChoices
45
45
  } from './templates/index.js'
46
46
  import { validateTemplate } from './templates/validator.js'
47
- import { scaffoldWorkspace, scaffoldFoundation, scaffoldSite, applyContent, applyStarter, mergeTemplateDependencies, getWorkspaceTemplateOutputs } from './utils/scaffold.js'
48
- import { detectPackageManager, filterCmd, installCmd, runCmd, isPnpmAvailable } from './utils/pm.js'
49
- import { isNonInteractive, getCliPrefix, stripNonInteractiveFlag, formatOptions } from './utils/interactive.js'
47
+ import {
48
+ scaffoldWorkspace,
49
+ scaffoldFoundation,
50
+ scaffoldSite,
51
+ applyContent,
52
+ applyStarter,
53
+ mergeTemplateDependencies,
54
+ getWorkspaceTemplateOutputs
55
+ } from './utils/scaffold.js'
56
+ import {
57
+ detectPackageManager,
58
+ filterCmd,
59
+ installCmd,
60
+ runCmd,
61
+ isPnpmAvailable
62
+ } from './utils/pm.js'
63
+ import {
64
+ isNonInteractive,
65
+ getCliPrefix,
66
+ stripNonInteractiveFlag,
67
+ formatOptions
68
+ } from './utils/interactive.js'
50
69
  import { findWorkspaceRoot } from './utils/workspace.js'
51
70
 
52
71
  // Colors for terminal output
@@ -57,7 +76,7 @@ const colors = {
57
76
  cyan: '\x1b[36m',
58
77
  green: '\x1b[32m',
59
78
  yellow: '\x1b[33m',
60
- red: '\x1b[31m',
79
+ red: '\x1b[31m'
61
80
  }
62
81
 
63
82
  // Template choices for the interactive prompt — built-ins first, then every
@@ -111,7 +130,9 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
111
130
  let _cliVersion = null
112
131
  function getCliVersion() {
113
132
  if (!_cliVersion) {
114
- const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'))
133
+ const pkg = JSON.parse(
134
+ readFileSync(join(__dirname, '..', 'package.json'), 'utf8')
135
+ )
115
136
  _cliVersion = pkg.version
116
137
  }
117
138
  return _cliVersion
@@ -126,7 +147,15 @@ function getCliVersion() {
126
147
  // install that's the whole point; delegating to the project-local copy
127
148
  // would align the project to the version it already has, i.e. a no-op.
128
149
  const STANDALONE_COMMANDS = new Set([
129
- 'create', 'clone', '--help', '-h', '--version', '-v', 'login', 'logout', 'update',
150
+ 'create',
151
+ 'clone',
152
+ '--help',
153
+ '-h',
154
+ '--version',
155
+ '-v',
156
+ 'login',
157
+ 'logout',
158
+ 'update'
130
159
  ])
131
160
 
132
161
  /**
@@ -138,8 +167,10 @@ function isGlobalInstall() {
138
167
  const scriptPath = process.argv[1]
139
168
  if (!scriptPath) return false
140
169
  // Normalize path separators for Windows compatibility
141
- return !scriptPath.split('/').includes('node_modules') &&
142
- !scriptPath.split('\\').includes('node_modules')
170
+ return (
171
+ !scriptPath.split('/').includes('node_modules') &&
172
+ !scriptPath.split('\\').includes('node_modules')
173
+ )
143
174
  }
144
175
 
145
176
  /**
@@ -173,9 +204,13 @@ function delegateToLocal(localCliPath) {
173
204
  const yellow = '\x1b[33m'
174
205
  const dim = '\x1b[2m'
175
206
  const reset = '\x1b[0m'
176
- console.error(`${yellow}Note:${reset} Global CLI is ${dim}${globalVersion}${reset}, project has ${dim}${localPkg.version}${reset} ${dim}(using project version)${reset}`)
207
+ console.error(
208
+ `${yellow}Note:${reset} Global CLI is ${dim}${globalVersion}${reset}, project has ${dim}${localPkg.version}${reset} ${dim}(using project version)${reset}`
209
+ )
177
210
  }
178
- } catch { /* ignore — version check is best-effort */ }
211
+ } catch {
212
+ /* ignore — version check is best-effort */
213
+ }
179
214
 
180
215
  return new Promise((resolve, reject) => {
181
216
  const child = spawnChild(
@@ -198,10 +233,15 @@ async function importProjectCommand(modulePath) {
198
233
  try {
199
234
  return await import(modulePath)
200
235
  } catch (err) {
201
- if (err.code === 'ERR_MODULE_NOT_FOUND' && err.message?.includes('@uniweb/')) {
236
+ if (
237
+ err.code === 'ERR_MODULE_NOT_FOUND' &&
238
+ err.message?.includes('@uniweb/')
239
+ ) {
202
240
  error('This command must be run from inside a Uniweb project.')
203
241
  log('')
204
- log(`Make sure you're in a project directory with dependencies installed:`)
242
+ log(
243
+ `Make sure you're in a project directory with dependencies installed:`
244
+ )
205
245
  log(` ${colors.cyan}cd your-project${colors.reset}`)
206
246
  log(` ${colors.cyan}npm install${colors.reset}`)
207
247
  log('')
@@ -216,7 +256,11 @@ async function importProjectCommand(modulePath) {
216
256
  /**
217
257
  * Create a project using the new package template flow (default)
218
258
  */
219
- async function createFromPackageTemplates(projectDir, projectName, options = {}) {
259
+ async function createFromPackageTemplates(
260
+ projectDir,
261
+ projectName,
262
+ options = {}
263
+ ) {
220
264
  const { onProgress, onWarning, pm = 'pnpm', includeStarter = true } = options
221
265
 
222
266
  onProgress?.('Setting up workspace...')
@@ -226,36 +270,48 @@ async function createFromPackageTemplates(projectDir, projectName, options = {})
226
270
  // (the verb resolves the right PM at runtime instead of locking the
227
271
  // root scripts to whichever PM ran `npx uniweb create`). preview stays
228
272
  // PM-filtered until a `uniweb preview` verb exists.
229
- await scaffoldWorkspace(projectDir, {
230
- projectName,
231
- workspaceGlobs: ['site', 'src'],
232
- scripts: {
233
- dev: 'uniweb dev',
234
- build: 'uniweb build',
235
- preview: filterCmd(pm, 'site', 'preview'),
273
+ await scaffoldWorkspace(
274
+ projectDir,
275
+ {
276
+ projectName,
277
+ workspaceGlobs: ['site', 'src'],
278
+ scripts: {
279
+ dev: 'uniweb dev',
280
+ build: 'uniweb build',
281
+ preview: filterCmd(pm, 'site', 'preview')
282
+ }
236
283
  },
237
- }, { onProgress, onWarning })
284
+ { onProgress, onWarning }
285
+ )
238
286
 
239
287
  // 2. Scaffold foundation (folder: src/, package name: src)
240
288
  // The folder name 'src' carries the meaning — a foundation is the site's
241
289
  // source code. The package name 'src' keeps it unique within the
242
290
  // workspace, since 'site' is taken by the site package.
243
291
  onProgress?.('Creating foundation...')
244
- await scaffoldFoundation(join(projectDir, 'src'), {
245
- name: 'src',
246
- projectName,
247
- isExtension: false,
248
- }, { onProgress, onWarning })
292
+ await scaffoldFoundation(
293
+ join(projectDir, 'src'),
294
+ {
295
+ name: 'src',
296
+ projectName,
297
+ isExtension: false
298
+ },
299
+ { onProgress, onWarning }
300
+ )
249
301
 
250
302
  // 3. Scaffold site
251
303
  onProgress?.('Creating site...')
252
- await scaffoldSite(join(projectDir, 'site'), {
253
- name: 'site',
254
- projectName,
255
- foundationName: 'src',
256
- foundationPath: 'file:../src',
257
- foundationRef: 'src',
258
- }, { onProgress, onWarning })
304
+ await scaffoldSite(
305
+ join(projectDir, 'site'),
306
+ {
307
+ name: 'site',
308
+ projectName,
309
+ foundationName: 'src',
310
+ foundationPath: 'file:../src',
311
+ foundationRef: 'src'
312
+ },
313
+ { onProgress, onWarning }
314
+ )
259
315
 
260
316
  // 4. Apply starter content (unless creating a "none" project)
261
317
  if (includeStarter) {
@@ -274,13 +330,17 @@ async function createBlankWorkspace(projectDir, projectName, options = {}) {
274
330
 
275
331
  onProgress?.('Setting up blank workspace...')
276
332
 
277
- await scaffoldWorkspace(projectDir, {
278
- projectName,
279
- workspaceGlobs: [],
280
- scripts: {
281
- build: 'uniweb build',
333
+ await scaffoldWorkspace(
334
+ projectDir,
335
+ {
336
+ projectName,
337
+ workspaceGlobs: [],
338
+ scripts: {
339
+ build: 'uniweb build'
340
+ }
282
341
  },
283
- }, { onProgress, onWarning })
342
+ { onProgress, onWarning }
343
+ )
284
344
 
285
345
  success(`Created blank workspace: ${projectName}`)
286
346
  }
@@ -291,7 +351,13 @@ async function createBlankWorkspace(projectDir, projectName, options = {}) {
291
351
  * Scaffolds workspace structure from package templates, then overlays
292
352
  * content (sections, pages, theme) from the content template.
293
353
  */
294
- async function createFromContentTemplate(projectDir, projectName, metadata, templateRootPath, options = {}) {
354
+ async function createFromContentTemplate(
355
+ projectDir,
356
+ projectName,
357
+ metadata,
358
+ templateRootPath,
359
+ options = {}
360
+ ) {
295
361
  const { onProgress, onWarning, pm = 'pnpm' } = options
296
362
 
297
363
  // Determine packages to create
@@ -300,17 +366,17 @@ async function createFromContentTemplate(projectDir, projectName, metadata, temp
300
366
  // convention is set in computePlacement() below.
301
367
  const packages = metadata.packages || [
302
368
  { type: 'foundation', name: 'src' },
303
- { type: 'site', name: 'site', foundation: 'src' },
369
+ { type: 'site', name: 'site', foundation: 'src' }
304
370
  ]
305
371
 
306
372
  // Compute placement for each package
307
373
  const placed = computePlacement(packages)
308
374
 
309
375
  // Compute workspace globs and scripts from placement
310
- const workspaceGlobs = placed.map(p => p.relativePath)
311
- const sites = placed.filter(p => p.type === 'site')
376
+ const workspaceGlobs = placed.map((p) => p.relativePath)
377
+ const sites = placed.filter((p) => p.type === 'site')
312
378
  const scripts = {
313
- build: 'uniweb build',
379
+ build: 'uniweb build'
314
380
  }
315
381
  // dev goes through `uniweb` (PM-agnostic; see computeRootScripts).
316
382
  // preview stays PM-filtered until a `uniweb preview` verb exists.
@@ -331,11 +397,15 @@ async function createFromContentTemplate(projectDir, projectName, metadata, temp
331
397
 
332
398
  // 1. Scaffold workspace
333
399
  onProgress?.('Setting up workspace...')
334
- await scaffoldWorkspace(projectDir, {
335
- projectName,
336
- workspaceGlobs,
337
- scripts,
338
- }, { onProgress, onWarning })
400
+ await scaffoldWorkspace(
401
+ projectDir,
402
+ {
403
+ projectName,
404
+ workspaceGlobs,
405
+ scripts
406
+ },
407
+ { onProgress, onWarning }
408
+ )
339
409
 
340
410
  // 2. Scaffold and apply content for each package
341
411
  for (const pkg of placed) {
@@ -343,19 +413,26 @@ async function createFromContentTemplate(projectDir, projectName, metadata, temp
343
413
 
344
414
  if (pkg.type === 'foundation' || pkg.type === 'extension') {
345
415
  onProgress?.(`Creating ${pkg.type}: ${pkg.name}...`)
346
- await scaffoldFoundation(fullPath, {
347
- name: pkg.name,
348
- projectName,
349
- isExtension: pkg.type === 'extension',
350
- }, { onProgress, onWarning })
416
+ await scaffoldFoundation(
417
+ fullPath,
418
+ {
419
+ name: pkg.name,
420
+ projectName,
421
+ isExtension: pkg.type === 'extension'
422
+ },
423
+ { onProgress, onWarning }
424
+ )
351
425
  } else if (pkg.type === 'site') {
352
426
  // Find the foundation this site wires to
353
427
  const foundationName = pkg.foundation || 'src'
354
- const foundationPkg = placed.find(p =>
355
- (p.type === 'foundation') && (p.name === foundationName)
428
+ const foundationPkg = placed.find(
429
+ (p) => p.type === 'foundation' && p.name === foundationName
356
430
  )
357
431
  const foundationPath = foundationPkg
358
- ? computeFoundationFilePath(pkg.relativePath, foundationPkg.relativePath)
432
+ ? computeFoundationFilePath(
433
+ pkg.relativePath,
434
+ foundationPkg.relativePath
435
+ )
359
436
  : 'file:../src'
360
437
 
361
438
  onProgress?.(`Creating site: ${pkg.name}...`)
@@ -363,29 +440,39 @@ async function createFromContentTemplate(projectDir, projectName, metadata, temp
363
440
  // never the implicit default in the new layout (the build's
364
441
  // `detectFoundationType` defaults to 'foundation' when absent,
365
442
  // which doesn't match 'src').
366
- await scaffoldSite(fullPath, {
367
- name: pkg.name,
368
- projectName,
369
- foundationName,
370
- foundationPath,
371
- foundationRef: foundationName,
372
- }, { onProgress, onWarning })
443
+ await scaffoldSite(
444
+ fullPath,
445
+ {
446
+ name: pkg.name,
447
+ projectName,
448
+ foundationName,
449
+ foundationPath,
450
+ foundationRef: foundationName
451
+ },
452
+ { onProgress, onWarning }
453
+ )
373
454
  }
374
455
 
375
456
  // Apply content from the matching content directory
376
457
  const contentDir = findContentDirFor(metadata.contentDirs, pkg)
377
458
  if (contentDir) {
378
459
  onProgress?.(`Applying ${metadata.name} content to ${pkg.name}...`)
379
- await applyContent(contentDir.dir, fullPath, { projectName }, {
380
- onProgress,
381
- onWarning,
382
- renames: contentDir.renames,
383
- })
460
+ await applyContent(
461
+ contentDir.dir,
462
+ fullPath,
463
+ { projectName },
464
+ {
465
+ onProgress,
466
+ onWarning,
467
+ renames: contentDir.renames
468
+ }
469
+ )
384
470
  }
385
471
 
386
472
  // Merge template dependencies into package.json
387
473
  if (metadata.dependencies) {
388
- const deps = metadata.dependencies[pkg.name] || metadata.dependencies[pkg.type]
474
+ const deps =
475
+ metadata.dependencies[pkg.name] || metadata.dependencies[pkg.type]
389
476
  if (deps) {
390
477
  await mergeTemplateDependencies(join(fullPath, 'package.json'), deps)
391
478
  }
@@ -408,9 +495,9 @@ async function createFromContentTemplate(projectDir, projectName, metadata, temp
408
495
  * - Multiple sites → sites/{name}/
409
496
  */
410
497
  function computePlacement(packages) {
411
- const foundations = packages.filter(p => p.type === 'foundation')
412
- const extensions = packages.filter(p => p.type === 'extension')
413
- const sites = packages.filter(p => p.type === 'site')
498
+ const foundations = packages.filter((p) => p.type === 'foundation')
499
+ const extensions = packages.filter((p) => p.type === 'extension')
500
+ const sites = packages.filter((p) => p.type === 'site')
414
501
 
415
502
  const placed = []
416
503
 
@@ -443,8 +530,10 @@ function computePlacement(packages) {
443
530
  function findContentDirFor(contentDirs, pkg) {
444
531
  if (!contentDirs) return null
445
532
  // Match by name first, then by type
446
- return contentDirs.find(d => d.name === pkg.name) ||
447
- contentDirs.find(d => d.type === pkg.type && d.name === pkg.type)
533
+ return (
534
+ contentDirs.find((d) => d.name === pkg.name) ||
535
+ contentDirs.find((d) => d.type === pkg.type && d.name === pkg.type)
536
+ )
448
537
  }
449
538
 
450
539
  /**
@@ -481,13 +570,16 @@ async function main() {
481
570
  console.log(`uniweb ${getCliVersion()}`)
482
571
  if (isGlobalInstall()) {
483
572
  try {
484
- const { fetchAndNotifyIfNewer, maybeNotifyFromCache } = await import('./utils/update-check.js')
573
+ const { fetchAndNotifyIfNewer, maybeNotifyFromCache } =
574
+ await import('./utils/update-check.js')
485
575
  if (process.stdout.isTTY) {
486
576
  await fetchAndNotifyIfNewer(getCliVersion(), { tone: 'soft' })
487
577
  } else {
488
578
  maybeNotifyFromCache(getCliVersion(), 'soft')
489
579
  }
490
- } catch { /* ignore */ }
580
+ } catch {
581
+ /* ignore */
582
+ }
491
583
  }
492
584
  return
493
585
  }
@@ -519,7 +611,8 @@ async function main() {
519
611
  let showUpdateNotification = () => {}
520
612
  if (global) {
521
613
  try {
522
- const { startUpdateCheck, maybeEagerNotification } = await import('./utils/update-check.js')
614
+ const { startUpdateCheck, maybeEagerNotification } =
615
+ await import('./utils/update-check.js')
523
616
  showUpdateNotification = startUpdateCheck(getCliVersion())
524
617
  if (command === 'create') {
525
618
  maybeEagerNotification(getCliVersion())
@@ -540,7 +633,7 @@ async function main() {
540
633
  // Critical for `deploy --help` (used to open a browser to production for
541
634
  // login because deploy.js doesn't parse --help and ensureAuth ran first).
542
635
  // Falls back to the global help when a command has no dedicated block.
543
- if (args.slice(1).some(a => a === '--help' || a === '-h')) {
636
+ if (args.slice(1).some((a) => a === '--help' || a === '-h')) {
544
637
  const printed = printCommandHelp(command)
545
638
  if (printed) {
546
639
  await showUpdateNotification()
@@ -718,18 +811,28 @@ async function main() {
718
811
  const { resolveBackendOrigin } = await import('./backend/client.js')
719
812
  const { readFlagValue } = await import('./utils/args.js')
720
813
  const { runRegistryLogin } = await import('./utils/registry-auth.js')
721
- const originFlag = readFlagValue(loginArgs, '--backend') || readFlagValue(loginArgs, '--registry')
722
- await runRegistryLogin({ apiBase: resolveBackendOrigin(originFlag), args: loginArgs })
814
+ const originFlag =
815
+ readFlagValue(loginArgs, '--backend') ||
816
+ readFlagValue(loginArgs, '--registry')
817
+ await runRegistryLogin({
818
+ apiBase: resolveBackendOrigin(originFlag),
819
+ args: loginArgs
820
+ })
723
821
  return
724
822
  }
725
823
 
726
824
  // Handle logout command — clear the stored backend session.
727
825
  if (command === 'logout') {
728
- const { clearRegistryAuth, getRegistryAuthPath } = await import('./utils/registry-auth.js')
826
+ const { clearRegistryAuth, getRegistryAuthPath } =
827
+ await import('./utils/registry-auth.js')
729
828
  const { existsSync } = await import('node:fs')
730
829
  const had = existsSync(getRegistryAuthPath())
731
830
  await clearRegistryAuth()
732
- console.log(had ? '\x1b[32m✓\x1b[0m Logged out (cleared the stored session).' : 'Not logged in — nothing to clear.')
831
+ console.log(
832
+ had
833
+ ? '\x1b[32m✓\x1b[0m Logged out (cleared the stored session).'
834
+ : 'Not logged in — nothing to clear.'
835
+ )
733
836
  return
734
837
  }
735
838
 
@@ -741,7 +844,8 @@ async function main() {
741
844
  }
742
845
 
743
846
  // Handle runtime command — `runtime register` uploads a built @uniweb/runtime to
744
- // the backend's runtime registry (/gateway/runtime/{version}); @std-gated.
847
+ // the backend's runtime registry; @std-gated. Where it is served from is the
848
+ // backend's to report, not ours to know.
745
849
  if (command === 'runtime') {
746
850
  const { runtime } = await import('./commands/runtime.js')
747
851
  const result = await runtime(args.slice(1))
@@ -782,13 +886,15 @@ async function main() {
782
886
  log(`\nTo add packages to this workspace, use:`)
783
887
  log(` ${colors.cyan}uniweb add foundation [name]${colors.reset}`)
784
888
  log(` ${colors.cyan}uniweb add site [name]${colors.reset}`)
785
- log(` ${colors.cyan}uniweb add foundation --from <template>${colors.reset}\n`)
889
+ log(
890
+ ` ${colors.cyan}uniweb add foundation --from <template>${colors.reset}\n`
891
+ )
786
892
  process.exit(1)
787
893
  }
788
894
 
789
895
  // Parse arguments
790
896
  let projectName = args[1]
791
- let templateType = null // null = use new package template flow
897
+ let templateType = null // null = use new package template flow
792
898
 
793
899
  // In-place mode: `uniweb create .` scaffolds into the current working
794
900
  // directory instead of creating a new one. Pairs with the GitHub-first
@@ -821,7 +927,7 @@ async function main() {
821
927
  if (nameIndex !== -1 && args[nameIndex + 1]) {
822
928
  displayName = args[nameIndex + 1]
823
929
  } else {
824
- const nameEq = args.find(a => a.startsWith('--name='))
930
+ const nameEq = args.find((a) => a.startsWith('--name='))
825
931
  if (nameEq) displayName = nameEq.slice('--name='.length)
826
932
  }
827
933
 
@@ -853,13 +959,17 @@ async function main() {
853
959
  const dirName = basename(process.cwd())
854
960
  const slug = slugifyName(dirName)
855
961
  if (!slug) {
856
- error(`Could not derive a valid project name from the current directory ("${dirName}").`)
962
+ error(
963
+ `Could not derive a valid project name from the current directory ("${dirName}").`
964
+ )
857
965
  log(`Re-run with ${colors.cyan}--name=<your-name>${colors.reset}.`)
858
966
  process.exit(1)
859
967
  }
860
968
  projectName = slug
861
969
  if (slug !== dirName) {
862
- log(`${colors.dim}Project name:${colors.reset} ${slug} ${colors.dim}(slugified from "${dirName}")${colors.reset}`)
970
+ log(
971
+ `${colors.dim}Project name:${colors.reset} ${slug} ${colors.dim}(slugified from "${dirName}")${colors.reset}`
972
+ )
863
973
  } else {
864
974
  log(`${colors.dim}Project name:${colors.reset} ${slug}`)
865
975
  }
@@ -879,26 +989,29 @@ async function main() {
879
989
  }
880
990
 
881
991
  // Interactive prompts (skipped in in-place mode — name was derived above)
882
- const response = await prompts([
883
- {
884
- type: projectName ? null : 'text',
885
- name: 'projectName',
886
- message: 'Project name:',
887
- initial: 'website',
888
- validate: (value) => {
889
- if (!value) return 'Project name is required'
890
- if (!/^[a-z0-9-]+$/.test(value)) {
891
- return 'Project name can only contain lowercase letters, numbers, and hyphens'
992
+ const response = await prompts(
993
+ [
994
+ {
995
+ type: projectName ? null : 'text',
996
+ name: 'projectName',
997
+ message: 'Project name:',
998
+ initial: 'website',
999
+ validate: (value) => {
1000
+ if (!value) return 'Project name is required'
1001
+ if (!/^[a-z0-9-]+$/.test(value)) {
1002
+ return 'Project name can only contain lowercase letters, numbers, and hyphens'
1003
+ }
1004
+ return true
892
1005
  }
893
- return true
894
- },
895
- },
896
- ], {
897
- onCancel: () => {
898
- log('\nScaffolding cancelled.')
899
- process.exit(0)
900
- },
901
- })
1006
+ }
1007
+ ],
1008
+ {
1009
+ onCancel: () => {
1010
+ log('\nScaffolding cancelled.')
1011
+ process.exit(0)
1012
+ }
1013
+ }
1014
+ )
902
1015
 
903
1016
  projectName = projectName || response.projectName
904
1017
 
@@ -909,18 +1022,21 @@ async function main() {
909
1022
 
910
1023
  // Prompt for template if not specified via --template or --blank
911
1024
  if (!templateType && !isBlank) {
912
- const templateResponse = await prompts({
913
- type: 'select',
914
- name: 'template',
915
- message: 'Template:',
916
- choices: TEMPLATE_CHOICES,
917
- initial: 1,
918
- }, {
919
- onCancel: () => {
920
- log('\nScaffolding cancelled.')
921
- process.exit(0)
1025
+ const templateResponse = await prompts(
1026
+ {
1027
+ type: 'select',
1028
+ name: 'template',
1029
+ message: 'Template:',
1030
+ choices: TEMPLATE_CHOICES,
1031
+ initial: 1
922
1032
  },
923
- })
1033
+ {
1034
+ onCancel: () => {
1035
+ log('\nScaffolding cancelled.')
1036
+ process.exit(0)
1037
+ }
1038
+ }
1039
+ )
924
1040
  templateType = templateResponse.template
925
1041
  // Handle "blank" selection from interactive prompt
926
1042
  if (templateType === 'blank') {
@@ -933,7 +1049,9 @@ async function main() {
933
1049
 
934
1050
  // Resolve target directory. In-place mode scaffolds into the cwd;
935
1051
  // otherwise create `./<projectName>`.
936
- const projectDir = inPlace ? process.cwd() : resolve(process.cwd(), projectName)
1052
+ const projectDir = inPlace
1053
+ ? process.cwd()
1054
+ : resolve(process.cwd(), projectName)
937
1055
 
938
1056
  if (!inPlace && existsSync(projectDir)) {
939
1057
  error(`Directory already exists: ${projectName}`)
@@ -960,21 +1078,24 @@ async function main() {
960
1078
  error(`Cannot scaffold in place — these files would be overwritten:`)
961
1079
  for (const c of conflicts) log(` ${colors.yellow}${c}${colors.reset}`)
962
1080
  log('')
963
- log(`Move or remove them, then re-run ${colors.cyan}uniweb create .${colors.reset}.`)
1081
+ log(
1082
+ `Move or remove them, then re-run ${colors.cyan}uniweb create .${colors.reset}.`
1083
+ )
964
1084
  process.exit(1)
965
1085
  }
966
1086
  }
967
1087
 
968
1088
  // Template routing logic
969
1089
  const progressCb = (msg) => log(` ${colors.dim}${msg}${colors.reset}`)
970
- const warningCb = (msg) => log(` ${colors.yellow}Warning: ${msg}${colors.reset}`)
1090
+ const warningCb = (msg) =>
1091
+ log(` ${colors.yellow}Warning: ${msg}${colors.reset}`)
971
1092
 
972
1093
  if (isBlank) {
973
1094
  // Blank workspace (--blank or --template blank)
974
1095
  log('\nCreating blank workspace...')
975
1096
  await createBlankWorkspace(projectDir, effectiveName, {
976
1097
  onProgress: progressCb,
977
- onWarning: warningCb,
1098
+ onWarning: warningCb
978
1099
  })
979
1100
  } else if (templateType === 'none') {
980
1101
  // Foundation + site with no content
@@ -983,7 +1104,7 @@ async function main() {
983
1104
  onProgress: progressCb,
984
1105
  onWarning: warningCb,
985
1106
  pm,
986
- includeStarter: false,
1107
+ includeStarter: false
987
1108
  })
988
1109
  } else if (templateType === 'starter') {
989
1110
  // Starter: foundation + site + sample content
@@ -991,7 +1112,7 @@ async function main() {
991
1112
  await createFromPackageTemplates(projectDir, effectiveName, {
992
1113
  onProgress: progressCb,
993
1114
  onWarning: warningCb,
994
- pm,
1115
+ pm
995
1116
  })
996
1117
  } else {
997
1118
  // External: official/npm/github/local
@@ -999,20 +1120,28 @@ async function main() {
999
1120
 
1000
1121
  try {
1001
1122
  const resolved = await resolveTemplate(templateType, {
1002
- onProgress: progressCb,
1123
+ onProgress: progressCb
1003
1124
  })
1004
1125
 
1005
- log(`\nCreating project from ${resolved.name || resolved.package || `${resolved.owner}/${resolved.repo}`}...`)
1126
+ log(
1127
+ `\nCreating project from ${resolved.name || resolved.package || `${resolved.owner}/${resolved.repo}`}...`
1128
+ )
1006
1129
 
1007
1130
  // Validate and apply as format 2 content template
1008
1131
  const metadata = await validateTemplate(resolved.path, {})
1009
1132
 
1010
1133
  try {
1011
- await createFromContentTemplate(projectDir, effectiveName, metadata, resolved.path, {
1012
- onProgress: progressCb,
1013
- onWarning: warningCb,
1014
- pm,
1015
- })
1134
+ await createFromContentTemplate(
1135
+ projectDir,
1136
+ effectiveName,
1137
+ metadata,
1138
+ resolved.path,
1139
+ {
1140
+ onProgress: progressCb,
1141
+ onWarning: warningCb,
1142
+ pm
1143
+ }
1144
+ )
1016
1145
  } finally {
1017
1146
  if (resolved.cleanup) await resolved.cleanup()
1018
1147
  }
@@ -1021,8 +1150,12 @@ async function main() {
1021
1150
  log('')
1022
1151
  log(`${colors.yellow}Troubleshooting:${colors.reset}`)
1023
1152
  log(` • Check your network connection`)
1024
- log(` • Official templates require GitHub access (may be blocked by corporate networks)`)
1025
- log(` • Try the starter template instead: ${colors.cyan}uniweb create ${projectName} --template starter${colors.reset}`)
1153
+ log(
1154
+ ` • Official templates require GitHub access (may be blocked by corporate networks)`
1155
+ )
1156
+ log(
1157
+ ` • Try the starter template instead: ${colors.cyan}uniweb create ${projectName} --template starter${colors.reset}`
1158
+ )
1026
1159
  process.exit(1)
1027
1160
  }
1028
1161
  }
@@ -1032,25 +1165,37 @@ async function main() {
1032
1165
  // Skip git init if already inside a git repo (common for monorepos/workspaces)
1033
1166
  let insideGitRepo = false
1034
1167
  try {
1035
- execSync('git rev-parse --is-inside-work-tree', { cwd: projectDir, stdio: 'ignore' })
1168
+ execSync('git rev-parse --is-inside-work-tree', {
1169
+ cwd: projectDir,
1170
+ stdio: 'ignore'
1171
+ })
1036
1172
  insideGitRepo = true
1037
1173
  } catch {
1038
1174
  // Not inside a git repo — proceed with init
1039
1175
  }
1040
1176
 
1041
1177
  if (insideGitRepo) {
1042
- log(` ${colors.dim}Skipping git init — already inside a git repository${colors.reset}`)
1178
+ log(
1179
+ ` ${colors.dim}Skipping git init — already inside a git repository${colors.reset}`
1180
+ )
1043
1181
  } else {
1044
1182
  try {
1045
1183
  execSync('git --version', { stdio: 'ignore' })
1046
1184
  try {
1047
1185
  execSync('git init', { cwd: projectDir, stdio: 'ignore' })
1048
1186
  execSync('git add -A', { cwd: projectDir, stdio: 'ignore' })
1049
- execSync('git commit -m "Initial commit from uniweb"', { cwd: projectDir, stdio: 'ignore' })
1187
+ execSync('git commit -m "Initial commit from uniweb"', {
1188
+ cwd: projectDir,
1189
+ stdio: 'ignore'
1190
+ })
1050
1191
  success('Git repository initialized')
1051
1192
  } catch {
1052
- log(` ${colors.yellow}Warning: Git repository initialized but initial commit failed${colors.reset}`)
1053
- log(` ${colors.dim}Run 'git commit -m "Initial commit"' after configuring git${colors.reset}`)
1193
+ log(
1194
+ ` ${colors.yellow}Warning: Git repository initialized but initial commit failed${colors.reset}`
1195
+ )
1196
+ log(
1197
+ ` ${colors.dim}Run 'git commit -m "Initial commit"' after configuring git${colors.reset}`
1198
+ )
1054
1199
  }
1055
1200
  } catch {
1056
1201
  // git not available — skip silently
@@ -1072,19 +1217,27 @@ async function main() {
1072
1217
  if (!inPlace) log(` ${colors.cyan}cd ${projectName}${colors.reset}`)
1073
1218
  log(` ${colors.cyan}${prefix} add project${colors.reset}`)
1074
1219
  log(` ${colors.cyan}${installCmd(recPm)}${colors.reset}`)
1075
- log(` ${colors.cyan}${runCmd(recPm, 'dev')}${colors.reset} ${colors.dim}# start the dev server${colors.reset}`)
1220
+ log(
1221
+ ` ${colors.cyan}${runCmd(recPm, 'dev')}${colors.reset} ${colors.dim}# start the dev server${colors.reset}`
1222
+ )
1076
1223
  } else {
1077
1224
  log(`Next steps:\n`)
1078
1225
  if (!inPlace) log(` ${colors.cyan}cd ${projectName}${colors.reset}`)
1079
1226
  log(` ${colors.cyan}${installCmd(recPm)}${colors.reset}`)
1080
- log(` ${colors.cyan}${runCmd(recPm, 'dev')}${colors.reset} ${colors.dim}# start the dev server${colors.reset}`)
1227
+ log(
1228
+ ` ${colors.cyan}${runCmd(recPm, 'dev')}${colors.reset} ${colors.dim}# start the dev server${colors.reset}`
1229
+ )
1081
1230
  }
1082
1231
  if (recPm !== 'pnpm') {
1083
1232
  log('')
1084
- log(` ${colors.dim}Tip: pnpm is recommended (https://pnpm.io) — npm works too.${colors.reset}`)
1233
+ log(
1234
+ ` ${colors.dim}Tip: pnpm is recommended (https://pnpm.io) — npm works too.${colors.reset}`
1235
+ )
1085
1236
  }
1086
1237
  log('')
1087
- log(` ${colors.dim}See ${colors.reset}${colors.cyan}${prefix} <command> --help${colors.reset}${colors.dim} for command-specific options.${colors.reset}`)
1238
+ log(
1239
+ ` ${colors.dim}See ${colors.reset}${colors.cyan}${prefix} <command> --help${colors.reset}${colors.dim} for command-specific options.${colors.reset}`
1240
+ )
1088
1241
  log('')
1089
1242
 
1090
1243
  await showUpdateNotification()
@@ -1474,7 +1627,7 @@ ${colors.bright}Project-local installs:${colors.reset}
1474
1627
  When run from a project-local CLI (in node_modules), it aligns the
1475
1628
  project to that pinned version — bump \`uniweb\` in package.json (or use
1476
1629
  \`npx uniweb@latest update\`) to align to something newer.
1477
- `,
1630
+ `
1478
1631
  }
1479
1632
 
1480
1633
  if (!blocks[command]) return false