uniweb 0.12.10 → 0.12.11
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 +3 -3
- package/partials/agents.md +22 -5
- package/src/commands/add.js +2 -2
- package/src/commands/deploy.js +21 -3
- package/src/commands/dev.js +111 -0
- package/src/commands/handoff.js +1 -1
- package/src/commands/invite.js +1 -1
- package/src/commands/publish.js +1 -1
- package/src/commands/template.js +1 -1
- package/src/framework-index.json +2 -2
- package/src/index.js +362 -12
- package/src/utils/auth.js +29 -1
- package/src/utils/config.js +15 -6
- package/src/utils/update-check.js +34 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.11",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
44
|
"@uniweb/runtime": "0.8.13",
|
|
45
|
-
"@uniweb/
|
|
46
|
-
"@uniweb/
|
|
45
|
+
"@uniweb/core": "0.7.11",
|
|
46
|
+
"@uniweb/kit": "0.9.11"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"@uniweb/build": "0.14.2",
|
package/partials/agents.md
CHANGED
|
@@ -136,11 +136,28 @@ Creates `sections/Hero/index.jsx` and `meta.js` with a minimal CCA-proper starte
|
|
|
136
136
|
## Commands
|
|
137
137
|
|
|
138
138
|
```bash
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
pnpm
|
|
142
|
-
pnpm
|
|
143
|
-
|
|
139
|
+
# Local development
|
|
140
|
+
uniweb dev # Start dev server (picks the site for you)
|
|
141
|
+
pnpm install # Install dependencies
|
|
142
|
+
pnpm build # Build for production
|
|
143
|
+
pnpm preview # Preview production build (SSG + SPA)
|
|
144
|
+
|
|
145
|
+
# Ship the site (uniweb verbs)
|
|
146
|
+
uniweb deploy # Deploy to Uniweb hosting (default; needs `uniweb login` first)
|
|
147
|
+
uniweb deploy --host=<adapter> # Deploy to a static host: cloudflare-pages, netlify,
|
|
148
|
+
# vercel, github-pages, s3-cloudfront, generic-static
|
|
149
|
+
uniweb deploy --dry-run # Resolve foundation/runtime + print summary; no writes
|
|
150
|
+
uniweb export # Build dist/ for any static host (no Uniweb account)
|
|
151
|
+
uniweb publish # Publish a foundation as a catalog product (deliberate;
|
|
152
|
+
# for site-bound foundations use `uniweb deploy` instead)
|
|
153
|
+
uniweb doctor # Diagnose project configuration issues (--fix to auto-repair)
|
|
154
|
+
|
|
155
|
+
# Help
|
|
156
|
+
uniweb --help # Top-level help
|
|
157
|
+
uniweb <command> --help # Per-command help (no side effects)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`uniweb deploy` auto-publishes a workspace-local foundation as part of the deploy under a site-scoped slot — no separate `uniweb publish` step needed for site-bound foundations.
|
|
144
161
|
|
|
145
162
|
---
|
|
146
163
|
|
package/src/commands/add.js
CHANGED
|
@@ -389,7 +389,7 @@ async function addSite(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
389
389
|
success(`Created site ${colors.bright}${siteName}${colors.reset} at ${relativePath}/`)
|
|
390
390
|
}
|
|
391
391
|
log('')
|
|
392
|
-
log(`Next: ${colors.cyan}${installCmd(pm)} && ${
|
|
392
|
+
log(`Next: ${colors.cyan}${installCmd(pm)} && uniweb dev ${siteName}${colors.reset}`)
|
|
393
393
|
if (!opts.from) {
|
|
394
394
|
log('')
|
|
395
395
|
log(`${colors.dim}To add your first page, create ${relativePath}/pages/home/page.yml and a .md file.${colors.reset}`)
|
|
@@ -631,7 +631,7 @@ async function addProject(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
631
631
|
log(` ${colors.dim}Foundation: ${name}/src/ (${foundationPkgName})${colors.reset}`)
|
|
632
632
|
log(` ${colors.dim}Site: ${name}/site/ (${sitePkgName})${colors.reset}`)
|
|
633
633
|
log('')
|
|
634
|
-
log(`Next: ${colors.cyan}${installCmd(pm)} && ${
|
|
634
|
+
log(`Next: ${colors.cyan}${installCmd(pm)} && uniweb dev ${sitePkgName}${colors.reset}`)
|
|
635
635
|
}
|
|
636
636
|
|
|
637
637
|
/**
|
package/src/commands/deploy.js
CHANGED
|
@@ -401,6 +401,21 @@ export async function deploy(args = []) {
|
|
|
401
401
|
// of the current source's git sha. This flag opts out.
|
|
402
402
|
const autoPublishFoundation = !args.includes('--no-auto-publish')
|
|
403
403
|
|
|
404
|
+
// --local: redirect platform URLs to the unicloud mock (localhost:4001)
|
|
405
|
+
// for internal end-to-end testing. Documented in the workspace root
|
|
406
|
+
// CLAUDE.md ("The --local Flag" section). NOT a public user-facing
|
|
407
|
+
// feature — a real user has no unicloud server running. The flag is
|
|
408
|
+
// intentionally absent from the global help to avoid leaking it into
|
|
409
|
+
// user docs; per-command help (uniweb deploy --help) lists it under
|
|
410
|
+
// an "Internal" caveat for the eval / test team.
|
|
411
|
+
//
|
|
412
|
+
// The override unconditionally pins both backend and worker to
|
|
413
|
+
// http://localhost:4001 (unicloud's default port) regardless of any
|
|
414
|
+
// env vars set in the calling shell. Auth is NOT skipped — the runbook
|
|
415
|
+
// expects mock-login.js to seed ~/.uniweb/auth.json with a JWT
|
|
416
|
+
// unicloud's verifyToken accepts.
|
|
417
|
+
const isLocal = args.includes('--local')
|
|
418
|
+
|
|
404
419
|
// Internal escape hatches — see framework/cli/docs/env-vars.md. These
|
|
405
420
|
// are not user-facing flags; they exist for the platform test team,
|
|
406
421
|
// CI scripts, and dev-loop unblockers. The bare `deploy` command should
|
|
@@ -415,8 +430,11 @@ export async function deploy(args = []) {
|
|
|
415
430
|
const treatDirtyAsStale = !parseBoolEnv('UNIWEB_ALLOW_DIRTY_FOUNDATION')
|
|
416
431
|
|
|
417
432
|
const siteDir = await resolveSiteDir(args)
|
|
418
|
-
const backendUrl = getBackendUrl()
|
|
419
|
-
const workerUrl = getRegistryUrl()
|
|
433
|
+
const backendUrl = isLocal ? 'http://localhost:4001' : getBackendUrl()
|
|
434
|
+
const workerUrl = isLocal ? 'http://localhost:4001' : getRegistryUrl()
|
|
435
|
+
if (isLocal) {
|
|
436
|
+
console.log(` \x1b[2m→ Local mock mode (unicloud at ${backendUrl}; see workspace root CLAUDE.md)\x1b[0m`)
|
|
437
|
+
}
|
|
420
438
|
|
|
421
439
|
// Read site.yml — declares the foundation (required) and optionally the
|
|
422
440
|
// site.id / site.handle from prior deploys.
|
|
@@ -599,7 +617,7 @@ export async function deploy(args = []) {
|
|
|
599
617
|
// doesn't fail the whole deploy.
|
|
600
618
|
const desiredFeatures = readFeaturesFromYaml(siteYml)
|
|
601
619
|
|
|
602
|
-
const cliToken = await ensureAuth({ command: 'Deploying' })
|
|
620
|
+
const cliToken = await ensureAuth({ command: 'Deploying', args })
|
|
603
621
|
|
|
604
622
|
// Always rebuild unless the user explicitly opts out with --skip-build.
|
|
605
623
|
// A stale dist/ from a previous build + edited content on disk would
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev Command
|
|
3
|
+
*
|
|
4
|
+
* Starts a dev server for a site in the current workspace. Wraps the
|
|
5
|
+
* project's `dev` script (set up by `uniweb create` to filter to the
|
|
6
|
+
* appropriate site package). Provides discoverability and consistency
|
|
7
|
+
* with `uniweb build` / `uniweb deploy` — users shouldn't have to know
|
|
8
|
+
* whether to type `pnpm dev` or `npm run dev` when the rest of the CLI
|
|
9
|
+
* is verb-shaped.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* uniweb dev Start dev server for the (single) site
|
|
13
|
+
* uniweb dev <site> Start dev server for a specific site
|
|
14
|
+
* uniweb dev --site <name> Same, with explicit flag form
|
|
15
|
+
*
|
|
16
|
+
* Resolution order for which site to launch:
|
|
17
|
+
* 1. --site <name> (if passed)
|
|
18
|
+
* 2. Positional <site> arg
|
|
19
|
+
* 3. The single site in the workspace (if exactly one)
|
|
20
|
+
* 4. The first site in the workspace, with a "multiple sites" notice
|
|
21
|
+
* pointing at --site for explicit selection
|
|
22
|
+
*
|
|
23
|
+
* Multi-site workspaces with no positional / flag will run the first
|
|
24
|
+
* site by default (mirrors the `pnpm dev` shortcut `uniweb create` writes).
|
|
25
|
+
* Use `--site` to pick a different one without editing the root scripts.
|
|
26
|
+
*
|
|
27
|
+
* Implementation: shells out to the package manager that invoked the CLI
|
|
28
|
+
* (detected via npm_config_user_agent), running the workspace-filtered
|
|
29
|
+
* dev command (`pnpm --filter <name> dev` or `npm -w <name> run dev`).
|
|
30
|
+
* No special handling of vite directly — the site package already owns
|
|
31
|
+
* its dev script, and shelling through pnpm/npm respects whatever the
|
|
32
|
+
* site has configured (Vite plugins, env vars, port overrides, etc.).
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { spawn } from 'node:child_process'
|
|
36
|
+
import { join } from 'node:path'
|
|
37
|
+
|
|
38
|
+
import { detectPackageManager, filterCmd } from '../utils/pm.js'
|
|
39
|
+
import { discoverSites, readWorkspaceConfig } from '../utils/config.js'
|
|
40
|
+
import { findWorkspaceRoot } from '../utils/workspace.js'
|
|
41
|
+
import { readFlagValue } from '../utils/args.js'
|
|
42
|
+
|
|
43
|
+
const RED = '\x1b[31m'
|
|
44
|
+
const YELLOW = '\x1b[33m'
|
|
45
|
+
const DIM = '\x1b[2m'
|
|
46
|
+
const CYAN = '\x1b[36m'
|
|
47
|
+
const RESET = '\x1b[0m'
|
|
48
|
+
|
|
49
|
+
export async function dev(args = []) {
|
|
50
|
+
const cwd = process.cwd()
|
|
51
|
+
const rootDir = findWorkspaceRoot(cwd) || cwd
|
|
52
|
+
|
|
53
|
+
// Verify we're in a Uniweb workspace (has pnpm-workspace.yaml or
|
|
54
|
+
// package.json::workspaces). discoverSites already handles both.
|
|
55
|
+
let workspaceConfig
|
|
56
|
+
try {
|
|
57
|
+
workspaceConfig = await readWorkspaceConfig(rootDir)
|
|
58
|
+
} catch {
|
|
59
|
+
workspaceConfig = { packages: [] }
|
|
60
|
+
}
|
|
61
|
+
if (workspaceConfig.packages.length === 0) {
|
|
62
|
+
console.error(`${RED}✗${RESET} Not in a Uniweb workspace (no pnpm-workspace.yaml or package.json::workspaces).`)
|
|
63
|
+
console.error(` Run \`uniweb create <name>\` to scaffold a project, or cd into an existing one.`)
|
|
64
|
+
process.exit(1)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const sites = await discoverSites(rootDir)
|
|
68
|
+
if (sites.length === 0) {
|
|
69
|
+
console.error(`${RED}✗${RESET} No sites found in this workspace.`)
|
|
70
|
+
console.error(` Add one with \`uniweb add site <name>\`.`)
|
|
71
|
+
process.exit(1)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Pick the site
|
|
75
|
+
const siteFlag = readFlagValue(args, '--site')
|
|
76
|
+
const positional = args.find(a => !a.startsWith('-'))
|
|
77
|
+
const requested = (typeof siteFlag === 'string' ? siteFlag : null) || positional || null
|
|
78
|
+
|
|
79
|
+
let site
|
|
80
|
+
if (requested) {
|
|
81
|
+
site = sites.find(s => s.name === requested) || sites.find(s => s.path === requested)
|
|
82
|
+
if (!site) {
|
|
83
|
+
console.error(`${RED}✗${RESET} Site "${requested}" not found.`)
|
|
84
|
+
console.error(` Available: ${sites.map(s => s.name).join(', ')}`)
|
|
85
|
+
process.exit(1)
|
|
86
|
+
}
|
|
87
|
+
} else if (sites.length === 1) {
|
|
88
|
+
site = sites[0]
|
|
89
|
+
} else {
|
|
90
|
+
site = sites[0]
|
|
91
|
+
console.error(`${YELLOW}⚠${RESET} Multiple sites found; using ${CYAN}${site.name}${RESET}.`)
|
|
92
|
+
console.error(` Pick a different one with \`uniweb dev --site <name>\`.`)
|
|
93
|
+
console.error(` Available: ${sites.map(s => s.name).join(', ')}`)
|
|
94
|
+
console.error('')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const pm = detectPackageManager()
|
|
98
|
+
const command = filterCmd(pm, site.name, 'dev')
|
|
99
|
+
const [bin, ...rest] = command.split(' ')
|
|
100
|
+
const sitePath = join(rootDir, site.path)
|
|
101
|
+
|
|
102
|
+
console.error(`${DIM}→ ${command}${RESET} ${DIM}(site: ${site.name}, dir: ${sitePath})${RESET}`)
|
|
103
|
+
console.error('')
|
|
104
|
+
|
|
105
|
+
const child = spawn(bin, rest, { cwd: rootDir, stdio: 'inherit' })
|
|
106
|
+
child.on('close', code => process.exit(code ?? 0))
|
|
107
|
+
child.on('error', err => {
|
|
108
|
+
console.error(`${RED}✗${RESET} Failed to start dev server: ${err.message}`)
|
|
109
|
+
process.exit(1)
|
|
110
|
+
})
|
|
111
|
+
}
|
package/src/commands/handoff.js
CHANGED
|
@@ -145,7 +145,7 @@ async function readSchema(foundationDir) {
|
|
|
145
145
|
* Create a RemoteRegistry instance with auth.
|
|
146
146
|
*/
|
|
147
147
|
async function createRegistry(args) {
|
|
148
|
-
const token = await ensureAuth({ command: 'Handing off' })
|
|
148
|
+
const token = await ensureAuth({ command: 'Handing off', args })
|
|
149
149
|
|
|
150
150
|
const registryUrl = parseFlag(args, '--registry')
|
|
151
151
|
const url = registryUrl || process.env.UNIWEB_REGISTRY_URL || 'http://localhost:4001'
|
package/src/commands/invite.js
CHANGED
|
@@ -146,7 +146,7 @@ async function readSchema(foundationDir) {
|
|
|
146
146
|
* Create a RemoteRegistry instance with auth.
|
|
147
147
|
*/
|
|
148
148
|
async function createRegistry(args) {
|
|
149
|
-
const token = await ensureAuth({ command: 'Creating invite' })
|
|
149
|
+
const token = await ensureAuth({ command: 'Creating invite', args })
|
|
150
150
|
|
|
151
151
|
const registryUrl = parseFlag(args, '--registry')
|
|
152
152
|
const url = registryUrl || process.env.UNIWEB_REGISTRY_URL || 'http://localhost:4001'
|
package/src/commands/publish.js
CHANGED
|
@@ -794,7 +794,7 @@ export async function publish(args = []) {
|
|
|
794
794
|
registry = createLocalRegistry(foundationDir)
|
|
795
795
|
} else {
|
|
796
796
|
// Remote publish — ensure authenticated (inline login if needed)
|
|
797
|
-
const token = await ensureAuth({ command: 'Publishing' })
|
|
797
|
+
const token = await ensureAuth({ command: 'Publishing', args })
|
|
798
798
|
|
|
799
799
|
const url = registryUrl || getRegistryUrl()
|
|
800
800
|
registry = new RemoteRegistry(url, token)
|
package/src/commands/template.js
CHANGED
|
@@ -191,7 +191,7 @@ async function templatePublish(args) {
|
|
|
191
191
|
console.log(` ${colors.dim}${fileCount} files${colors.reset}`)
|
|
192
192
|
|
|
193
193
|
// 5. Authenticate
|
|
194
|
-
const token = await ensureAuth({ command: 'Publishing template' })
|
|
194
|
+
const token = await ensureAuth({ command: 'Publishing template', args })
|
|
195
195
|
|
|
196
196
|
// 6. Build payload
|
|
197
197
|
const url = registryUrl || process.env.UNIWEB_REGISTRY_URL || 'http://localhost:4001'
|
package/src/framework-index.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-05-
|
|
3
|
+
"generatedAt": "2026-05-05T20:43:37.314Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/build": {
|
|
6
6
|
"version": "0.14.2",
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
"deps": []
|
|
93
93
|
},
|
|
94
94
|
"@uniweb/unipress": {
|
|
95
|
-
"version": "0.4.
|
|
95
|
+
"version": "0.4.7",
|
|
96
96
|
"path": "framework/unipress",
|
|
97
97
|
"deps": [
|
|
98
98
|
"@uniweb/build",
|
package/src/index.js
CHANGED
|
@@ -199,12 +199,16 @@ async function createFromPackageTemplates(projectDir, projectName, options = {})
|
|
|
199
199
|
|
|
200
200
|
onProgress?.('Setting up workspace...')
|
|
201
201
|
|
|
202
|
-
// 1. Scaffold workspace
|
|
202
|
+
// 1. Scaffold workspace.
|
|
203
|
+
// dev/build go through `uniweb` verbs so the scripts stay PM-agnostic
|
|
204
|
+
// (the verb resolves the right PM at runtime instead of locking the
|
|
205
|
+
// root scripts to whichever PM ran `npx uniweb create`). preview stays
|
|
206
|
+
// PM-filtered until a `uniweb preview` verb exists.
|
|
203
207
|
await scaffoldWorkspace(projectDir, {
|
|
204
208
|
projectName,
|
|
205
209
|
workspaceGlobs: ['site', 'src'],
|
|
206
210
|
scripts: {
|
|
207
|
-
dev:
|
|
211
|
+
dev: 'uniweb dev',
|
|
208
212
|
build: 'uniweb build',
|
|
209
213
|
preview: filterCmd(pm, 'site', 'preview'),
|
|
210
214
|
},
|
|
@@ -286,17 +290,19 @@ async function createFromContentTemplate(projectDir, projectName, metadata, temp
|
|
|
286
290
|
const scripts = {
|
|
287
291
|
build: 'uniweb build',
|
|
288
292
|
}
|
|
293
|
+
// dev goes through `uniweb` (PM-agnostic; see computeRootScripts).
|
|
294
|
+
// preview stays PM-filtered until a `uniweb preview` verb exists.
|
|
289
295
|
if (sites.length === 1) {
|
|
290
|
-
scripts.dev =
|
|
296
|
+
scripts.dev = 'uniweb dev'
|
|
291
297
|
scripts.preview = filterCmd(pm, sites[0].name, 'preview')
|
|
292
298
|
} else {
|
|
293
299
|
for (const s of sites) {
|
|
294
|
-
scripts[`dev:${s.name}`] =
|
|
300
|
+
scripts[`dev:${s.name}`] = `uniweb dev ${s.name}`
|
|
295
301
|
scripts[`preview:${s.name}`] = filterCmd(pm, s.name, 'preview')
|
|
296
302
|
}
|
|
297
303
|
// First site gets unqualified aliases
|
|
298
304
|
if (sites.length > 0) {
|
|
299
|
-
scripts.dev =
|
|
305
|
+
scripts.dev = 'uniweb dev'
|
|
300
306
|
scripts.preview = filterCmd(pm, sites[0].name, 'preview')
|
|
301
307
|
}
|
|
302
308
|
}
|
|
@@ -452,12 +458,26 @@ async function main() {
|
|
|
452
458
|
// Commands that need @uniweb/build will get a helpful error via importProjectCommand().
|
|
453
459
|
}
|
|
454
460
|
|
|
455
|
-
// Start non-blocking update check for global installs
|
|
461
|
+
// Start non-blocking update check for global installs.
|
|
462
|
+
//
|
|
463
|
+
// Two surfaces:
|
|
464
|
+
// - showUpdateNotification (soft, trailing): printed at command end for
|
|
465
|
+
// any verb. Doesn't interrupt the user's workflow.
|
|
466
|
+
// - eager (loud, leading): printed BEFORE staleness-sensitive verbs do
|
|
467
|
+
// their work. Today: only `create` (templates ship with the CLI, so
|
|
468
|
+
// a stale CLI scaffolds stale starter content; the user needs to know
|
|
469
|
+
// before files hit disk). Other verbs are insensitive — `deploy` etc.
|
|
470
|
+
// are project-bound (delegated to local node_modules), and the
|
|
471
|
+
// local-vs-global mismatch warning in delegateToLocal already covers
|
|
472
|
+
// that case.
|
|
456
473
|
let showUpdateNotification = () => {}
|
|
457
474
|
if (global) {
|
|
458
475
|
try {
|
|
459
|
-
const { startUpdateCheck } = await import('./utils/update-check.js')
|
|
476
|
+
const { startUpdateCheck, maybeEagerNotification } = await import('./utils/update-check.js')
|
|
460
477
|
showUpdateNotification = startUpdateCheck(getCliVersion())
|
|
478
|
+
if (command === 'create') {
|
|
479
|
+
maybeEagerNotification(getCliVersion())
|
|
480
|
+
}
|
|
461
481
|
} catch {
|
|
462
482
|
// Update check is optional — don't fail if the module is missing
|
|
463
483
|
}
|
|
@@ -470,6 +490,23 @@ async function main() {
|
|
|
470
490
|
return
|
|
471
491
|
}
|
|
472
492
|
|
|
493
|
+
// Per-command --help: short-circuit BEFORE the command's side effects run.
|
|
494
|
+
// Critical for `deploy --help` (used to open a browser to production for
|
|
495
|
+
// login because deploy.js doesn't parse --help and ensureAuth ran first).
|
|
496
|
+
// Falls back to the global help when a command has no dedicated block.
|
|
497
|
+
if (args.slice(1).some(a => a === '--help' || a === '-h')) {
|
|
498
|
+
const printed = printCommandHelp(command)
|
|
499
|
+
if (printed) {
|
|
500
|
+
await showUpdateNotification()
|
|
501
|
+
return
|
|
502
|
+
}
|
|
503
|
+
// No dedicated block — show global help as a useful fallback rather
|
|
504
|
+
// than executing the command (which often has side effects).
|
|
505
|
+
showHelp()
|
|
506
|
+
await showUpdateNotification()
|
|
507
|
+
return
|
|
508
|
+
}
|
|
509
|
+
|
|
473
510
|
// Handle build command (dynamic import — depends on @uniweb/build)
|
|
474
511
|
if (command === 'build') {
|
|
475
512
|
const { build } = await importProjectCommand('./commands/build.js')
|
|
@@ -478,6 +515,16 @@ async function main() {
|
|
|
478
515
|
return
|
|
479
516
|
}
|
|
480
517
|
|
|
518
|
+
// Handle dev command — thin wrapper that shells to the package manager's
|
|
519
|
+
// workspace-filtered `dev` script (mirrors what `uniweb create` writes
|
|
520
|
+
// into the root package.json::scripts.dev). Lazy import keeps startup
|
|
521
|
+
// fast when the user is not running dev.
|
|
522
|
+
if (command === 'dev') {
|
|
523
|
+
const { dev } = await import('./commands/dev.js')
|
|
524
|
+
await dev(args.slice(1))
|
|
525
|
+
return
|
|
526
|
+
}
|
|
527
|
+
|
|
481
528
|
// Handle docs command (dynamic import — depends on @uniweb/build)
|
|
482
529
|
if (command === 'docs') {
|
|
483
530
|
const { docs } = await importProjectCommand('./commands/docs.js')
|
|
@@ -807,18 +854,307 @@ async function main() {
|
|
|
807
854
|
log(` ${colors.cyan}cd ${projectName}${colors.reset}`)
|
|
808
855
|
log(` ${colors.cyan}${prefix} add project${colors.reset}`)
|
|
809
856
|
log(` ${colors.cyan}${installCmd(pm)}${colors.reset}`)
|
|
810
|
-
log(` ${colors.cyan}${
|
|
857
|
+
log(` ${colors.cyan}${prefix} dev${colors.reset} ${colors.dim}# Start dev server${colors.reset}`)
|
|
811
858
|
} else {
|
|
812
859
|
log(`Next steps:\n`)
|
|
813
860
|
log(` ${colors.cyan}cd ${projectName}${colors.reset}`)
|
|
814
861
|
log(` ${colors.cyan}${installCmd(pm)}${colors.reset}`)
|
|
815
|
-
log(` ${colors.cyan}${
|
|
862
|
+
log(` ${colors.cyan}${prefix} dev${colors.reset} ${colors.dim}# Start dev server${colors.reset}`)
|
|
816
863
|
}
|
|
817
864
|
log('')
|
|
865
|
+
log(`When ready to ship:\n`)
|
|
866
|
+
log(` ${colors.cyan}${prefix} deploy${colors.reset} ${colors.dim}# Uniweb hosting (default; uniweb login first)${colors.reset}`)
|
|
867
|
+
log(` ${colors.cyan}${prefix} deploy --host=<adapter>${colors.reset} ${colors.dim}# cloudflare-pages, netlify, vercel, github-pages, s3-cloudfront${colors.reset}`)
|
|
868
|
+
log(` ${colors.cyan}${prefix} export${colors.reset} ${colors.dim}# Build dist/ for any static host (no Uniweb account)${colors.reset}`)
|
|
869
|
+
log('')
|
|
870
|
+
log(` ${colors.dim}See ${colors.reset}${colors.cyan}${prefix} <command> --help${colors.reset}${colors.dim} for command-specific options.${colors.reset}`)
|
|
871
|
+
log('')
|
|
818
872
|
|
|
819
873
|
await showUpdateNotification()
|
|
820
874
|
}
|
|
821
875
|
|
|
876
|
+
/**
|
|
877
|
+
* Print help for a specific command. Returns true if a dedicated help
|
|
878
|
+
* block exists for the command, false to signal "fall back to global
|
|
879
|
+
* help."
|
|
880
|
+
*
|
|
881
|
+
* Help text intentionally lives next to the dispatcher rather than in
|
|
882
|
+
* the per-command files because most help-seekers haven't run that
|
|
883
|
+
* command yet — keeping it here means `uniweb foo --help` prints
|
|
884
|
+
* without loading @uniweb/build or any project context.
|
|
885
|
+
*/
|
|
886
|
+
function printCommandHelp(command) {
|
|
887
|
+
const blocks = {
|
|
888
|
+
deploy: `
|
|
889
|
+
${colors.cyan}${colors.bright}uniweb deploy${colors.reset} ${colors.dim}— Deploy a site${colors.reset}
|
|
890
|
+
|
|
891
|
+
${colors.bright}Usage:${colors.reset}
|
|
892
|
+
uniweb deploy [options]
|
|
893
|
+
|
|
894
|
+
The host is determined by the resolved deploy.yml target. Defaults to
|
|
895
|
+
${colors.cyan}uniweb${colors.reset} hosting (link-mode, edge JIT prerender) when no deploy.yml exists.
|
|
896
|
+
|
|
897
|
+
${colors.bright}Hosts:${colors.reset}
|
|
898
|
+
uniweb Uniweb hosting (default; requires \`uniweb login\`)
|
|
899
|
+
cloudflare-pages Cloudflare Pages (build artifact + adapter postBuild)
|
|
900
|
+
netlify Netlify (alias of cloudflare-pages adapter)
|
|
901
|
+
vercel Vercel (build-only — deploy via \`npx vercel\`)
|
|
902
|
+
github-pages GitHub Pages (build-only — push dist/ to gh-pages)
|
|
903
|
+
s3-cloudfront AWS S3 + CloudFront (uploads + invalidates via CLI)
|
|
904
|
+
generic-static Plain static-host build, no host-specific helpers
|
|
905
|
+
|
|
906
|
+
${colors.bright}Options:${colors.reset}
|
|
907
|
+
--target <name> Pick a target from deploy.yml (default: deploy.yml's \`default:\`)
|
|
908
|
+
--host <name> Override the resolved target's host (does not persist)
|
|
909
|
+
--host No value → interactive picker (TTY only)
|
|
910
|
+
--dry-run Resolve site.yml + foundation/runtime; print summary; no writes
|
|
911
|
+
--no-auto-publish Don't auto-publish workspace-local foundation as part of deploy
|
|
912
|
+
--no-save Skip the auto-save of lastDeploy in deploy.yml
|
|
913
|
+
--local Internal: target the unicloud mock (see workspace root CLAUDE.md)
|
|
914
|
+
--non-interactive Fail with usage info instead of prompting
|
|
915
|
+
|
|
916
|
+
${colors.bright}Auth:${colors.reset}
|
|
917
|
+
\`host: uniweb\` requires authentication. Run \`uniweb login\` first, set
|
|
918
|
+
\`UNIWEB_TOKEN=<bearer>\` env var, or use a static-host adapter that
|
|
919
|
+
doesn't need a Uniweb account. CI / agents / piped stdin auto-detect
|
|
920
|
+
non-interactive mode and bail with an actionable error instead of
|
|
921
|
+
hanging on a browser callback.
|
|
922
|
+
|
|
923
|
+
${colors.bright}Examples:${colors.reset}
|
|
924
|
+
uniweb deploy # Default (host=uniweb)
|
|
925
|
+
uniweb deploy --dry-run # Print summary, no writes
|
|
926
|
+
uniweb deploy --host=cloudflare-pages # One-off override
|
|
927
|
+
uniweb deploy --target=preview # Pick named target from deploy.yml
|
|
928
|
+
`,
|
|
929
|
+
publish: `
|
|
930
|
+
${colors.cyan}${colors.bright}uniweb publish${colors.reset} ${colors.dim}— Publish a foundation to the catalog${colors.reset}
|
|
931
|
+
|
|
932
|
+
${colors.bright}Usage:${colors.reset}
|
|
933
|
+
uniweb publish [@org/name] [options]
|
|
934
|
+
|
|
935
|
+
For site-bound foundations (one foundation, one site), use \`uniweb deploy\`
|
|
936
|
+
instead — it auto-publishes under a site-scoped slot, no naming ceremony.
|
|
937
|
+
|
|
938
|
+
${colors.bright}Options:${colors.reset}
|
|
939
|
+
--catalog Confirm publish to the public catalog (required in CI)
|
|
940
|
+
--propagate Walk trusting sites' policy waves (default: silent)
|
|
941
|
+
--name <id> Foundation id (overrides package.json::uniweb.id)
|
|
942
|
+
--namespace <ns> Force org-scope namespace (overrides package.json)
|
|
943
|
+
--local Internal: publish to the unicloud mock (see workspace root CLAUDE.md)
|
|
944
|
+
--registry <url> Use a specific registry URL
|
|
945
|
+
--edit-access <p> "open" or "restricted" (default: restricted)
|
|
946
|
+
--dry-run Show what would be published without uploading
|
|
947
|
+
--non-interactive Fail with usage info instead of prompting
|
|
948
|
+
`,
|
|
949
|
+
create: `
|
|
950
|
+
${colors.cyan}${colors.bright}uniweb create${colors.reset} ${colors.dim}— Create a new project${colors.reset}
|
|
951
|
+
|
|
952
|
+
${colors.bright}Usage:${colors.reset}
|
|
953
|
+
uniweb create [name] [options]
|
|
954
|
+
|
|
955
|
+
${colors.bright}Options:${colors.reset}
|
|
956
|
+
--template <type> Project template (default: starter)
|
|
957
|
+
Built-in: starter, none, marketing
|
|
958
|
+
Local: ./path/to/template
|
|
959
|
+
npm: @scope/template-name
|
|
960
|
+
GitHub: github:user/repo or https://github.com/user/repo
|
|
961
|
+
--blank Create an empty workspace (grow with \`uniweb add\`)
|
|
962
|
+
--name <name> Project display name
|
|
963
|
+
--no-git Skip git repository initialization
|
|
964
|
+
|
|
965
|
+
${colors.bright}Examples:${colors.reset}
|
|
966
|
+
uniweb create my-project # Foundation + site + starter content
|
|
967
|
+
uniweb create my-project --template marketing # Official template
|
|
968
|
+
uniweb create my-project --blank # Empty workspace
|
|
969
|
+
`,
|
|
970
|
+
dev: `
|
|
971
|
+
${colors.cyan}${colors.bright}uniweb dev${colors.reset} ${colors.dim}— Start a dev server for a site${colors.reset}
|
|
972
|
+
|
|
973
|
+
${colors.bright}Usage:${colors.reset}
|
|
974
|
+
uniweb dev Start dev server for the (single) site
|
|
975
|
+
uniweb dev <site> Start dev server for a specific site
|
|
976
|
+
uniweb dev --site <name> Same, with explicit flag form
|
|
977
|
+
|
|
978
|
+
Thin wrapper around the package manager's workspace-filtered \`dev\`
|
|
979
|
+
script (\`pnpm --filter <site> dev\` or \`npm -w <site> run dev\`). Picks
|
|
980
|
+
the single site automatically; for multi-site workspaces the first
|
|
981
|
+
site runs by default with a notice pointing at \`--site\` for explicit
|
|
982
|
+
selection.
|
|
983
|
+
`,
|
|
984
|
+
build: `
|
|
985
|
+
${colors.cyan}${colors.bright}uniweb build${colors.reset} ${colors.dim}— Build the current project${colors.reset}
|
|
986
|
+
|
|
987
|
+
${colors.bright}Usage:${colors.reset}
|
|
988
|
+
uniweb build [options]
|
|
989
|
+
|
|
990
|
+
At workspace root, builds all foundations first, then all sites.
|
|
991
|
+
Pre-rendering is enabled by default when build.prerender: true in site.yml.
|
|
992
|
+
|
|
993
|
+
${colors.bright}Options:${colors.reset}
|
|
994
|
+
--target <type> Build target (foundation, site) — auto-detected if not specified
|
|
995
|
+
--prerender Force pre-rendering (overrides site.yml)
|
|
996
|
+
--no-prerender Skip pre-rendering (overrides site.yml)
|
|
997
|
+
--foundation-dir Path to foundation directory (for prerendering)
|
|
998
|
+
--host <name> Apply host-specific postBuild (e.g., cloudflare-pages emits _redirects)
|
|
999
|
+
--platform <name> (Deprecated alias for --host)
|
|
1000
|
+
`,
|
|
1001
|
+
add: `
|
|
1002
|
+
${colors.cyan}${colors.bright}uniweb add${colors.reset} ${colors.dim}— Add a foundation, site, or extension${colors.reset}
|
|
1003
|
+
|
|
1004
|
+
${colors.bright}Subcommands:${colors.reset}
|
|
1005
|
+
add project [name] Add a co-located foundation + site pair
|
|
1006
|
+
add foundation [name] Add a foundation (--from, --path, --project)
|
|
1007
|
+
add site [name] Add a site (--from, --foundation, --path, --project)
|
|
1008
|
+
add extension <name> Add an extension (--from, --site, --path)
|
|
1009
|
+
add section <name> Add a section type to a foundation (--foundation)
|
|
1010
|
+
|
|
1011
|
+
${colors.bright}Common options:${colors.reset}
|
|
1012
|
+
--from <template> Source content from a template
|
|
1013
|
+
--path <dir> Override default folder location
|
|
1014
|
+
--foundation <name> Wire site/extension to this foundation (CI-friendly)
|
|
1015
|
+
--site <name> Wire extension to this site (CI-friendly)
|
|
1016
|
+
--non-interactive Fail with usage info instead of prompting
|
|
1017
|
+
`,
|
|
1018
|
+
export: `
|
|
1019
|
+
${colors.cyan}${colors.bright}uniweb export${colors.reset} ${colors.dim}— Export a self-contained site for third-party hosting${colors.reset}
|
|
1020
|
+
|
|
1021
|
+
${colors.bright}Usage:${colors.reset}
|
|
1022
|
+
uniweb export [options]
|
|
1023
|
+
|
|
1024
|
+
Builds dist/ and prints upload examples for common static hosts. No login,
|
|
1025
|
+
no deploy step — you push the artifact to your host of choice yourself.
|
|
1026
|
+
For Uniweb-hosted sites, use \`uniweb deploy\`.
|
|
1027
|
+
|
|
1028
|
+
${colors.bright}Options:${colors.reset}
|
|
1029
|
+
--no-prerender Skip per-page prerendered HTML
|
|
1030
|
+
--host <name> Apply host-specific postBuild (cloudflare-pages, github-pages, …)
|
|
1031
|
+
`,
|
|
1032
|
+
doctor: `
|
|
1033
|
+
${colors.cyan}${colors.bright}uniweb doctor${colors.reset} ${colors.dim}— Diagnose project configuration issues${colors.reset}
|
|
1034
|
+
|
|
1035
|
+
${colors.bright}Usage:${colors.reset}
|
|
1036
|
+
uniweb doctor [options]
|
|
1037
|
+
|
|
1038
|
+
${colors.bright}Options:${colors.reset}
|
|
1039
|
+
--fix Apply fixes for safely-fixable issues
|
|
1040
|
+
--fix <issue-id> Apply fix for a specific issue id only
|
|
1041
|
+
--non-interactive Fail with usage info instead of prompting
|
|
1042
|
+
|
|
1043
|
+
Exit code is 1 if errors are found (warnings only → exit 0).
|
|
1044
|
+
`,
|
|
1045
|
+
rename: `
|
|
1046
|
+
${colors.cyan}${colors.bright}uniweb rename${colors.reset} ${colors.dim}— Rename a workspace package${colors.reset}
|
|
1047
|
+
|
|
1048
|
+
${colors.bright}Usage:${colors.reset}
|
|
1049
|
+
uniweb rename foundation <old> <new>
|
|
1050
|
+
|
|
1051
|
+
Today supports renaming foundations only. Updates folder name, foundation
|
|
1052
|
+
package.json::name, every dependent site's site.yml::foundation, every
|
|
1053
|
+
dependent site's package.json::dependencies, pnpm-workspace.yaml, and
|
|
1054
|
+
package.json::workspaces. Transactional — bails on conflict before any
|
|
1055
|
+
filesystem mutation.
|
|
1056
|
+
`,
|
|
1057
|
+
login: `
|
|
1058
|
+
${colors.cyan}${colors.bright}uniweb login${colors.reset} ${colors.dim}— Log in to your Uniweb account${colors.reset}
|
|
1059
|
+
|
|
1060
|
+
${colors.bright}Usage:${colors.reset}
|
|
1061
|
+
uniweb login [options]
|
|
1062
|
+
|
|
1063
|
+
Opens a browser to hub.uniweb.app for OAuth-style login, then captures
|
|
1064
|
+
the token via a loopback callback. Falls back to a paste-token prompt
|
|
1065
|
+
if the browser flow fails.
|
|
1066
|
+
|
|
1067
|
+
${colors.bright}Options:${colors.reset}
|
|
1068
|
+
--backend <url> Override the auth backend (default: https://hub.uniweb.app)
|
|
1069
|
+
|
|
1070
|
+
In non-interactive mode (CI / no TTY / --non-interactive), this command
|
|
1071
|
+
errors out — set the \`UNIWEB_TOKEN\` env var instead, or run \`login\`
|
|
1072
|
+
once on a machine with a browser to seed ~/.uniweb/auth.json.
|
|
1073
|
+
`,
|
|
1074
|
+
invite: `
|
|
1075
|
+
${colors.cyan}${colors.bright}uniweb invite${colors.reset} ${colors.dim}— Create a foundation invite for a client${colors.reset}
|
|
1076
|
+
|
|
1077
|
+
${colors.bright}Usage:${colors.reset}
|
|
1078
|
+
uniweb invite <email> [options]
|
|
1079
|
+
|
|
1080
|
+
${colors.bright}Options:${colors.reset}
|
|
1081
|
+
--uses <n> Max sites per invite (default: 1)
|
|
1082
|
+
--expires <days> Days until expiry (default: 30)
|
|
1083
|
+
--version <n> Major version to license (default: current)
|
|
1084
|
+
--list List invites for your foundation
|
|
1085
|
+
--revoke <id> Revoke an invite
|
|
1086
|
+
--resend <id> Resend an invite
|
|
1087
|
+
`,
|
|
1088
|
+
handoff: `
|
|
1089
|
+
${colors.cyan}${colors.bright}uniweb handoff${colors.reset} ${colors.dim}— Hand off a site to a client${colors.reset}
|
|
1090
|
+
|
|
1091
|
+
${colors.bright}Usage:${colors.reset}
|
|
1092
|
+
uniweb handoff <email> [options]
|
|
1093
|
+
|
|
1094
|
+
${colors.bright}Options:${colors.reset}
|
|
1095
|
+
--site <id> Site identifier (default: auto-generated)
|
|
1096
|
+
--web Show web-based handoff instructions instead
|
|
1097
|
+
`,
|
|
1098
|
+
template: `
|
|
1099
|
+
${colors.cyan}${colors.bright}uniweb template${colors.reset} ${colors.dim}— Manage cloud templates${colors.reset}
|
|
1100
|
+
|
|
1101
|
+
${colors.bright}Subcommands:${colors.reset}
|
|
1102
|
+
template publish Publish a site as a cloud template
|
|
1103
|
+
|
|
1104
|
+
${colors.bright}Publish Options:${colors.reset}
|
|
1105
|
+
--name <name> Template registry name (overrides site.yml template: field)
|
|
1106
|
+
--title <title> Display title (overrides site.yml name: field)
|
|
1107
|
+
--description <t> Description
|
|
1108
|
+
--registry <url> Registry URL (default: http://localhost:4001)
|
|
1109
|
+
`,
|
|
1110
|
+
docs: `
|
|
1111
|
+
${colors.cyan}${colors.bright}uniweb docs${colors.reset} ${colors.dim}— Generate component documentation${colors.reset}
|
|
1112
|
+
|
|
1113
|
+
${colors.bright}Subcommands:${colors.reset}
|
|
1114
|
+
docs Generate COMPONENTS.md from foundation schema
|
|
1115
|
+
docs site Show site.yml configuration reference
|
|
1116
|
+
docs page Show page.yml configuration reference
|
|
1117
|
+
docs meta Show component meta.js reference
|
|
1118
|
+
|
|
1119
|
+
${colors.bright}Options:${colors.reset}
|
|
1120
|
+
--output <file> Output filename (default: COMPONENTS.md)
|
|
1121
|
+
--from-source Read meta.js files directly instead of schema.json
|
|
1122
|
+
`,
|
|
1123
|
+
i18n: `
|
|
1124
|
+
${colors.cyan}${colors.bright}uniweb i18n${colors.reset} ${colors.dim}— Internationalization workflow${colors.reset}
|
|
1125
|
+
|
|
1126
|
+
${colors.bright}Subcommands:${colors.reset}
|
|
1127
|
+
i18n extract Extract translatable strings to manifest
|
|
1128
|
+
i18n sync Update manifest with content changes
|
|
1129
|
+
i18n status Show translation coverage per locale
|
|
1130
|
+
`,
|
|
1131
|
+
inspect: `
|
|
1132
|
+
${colors.cyan}${colors.bright}uniweb inspect${colors.reset} ${colors.dim}— Inspect parsed content shape${colors.reset}
|
|
1133
|
+
|
|
1134
|
+
${colors.bright}Usage:${colors.reset}
|
|
1135
|
+
uniweb inspect <path>
|
|
1136
|
+
|
|
1137
|
+
Prints the parsed content shape of a markdown file or folder — the
|
|
1138
|
+
{ content, params, items, … } object that components actually receive.
|
|
1139
|
+
Useful for debugging "why isn't my section getting X?".
|
|
1140
|
+
`,
|
|
1141
|
+
update: `
|
|
1142
|
+
${colors.cyan}${colors.bright}uniweb update${colors.reset} ${colors.dim}— Update AGENTS.md to match installed CLI version${colors.reset}
|
|
1143
|
+
|
|
1144
|
+
${colors.bright}Usage:${colors.reset}
|
|
1145
|
+
uniweb update
|
|
1146
|
+
|
|
1147
|
+
Refreshes the project's AGENTS.md from the CLI's bundled version. Run
|
|
1148
|
+
after upgrading the \`uniweb\` package to pick up new content authoring
|
|
1149
|
+
patterns and platform documentation.
|
|
1150
|
+
`,
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
if (!blocks[command]) return false
|
|
1154
|
+
log(blocks[command])
|
|
1155
|
+
return true
|
|
1156
|
+
}
|
|
1157
|
+
|
|
822
1158
|
function showHelp() {
|
|
823
1159
|
log(`
|
|
824
1160
|
${colors.cyan}${colors.bright}Uniweb CLI${colors.reset} ${colors.dim}v${getCliVersion()}${colors.reset}
|
|
@@ -830,6 +1166,7 @@ ${colors.bright}Commands:${colors.reset}
|
|
|
830
1166
|
create [name] Create a new project
|
|
831
1167
|
add <type> [name] Add a foundation, site, or extension to a project
|
|
832
1168
|
rename <type> Rename a workspace package (foundation today)
|
|
1169
|
+
dev Start a dev server for a site
|
|
833
1170
|
build Build the current project
|
|
834
1171
|
deploy Deploy a site to Uniweb hosting
|
|
835
1172
|
export Export a self-contained site for third-party hosting
|
|
@@ -896,21 +1233,34 @@ ${colors.bright}Template Options:${colors.reset}
|
|
|
896
1233
|
--registry <url> Registry URL (default: http://localhost:4001)
|
|
897
1234
|
|
|
898
1235
|
${colors.bright}Deploy Options:${colors.reset}
|
|
1236
|
+
--target <name> Pick a target from deploy.yml (default: deploy.yml's \`default:\`)
|
|
1237
|
+
--host <name> Override the resolved target's host (does not persist).
|
|
1238
|
+
Without a value, opens an interactive picker (TTY only).
|
|
1239
|
+
Hosts: uniweb, cloudflare-pages, netlify, vercel,
|
|
1240
|
+
github-pages, s3-cloudfront, generic-static.
|
|
899
1241
|
--dry-run Resolve site.yml + foundation/runtime; print summary; no writes
|
|
900
1242
|
--no-auto-publish Don't auto-publish workspace-local foundation as part of deploy
|
|
1243
|
+
--no-save Skip the auto-save of lastDeploy in deploy.yml
|
|
1244
|
+
|
|
1245
|
+
${colors.bright}Dev Options:${colors.reset}
|
|
1246
|
+
<site> Site name to run (positional)
|
|
1247
|
+
--site <name> Site name to run (explicit form)
|
|
901
1248
|
|
|
902
1249
|
${colors.bright}Export Options:${colors.reset}
|
|
903
1250
|
--no-prerender Skip per-page prerendered HTML
|
|
1251
|
+
--host <name> Apply host-specific postBuild (cloudflare-pages, github-pages, …)
|
|
904
1252
|
|
|
905
1253
|
${colors.bright}Build Options:${colors.reset}
|
|
906
|
-
--target <type> Build target (foundation, site)
|
|
1254
|
+
--target <type> Build target (foundation, site) — auto-detected if not specified
|
|
907
1255
|
--prerender Force pre-rendering (overrides site.yml)
|
|
908
1256
|
--no-prerender Skip pre-rendering (overrides site.yml)
|
|
909
1257
|
--foundation-dir Path to foundation directory (for prerendering)
|
|
910
|
-
--
|
|
1258
|
+
--host <name> Apply host-specific postBuild (cloudflare-pages, s3-cloudfront, …)
|
|
1259
|
+
--platform <name> (Deprecated alias for --host)
|
|
911
1260
|
|
|
912
1261
|
At workspace root, builds all foundations first, then all sites.
|
|
913
|
-
Pre-rendering is enabled by default when build.prerender: true in site.yml
|
|
1262
|
+
Pre-rendering is enabled by default when build.prerender: true in site.yml.
|
|
1263
|
+
See \`uniweb <command> --help\` for command-specific detail and examples.
|
|
914
1264
|
|
|
915
1265
|
${colors.bright}Docs Subcommands:${colors.reset}
|
|
916
1266
|
docs Generate COMPONENTS.md from foundation schema
|
package/src/utils/auth.js
CHANGED
|
@@ -153,17 +153,45 @@ export function isExpired(auth) {
|
|
|
153
153
|
* Ensure the user is authenticated. If not, prompt inline login.
|
|
154
154
|
* Returns the auth token on success, exits the process on cancel.
|
|
155
155
|
*
|
|
156
|
+
* In non-interactive mode (CI, no TTY, or --non-interactive in args),
|
|
157
|
+
* bails with an actionable error instead of opening a browser. The browser
|
|
158
|
+
* login flow waits 120 seconds for a callback that can never arrive without
|
|
159
|
+
* a user, then drops to a token-paste prompt that pipes can't answer —
|
|
160
|
+
* silently burning two minutes per invocation. CI / agent / piped callers
|
|
161
|
+
* must set `UNIWEB_TOKEN`, run `uniweb login` interactively first, or use
|
|
162
|
+
* `--local` for the unicloud mock (see workspace root CLAUDE.md).
|
|
163
|
+
*
|
|
156
164
|
* @param {Object} options
|
|
157
165
|
* @param {string} options.command - The command that needs auth (for messaging)
|
|
166
|
+
* @param {string[]} [options.args] - Argv slice; checked for --non-interactive
|
|
158
167
|
* @returns {Promise<string>} Bearer token
|
|
159
168
|
*/
|
|
160
|
-
export async function ensureAuth({ command = 'This command' } = {}) {
|
|
169
|
+
export async function ensureAuth({ command = 'This command', args = [] } = {}) {
|
|
170
|
+
// Honor explicit token from env — useful for CI and agents.
|
|
171
|
+
if (process.env.UNIWEB_TOKEN) {
|
|
172
|
+
return process.env.UNIWEB_TOKEN
|
|
173
|
+
}
|
|
174
|
+
|
|
161
175
|
const auth = await readAuth()
|
|
162
176
|
|
|
163
177
|
if (auth?.token && !isExpired(auth)) {
|
|
164
178
|
return auth.token
|
|
165
179
|
}
|
|
166
180
|
|
|
181
|
+
// Non-interactive bail: don't open a browser, don't wait 120s, don't
|
|
182
|
+
// prompt for a token paste. Print an actionable error and exit.
|
|
183
|
+
const { isNonInteractive, getCliPrefix } = await import('./interactive.js')
|
|
184
|
+
if (isNonInteractive(args)) {
|
|
185
|
+
const prefix = getCliPrefix()
|
|
186
|
+
const reason = auth && isExpired(auth) ? 'Session expired.' : 'Not logged in.'
|
|
187
|
+
console.error(`\x1b[31m✗\x1b[0m ${reason} ${command} requires a Uniweb account, and the CLI is in non-interactive mode (CI / no TTY / --non-interactive).`)
|
|
188
|
+
console.error(` Options:`)
|
|
189
|
+
console.error(` • Run \`${prefix} login\` interactively first, then re-run.`)
|
|
190
|
+
console.error(` • Set the \`UNIWEB_TOKEN\` env var to a bearer token.`)
|
|
191
|
+
console.error(` • Use \`--local\` to target the unicloud mock (internal testing only — see workspace root CLAUDE.md).`)
|
|
192
|
+
process.exit(1)
|
|
193
|
+
}
|
|
194
|
+
|
|
167
195
|
// Need to log in — delegate to the login command
|
|
168
196
|
if (auth && isExpired(auth)) {
|
|
169
197
|
console.log(`\x1b[33mSession expired.\x1b[0m ${command} requires a Uniweb account.\n`)
|
package/src/utils/config.js
CHANGED
|
@@ -157,9 +157,17 @@ export async function writeRootPackageJson(rootDir, pkg) {
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
/**
|
|
160
|
-
* Compute root scripts based on discovered sites
|
|
160
|
+
* Compute root scripts based on discovered sites.
|
|
161
|
+
*
|
|
162
|
+
* `dev` and `build` route through the uniweb CLI verb (PM-agnostic — they
|
|
163
|
+
* resolve `uniweb` from the project's local node_modules/.bin, so `pnpm
|
|
164
|
+
* dev` and `npm run dev` both work without locking the scripts to one PM
|
|
165
|
+
* at create-time). `preview` stays on a PM-specific filter because there
|
|
166
|
+
* isn't a `uniweb preview` verb yet (the site's own `vite preview` is what
|
|
167
|
+
* runs); switch it over when one ships.
|
|
168
|
+
*
|
|
161
169
|
* @param {Array<{name: string, path: string}>} sites - Discovered sites
|
|
162
|
-
* @param {'pnpm' | 'npm'} [pm='pnpm'] - Package manager
|
|
170
|
+
* @param {'pnpm' | 'npm'} [pm='pnpm'] - Package manager (used only for preview)
|
|
163
171
|
* @returns {Object} Scripts object for package.json
|
|
164
172
|
*/
|
|
165
173
|
export function computeRootScripts(sites, pm = 'pnpm') {
|
|
@@ -172,16 +180,17 @@ export function computeRootScripts(sites, pm = 'pnpm') {
|
|
|
172
180
|
}
|
|
173
181
|
|
|
174
182
|
if (sites.length === 1) {
|
|
175
|
-
scripts.dev =
|
|
183
|
+
scripts.dev = 'uniweb dev'
|
|
176
184
|
scripts.preview = filterCmd(pm, sites[0].name, 'preview')
|
|
177
185
|
} else {
|
|
178
|
-
// First site gets unqualified dev/preview
|
|
179
|
-
|
|
186
|
+
// First site gets unqualified dev/preview (matches `uniweb dev`'s
|
|
187
|
+
// default-to-first-site behavior).
|
|
188
|
+
scripts.dev = 'uniweb dev'
|
|
180
189
|
scripts.preview = filterCmd(pm, sites[0].name, 'preview')
|
|
181
190
|
|
|
182
191
|
// Subsequent sites get qualified dev:{name}/preview:{name}
|
|
183
192
|
for (let i = 1; i < sites.length; i++) {
|
|
184
|
-
scripts[`dev:${sites[i].name}`] =
|
|
193
|
+
scripts[`dev:${sites[i].name}`] = `uniweb dev ${sites[i].name}`
|
|
185
194
|
scripts[`preview:${sites[i].name}`] = filterCmd(pm, sites[i].name, 'preview')
|
|
186
195
|
}
|
|
187
196
|
}
|
|
@@ -52,15 +52,46 @@ function writeState(state) {
|
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
54
|
* Print update notification to stderr (doesn't interfere with piped output).
|
|
55
|
+
* `tone` controls the lead-in: 'soft' (default — trailing notice for finished
|
|
56
|
+
* commands) vs 'eager' (leading notice for staleness-sensitive commands like
|
|
57
|
+
* `create`, where the user is about to scaffold files from CLI-bundled
|
|
58
|
+
* templates and a stale CLI means stale starter content).
|
|
55
59
|
*/
|
|
56
|
-
function printNotification(current, latest) {
|
|
60
|
+
function printNotification(current, latest, tone = 'soft') {
|
|
57
61
|
const yellow = '\x1b[33m'
|
|
58
62
|
const cyan = '\x1b[36m'
|
|
59
63
|
const dim = '\x1b[2m'
|
|
60
64
|
const reset = '\x1b[0m'
|
|
61
65
|
console.error('')
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
if (tone === 'eager') {
|
|
67
|
+
console.error(`${yellow}Heads up:${reset} this CLI is ${dim}${current}${reset}; latest is ${cyan}${latest}${reset}.`)
|
|
68
|
+
console.error(`${dim}Templates ship with the CLI — consider updating first:${reset} npm i -g uniweb`)
|
|
69
|
+
console.error(`${dim}Or run a one-shot fresh:${reset} npx uniweb@latest <command>`)
|
|
70
|
+
} else {
|
|
71
|
+
console.error(`${yellow}Update available:${reset} ${dim}${current}${reset} → ${cyan}${latest}${reset}`)
|
|
72
|
+
console.error(`${dim}Run${reset} npm i -g uniweb ${dim}to update${reset}`)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Synchronously read the cache and print an eager notification if a newer
|
|
78
|
+
* version is known. No network fetch — only reads what `startUpdateCheck`
|
|
79
|
+
* has previously cached. Returns true if a notification was printed.
|
|
80
|
+
*
|
|
81
|
+
* Use this for staleness-sensitive verbs (`create`) BEFORE the verb does
|
|
82
|
+
* its work, so the user sees the warning before any files are written
|
|
83
|
+
* from CLI-bundled templates. For other verbs, the trailing soft
|
|
84
|
+
* notification from startUpdateCheck() is sufficient.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} currentVersion
|
|
87
|
+
* @returns {boolean} true if a notification was printed
|
|
88
|
+
*/
|
|
89
|
+
export function maybeEagerNotification(currentVersion) {
|
|
90
|
+
const state = readState()
|
|
91
|
+
if (!state.latestVersion) return false
|
|
92
|
+
if (compareSemver(state.latestVersion, currentVersion) <= 0) return false
|
|
93
|
+
printNotification(currentVersion, state.latestVersion, 'eager')
|
|
94
|
+
return true
|
|
64
95
|
}
|
|
65
96
|
|
|
66
97
|
/**
|