uniweb 0.7.1 → 0.7.2

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 (59) hide show
  1. package/README.md +64 -17
  2. package/package.json +4 -3
  3. package/src/commands/add.js +563 -0
  4. package/src/commands/build.js +49 -6
  5. package/src/commands/doctor.js +181 -2
  6. package/src/index.js +273 -131
  7. package/src/templates/index.js +0 -94
  8. package/src/templates/processor.js +10 -87
  9. package/src/templates/resolver.js +3 -3
  10. package/src/templates/validator.js +59 -17
  11. package/src/utils/config.js +229 -0
  12. package/src/utils/scaffold.js +175 -0
  13. package/templates/{single/foundation → foundation}/package.json.hbs +2 -2
  14. package/templates/foundation/src/foundation.js.hbs +7 -0
  15. package/templates/foundation/src/sections/.gitkeep +0 -0
  16. package/templates/{multi/sites/main → site}/package.json.hbs +2 -2
  17. package/templates/site/site.yml.hbs +10 -0
  18. package/templates/site/theme.yml +1 -0
  19. package/templates/{_shared → workspace}/package.json.hbs +3 -9
  20. package/templates/workspace/pnpm-workspace.yaml.hbs +4 -0
  21. package/templates/_shared/pnpm-workspace.yaml +0 -5
  22. package/templates/multi/README.md.hbs +0 -85
  23. package/templates/multi/foundations/default/package.json.hbs +0 -38
  24. package/templates/multi/foundations/default/src/foundation.js +0 -41
  25. package/templates/multi/package.json.hbs +0 -26
  26. package/templates/multi/sites/main/pages/home/1-welcome.md.hbs +0 -14
  27. package/templates/multi/sites/main/site.yml.hbs +0 -12
  28. package/templates/multi/sites/main/vite.config.js +0 -7
  29. package/templates/multi/template/.vscode/settings.json +0 -6
  30. package/templates/multi/template.json +0 -5
  31. package/templates/single/foundation/src/sections/Section/index.jsx +0 -121
  32. package/templates/single/foundation/src/sections/Section/meta.js +0 -61
  33. package/templates/single/foundation/src/styles.css +0 -5
  34. package/templates/single/foundation/vite.config.js +0 -3
  35. package/templates/single/site/index.html.hbs +0 -13
  36. package/templates/single/site/main.js +0 -7
  37. package/templates/single/site/package.json.hbs +0 -27
  38. package/templates/single/site/pages/about/1-about.md.hbs +0 -13
  39. package/templates/single/site/pages/about/page.yml +0 -2
  40. package/templates/single/site/pages/home/page.yml +0 -2
  41. package/templates/single/site/public/favicon.svg +0 -7
  42. package/templates/single/site/site.yml.hbs +0 -10
  43. package/templates/single/template.json +0 -10
  44. /package/{templates/single → starter}/foundation/src/foundation.js +0 -0
  45. /package/{templates/multi/foundations/default → starter/foundation}/src/sections/Section/index.jsx +0 -0
  46. /package/{templates/multi/foundations/default → starter/foundation}/src/sections/Section/meta.js +0 -0
  47. /package/{templates/multi/sites/main → starter/site}/pages/about/1-about.md.hbs +0 -0
  48. /package/{templates/multi/sites/main → starter/site}/pages/about/page.yml +0 -0
  49. /package/{templates/single → starter}/site/pages/home/1-welcome.md.hbs +0 -0
  50. /package/{templates/multi/sites/main → starter/site}/pages/home/page.yml +0 -0
  51. /package/templates/{multi/foundations/default → foundation}/src/styles.css +0 -0
  52. /package/templates/{multi/foundations/default → foundation}/vite.config.js +0 -0
  53. /package/templates/{multi/sites/main → site}/index.html.hbs +0 -0
  54. /package/templates/{multi/sites/main → site}/main.js +0 -0
  55. /package/templates/{multi/sites/main → site}/public/favicon.svg +0 -0
  56. /package/templates/{single/site → site}/vite.config.js +0 -0
  57. /package/templates/{_shared → workspace}/AGENTS.md.hbs +0 -0
  58. /package/templates/{single → workspace}/README.md.hbs +0 -0
  59. /package/templates/{single/template/.vscode → workspace/_vscode}/settings.json +0 -0
