uniweb 0.8.14 → 0.8.16

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.
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Template Command
3
+ *
4
+ * Publish a site as a cloud template.
5
+ *
6
+ * Usage:
7
+ * uniweb template publish # Reads template name from site.yml `template:` field
8
+ * uniweb template publish --name my-tpl # Override template name
9
+ * uniweb template publish --title "My Tpl" # Display title
10
+ * uniweb template publish --description "A starter template"
11
+ * uniweb template publish --registry <url> # Publish to a specific registry URL
12
+ */
13
+
14
+ import { existsSync } from 'node:fs'
15
+ import { readFile, readdir } from 'node:fs/promises'
16
+ import { resolve, join, relative } from 'node:path'
17
+ import yaml from 'js-yaml'
18
+
19
+ import { ensureAuth } from '../utils/auth.js'
20
+ import { findWorkspaceRoot, findSites, classifyPackage } from '../utils/workspace.js'
21
+ import { isNonInteractive, getCliPrefix } from '../utils/interactive.js'
22
+
23
+ const colors = {
24
+ reset: '\x1b[0m',
25
+ bright: '\x1b[1m',
26
+ dim: '\x1b[2m',
27
+ cyan: '\x1b[36m',
28
+ green: '\x1b[32m',
29
+ yellow: '\x1b[33m',
30
+ red: '\x1b[31m',
31
+ }
32
+
33
+ function success(message) {
34
+ console.log(`${colors.green}✓${colors.reset} ${message}`)
35
+ }
36
+
37
+ function error(message) {
38
+ console.error(`${colors.red}✗${colors.reset} ${message}`)
39
+ }
40
+
41
+ function info(message) {
42
+ console.log(`${colors.cyan}→${colors.reset} ${message}`)
43
+ }
44
+
45
+ /**
46
+ * Parse a named flag from args.
47
+ * @param {string[]} args
48
+ * @param {string} flag - e.g. '--name'
49
+ * @returns {string|null}
50
+ */
51
+ function parseFlag(args, flag) {
52
+ const idx = args.indexOf(flag)
53
+ if (idx === -1 || !args[idx + 1]) return null
54
+ return args[idx + 1]
55
+ }
56
+
57
+ // Build infrastructure files to exclude from templates
58
+ const EXCLUDED_FILES = new Set([
59
+ 'package.json', 'package-lock.json', 'pnpm-lock.yaml',
60
+ 'vite.config.js', 'vite.config.ts',
61
+ 'index.html', 'main.js', 'main.ts',
62
+ ])
63
+ const EXCLUDED_DIRS = new Set([
64
+ 'node_modules', 'dist', '.git', '.vite',
65
+ ])
66
+
67
+ /**
68
+ * Resolve the site directory to publish as a template.
69
+ *
70
+ * Priority:
71
+ * 1. In a site directory → use it
72
+ * 2. site.yml in cwd (non-package site, e.g. cloud site) → use it
73
+ * 3. At workspace root, one site → use it
74
+ * 4. At workspace root, multiple → error
75
+ * 5. No site → educational error
76
+ */
77
+ async function resolveSiteDir() {
78
+ const cwd = process.cwd()
79
+
80
+ // Check if current directory is a site package
81
+ const type = await classifyPackage(cwd)
82
+ if (type === 'site') return cwd
83
+
84
+ // Check for site.yml directly (non-package site, e.g. cloud site)
85
+ if (existsSync(join(cwd, 'site.yml'))) return cwd
86
+
87
+ // Check workspace
88
+ const workspaceRoot = findWorkspaceRoot(cwd)
89
+ if (workspaceRoot) {
90
+ const sites = await findSites(workspaceRoot)
91
+ if (sites.length === 1) return resolve(workspaceRoot, sites[0])
92
+ if (sites.length > 1) {
93
+ error('Multiple sites found. Run this command from inside the site directory.')
94
+ process.exit(1)
95
+ }
96
+ }
97
+
98
+ error('No site found. Run this command from a site directory (must contain site.yml).')
99
+ process.exit(1)
100
+ }
101
+
102
+ /**
103
+ * Recursively read all files in a directory, returning { relativePath: base64Content }.
104
+ * Skips build infrastructure files and directories.
105
+ */
106
+ async function readAllFiles(dir, baseDir = dir) {
107
+ const files = {}
108
+ const entries = await readdir(dir, { withFileTypes: true })
109
+
110
+ for (const entry of entries) {
111
+ const fullPath = join(dir, entry.name)
112
+ if (entry.isDirectory()) {
113
+ if (EXCLUDED_DIRS.has(entry.name)) continue
114
+ Object.assign(files, await readAllFiles(fullPath, baseDir))
115
+ } else if (entry.isFile()) {
116
+ if (EXCLUDED_FILES.has(entry.name)) continue
117
+ const relPath = relative(baseDir, fullPath)
118
+ const content = await readFile(fullPath)
119
+ files[relPath] = content.toString('base64')
120
+ }
121
+ }
122
+
123
+ return files
124
+ }
125
+
126
+ /**
127
+ * Main template command dispatcher.
128
+ */
129
+ export async function template(args = []) {
130
+ const subcommand = args[0]
131
+
132
+ if (subcommand === 'publish') {
133
+ await templatePublish(args.slice(1))
134
+ return
135
+ }
136
+
137
+ const prefix = getCliPrefix()
138
+ error(subcommand ? `Unknown subcommand: template ${subcommand}` : 'Missing subcommand')
139
+ console.log('')
140
+ console.log(`${colors.bright}Usage:${colors.reset}`)
141
+ console.log(` ${prefix} template publish Publish a site as a cloud template`)
142
+ console.log('')
143
+ console.log(`${colors.bright}Options:${colors.reset}`)
144
+ console.log(` --name <name> Template registry name (overrides site.yml \`template:\` field)`)
145
+ console.log(` --title <title> Display title (overrides site.yml \`name:\` field)`)
146
+ console.log(` --description <txt> Description`)
147
+ console.log(` --registry <url> Registry URL (default: http://localhost:4001)`)
148
+ process.exit(1)
149
+ }
150
+
151
+ /**
152
+ * Publish a site directory as a cloud template.
153
+ */
154
+ async function templatePublish(args) {
155
+ const registryUrl = parseFlag(args, '--registry')
156
+ const nameOverride = parseFlag(args, '--name')
157
+ const titleOverride = parseFlag(args, '--title')
158
+ const descOverride = parseFlag(args, '--description')
159
+
160
+ // 1. Resolve site directory
161
+ const siteDir = await resolveSiteDir()
162
+
163
+ // 2. Read and parse site.yml
164
+ const siteYmlPath = join(siteDir, 'site.yml')
165
+ if (!existsSync(siteYmlPath)) {
166
+ error('No site.yml found in this directory')
167
+ process.exit(1)
168
+ }
169
+
170
+ const siteYmlContent = await readFile(siteYmlPath, 'utf8')
171
+ const siteConfig = yaml.load(siteYmlContent) || {}
172
+
173
+ // 3. Determine template name: --name flag > site.yml `template:` field > directory name
174
+ const templateName = nameOverride || siteConfig.template || siteDir.split('/').pop()
175
+
176
+ if (!siteConfig.foundation) {
177
+ error('site.yml must declare a foundation')
178
+ process.exit(1)
179
+ }
180
+
181
+ // 4. Collect all content files (skip build infrastructure)
182
+ info(`Collecting files from ${colors.dim}${siteDir}${colors.reset}`)
183
+ const files = await readAllFiles(siteDir)
184
+ const fileCount = Object.keys(files).length
185
+
186
+ if (fileCount === 0) {
187
+ error('No files found to publish')
188
+ process.exit(1)
189
+ }
190
+
191
+ console.log(` ${colors.dim}${fileCount} files${colors.reset}`)
192
+
193
+ // 5. Authenticate
194
+ const token = await ensureAuth({ command: 'Publishing template' })
195
+
196
+ // 6. Build payload
197
+ const url = registryUrl || process.env.UNIWEB_REGISTRY_URL || 'http://localhost:4001'
198
+
199
+ const payload = { name: templateName, files }
200
+ if (titleOverride || siteConfig.name) {
201
+ payload.title = titleOverride || siteConfig.name
202
+ }
203
+ if (descOverride) payload.description = descOverride
204
+
205
+ // 7. Publish via API
206
+ info(`Publishing template ${colors.bright}${templateName}${colors.reset} to ${url}`)
207
+
208
+ const headers = { 'Content-Type': 'application/json' }
209
+ if (token) headers['Authorization'] = `Bearer ${token}`
210
+
211
+ const res = await fetch(`${url}/api/templates`, {
212
+ method: 'POST',
213
+ headers,
214
+ body: JSON.stringify(payload),
215
+ })
216
+
217
+ const body = await res.json()
218
+
219
+ if (!res.ok) {
220
+ error(body.error || `Server error (${res.status})`)
221
+ process.exit(1)
222
+ }
223
+
224
+ console.log('')
225
+ success(`Published template ${colors.bright}${templateName}${colors.reset}`)
226
+ console.log(` ${colors.dim}Foundation: ${body.foundation}${colors.reset}`)
227
+ console.log(` ${colors.dim}Files: ${body.filesCount}${colors.reset}`)
228
+ }
229
+
230
+ export default template
package/src/index.js CHANGED
@@ -5,24 +5,37 @@
5
5
  *
