uniweb 0.2.12 → 0.2.14

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uniweb",
3
- "version": "0.2.12",
3
+ "version": "0.2.14",
4
4
  "description": "Create structured Vite + React sites with content/code separation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,8 +32,9 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "handlebars": "^4.7.8",
35
+ "js-yaml": "^4.1.0",
35
36
  "prompts": "^2.4.2",
36
37
  "tar": "^7.0.0",
37
- "@uniweb/build": "0.1.4"
38
+ "@uniweb/build": "0.1.6"
38
39
  }
39
40
  }
@@ -178,6 +178,112 @@ async function buildFoundation(projectDir, options = {}) {
178
178
  }
179
179
  }
180
180
 
181
+ /**
182
+ * Load site i18n configuration
183
+ *
184
+ * Resolves locales from config:
185
+ * - undefined → all available locales (from locales/*.json)
186
+ * - '*' → explicitly all available locales
187
+ * - ['es', 'fr'] → only those specific locales
188
+ */
189
+ async function loadI18nConfig(projectDir) {
190
+ const siteYmlPath = join(projectDir, 'site.yml')
191
+ if (!existsSync(siteYmlPath)) return null
192
+
193
+ const { readFile } = await import('node:fs/promises')
194
+ const yaml = await import('js-yaml')
195
+ const content = await readFile(siteYmlPath, 'utf-8')
196
+ const config = yaml.load(content) || {}
197
+
198
+ const localesDir = config.i18n?.localesDir || 'locales'
199
+ const localesPath = join(projectDir, localesDir)
200
+
201
+ // Resolve locales (undefined/'*' → all available, array → specific)
202
+ const { resolveLocales } = await import('@uniweb/build/i18n')
203
+ const locales = await resolveLocales(config.i18n?.locales, localesPath)
204
+
205
+ if (locales.length === 0) return null
206
+
207
+ return {
208
+ defaultLocale: config.defaultLanguage || 'en',
209
+ locales,
210
+ localesDir,
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Build localized content for all configured locales
216
+ */
217
+ async function buildLocalizedContent(projectDir, i18nConfig) {
218
+ const { buildLocalizedContent } = await import('@uniweb/build/i18n')
219
+
220
+ const outputs = await buildLocalizedContent(projectDir, {
221
+ localesDir: i18nConfig.localesDir,
222
+ locales: i18nConfig.locales,
223
+ outputDir: join(projectDir, 'dist'),
224
+ fallbackToSource: true,
225
+ })
226
+
227
+ return outputs
228
+ }
229
+
230
+ /**
231
+ * Generate index.html for each locale with hreflang tags
232
+ */
233
+ async function generateLocalizedHtml(projectDir, i18nConfig) {
234
+ const { readFile, writeFile, mkdir, copyFile } = await import('node:fs/promises')
235
+ const distDir = join(projectDir, 'dist')
236
+ const baseHtmlPath = join(distDir, 'index.html')
237
+
238
+ if (!existsSync(baseHtmlPath)) {
239
+ return
240
+ }
241
+
242
+ let baseHtml = await readFile(baseHtmlPath, 'utf-8')
243
+
244
+ // Build hreflang tags
245
+ const hreflangTags = i18nConfig.locales.map(locale =>
246
+ `<link rel="alternate" hreflang="${locale}" href="/${locale}/" />`
247
+ ).join('\n ')
248
+
249
+ const defaultHreflang = `<link rel="alternate" hreflang="x-default" href="/" />`
250
+ const allHreflangTags = `${hreflangTags}\n ${defaultHreflang}`
251
+
252
+ // Add hreflang to base HTML (for default locale)
253
+ if (!baseHtml.includes('hreflang')) {
254
+ baseHtml = baseHtml.replace('</head>', ` ${allHreflangTags}\n </head>`)
255
+ await writeFile(baseHtmlPath, baseHtml)
256
+ }
257
+
258
+ // Generate index.html for each locale
259
+ for (const locale of i18nConfig.locales) {
260
+ const localeDir = join(distDir, locale)
261
+ await mkdir(localeDir, { recursive: true })
262
+
263
+ // Read locale-specific site-content.json
264
+ const localeContentPath = join(localeDir, 'site-content.json')
265
+ let localeHtml = baseHtml
266
+
267
+ // Update html lang attribute
268
+ localeHtml = localeHtml.replace(/<html[^>]*lang="[^"]*"/, `<html lang="${locale}"`)
269
+ if (!localeHtml.includes('lang=')) {
270
+ localeHtml = localeHtml.replace('<html', `<html lang="${locale}"`)
271
+ }
272
+
273
+ // Update script src for locale-specific content if inlined
274
+ if (existsSync(localeContentPath)) {
275
+ const localeContent = await readFile(localeContentPath, 'utf-8')
276
+ // Replace the inlined content if present
277
+ localeHtml = localeHtml.replace(
278
+ /<script id="__SITE_CONTENT__"[^>]*>[\s\S]*?<\/script>/,
279
+ `<script id="__SITE_CONTENT__" type="application/json">${localeContent}</script>`
280
+ )
281
+ }
282
+
283
+ await writeFile(join(localeDir, 'index.html'), localeHtml)
284
+ }
285
+ }
286
+
181
287
  /**
182
288
  * Build a site
183
289
  */
@@ -191,6 +297,35 @@ async function buildSite(projectDir, options = {}) {
191
297
 
192
298
  success('Site build complete')
193
299
 
300
+ // Check for i18n configuration
301
+ const i18nConfig = await loadI18nConfig(projectDir)
302
+
303
+ if (i18nConfig && i18nConfig.locales.length > 0) {
304
+ log('')
305
+ info(`Building localized content for: ${i18nConfig.locales.join(', ')}`)
306
+
307
+ try {
308
+ // Generate locale-specific site-content.json
309
+ const outputs = await buildLocalizedContent(projectDir, i18nConfig)
310
+
311
+ // Generate locale-specific index.html files
312
+ await generateLocalizedHtml(projectDir, i18nConfig)
313
+
314
+ success(`Generated ${Object.keys(outputs).length} locale(s)`)
315
+
316
+ for (const [locale, path] of Object.entries(outputs)) {
317
+ log(` ${colors.dim}dist/${locale}/site-content.json${colors.reset}`)
318
+ }
319
+ } catch (err) {
320
+ error(`i18n build failed: ${err.message}`)
321
+ if (process.env.DEBUG) {
322
+ console.error(err.stack)
323
+ }
324
+ // Don't fail the build, just warn
325
+ log(`${colors.yellow}Continuing without localized content${colors.reset}`)
326
+ }
327
+ }
328
+
194
329
  // Pre-render if requested
195
330
  if (prerender) {
196
331
  log('')
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Docs Command
3
+ *
4
+ * Generates markdown documentation from foundation schema.
5
+ *
6
+ * Usage:
7
+ * uniweb docs # Generate docs for current directory
8
+ * uniweb docs --output README.md # Custom output filename
9
+ * uniweb docs --from-source # Build schema from source (no build required)
10
+ */
11
+
12
+ import { existsSync } from 'node:fs'
13
+ import { resolve, join } from 'node:path'
14
+ import { generateDocs } from '@uniweb/build'
15
+
16
+ // Colors for terminal output
17
+ const colors = {
18
+ reset: '\x1b[0m',
19
+ bright: '\x1b[1m',
20
+ dim: '\x1b[2m',
21
+ cyan: '\x1b[36m',
22
+ green: '\x1b[32m',
23
+ yellow: '\x1b[33m',
24
+ red: '\x1b[31m',
25
+ }
26
+
27
+ function log(message) {
28
+ console.log(message)
29
+ }
30
+
31
+ function success(message) {
32
+ console.log(`${colors.green}✓${colors.reset} ${message}`)
33
+ }
34
+
35
+ function error(message) {
36
+ console.error(`${colors.red}✗${colors.reset} ${message}`)
37
+ }
38
+
39
+ function info(message) {
40
+ console.log(`${colors.cyan}→${colors.reset} ${message}`)
41
+ }
42
+
43
+ /**
44
+ * Parse command line arguments
45
+ */
46
+ function parseArgs(args) {
47
+ const options = {
48
+ output: 'COMPONENTS.md',
49
+ fromSource: false,
50
+ }
51
+
52
+ for (let i = 0; i < args.length; i++) {
53
+ const arg = args[i]
54
+
55
+ if (arg === '--output' || arg === '-o') {
56
+ options.output = args[++i]
57
+ } else if (arg === '--from-source' || arg === '-s') {
58
+ options.fromSource = true
59
+ } else if (arg === '--help' || arg === '-h') {
60
+ options.help = true
61
+ }
62
+ }
63
+
64
+ return options
65
+ }
66
+
67
+ /**
68
+ * Show help message
69
+ */
70
+ function showHelp() {
71
+ log(`
72
+ ${colors.bright}uniweb docs${colors.reset} - Generate component documentation
73
+
74
+ ${colors.dim}Usage:${colors.reset}
75
+ uniweb docs Generate COMPONENTS.md from schema.json
76
+ uniweb docs --output DOCS.md Custom output filename
77
+ uniweb docs --from-source Build schema from source (no build required)
78
+
79
+ ${colors.dim}Options:${colors.reset}
80
+ -o, --output <file> Output filename (default: COMPONENTS.md)
81
+ -s, --from-source Read meta.js files directly instead of schema.json
82
+ -h, --help Show this help message
83
+
84
+ ${colors.dim}Notes:${colors.reset}
85
+ By default, docs are generated from dist/schema.json (requires build).
86
+ Use --from-source to generate without building first.
87
+ `)
88
+ }
89
+
90
+ /**
91
+ * Detect if current directory is a foundation
92
+ */
93
+ function isFoundation(dir) {
94
+ const srcDir = join(dir, 'src')
95
+ const componentsDir = join(srcDir, 'components')
96
+ return existsSync(componentsDir)
97
+ }
98
+
99
+ /**
100
+ * Main docs command
101
+ */
102
+ export async function docs(args) {
103
+ const options = parseArgs(args)
104
+
105
+ if (options.help) {
106
+ showHelp()
107
+ return
108
+ }
109
+
110
+ const projectDir = resolve(process.cwd())
111
+
112
+ // Verify this is a foundation
113
+ if (!isFoundation(projectDir)) {
114
+ error('This directory does not appear to be a foundation.')
115
+ log(`${colors.dim}Foundations have a src/components/ directory with component folders.${colors.reset}`)
116
+ process.exit(1)
117
+ }
118
+
119
+ // Check if schema.json exists (if not using --from-source)
120
+ const schemaPath = join(projectDir, 'dist', 'schema.json')
121
+ if (!options.fromSource && !existsSync(schemaPath)) {
122
+ log(`${colors.yellow}⚠${colors.reset} No dist/schema.json found.`)
123
+ log(`${colors.dim}Run 'uniweb build' first, or use '--from-source' to read meta.js files directly.${colors.reset}`)
124
+ log('')
125
+ info('Falling back to --from-source mode')
126
+ options.fromSource = true
127
+ }
128
+
129
+ try {
130
+ info('Generating documentation...')
131
+
132
+ const result = await generateDocs(projectDir, {
133
+ output: options.output,
134
+ fromSource: options.fromSource,
135
+ })
136
+
137
+ success(`Generated ${result.outputPath}`)
138
+ log(`${colors.dim}Documented ${result.componentCount} components${colors.reset}`)
139
+ } catch (err) {
140
+ error(`Failed to generate docs: ${err.message}`)
141
+ process.exit(1)
142
+ }
143
+ }
@@ -0,0 +1,325 @@
1
+ /**
2
+ * i18n CLI Commands
3
+ *
4
+ * Commands for managing site content internationalization.
5
+ *
6
+ * Usage:
7
+ * uniweb i18n extract Extract translatable strings to manifest
8
+ * uniweb i18n sync Sync manifest with content changes
9
+ * uniweb i18n status Show translation coverage per locale
10
+ */
11
+
12
+ import { resolve, join } from 'path'
13
+ import { existsSync } from 'fs'
14
+ import { readFile } from 'fs/promises'
15
+ import yaml from 'js-yaml'
16
+
17
+ // Colors for terminal output
18
+ const colors = {
19
+ reset: '\x1b[0m',
20
+ bright: '\x1b[1m',
21
+ dim: '\x1b[2m',
22
+ cyan: '\x1b[36m',
23
+ green: '\x1b[32m',
24
+ yellow: '\x1b[33m',
25
+ red: '\x1b[31m',
26
+ }
27
+
28
+ function log(message) {
29
+ console.log(message)
30
+ }
31
+
32
+ function success(message) {
33
+ console.log(`${colors.green}✓${colors.reset} ${message}`)
34
+ }
35
+
36
+ function warn(message) {
37
+ console.log(`${colors.yellow}⚠${colors.reset} ${message}`)
38
+ }
39
+
40
+ function error(message) {
41
+ console.error(`${colors.red}✗${colors.reset} ${message}`)
42
+ }
43
+
44
+ /**
45
+ * Main i18n command handler
46
+ * @param {string[]} args - Command arguments
47
+ */
48
+ export async function i18n(args) {
49
+ const subcommand = args[0]
50
+
51
+ if (!subcommand || subcommand === '--help' || subcommand === '-h') {
52
+ showHelp()
53
+ return
54
+ }
55
+
56
+ // Find site root
57
+ const siteRoot = await findSiteRoot()
58
+ if (!siteRoot) {
59
+ error('Could not find site root. Make sure you are in a Uniweb site directory.')
60
+ process.exit(1)
61
+ }
62
+
63
+ // Load site config for locale settings
64
+ const config = await loadSiteConfig(siteRoot)
65
+
66
+ switch (subcommand) {
67
+ case 'extract':
68
+ await runExtract(siteRoot, config, args.slice(1))
69
+ break
70
+ case 'sync':
71
+ await runSync(siteRoot, config, args.slice(1))
72
+ break
73
+ case 'status':
74
+ await runStatus(siteRoot, config, args.slice(1))
75
+ break
76
+ default:
77
+ error(`Unknown subcommand: ${subcommand}`)
78
+ showHelp()
79
+ process.exit(1)
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Find site root by looking for site.yml
85
+ */
86
+ async function findSiteRoot() {
87
+ let dir = process.cwd()
88
+
89
+ // Check current directory and parents
90
+ for (let i = 0; i < 5; i++) {
91
+ if (existsSync(join(dir, 'site.yml'))) {
92
+ return dir
93
+ }
94
+ const parent = resolve(dir, '..')
95
+ if (parent === dir) break
96
+ dir = parent
97
+ }
98
+
99
+ return null
100
+ }
101
+
102
+ /**
103
+ * Load site configuration
104
+ *
105
+ * Resolves locales from config:
106
+ * - undefined → all available locales (from locales/*.json)
107
+ * - '*' → explicitly all available locales
108
+ * - ['es', 'fr'] → only those specific locales
109
+ */
110
+ async function loadSiteConfig(siteRoot) {
111
+ const configPath = join(siteRoot, 'site.yml')
112
+ const content = await readFile(configPath, 'utf-8')
113
+ const config = yaml.load(content) || {}
114
+
115
+ const localesDir = config.i18n?.localesDir || 'locales'
116
+ const localesPath = join(siteRoot, localesDir)
117
+
118
+ // Resolve locales (undefined/'*' → all available, array → specific)
119
+ const { resolveLocales } = await import('@uniweb/build/i18n')
120
+ const locales = await resolveLocales(config.i18n?.locales, localesPath)
121
+
122
+ return {
123
+ defaultLocale: config.defaultLanguage || 'en',
124
+ locales,
125
+ localesDir,
126
+ ...config.i18n,
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Extract command - extract translatable strings from site content
132
+ */
133
+ async function runExtract(siteRoot, config, args) {
134
+ const verbose = args.includes('--verbose') || args.includes('-v')
135
+
136
+ log(`\n${colors.cyan}Extracting translatable content...${colors.reset}\n`)
137
+
138
+ // Check if site has been built
139
+ const siteContentPath = join(siteRoot, 'dist', 'site-content.json')
140
+ if (!existsSync(siteContentPath)) {
141
+ error('Site content not found. Run "uniweb build" first.')
142
+ process.exit(1)
143
+ }
144
+
145
+ try {
146
+ // Dynamic import to avoid loading at CLI startup
147
+ const { extractManifest, formatSyncReport } = await import('@uniweb/build/i18n')
148
+
149
+ const { manifest, report } = await extractManifest(siteRoot, {
150
+ localesDir: config.localesDir,
151
+ siteContentPath,
152
+ verbose,
153
+ })
154
+
155
+ // Show results
156
+ const unitCount = Object.keys(manifest.units).length
157
+ success(`Extracted ${unitCount} translatable strings`)
158
+
159
+ if (report) {
160
+ log('')
161
+ log(formatSyncReport(report))
162
+ }
163
+
164
+ log(`\nManifest written to: ${colors.dim}${config.localesDir}/manifest.json${colors.reset}`)
165
+
166
+ if (config.locales.length === 0) {
167
+ log(`\n${colors.dim}No translation files found in ${config.localesDir}/.`)
168
+ log(`After translating, create locale files like ${config.localesDir}/es.json${colors.reset}`)
169
+ }
170
+ } catch (err) {
171
+ error(`Extraction failed: ${err.message}`)
172
+ if (verbose) console.error(err)
173
+ process.exit(1)
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Sync command - detect changes and update manifest
179
+ */
180
+ async function runSync(siteRoot, config, args) {
181
+ const verbose = args.includes('--verbose') || args.includes('-v')
182
+ const dryRun = args.includes('--dry-run')
183
+
184
+ log(`\n${colors.cyan}Syncing i18n manifest...${colors.reset}\n`)
185
+
186
+ // Check if site has been built
187
+ const siteContentPath = join(siteRoot, 'dist', 'site-content.json')
188
+ if (!existsSync(siteContentPath)) {
189
+ error('Site content not found. Run "uniweb build" first.')
190
+ process.exit(1)
191
+ }
192
+
193
+ // Check if manifest exists
194
+ const manifestPath = join(siteRoot, config.localesDir, 'manifest.json')
195
+ if (!existsSync(manifestPath)) {
196
+ warn('No existing manifest found. Running extract instead.')
197
+ return runExtract(siteRoot, config, args)
198
+ }
199
+
200
+ try {
201
+ const { extractManifest, formatSyncReport } = await import('@uniweb/build/i18n')
202
+
203
+ if (dryRun) {
204
+ log(`${colors.dim}(dry run - no files will be modified)${colors.reset}\n`)
205
+ }
206
+
207
+ const { manifest, report } = await extractManifest(siteRoot, {
208
+ localesDir: config.localesDir,
209
+ siteContentPath,
210
+ verbose,
211
+ dryRun,
212
+ })
213
+
214
+ log(formatSyncReport(report))
215
+
216
+ if (!dryRun) {
217
+ success('\nManifest updated')
218
+ }
219
+ } catch (err) {
220
+ error(`Sync failed: ${err.message}`)
221
+ if (verbose) console.error(err)
222
+ process.exit(1)
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Status command - show translation coverage
228
+ */
229
+ async function runStatus(siteRoot, config, args) {
230
+ const locale = args.find(a => !a.startsWith('-'))
231
+
232
+ log(`\n${colors.cyan}Translation Status${colors.reset}\n`)
233
+
234
+ // Check if manifest exists
235
+ const manifestPath = join(siteRoot, config.localesDir, 'manifest.json')
236
+ if (!existsSync(manifestPath)) {
237
+ error('No manifest found. Run "uniweb i18n extract" first.')
238
+ process.exit(1)
239
+ }
240
+
241
+ if (config.locales.length === 0) {
242
+ log(`${colors.dim}No translation files found in ${config.localesDir}/.`)
243
+ log(`Create locale files like ${config.localesDir}/es.json to add translations.${colors.reset}`)
244
+ return
245
+ }
246
+
247
+ try {
248
+ const { getTranslationStatus, formatTranslationStatus } = await import('@uniweb/build/i18n')
249
+
250
+ const localesToCheck = locale ? [locale] : config.locales
251
+
252
+ const status = await getTranslationStatus(siteRoot, {
253
+ localesDir: config.localesDir,
254
+ locales: localesToCheck,
255
+ })
256
+
257
+ log(formatTranslationStatus(status))
258
+
259
+ // Show next steps if there are missing translations
260
+ const hasMissing = Object.values(status.locales).some(l => l.missing > 0)
261
+ if (hasMissing) {
262
+ log(`\n${colors.dim}To translate missing strings, edit the locale files in ${config.localesDir}/`)
263
+ log(`Or use AI tools with the manifest.json as reference.${colors.reset}`)
264
+ }
265
+ } catch (err) {
266
+ error(`Status check failed: ${err.message}`)
267
+ process.exit(1)
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Show help for i18n commands
273
+ */
274
+ function showHelp() {
275
+ log(`
276
+ ${colors.cyan}${colors.bright}Uniweb i18n${colors.reset}
277
+
278
+ Site content internationalization commands.
279
+
280
+ ${colors.bright}Usage:${colors.reset}
281
+ uniweb i18n <command> [options]
282
+
283
+ ${colors.bright}Commands:${colors.reset}
284
+ extract Extract translatable strings to locales/manifest.json
285
+ sync Update manifest with content changes (detects moved/changed content)
286
+ status Show translation coverage per locale
287
+
288
+ ${colors.bright}Options:${colors.reset}
289
+ --verbose Show detailed output
290
+ --dry-run (sync) Show changes without writing files
291
+
292
+ ${colors.bright}Configuration:${colors.reset}
293
+ Optional site.yml settings:
294
+
295
+ i18n:
296
+ locales: [es, fr] # Specific locales only (default: all available)
297
+ locales: '*' # Explicitly all available locales
298
+ localesDir: locales # Directory for translation files (default: locales)
299
+
300
+ By default, all *.json files in locales/ are treated as translation targets.
301
+
302
+ ${colors.bright}Workflow:${colors.reset}
303
+ 1. Build your site: uniweb build
304
+ 2. Extract strings: uniweb i18n extract
305
+ 3. Translate locale files: Edit locales/es.json, locales/fr.json, etc.
306
+ 4. Build with translations: uniweb build (generates locale-specific output)
307
+
308
+ ${colors.bright}File Structure:${colors.reset}
309
+ locales/
310
+ manifest.json Auto-generated: source strings + hashes + contexts
311
+ es.json Translations for Spanish
312
+ fr.json Translations for French
313
+ _memory.json Optional: translation memory for reuse
314
+
315
+ ${colors.bright}Examples:${colors.reset}
316
+ uniweb i18n extract # Extract all translatable strings
317
+ uniweb i18n extract --verbose # Show extracted strings
318
+ uniweb i18n sync # Update manifest after content changes
319
+ uniweb i18n sync --dry-run # Preview changes without writing
320
+ uniweb i18n status # Show coverage for all locales
321
+ uniweb i18n status es # Show coverage for Spanish only
322
+ `)
323
+ }
324
+
325
+ export default i18n
package/src/index.js CHANGED
@@ -3,14 +3,13 @@
3
3
  /**
4
4
  * Uniweb CLI
5
5
  *
6
- * Scaffolds new Uniweb sites and foundations, and builds projects.
6
+ * Scaffolds new Uniweb sites and foundations, builds projects, and generates docs.
7
7
  *
8
8
  * Usage:
9
9
  * npx uniweb create [project-name]
10
- * npx uniweb create --template single # site/ + foundation/ (default)
11
- * npx uniweb create --template multi # sites/* + foundations/*
10
+ * npx uniweb create --template marketing
12
11
  * npx uniweb build
13
- * npx uniweb build --target foundation
12
+ * npx uniweb docs # Generate COMPONENTS.md from schema
14
13
  */
15
14
 
16
15
  import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs'
@@ -18,6 +17,8 @@ import { resolve, join, dirname } from 'node:path'
18
17
  import { fileURLToPath } from 'node:url'
19
18
  import prompts from 'prompts'
20
19
  import { build } from './commands/build.js'
20
+ import { docs } from './commands/docs.js'
21
+ import { i18n } from './commands/i18n.js'
21
22
  import { getVersionsForTemplates, getVersion } from './versions.js'
22
23
  import {
23
24
  resolveTemplate,
@@ -84,6 +85,18 @@ async function main() {
84
85
  return
85
86
  }
86
87
 
88
+ // Handle docs command
89
+ if (command === 'docs') {
90
+ await docs(args.slice(1))
91
+ return
92
+ }
93
+
94
+ // Handle i18n command
95
+ if (command === 'i18n') {
96
+ await i18n(args.slice(1))
97
+ return
98
+ }
99
+
87
100
  // Handle create command
88
101
  if (command !== 'create') {
89
102
  error(`Unknown command: ${command}`)
@@ -239,6 +252,8 @@ ${colors.bright}Usage:${colors.reset}
239
252
  ${colors.bright}Commands:${colors.reset}
240
253
  create [name] Create a new project
241
254
  build Build the current project
255
+ docs Generate component documentation
256
+ i18n <cmd> Internationalization (extract, sync, status)
242
257
 
243
258
  ${colors.bright}Create Options:${colors.reset}
244
259
  --template <type> Project template
@@ -251,6 +266,15 @@ ${colors.bright}Build Options:${colors.reset}
251
266
  --foundation-dir Path to foundation directory (for prerendering)
252
267
  --platform <name> Deployment platform (e.g., vercel) for platform-specific output
253
268
 
269
+ ${colors.bright}Docs Options:${colors.reset}
270
+ --output <file> Output filename (default: COMPONENTS.md)
271
+ --from-source Read meta.js files directly instead of schema.json
272
+
273
+ ${colors.bright}i18n Commands:${colors.reset}
274
+ extract Extract translatable strings to manifest
275
+ sync Update manifest with content changes
276
+ status Show translation coverage per locale
277
+
254
278
  ${colors.bright}Template Types:${colors.reset}
255
279
  single One site + one foundation (default)
256
280
  multi Multiple sites and foundations
@@ -268,6 +292,7 @@ ${colors.bright}Examples:${colors.reset}
268
292
  npx uniweb build
269
293
  npx uniweb build --target foundation
270
294
  npx uniweb build --prerender # Build site + pre-render to static HTML
295
+ cd foundation && npx uniweb docs # Generate COMPONENTS.md
271
296
  `)
272
297
  }
273
298
 
package/src/versions.js CHANGED
@@ -72,6 +72,9 @@ export function getResolvedVersions() {
72
72
  '@uniweb/runtime': '^0.1.0',
73
73
  '@uniweb/core': '^0.1.0',
74
74
 
75
+ // Foundation utility library (used by official templates)
76
+ '@uniweb/kit': '^0.1.0',
77
+
75
78
  // CLI itself (use current version)
76
79
  'uniweb': `^${pkg.version}`,
77
80
  }
@@ -106,6 +109,7 @@ export function getVersionsForTemplates() {
106
109
  build: versions['@uniweb/build'],
107
110
  runtime: versions['@uniweb/runtime'],
108
111
  core: versions['@uniweb/core'],
112
+ kit: versions['@uniweb/kit'],
109
113
  templates: versions['@uniweb/templates'],
110
114
  cli: versions['uniweb'],
111
115
  }