package/src/index.js CHANGED
@@ -8,30 +8,26 @@
8
8
  * Usage:
9
9
  * npx uniweb create [project-name]
10
10
  * npx uniweb create --template marketing
11
+ * npx uniweb add foundation [name]
11
12
  * npx uniweb build
12
13
  * npx uniweb docs # Generate COMPONENTS.md from schema
13
14
  */
14
15
 
15
- import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync } from 'node:fs'
16
+ import { existsSync } from 'node:fs'
16
17
  import { execSync } from 'node:child_process'
17
- import { resolve, join, dirname } from 'node:path'
18
- import { fileURLToPath } from 'node:url'
18
+ import { resolve, join, relative } from 'node:path'
19
19
  import prompts from 'prompts'
20
20
  import { build } from './commands/build.js'
21
21
  import { docs } from './commands/docs.js'
22
22
  import { doctor } from './commands/doctor.js'
23
23
  import { i18n } from './commands/i18n.js'
24
- import { getVersionsForTemplates, getVersion } from './versions.js'
24
+ import { add } from './commands/add.js'
25
25
  import {
26
26
  resolveTemplate,
27
- applyExternalTemplate,
28
27
  parseTemplateId,
29
- listAvailableTemplates,
30
- BUILTIN_TEMPLATES,
31
28
  } from './templates/index.js'
32
- import { copyTemplateDirectory, registerVersions } from './templates/processor.js'
33
-
34
- const __dirname = dirname(fileURLToPath(import.meta.url))
29
+ import { validateTemplate } from './templates/validator.js'
30
+ import { scaffoldWorkspace, scaffoldFoundation, scaffoldSite, applyContent, applyStarter } from './utils/scaffold.js'
35
31
 
36
32
  // Colors for terminal output
