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.
- package/README.md +42 -12
- package/package.json +19 -6
- package/partials/agents.md +16 -13
- package/partials/components-docs.hbs +2 -2
- package/partials/config-reference.hbs +1 -1
- package/src/commands/add.js +21 -10
- package/src/commands/build.js +29 -41
- package/src/commands/deploy.js +272 -0
- package/src/commands/doctor.js +2 -2
- package/src/commands/handoff.js +254 -0
- package/src/commands/invite.js +326 -0
- package/src/commands/login.js +87 -0
- package/src/commands/publish.js +300 -0
- package/src/commands/template.js +230 -0
- package/src/index.js +265 -28
- package/src/utils/auth.js +150 -0
- package/src/utils/registry.js +361 -0
- package/src/utils/update-check.js +105 -0
- package/src/versions.js +1 -1
|
@@ -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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
cd foundation &&
|
|
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
|
|