6
6
  * Scaffolds new Uniweb sites and foundations, builds projects, and generates docs.
7
7
  *
8
+ * Install globally:
9
+ * npm i -g uniweb
10
+ *
8
11
  * Usage:
9
- * npx uniweb create [project-name]
10
- * npx uniweb create --template marketing
11
- * npx uniweb add foundation [name]
12
- * npx uniweb build
13
- * npx uniweb docs # Generate COMPONENTS.md from schema
12
+ * uniweb create [project-name]
13
+ * uniweb create --template marketing
14
+ * uniweb add foundation [name]
15
+ * uniweb build
16
+ * uniweb docs
17
+ *
18
+ * Global install delegation:
19
+ * When installed globally, project-bound commands (build, docs, etc.) are
20
+ * delegated to the project-local CLI if one exists in node_modules. This
21
+ * ensures version alignment between the CLI and @uniweb/build.
14
22
  */
15
23
 
16
- import { existsSync } from 'node:fs'
17
- import { execSync } from 'node:child_process'
18
- import { resolve, join, relative } from 'node:path'
24
+ import { existsSync, readFileSync } from 'node:fs'
25
+ import { execSync, spawn as spawnChild } from 'node:child_process'
26
+ import { resolve, join, relative, dirname } from 'node:path'
27
+ import { fileURLToPath } from 'node:url'
19
28
  import prompts from 'prompts'