37
33
  const colors = {
@@ -60,79 +56,213 @@ function title(message) {
60
56
  console.log(`\n${colors.cyan}${colors.bright}${message}${colors.reset}\n`)
61
57
  }
62
58
 
63
- // Built-in template definitions (metadata for display)
64
- const templates = {
65
- single: {
66
- name: 'Single Project',
67
- description: 'One site + one foundation in site/ and foundation/ (recommended)',
68
- },
69
- multi: {
70
- name: 'Multi-Site Workspace',
71
- description: 'Multiple sites and foundations in sites/* and foundations/*',
72
- },
73
- }
74
-
75
59
  /**
76
- * Get the path to a built-in template
60
+ * Create a project using the new package template flow (default)
77
61
  */
78
- function getBuiltinTemplatePath(templateName) {
79
- return join(__dirname, '..', 'templates', templateName)
62
+ async function createFromPackageTemplates(projectDir, projectName, options = {}) {
63
+ const { onProgress, onWarning } = options
64
+
65
+ onProgress?.('Setting up workspace...')
66
+
67
+ // 1. Scaffold workspace
68
+ await scaffoldWorkspace(projectDir, {
69
+ projectName,
70
+ workspaceGlobs: ['foundation', 'site'],
71
+ scripts: {
72
+ dev: 'pnpm --filter site dev',
73
+ build: 'uniweb build',
74
+ preview: 'pnpm --filter site preview',
75
+ },
76
+ }, { onProgress, onWarning })
77
+
78
+ // 2. Scaffold foundation
79
+ onProgress?.('Creating foundation...')
80
+ await scaffoldFoundation(join(projectDir, 'foundation'), {
81
+ name: 'foundation',
82
+ projectName,
83
+ isExtension: false,
84
+ }, { onProgress, onWarning })
85
+
86
+ // 3. Scaffold site
87
+ onProgress?.('Creating site...')
88
+ await scaffoldSite(join(projectDir, 'site'), {
89
+ name: 'site',
90
+ projectName,
91
+ foundationName: 'foundation',
92
+ foundationPath: 'file:../foundation',
93
+ }, { onProgress, onWarning })
94
+
95
+ // 4. Apply starter content
96
+ onProgress?.('Adding starter content...')
97
+ await applyStarter(projectDir, { projectName }, { onProgress, onWarning })
98
+
99
+ success(`Created project: ${projectName}`)
80
100
  }
81
101
 
82
102
  /**
83
- * Get the shared base template path
103
+ * Create a blank workspace (no packages, grow with `add`)
84
104
  */
85
- function getSharedTemplatePath() {
86
- return join(__dirname, '..', 'templates', '_shared')
105
+ async function createBlankWorkspace(projectDir, projectName, options = {}) {
106
+ const { onProgress, onWarning } = options
107
+
108
+ onProgress?.('Setting up blank workspace...')
109
+
110
+ await scaffoldWorkspace(projectDir, {
111
+ projectName,
112
+ workspaceGlobs: [],
113
+ scripts: {
114
+ build: 'uniweb build',
115
+ },
116
+ }, { onProgress, onWarning })
117
+
118
+ success(`Created blank workspace: ${projectName}`)
87
119
  }
88
120
 
89
121
  /**
90
- * Apply a built-in template using file-based templates
122
+ * Create a project from a format 2 content template
123
+ *
124
+ * Scaffolds workspace structure from package templates, then overlays
125
+ * content (sections, pages, theme) from the content template.
91
126
  */
92
- async function applyBuiltinTemplate(templateName, targetPath, options = {}) {
93
- const { projectName, variant, onProgress, onWarning } = options
127
+ async function createFromContentTemplate(projectDir, projectName, metadata, templateRootPath, options = {}) {
128
+ const { onProgress, onWarning } = options
129
+
130
+ // Determine packages to create
131
+ const packages = metadata.packages || [
132
+ { type: 'foundation', name: 'foundation' },
133
+ { type: 'site', name: 'site', foundation: 'foundation' },
134
+ ]
135
+
136
+ // Compute placement for each package
137
+ const placed = computePlacement(packages)
138
+
139
+ // Compute workspace globs and scripts from placement
140
+ const workspaceGlobs = placed.map(p => p.relativePath)
141
+ const sites = placed.filter(p => p.type === 'site')
142
+ const scripts = {
143
+ build: 'uniweb build',
144
+ }
145
+ if (sites.length === 1) {
146
+ scripts.dev = `pnpm --filter ${sites[0].name} dev`
147
+ scripts.preview = `pnpm --filter ${sites[0].name} preview`
148
+ } else {
149
+ for (const s of sites) {
150
+ scripts[`dev:${s.name}`] = `pnpm --filter ${s.name} dev`
151
+ scripts[`preview:${s.name}`] = `pnpm --filter ${s.name} preview`
152
+ }
153
+ // First site gets unqualified aliases
154
+ if (sites.length > 0) {
155
+ scripts.dev = `pnpm --filter ${sites[0].name} dev`
156
+ scripts.preview = `pnpm --filter ${sites[0].name} preview`
157
+ }
158
+ }
94
159
 
95
- const templatePath = getBuiltinTemplatePath(templateName)
160
+ // 1. Scaffold workspace
161
+ onProgress?.('Setting up workspace...')
162
+ await scaffoldWorkspace(projectDir, {
163
+ projectName,
164
+ workspaceGlobs,
165
+ scripts,
166
+ }, { onProgress, onWarning })
167
+
168
+ // 2. Scaffold and apply content for each package
169
+ for (const pkg of placed) {
170
+ const fullPath = join(projectDir, pkg.relativePath)
171
+
172
+ if (pkg.type === 'foundation' || pkg.type === 'extension') {
173
+ onProgress?.(`Creating ${pkg.type}: ${pkg.name}...`)
174
+ await scaffoldFoundation(fullPath, {
175
+ name: pkg.name,
176
+ projectName,
177
+ isExtension: pkg.type === 'extension',
178
+ }, { onProgress, onWarning })
179
+ } else if (pkg.type === 'site') {
180
+ // Find the foundation this site wires to
181
+ const foundationName = pkg.foundation || 'foundation'
182
+ const foundationPkg = placed.find(p =>
183
+ (p.type === 'foundation') && (p.name === foundationName)
184
+ )
185
+ const foundationPath = foundationPkg
186
+ ? computeFoundationFilePath(pkg.relativePath, foundationPkg.relativePath)
187
+ : 'file:../foundation'
188
+
189
+ onProgress?.(`Creating site: ${pkg.name}...`)
190
+ await scaffoldSite(fullPath, {
191
+ name: pkg.name,
192
+ projectName,
193
+ foundationName,
194
+ foundationPath,
195
+ foundationRef: foundationName !== 'foundation' ? foundationName : undefined,
196
+ }, { onProgress, onWarning })
197
+ }
96
198
 
97
- // Load template.json for metadata
98
- let templateConfig = {}
99
- const configPath = join(templatePath, 'template.json')
100
- if (existsSync(configPath)) {
101
- templateConfig = JSON.parse(readFileSync(configPath, 'utf8'))
199
+ // Apply content from the matching content directory
200
+ const contentDir = findContentDirFor(metadata.contentDirs, pkg)
201
+ if (contentDir) {
202
+ onProgress?.(`Applying ${metadata.name} content to ${pkg.name}...`)
203
+ await applyContent(contentDir.dir, fullPath, { projectName }, { onProgress, onWarning })
204
+ }
102
205
  }
103
206
 
104
- // Determine base template path if specified
105
- let basePath = null
106
- if (templateConfig.base) {
107
- basePath = join(__dirname, '..', 'templates', templateConfig.base)
108
- if (!existsSync(basePath)) {
109
- if (onWarning) {
110
- onWarning(`Base template '${templateConfig.base}' not found at ${basePath}`)
111
- }
112
- basePath = null
207
+ success(`Created project: ${projectName}`)
208
+ }
209
+
210
+ /**
211
+ * Compute placement (relative paths) for packages
212
+ *
213
+ * Rules:
214
+ * - 1 foundation named "foundation" → foundation/
215
+ * - Multiple foundations → foundations/{name}/
216
+ * - Extensions → extensions/{name}/
217
+ * - 1 site named "site" → site/
218
+ * - Multiple sites → sites/{name}/
219
+ */
220
+ function computePlacement(packages) {
221
+ const foundations = packages.filter(p => p.type === 'foundation')
222
+ const extensions = packages.filter(p => p.type === 'extension')
223
+ const sites = packages.filter(p => p.type === 'site')
224
+
225
+ const placed = []
226
+
227
+ for (const f of foundations) {
228
+ if (foundations.length === 1 && f.name === 'foundation') {
229
+ placed.push({ ...f, relativePath: 'foundation' })
230
+ } else {
231
+ placed.push({ ...f, relativePath: `foundations/${f.name}` })
113
232
  }
114
233
  }
115
234
 
116
- // Register versions for Handlebars templates
117
- registerVersions(getVersionsForTemplates())
235
+ for (const e of extensions) {
236
+ placed.push({ ...e, relativePath: `extensions/${e.name}` })
237
+ }
118
238
 
119
- // Prepare template data
120
- const templateData = {
121
- projectName: projectName || 'my-project',
122
- templateName: templateName,
123
- templateTitle: projectName || 'My Project',
124
- templateDescription: templateConfig.description || 'A Uniweb project',
239
+ for (const s of sites) {
240
+ if (sites.length === 1 && s.name === 'site') {
241
+ placed.push({ ...s, relativePath: 'site' })
242
+ } else {
243
+ placed.push({ ...s, relativePath: `sites/${s.name}` })
244
+ }
125
245
  }
126
246
 
127
- // Copy template files
128
- await copyTemplateDirectory(templatePath, targetPath, templateData, {
129
- variant,
130
- basePath,
131
- onProgress,
132
- onWarning,
133
- })
247
+ return placed
248
+ }
134
249
 
135
- success(`Created project: ${projectName || 'my-project'}`)
250
+ /**
251
+ * Find the content directory that matches a placed package
252
+ */
253
+ function findContentDirFor(contentDirs, pkg) {
254
+ if (!contentDirs) return null
255
+ // Match by name first, then by type
256
+ return contentDirs.find(d => d.name === pkg.name) ||
257
+ contentDirs.find(d => d.type === pkg.type && d.name === pkg.type)
258
+ }
259
+
260
+ /**
261
+ * Compute relative file: path from site to foundation
262
+ */
263
+ function computeFoundationFilePath(sitePath, foundationPath) {
264
+ const rel = relative(sitePath, foundationPath)
265
+ return `file:${rel}`
136
266
  }
137
267
 
138
268
  async function main() {
@@ -169,6 +299,12 @@ async function main() {
169
299
  return
170
300
  }
171
301
 
302
+ // Handle add command
303
+ if (command === 'add') {
304
+ await add(args.slice(1))
305
+ return
306
+ }
307
+
172
308
  // Handle create command
173
309
  if (command !== 'create') {
174
310
  error(`Unknown command: ${command}`)
@@ -180,7 +316,7 @@ async function main() {
180
316
 
181
317
  // Parse arguments
182
318
  let projectName = args[1]
183
- let templateType = "single"; // or null for iteractive selection
319
+ let templateType = null // null = use new package template flow
184
320
 
185
321
  // Check for --template flag
186
322
  const templateIndex = args.indexOf('--template')
@@ -195,13 +331,6 @@ async function main() {
195
331
  }
196
332
  }
197
333
 
198
- // Check for --variant flag
199
- let variant = null
200
- const variantIndex = args.indexOf('--variant')
201
- if (variantIndex !== -1 && args[variantIndex + 1]) {
202
- variant = args[variantIndex + 1]
203
- }
204
-
205
334
  // Check for --name flag (used for project display name)
206
335
  let displayName = null
207
336
  const nameIndex = args.indexOf('--name')
@@ -212,6 +341,11 @@ async function main() {
212
341
  // Check for --no-git flag
213
342
  const noGit = args.includes('--no-git')
214
343
 
344
+ // Skip positional name if it starts with -- (it's a flag, not a name)
345
+ if (projectName && projectName.startsWith('--')) {
346
+ projectName = null
347
+ }
348
+
215
349
  // Interactive prompts
216
350
  const response = await prompts([
217
351
  {
@@ -227,23 +361,6 @@ async function main() {
227
361
  return true
228
362
  },
229
363
  },
230
- {
231
- type: templateType ? null : 'select',
232
- name: 'template',
233
- message: 'What would you like to create?',
234
- choices: [
235
- {
236
- title: templates.single.name,
237
- description: templates.single.description,
238
- value: 'single',
239
- },
240
- {
241
- title: templates.multi.name,
242
- description: templates.multi.description,
243
- value: 'multi',
244
- },
245
- ],
246
- },
247
364
  ], {
248
365
  onCancel: () => {
249
366
  log('\nScaffolding cancelled.')
@@ -252,13 +369,14 @@ async function main() {
252
369
  })
253
370
 
254
371
  projectName = projectName || response.projectName
255
- templateType = templateType || response.template
256
372
 
257
- if (!projectName || !templateType) {
258
- error('Missing project name or template type')
373
+ if (!projectName) {
374
+ error('Missing project name')
259
375
  process.exit(1)
260
376
  }
261
377
 
378
+ const effectiveName = displayName || projectName
379
+
262
380
  // Create project directory
263
381
  const projectDir = resolve(process.cwd(), projectName)
264
382
 
@@ -267,46 +385,53 @@ async function main() {
267
385
  process.exit(1)
268
386
  }
269
387
 
270
- // Resolve and create project based on template
271
- const parsed = parseTemplateId(templateType)
272
-
273
- if (parsed.type === 'builtin') {
274
- const templateMeta = templates[templateType]
275
- log(`\nCreating ${templateMeta ? templateMeta.name.toLowerCase() : templateType}...`)
388
+ // Template routing logic
389
+ const progressCb = (msg) => log(` ${colors.dim}${msg}${colors.reset}`)
390
+ const warningCb = (msg) => log(` ${colors.yellow}Warning: ${msg}${colors.reset}`)
276
391
 
277
- // Apply file-based built-in template
278
- await applyBuiltinTemplate(templateType, projectDir, {
279
- projectName: displayName || projectName,
280
- variant,
281
- onProgress: (msg) => log(` ${colors.dim}${msg}${colors.reset}`),
282
- onWarning: (msg) => log(` ${colors.yellow}Warning: ${msg}${colors.reset}`),
392
+ if (templateType === 'blank') {
393
+ // Blank workspace
394
+ log('\nCreating blank workspace...')
395
+ await createBlankWorkspace(projectDir, effectiveName, {
396
+ onProgress: progressCb,
397
+ onWarning: warningCb,
398
+ })
399
+ } else if (!templateType) {
400
+ // Default flow (package templates + starter)
401
+ log('\nCreating project...')
402
+ await createFromPackageTemplates(projectDir, effectiveName, {
403
+ onProgress: progressCb,
404
+ onWarning: warningCb,
283
405
  })
284
406
  } else {
285
- // External template (official, npm, or github)
407
+ // External: official/npm/github/local
286
408
  log(`\nResolving template: ${templateType}...`)
287
409
 
288
410
  try {
289
411
  const resolved = await resolveTemplate(templateType, {
290
- onProgress: (msg) => log(` ${colors.dim}${msg}${colors.reset}`),
412
+ onProgress: progressCb,
291
413
  })
292
414
 
293
415
  log(`\nCreating project from ${resolved.name || resolved.package || `${resolved.owner}/${resolved.repo}`}...`)
294
416
 
295
- await applyExternalTemplate(resolved, projectDir, {
296
- projectName: displayName || projectName,
297
- versions: getVersionsForTemplates(),
298
- }, {
299
- variant,
300
- onProgress: (msg) => log(` ${colors.dim}${msg}${colors.reset}`),
301
- onWarning: (msg) => log(` ${colors.yellow}Warning: ${msg}${colors.reset}`),
302
- })
417
+ // Validate and apply as format 2 content template
418
+ const metadata = await validateTemplate(resolved.path, {})
419
+
420
+ try {
421
+ await createFromContentTemplate(projectDir, effectiveName, metadata, resolved.path, {
422
+ onProgress: progressCb,
423
+ onWarning: warningCb,
424
+ })
425
+ } finally {
426
+ if (resolved.cleanup) await resolved.cleanup()
427
+ }
303
428
  } catch (err) {
304
429
  error(`Failed to apply template: ${err.message}`)
305
430
  log('')
306
431
  log(`${colors.yellow}Troubleshooting:${colors.reset}`)
307
432
  log(` • Check your network connection`)
308
433
  log(` • Official templates require GitHub access (may be blocked by corporate networks)`)
309
- log(` • Try the built-in template instead: ${colors.cyan}uniweb create ${projectName}${colors.reset}`)
434
+ log(` • Try the default template instead: ${colors.cyan}uniweb create ${projectName}${colors.reset}`)
310
435
  process.exit(1)
311
436
  }
312
437
  }
@@ -332,10 +457,19 @@ async function main() {
332
457
  // Success message
333
458
  title('Project created successfully!')
334
459
 
335
- log(`Next steps:\n`)
336
- log(` ${colors.cyan}cd ${projectName}${colors.reset}`)
337
- log(` ${colors.cyan}pnpm install${colors.reset}`)
338
- log(` ${colors.cyan}pnpm dev${colors.reset}`)
460
+ if (templateType === 'blank') {
461
+ log(`Next steps:\n`)
462
+ log(` ${colors.cyan}cd ${projectName}${colors.reset}`)
463
+ log(` ${colors.cyan}uniweb add foundation${colors.reset}`)
464
+ log(` ${colors.cyan}uniweb add site${colors.reset}`)
465
+ log(` ${colors.cyan}pnpm install${colors.reset}`)
466
+ log(` ${colors.cyan}pnpm dev${colors.reset}`)
467
+ } else {
468
+ log(`Next steps:\n`)
469
+ log(` ${colors.cyan}cd ${projectName}${colors.reset}`)
470
+ log(` ${colors.cyan}pnpm install${colors.reset}`)
471
+ log(` ${colors.cyan}pnpm dev${colors.reset}`)
472
+ }
339
473
  log('')
340
474
  }
341
475
 
@@ -348,16 +482,21 @@ ${colors.bright}Usage:${colors.reset}
348
482
 
349
483
  ${colors.bright}Commands:${colors.reset}
350
484
  create [name] Create a new project
485
+ add <type> [name] Add a foundation, site, or extension to a project
351
486
  build Build the current project
352
487
  docs Generate component documentation
353
488
  doctor Diagnose project configuration issues
354
489
  i18n <cmd> Internationalization (extract, sync, status)
355
490
 
356
491
  ${colors.bright}Create Options:${colors.reset}
357
- --template <type> Project template
358
- --variant <name> Template variant (e.g., tailwind3 for legacy)
492
+ --template <type> Project template (default: creates foundation + site + starter)
359
493
  --name <name> Project display name
360
- --no-git Skip git repository initialization
494
+ --no-git Skip git repository initialization
495
+
496
+ ${colors.bright}Add Subcommands:${colors.reset}
497
+ add foundation [name] Add a foundation (--from, --path, --project)
498
+ add site [name] Add a site (--from, --foundation, --path, --project)
499
+ add extension <name> Add an extension (--from, --site, --path)
361
500
 
362
501
  ${colors.bright}Build Options:${colors.reset}
363
502
  --target <type> Build target (foundation, site) - auto-detected if not specified
@@ -386,8 +525,7 @@ ${colors.bright}i18n Commands:${colors.reset}
386
525
  status Show translation coverage per locale
387
526
 
388
527
  ${colors.bright}Template Types:${colors.reset}
389
- single One site + one foundation (default)
390
- multi Multiple sites and foundations
528
+ blank Empty workspace (grow with 'add')
391
529
  marketing Official marketing template
392
530
  ./path/to/template Local directory
393
531
  @scope/template-name npm package
@@ -395,17 +533,21 @@ ${colors.bright}Template Types:${colors.reset}
395
533
  https://github.com/user/repo GitHub URL
396
534
 
397
535
  ${colors.bright}Examples:${colors.reset}
398
- npx uniweb create my-project
399
- npx uniweb create my-project --template single
400
- npx uniweb create my-project --template marketing
401
- npx uniweb create my-project --template marketing --variant tailwind3
402
- npx uniweb create my-project --template ./my-template
403
- npx uniweb create my-project --template github:myorg/template
536
+ npx uniweb create my-project # Default (foundation + site + starter)
537
+ npx uniweb create my-project --template blank # Blank workspace
538
+ npx uniweb create my-project --template marketing # Official template
539
+ npx uniweb create my-project --template ./my-template # Local template
540
+
541
+ cd my-project
542
+ npx uniweb add foundation marketing # Add foundations/marketing/
543
+ npx uniweb add foundation marketing --from marketing # Scaffold + marketing sections
544
+ npx uniweb add site blog --foundation marketing # Add sites/blog/ wired to marketing
545
+ npx uniweb add site blog --from docs --foundation blog # Scaffold + docs pages
546
+ npx uniweb add extension effects --site site # Add extensions/effects/
547
+
404
548
  npx uniweb build
405
549
  npx uniweb build --target foundation
406
- npx uniweb build # Auto-prerenders if site.yml has build.prerender: true
407
- npx uniweb build --no-prerender # Skip prerendering even if enabled in config
408
- cd foundation && npx uniweb docs # Generate COMPONENTS.md
550
+ cd foundation && npx uniweb docs # Generate COMPONENTS.md
409
551
  `)
410
552
  }
411
553
 
@@ -12,12 +12,6 @@ import { fetchNpmTemplate } from './fetchers/npm.js'
12
12
  import { fetchGitHubTemplate } from './fetchers/github.js'
13
13
  import { fetchOfficialTemplate, listOfficialTemplates } from './fetchers/release.js'
14
14
  import { validateTemplate } from './validator.js'
15
- import {
16
- copyTemplateDirectory,
17
- registerVersions,
18
- getMissingVersions,
19
- clearMissingVersions
20
- } from './processor.js'
21
15
 
22
16
  /**
23
17
  * Resolve a template identifier and return the template path
@@ -163,100 +157,12 @@ async function resolveLocalTemplate(templatePath, options = {}) {
163
157
  }
164
158
  }
165
159
 
166
- /**
167
- * Apply a template to a target directory
168
- *
169
- * @param {string} templatePath - Path to the template root (contains template.json)
170
- * @param {string} targetPath - Destination directory for the scaffolded project
171
- * @param {Object} data - Template variables
172
- * @param {Object} options - Apply options
173
- * @param {string} options.variant - Template variant to use
174
- * @param {string} options.uniwebVersion - Current Uniweb version for compatibility check
175
- * @param {Function} options.onWarning - Warning callback
176
- * @param {Function} options.onProgress - Progress callback
177
- * @returns {Promise<Object>} Template metadata
178
- */
179
- export async function applyTemplate(templatePath, targetPath, data = {}, options = {}) {
180
- const { uniwebVersion, variant, onWarning, onProgress } = options
181
-
182
- // Validate the template
183
- const metadata = await validateTemplate(templatePath, { uniwebVersion })
184
-
185
- // Register versions for the {{version}} helper
186
- if (data.versions) {
187
- registerVersions(data.versions)
188
- }
189
-
190
- // Apply default variables
191
- const templateData = {
192
- year: new Date().getFullYear(),
193
- ...data
194
- }
195
-
196
- // Copy template files
197
- await copyTemplateDirectory(
198
- metadata.templateDir,
199
- targetPath,
200
- templateData,
201
- { variant, onWarning, onProgress }
202
- )
203
-
204
- // Check for missing versions and warn
205
- const missingVersions = getMissingVersions()
206
- if (missingVersions.length > 0 && onWarning) {
207
- onWarning(`Missing version data for packages: ${missingVersions.join(', ')}. Using fallback version.`)
208
- }
209
- clearMissingVersions()
210
-
211
- return metadata
212
- }
213
-
214
- /**
215
- * Apply an external template to a target directory
216
- *
217
- * @param {Object} resolved - Resolved template from resolveTemplate()
218
- * @param {string} targetPath - Target directory
219
- * @param {Object} data - Template variables
220
- * @param {Object} options - Apply options
221
- */
222
- export async function applyExternalTemplate(resolved, targetPath, data, options = {}) {
223
- const { variant, onProgress, onWarning } = options
224
-
225
- try {
226
- const metadata = await applyTemplate(
227
- resolved.path,
228
- targetPath,
229
- data,
230
- { variant, onProgress, onWarning }
231
- )
232
-
233
- return metadata
234
- } finally {
235
- // Clean up temp directory if there is one
236
- if (resolved.cleanup) {
237
- await resolved.cleanup()
238
- }
239
- }
240
- }
241
-
242
160
  /**
243
161
  * List all available templates
244
162
  */
245
163
  export async function listAvailableTemplates() {
246
164
  const templates = []
247
165
 
248
- // Built-in templates
249
- for (const name of BUILTIN_TEMPLATES) {
250
- templates.push({
251
- type: 'builtin',
252
- id: name,
253
- name: name.charAt(0).toUpperCase() + name.slice(1),
254
- description: name === 'single'
255
- ? 'One site + one foundation'
256
- : 'Multiple sites and foundations',
257
- })
258
- }
259
-
260
166
  // Official templates from GitHub releases
261
167
  try {
262
168
  const official = await listOfficialTemplates()