20
- import { build } from './commands/build.js'
21
- import { docs } from './commands/docs.js'
22
29
  import { doctor } from './commands/doctor.js'
23
30
  import { i18n } from './commands/i18n.js'
24
31
  import { inspect } from './commands/inspect.js'
25
32
  import { add } from './commands/add.js'
33
+ import { login } from './commands/login.js'
34
+ import { publish } from './commands/publish.js'
35
+ import { deploy } from './commands/deploy.js'
36
+ import { invite } from './commands/invite.js'
37
+ import { handoff } from './commands/handoff.js'
38
+ import { template } from './commands/template.js'
26
39
  import {
27
40
  resolveTemplate,
28
41
  parseTemplateId,
@@ -31,6 +44,7 @@ import { validateTemplate } from './templates/validator.js'
31
44
  import { scaffoldWorkspace, scaffoldFoundation, scaffoldSite, applyContent, applyStarter, mergeTemplateDependencies } from './utils/scaffold.js'
32
45
  import { detectPackageManager, filterCmd, installCmd, runCmd } from './utils/pm.js'
33
46
  import { isNonInteractive, getCliPrefix, stripNonInteractiveFlag, formatOptions } from './utils/interactive.js'
47
+ import { findWorkspaceRoot } from './utils/workspace.js'
34
48
 
35
49
  // Colors for terminal output
36
50
  const colors = {
@@ -73,6 +87,108 @@ function title(message) {
73
87
  console.log(`\n${colors.cyan}${colors.bright}${message}${colors.reset}\n`)
74
88
  }
75
89
 
90
+ // CLI version (read once, lazily)
91
+ const __dirname = dirname(fileURLToPath(import.meta.url))
92
+ let _cliVersion = null
93
+ function getCliVersion() {
94
+ if (!_cliVersion) {
95
+ const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'))
96
+ _cliVersion = pkg.version
97
+ }
98
+ return _cliVersion
99
+ }
100
+
101
+ /**
102
+ * Commands that always run from the global CLI (no project context needed)
103
+ */
104
+ const STANDALONE_COMMANDS = new Set([
105
+ 'create', '--help', '-h', '--version', '-v', 'login',
106
+ ])
107
+
108
+ /**
109
+ * Check if this CLI is running from a global install.
110
+ * When installed globally, process.argv[1] points outside any node_modules.
111
+ * When run via npx or as a local dependency, it's inside node_modules.
112
+ */
113
+ function isGlobalInstall() {
114
+ const scriptPath = process.argv[1]
115
+ if (!scriptPath) return false
116
+ // Normalize path separators for Windows compatibility
117
+ return !scriptPath.split('/').includes('node_modules') &&
118
+ !scriptPath.split('\\').includes('node_modules')
119
+ }
120
+
121
+ /**
122
+ * Find the project-local CLI entry point, if one exists.
123
+ * Walks up from cwd looking for node_modules/uniweb/src/index.js.
124
+ */
125
+ function findLocalCli() {
126
+ let dir = process.cwd()
127
+ while (true) {
128
+ const localCli = join(dir, 'node_modules', 'uniweb', 'src', 'index.js')
129
+ if (existsSync(localCli)) return localCli
130
+ const parent = dirname(dir)
131
+ if (parent === dir) break
132
+ dir = parent
133
+ }
134
+ return null
135
+ }
136
+
137
+ /**
138
+ * Delegate execution to the project-local CLI.
139
+ * Spawns the local CLI with the same arguments and inherits stdio.
140
+ * Warns if the local version differs from the global version.
141
+ */
142
+ function delegateToLocal(localCliPath) {
143
+ // Check for version mismatch between global and local CLI
144
+ try {
145
+ const localPkgPath = join(dirname(localCliPath), '..', 'package.json')
146
+ const localPkg = JSON.parse(readFileSync(localPkgPath, 'utf8'))
147
+ const globalVersion = getCliVersion()
148
+ if (localPkg.version && localPkg.version !== globalVersion) {
149
+ const yellow = '\x1b[33m'
150
+ const dim = '\x1b[2m'
151
+ const reset = '\x1b[0m'
152
+ console.error(`${yellow}Note:${reset} Global CLI is ${dim}${globalVersion}${reset}, project has ${dim}${localPkg.version}${reset} ${dim}(using project version)${reset}`)
153
+ }
154
+ } catch { /* ignore — version check is best-effort */ }
155
+
156
+ return new Promise((resolve, reject) => {
157
+ const child = spawnChild(
158
+ process.execPath,
159
+ [localCliPath, ...process.argv.slice(2)],
160
+ { stdio: 'inherit' }
161
+ )
162
+ child.on('close', (code) => process.exit(code ?? 0))
163
+ child.on('error', reject)
164
+ })
165
+ }
166
+
167
+ /**
168
+ * Import a command module that may depend on @uniweb/build.
169
+ * Provides a helpful error when the dependency can't be resolved
170
+ * (e.g., running a project-bound command from a global install
171
+ * outside a project directory).
172
+ */
173
+ async function importProjectCommand(modulePath) {
174
+ try {
175
+ return await import(modulePath)
176
+ } catch (err) {
177
+ if (err.code === 'ERR_MODULE_NOT_FOUND' && err.message?.includes('@uniweb/')) {
178
+ error('This command must be run from inside a Uniweb project.')
179
+ log('')
180
+ log(`Make sure you're in a project directory with dependencies installed:`)
181
+ log(` ${colors.cyan}cd your-project${colors.reset}`)
182
+ log(` ${colors.cyan}npm install${colors.reset}`)
183
+ log('')
184
+ log(`Or create a new project:`)
185
+ log(` ${colors.cyan}uniweb create my-project${colors.reset}`)
186
+ process.exit(1)
187
+ }
188
+ throw err
189
+ }
190
+ }
191
+
76
192
  /**
77
193
  * Create a project using the new package template flow (default)
78
194
  */
@@ -299,21 +415,55 @@ async function main() {
299
415
  const command = args[0]
300
416
  const pm = detectPackageManager()
301
417
 
418
+ // Handle --version / -v
419
+ if (command === '--version' || command === '-v') {
420
+ console.log(`uniweb ${getCliVersion()}`)
421
+ return
422
+ }
423
+
424
+ // Global install launcher: delegate project-bound commands to local CLI
425
+ const global = isGlobalInstall()
426
+ if (global && command && !STANDALONE_COMMANDS.has(command)) {
427
+ const localCli = findLocalCli()
428
+ if (localCli) {
429
+ await delegateToLocal(localCli)
430
+ return
431
+ }
432
+ // No local CLI found — fall through and try to run the command directly.
433
+ // Commands that need @uniweb/build will get a helpful error via importProjectCommand().
434
+ }
435
+
436
+ // Start non-blocking update check for global installs
437
+ let showUpdateNotification = () => {}
438
+ if (global) {
439
+ try {
440
+ const { startUpdateCheck } = await import('./utils/update-check.js')
441
+ showUpdateNotification = startUpdateCheck(getCliVersion())
442
+ } catch {
443
+ // Update check is optional — don't fail if the module is missing
444
+ }
445
+ }
446
+
302
447
  // Show help
303
448
  if (!command || command === '--help' || command === '-h') {
304
449
  showHelp()
450
+ await showUpdateNotification()
305
451
  return
306
452
  }
307
453
 
308
- // Handle build command
454
+ // Handle build command (dynamic import — depends on @uniweb/build)
309
455
  if (command === 'build') {
456
+ const { build } = await importProjectCommand('./commands/build.js')
310
457
  await build(args.slice(1))
458
+ await showUpdateNotification()
311
459
  return
312
460
  }
313
461
 
314
- // Handle docs command
462
+ // Handle docs command (dynamic import — depends on @uniweb/build)
315
463
  if (command === 'docs') {
464
+ const { docs } = await importProjectCommand('./commands/docs.js')
316
465
  await docs(args.slice(1))
466
+ await showUpdateNotification()
317
467
  return
318
468
  }
319
469
 
@@ -341,6 +491,42 @@ async function main() {
341
491
  return
342
492
  }
343
493
 
494
+ // Handle publish command
495
+ if (command === 'publish') {
496
+ await publish(args.slice(1))
497
+ return
498
+ }
499
+
500
+ // Handle deploy command
501
+ if (command === 'deploy') {
502
+ await deploy(args.slice(1))
503
+ return
504
+ }
505
+
506
+ // Handle login command
507
+ if (command === 'login') {
508
+ await login(args.slice(1))
509
+ return
510
+ }
511
+
512
+ // Handle invite command
513
+ if (command === 'invite') {
514
+ await invite(args.slice(1))
515
+ return
516
+ }
517
+
518
+ // Handle handoff command
519
+ if (command === 'handoff') {
520
+ await handoff(args.slice(1))
521
+ return
522
+ }
523
+
524
+ // Handle template command
525
+ if (command === 'template') {
526
+ await template(args.slice(1))
527
+ return
528
+ }
529
+
344
530
  // Handle create command
345
531
  if (command !== 'create') {
346
532
  error(`Unknown command: ${command}`)
@@ -350,6 +536,17 @@ async function main() {
350
536
 
351
537
  title('Uniweb Project Generator')
352
538
 
539
+ // Guard: prevent creating nested workspaces
540
+ const existingRoot = findWorkspaceRoot(process.cwd())
541
+ if (existingRoot) {
542
+ error(`Already inside a Uniweb workspace: ${existingRoot}`)
543
+ log(`\nTo add packages to this workspace, use:`)
544
+ log(` ${colors.cyan}uniweb add foundation [name]${colors.reset}`)
545
+ log(` ${colors.cyan}uniweb add site [name]${colors.reset}`)
546
+ log(` ${colors.cyan}uniweb add foundation --from <template>${colors.reset}\n`)
547
+ process.exit(1)
548
+ }
549
+
353
550
  // Parse arguments
354
551
  let projectName = args[1]
355
552
  let templateType = null // null = use new package template flow
@@ -562,23 +759,31 @@ async function main() {
562
759
  log(` ${colors.cyan}${runCmd(pm, 'dev')}${colors.reset}`)
563
760
  }
564
761
  log('')
762
+
763
+ await showUpdateNotification()
565
764
  }
566
765
 
567
766
  function showHelp() {
568
767
  log(`
569
- ${colors.cyan}${colors.bright}Uniweb CLI${colors.reset}
768
+ ${colors.cyan}${colors.bright}Uniweb CLI${colors.reset} ${colors.dim}v${getCliVersion()}${colors.reset}
570
769
 
571
770
  ${colors.bright}Usage:${colors.reset}
572
- npx uniweb <command> [options]
771
+ uniweb <command> [options]
573
772
 
574
773
  ${colors.bright}Commands:${colors.reset}
575
774
  create [name] Create a new project
576
775
  add <type> [name] Add a foundation, site, or extension to a project
577
776
  build Build the current project
777
+ deploy Deploy a site to Uniweb hosting
778
+ publish Publish a foundation to the Uniweb Registry
779
+ invite <email> Create a foundation invite for a client
780
+ handoff <email> Hand off a site to a client
578
781
  inspect <path> Inspect parsed content shape of a markdown file or folder
579
782
  docs Generate component documentation
580
783
  doctor Diagnose project configuration issues
581
784
  i18n <cmd> Internationalization (extract, sync, status)
785
+ template publish Publish a site as a cloud template
786
+ login Log in to your Uniweb account
582
787
 
583
788
  ${colors.bright}Create Options:${colors.reset}
584
789
  --template <type> Project template (default: starter)
@@ -594,9 +799,37 @@ ${colors.bright}Add Subcommands:${colors.reset}
594
799
  add section <name> Add a section type to a foundation (--foundation)
595
800
 
596
801
  ${colors.bright}Global Options:${colors.reset}
802
+ --version, -v Show version
597
803
  --non-interactive Fail with usage info instead of prompting
598
804
  Auto-detected when CI=true or no TTY (pipes, agents)
599
805
 
806
+ ${colors.bright}Publish Options:${colors.reset}
807
+ --local Publish to the local registry (.unicloud/) instead of Uniweb Registry
808
+ --edit-access <p> Set edit access policy: "open" or "restricted" (default: restricted)
809
+ --dry-run Show what would be published without uploading
810
+
811
+ ${colors.bright}Invite Options:${colors.reset}
812
+ --uses <n> Max sites per invite (default: 1)
813
+ --expires <days> Days until expiry (default: 30)
814
+ --version <n> Major version to license (default: current)
815
+ --list List invites for your foundation
816
+ --revoke <id> Revoke an invite
817
+ --resend <id> Resend an invite
818
+
819
+ ${colors.bright}Handoff Options:${colors.reset}
820
+ --site <id> Site identifier (default: auto-generated)
821
+ --web Show web-based handoff instructions instead
822
+
823
+ ${colors.bright}Template Options:${colors.reset}
824
+ --name <name> Template registry name (overrides site.yml template: field)
825
+ --title <title> Display title (overrides site.yml name: field)
826
+ --description <t> Description
827
+ --registry <url> Registry URL (default: http://localhost:4001)
828
+
829
+ ${colors.bright}Deploy Options:${colors.reset}
830
+ --prod Deploy to production (default: preview URL)
831
+ --dry-run Show what would be deployed without uploading
832
+
600
833
  ${colors.bright}Build Options:${colors.reset}
601
834
  --target <type> Build target (foundation, site) - auto-detected if not specified
602
835
  --prerender Force pre-rendering (overrides site.yml)
@@ -633,22 +866,26 @@ ${colors.bright}Template Types:${colors.reset}
633
866
  https://github.com/user/repo GitHub URL
634
867
 
635
868
  ${colors.bright}Examples:${colors.reset}
636
- npx uniweb create my-project # Foundation + site + starter content
637
- npx uniweb create my-project --template none # Foundation + site, no content
638
- npx uniweb create my-project --blank # Empty workspace
639
- npx uniweb create my-project --template marketing # Official template
640
- npx uniweb create my-project --template ./my-template # Local template
869
+ uniweb create my-project # Foundation + site + starter content
870
+ uniweb create my-project --template none # Foundation + site, no content
871
+ uniweb create my-project --blank # Empty workspace
872
+ uniweb create my-project --template marketing # Official template
873
+ uniweb create my-project --template ./my-template # Local template
641
874
 
642
875
  cd my-project
643
- npx uniweb add project docs # Add docs/foundation/ + docs/site/
644
- npx uniweb add project docs --from academic # Co-located pair + academic content
645
- npx uniweb add foundation # Add foundation at root
646
- npx uniweb add site blog --foundation marketing # Add site wired to marketing
647
- npx uniweb add extension effects --site site # Add extensions/effects/
648
-
649
- npx uniweb build
650
- npx uniweb build --target foundation
651
- cd foundation && npx uniweb docs # Generate COMPONENTS.md
876
+ uniweb add project docs # Add docs/foundation/ + docs/site/
877
+ uniweb add project docs --from academic # Co-located pair + academic content
878
+ uniweb add foundation # Add foundation at root
879
+ uniweb add site blog --foundation marketing # Add site wired to marketing
880
+ uniweb add extension effects --site site # Add extensions/effects/
881
+
882
+ uniweb build
883
+ uniweb build --target foundation
884
+ cd foundation && uniweb docs # Generate COMPONENTS.md
885
+
886
+ ${colors.bright}Install:${colors.reset}
887
+ npm i -g uniweb Global install (recommended)
888
+ npx uniweb <command> Run without installing
652
889
  `)
653
890
  }
654
891