uniweb 0.12.10 → 0.12.12
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 +5 -5
- 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/commands/update.js +217 -20
- package/src/framework-index.json +2 -2
- package/src/index.js +393 -13
- package/src/utils/auth.js +29 -1
- package/src/utils/config.js +15 -6
- package/src/utils/update-check.js +41 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.12",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,14 +41,14 @@
|
|
|
41
41
|
"js-yaml": "^4.1.0",
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
|
+
"@uniweb/core": "0.7.11",
|
|
44
45
|
"@uniweb/runtime": "0.8.13",
|
|
45
|
-
"@uniweb/kit": "0.9.11"
|
|
46
|
-
"@uniweb/core": "0.7.11"
|
|
46
|
+
"@uniweb/kit": "0.9.11"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"@uniweb/build": "0.14.2",
|
|
50
|
-
"@uniweb/
|
|
51
|
-
"@uniweb/
|
|
50
|
+
"@uniweb/content-reader": "1.1.10",
|
|
51
|
+
"@uniweb/semantic-parser": "1.1.17"
|
|
52
52
|
},
|
|
53
53
|
"peerDependenciesMeta": {
|
|
54
54
|
"@uniweb/build": {
|
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/commands/update.js
CHANGED
|
@@ -1,16 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* uniweb update
|
|
2
|
+
* uniweb update — Update the CLI itself, and (in a Uniweb project) the
|
|
3
|
+
* project's AGENTS.md.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
+
* Two responsibilities, in priority order:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Self-update the global install.** Most users running `uniweb update`
|
|
8
|
+
* expect the verb to update the CLI binary itself (this is what `npm
|
|
9
|
+
* update -g`, `gh update`, `claude update`, etc. all do). The CLI
|
|
10
|
+
* detects the relevant package manager and runs the global-install
|
|
11
|
+
* command for it (`npm i -g uniweb@latest`, `pnpm add -g uniweb@latest`,
|
|
12
|
+
* `yarn global add uniweb@latest`). In TTY, prompts before executing.
|
|
13
|
+
* In non-interactive mode, prints the command and exits — never runs an
|
|
14
|
+
* unconfirmed self-update from a script.
|
|
15
|
+
*
|
|
16
|
+
* 2. **Refresh AGENTS.md** (only when the cwd resolves to a *Uniweb*
|
|
17
|
+
* project — checked via `package.json::devDependencies::uniweb` or
|
|
18
|
+
* `dependencies::uniweb` at the workspace root). The previous
|
|
19
|
+
* implementation walked up looking for ANY pnpm-workspace.yaml or
|
|
20
|
+
* `package.json::workspaces` root, which falsely identified unrelated
|
|
21
|
+
* monorepos as Uniweb projects and wrote AGENTS.md into them.
|
|
22
|
+
*
|
|
23
|
+
* Flags:
|
|
24
|
+
* --agents-only Skip self-update; only refresh AGENTS.md.
|
|
25
|
+
* --no-agents Skip AGENTS.md; only self-update.
|
|
26
|
+
* --yes Skip the confirmation prompt before self-update.
|
|
27
|
+
* --non-interactive Auto-detected; never runs unconfirmed self-update.
|
|
28
|
+
*
|
|
29
|
+
* Project-local case (CLI lives in node_modules, not global): self-update
|
|
30
|
+
* isn't possible — that's a project decision (bump the dep in
|
|
31
|
+
* package.json). The verb prints that explanation and proceeds with the
|
|
32
|
+
* AGENTS.md refresh path only.
|
|
5
33
|
*/
|
|
6
34
|
|
|
7
|
-
import { existsSync, writeFileSync } from 'node:fs'
|
|
8
|
-
import { join
|
|
35
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
36
|
+
import { join } from 'node:path'
|
|
37
|
+
import { spawn } from 'node:child_process'
|
|
38
|
+
import prompts from 'prompts'
|
|
39
|
+
|
|
9
40
|
import { findWorkspaceRoot } from '../utils/workspace.js'
|
|
10
41
|
import { readAgentsVersion, generateAgentsContent } from '../utils/agents-stamp.js'
|
|
11
42
|
import { getCliVersion } from '../versions.js'
|
|
43
|
+
import { isNonInteractive } from '../utils/interactive.js'
|
|
12
44
|
|
|
13
|
-
// ANSI colors
|
|
14
45
|
const colors = {
|
|
15
46
|
reset: '\x1b[0m',
|
|
16
47
|
bright: '\x1b[1m',
|
|
@@ -18,7 +49,7 @@ const colors = {
|
|
|
18
49
|
red: '\x1b[31m',
|
|
19
50
|
green: '\x1b[32m',
|
|
20
51
|
yellow: '\x1b[33m',
|
|
21
|
-
|
|
52
|
+
cyan: '\x1b[36m',
|
|
22
53
|
}
|
|
23
54
|
|
|
24
55
|
const success = (msg) => console.log(`${colors.green}✓${colors.reset} ${msg}`)
|
|
@@ -26,30 +57,196 @@ const warn = (msg) => console.log(`${colors.yellow}⚠${colors.reset} ${msg}`)
|
|
|
26
57
|
const error = (msg) => console.log(`${colors.red}✗${colors.reset} ${msg}`)
|
|
27
58
|
const log = console.log
|
|
28
59
|
|
|
29
|
-
|
|
30
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Detect whether this CLI is running from a global install. Mirrors the
|
|
62
|
+
* logic in index.js::isGlobalInstall — when global, process.argv[1]
|
|
63
|
+
* points outside any node_modules.
|
|
64
|
+
*/
|
|
65
|
+
function isGlobalInstall() {
|
|
66
|
+
const scriptPath = process.argv[1]
|
|
67
|
+
if (!scriptPath) return false
|
|
68
|
+
return !scriptPath.split('/').includes('node_modules') &&
|
|
69
|
+
!scriptPath.split('\\').includes('node_modules')
|
|
70
|
+
}
|
|
31
71
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Find a *Uniweb* workspace root from cwd. Stricter than findWorkspaceRoot
|
|
74
|
+
* — also requires that the workspace's root package.json declares uniweb
|
|
75
|
+
* as a dep or devDep. Otherwise the previous behavior (walking up to any
|
|
76
|
+
* pnpm-workspace.yaml) writes AGENTS.md into unrelated monorepos.
|
|
77
|
+
*/
|
|
78
|
+
function findUniwebWorkspace(cwd) {
|
|
79
|
+
const workspaceDir = findWorkspaceRoot(cwd)
|
|
80
|
+
if (!workspaceDir) return null
|
|
81
|
+
const pkgPath = join(workspaceDir, 'package.json')
|
|
82
|
+
if (!existsSync(pkgPath)) return null
|
|
83
|
+
try {
|
|
84
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
85
|
+
const hasUniwebDep = !!(pkg.devDependencies?.uniweb || pkg.dependencies?.uniweb)
|
|
86
|
+
return hasUniwebDep ? workspaceDir : null
|
|
87
|
+
} catch {
|
|
88
|
+
return null
|
|
36
89
|
}
|
|
90
|
+
}
|
|
37
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Detect the package manager that owns the global install. Heuristic
|
|
94
|
+
* based on the CLI's filesystem path — pnpm and yarn berry use distinctive
|
|
95
|
+
* directory layouts; npm is the fallback.
|
|
96
|
+
*
|
|
97
|
+
* @returns {'pnpm'|'yarn'|'npm'}
|
|
98
|
+
*/
|
|
99
|
+
function detectGlobalPm() {
|
|
100
|
+
const path = (process.argv[1] || '').toLowerCase()
|
|
101
|
+
if (path.includes('/pnpm/') || path.includes('\\pnpm\\')) return 'pnpm'
|
|
102
|
+
if (path.includes('/yarn/') || path.includes('\\yarn\\')) return 'yarn'
|
|
103
|
+
return 'npm'
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Build the global-install command for a given PM.
|
|
108
|
+
*/
|
|
109
|
+
function globalInstallCmd(pm) {
|
|
110
|
+
if (pm === 'pnpm') return 'pnpm add -g uniweb@latest'
|
|
111
|
+
if (pm === 'yarn') return 'yarn global add uniweb@latest'
|
|
112
|
+
return 'npm i -g uniweb@latest'
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Fetch the latest published version. Returns null on network error.
|
|
117
|
+
*/
|
|
118
|
+
async function fetchLatestVersion() {
|
|
119
|
+
try {
|
|
120
|
+
const res = await fetch('https://registry.npmjs.org/uniweb/latest')
|
|
121
|
+
if (!res.ok) return null
|
|
122
|
+
const data = await res.json()
|
|
123
|
+
return data?.version || null
|
|
124
|
+
} catch {
|
|
125
|
+
return null
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Compare two semver strings: 1 if a>b, -1 if a<b, 0 if equal.
|
|
131
|
+
*/
|
|
132
|
+
function compareSemver(a, b) {
|
|
133
|
+
const pa = a.split('.').map(Number)
|
|
134
|
+
const pb = b.split('.').map(Number)
|
|
135
|
+
for (let i = 0; i < 3; i++) {
|
|
136
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return 1
|
|
137
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return -1
|
|
138
|
+
}
|
|
139
|
+
return 0
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Run a shell command, inheriting stdio. Resolves with the exit code.
|
|
144
|
+
*/
|
|
145
|
+
function runCommand(cmd) {
|
|
146
|
+
return new Promise((resolve) => {
|
|
147
|
+
const [bin, ...rest] = cmd.split(' ')
|
|
148
|
+
const child = spawn(bin, rest, { stdio: 'inherit' })
|
|
149
|
+
child.on('close', code => resolve(code ?? 0))
|
|
150
|
+
child.on('error', () => resolve(1))
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function update(args = []) {
|
|
155
|
+
const agentsOnly = args.includes('--agents-only')
|
|
156
|
+
const skipAgents = args.includes('--no-agents')
|
|
157
|
+
const skipPrompt = args.includes('--yes') || isNonInteractive(args)
|
|
158
|
+
const isGlobal = isGlobalInstall()
|
|
159
|
+
const workspaceDir = findUniwebWorkspace(process.cwd())
|
|
160
|
+
const inProject = !!workspaceDir
|
|
38
161
|
const cliVersion = getCliVersion()
|
|
39
|
-
const agentsPath = join(workspaceDir, 'AGENTS.md')
|
|
40
|
-
const currentVersion = readAgentsVersion(agentsPath)
|
|
41
162
|
|
|
42
|
-
|
|
43
|
-
|
|
163
|
+
// ─── Step 1: Self-update path ─────────────────────────────────
|
|
164
|
+
if (!agentsOnly) {
|
|
165
|
+
if (!isGlobal) {
|
|
166
|
+
// Project-local: can't self-update meaningfully.
|
|
167
|
+
log(`${colors.dim}Running the project-local CLI (v${cliVersion}). This copy is pinned by your${colors.reset}`)
|
|
168
|
+
log(`${colors.dim}project's package.json. To update it, bump${colors.reset} ${colors.cyan}uniweb${colors.reset}${colors.dim} in${colors.reset} ${colors.cyan}package.json${colors.reset}${colors.dim} and re-install.${colors.reset}`)
|
|
169
|
+
log('')
|
|
170
|
+
} else {
|
|
171
|
+
const latest = await fetchLatestVersion()
|
|
172
|
+
if (latest === null) {
|
|
173
|
+
warn('Could not reach the npm registry to check for updates.')
|
|
174
|
+
log(`${colors.dim}Current: ${cliVersion}. Try later, or run${colors.reset} ${colors.cyan}${globalInstallCmd(detectGlobalPm())}${colors.reset}${colors.dim} manually.${colors.reset}`)
|
|
175
|
+
log('')
|
|
176
|
+
} else if (compareSemver(latest, cliVersion) <= 0) {
|
|
177
|
+
success(`uniweb is up to date (v${cliVersion}).`)
|
|
178
|
+
log('')
|
|
179
|
+
} else {
|
|
180
|
+
const pm = detectGlobalPm()
|
|
181
|
+
const cmd = globalInstallCmd(pm)
|
|
182
|
+
log(`${colors.yellow}Update available:${colors.reset} ${colors.dim}${cliVersion}${colors.reset} → ${colors.cyan}${latest}${colors.reset}`)
|
|
183
|
+
log(`${colors.dim}Detected package manager:${colors.reset} ${pm}`)
|
|
184
|
+
log(`${colors.dim}Will run:${colors.reset} ${colors.cyan}${cmd}${colors.reset}`)
|
|
185
|
+
log('')
|
|
186
|
+
|
|
187
|
+
if (skipPrompt) {
|
|
188
|
+
log(`${colors.dim}Non-interactive — skipping self-update. Run the command above to update.${colors.reset}`)
|
|
189
|
+
log('')
|
|
190
|
+
} else {
|
|
191
|
+
const { go } = await prompts({
|
|
192
|
+
type: 'confirm',
|
|
193
|
+
name: 'go',
|
|
194
|
+
message: `Run \`${cmd}\` now?`,
|
|
195
|
+
initial: true,
|
|
196
|
+
})
|
|
197
|
+
if (go) {
|
|
198
|
+
const code = await runCommand(cmd)
|
|
199
|
+
if (code === 0) {
|
|
200
|
+
success(`Self-update complete.`)
|
|
201
|
+
} else {
|
|
202
|
+
error(`Self-update failed (exit ${code}). Run the command above manually if needed.`)
|
|
203
|
+
}
|
|
204
|
+
log('')
|
|
205
|
+
} else {
|
|
206
|
+
log(`${colors.dim}Skipped self-update.${colors.reset}`)
|
|
207
|
+
log('')
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ─── Step 2: AGENTS.md refresh path ───────────────────────────
|
|
215
|
+
if (skipAgents) return
|
|
216
|
+
if (!inProject) {
|
|
217
|
+
if (agentsOnly) {
|
|
218
|
+
error('Not in a Uniweb project (no `uniweb` dep in the workspace root package.json).')
|
|
219
|
+
log(`${colors.dim}Run this command from inside a project created by${colors.reset} ${colors.cyan}uniweb create${colors.reset}${colors.dim}.${colors.reset}`)
|
|
220
|
+
process.exit(1)
|
|
221
|
+
}
|
|
222
|
+
// Self-update-only path. Quietly skip AGENTS.md.
|
|
44
223
|
return
|
|
45
224
|
}
|
|
46
225
|
|
|
226
|
+
const agentsPath = join(workspaceDir, 'AGENTS.md')
|
|
227
|
+
const currentAgentsVersion = readAgentsVersion(agentsPath)
|
|
228
|
+
if (currentAgentsVersion === cliVersion) {
|
|
229
|
+
success(`AGENTS.md is already up to date (v${cliVersion}).`)
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Prompt before writing in TTY, unless --yes / non-interactive (in which
|
|
234
|
+
// case we err on the side of doing the right thing — refresh — since the
|
|
235
|
+
// user explicitly invoked `uniweb update` from a Uniweb project).
|
|
236
|
+
if (!skipPrompt && !agentsOnly) {
|
|
237
|
+
const action = currentAgentsVersion ? `Update AGENTS.md (v${currentAgentsVersion} → v${cliVersion})?` : `Create AGENTS.md (v${cliVersion})?`
|
|
238
|
+
const { yes } = await prompts({ type: 'confirm', name: 'yes', message: action, initial: true })
|
|
239
|
+
if (!yes) {
|
|
240
|
+
log(`${colors.dim}Skipped AGENTS.md.${colors.reset}`)
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
47
245
|
const content = generateAgentsContent()
|
|
48
246
|
writeFileSync(agentsPath, content)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
success(`Updated AGENTS.md (v${currentVersion} → v${cliVersion})`)
|
|
247
|
+
if (currentAgentsVersion) {
|
|
248
|
+
success(`Updated AGENTS.md (v${currentAgentsVersion} → v${cliVersion}).`)
|
|
52
249
|
} else {
|
|
53
|
-
success(`Created AGENTS.md (v${cliVersion})
|
|
250
|
+
success(`Created AGENTS.md (v${cliVersion}).`)
|
|
54
251
|
}
|
|
55
252
|
}
|
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
|
@@ -103,8 +103,12 @@ function getCliVersion() {
|
|
|
103
103
|
/**
|
|
104
104
|
* Commands that always run from the global CLI (no project context needed)
|
|
105
105
|
*/
|
|
106
|
+
// Commands that always run from the global CLI, never delegating to a
|
|
107
|
+
// project-local copy. `update` is here because its primary job is to
|
|
108
|
+
// self-update the GLOBAL install — delegating it to project-local would
|
|
109
|
+
// short-circuit that intent.
|
|
106
110
|
const STANDALONE_COMMANDS = new Set([
|
|
107
|
-
'create', '--help', '-h', '--version', '-v', 'login',
|
|
111
|
+
'create', '--help', '-h', '--version', '-v', 'login', 'update',
|
|
108
112
|
])
|
|
109
113
|
|
|
110
114
|
/**
|
|
@@ -199,12 +203,16 @@ async function createFromPackageTemplates(projectDir, projectName, options = {})
|
|
|
199
203
|
|
|
200
204
|
onProgress?.('Setting up workspace...')
|
|
201
205
|
|
|
202
|
-
// 1. Scaffold workspace
|
|
206
|
+
// 1. Scaffold workspace.
|
|
207
|
+
// dev/build go through `uniweb` verbs so the scripts stay PM-agnostic
|
|
208
|
+
// (the verb resolves the right PM at runtime instead of locking the
|
|
209
|
+
// root scripts to whichever PM ran `npx uniweb create`). preview stays
|
|
210
|
+
// PM-filtered until a `uniweb preview` verb exists.
|
|
203
211
|
await scaffoldWorkspace(projectDir, {
|
|
204
212
|
projectName,
|
|
205
213
|
workspaceGlobs: ['site', 'src'],
|
|
206
214
|
scripts: {
|
|
207
|
-
dev:
|
|
215
|
+
dev: 'uniweb dev',
|
|
208
216
|
build: 'uniweb build',
|
|
209
217
|
preview: filterCmd(pm, 'site', 'preview'),
|
|
210
218
|
},
|
|
@@ -286,17 +294,19 @@ async function createFromContentTemplate(projectDir, projectName, metadata, temp
|
|
|
286
294
|
const scripts = {
|
|
287
295
|
build: 'uniweb build',
|
|
288
296
|
}
|
|
297
|
+
// dev goes through `uniweb` (PM-agnostic; see computeRootScripts).
|
|
298
|
+
// preview stays PM-filtered until a `uniweb preview` verb exists.
|
|
289
299
|
if (sites.length === 1) {
|
|
290
|
-
scripts.dev =
|
|
300
|
+
scripts.dev = 'uniweb dev'
|
|
291
301
|
scripts.preview = filterCmd(pm, sites[0].name, 'preview')
|
|
292
302
|
} else {
|
|
293
303
|
for (const s of sites) {
|
|
294
|
-
scripts[`dev:${s.name}`] =
|
|
304
|
+
scripts[`dev:${s.name}`] = `uniweb dev ${s.name}`
|
|
295
305
|
scripts[`preview:${s.name}`] = filterCmd(pm, s.name, 'preview')
|
|
296
306
|
}
|
|
297
307
|
// First site gets unqualified aliases
|
|
298
308
|
if (sites.length > 0) {
|
|
299
|
-
scripts.dev =
|
|
309
|
+
scripts.dev = 'uniweb dev'
|
|
300
310
|
scripts.preview = filterCmd(pm, sites[0].name, 'preview')
|
|
301
311
|
}
|
|
302
312
|
}
|
|
@@ -435,8 +445,20 @@ async function main() {
|
|
|
435
445
|
const pm = detectPackageManager()
|
|
436
446
|
|
|
437
447
|
// Handle --version / -v
|
|
448
|
+
//
|
|
449
|
+
// Output convention: the version goes to stdout (parseable, scriptable —
|
|
450
|
+
// `version=$(uniweb --version)` should keep working). Any staleness
|
|
451
|
+
// notice goes to stderr, so it shows in interactive terminals but
|
|
452
|
+
// doesn't pollute captured output. Cache-only — never makes a network
|
|
453
|
+
// call from this path.
|
|
438
454
|
if (command === '--version' || command === '-v') {
|
|
439
455
|
console.log(`uniweb ${getCliVersion()}`)
|
|
456
|
+
if (isGlobalInstall()) {
|
|
457
|
+
try {
|
|
458
|
+
const { maybeNotifyFromCache } = await import('./utils/update-check.js')
|
|
459
|
+
maybeNotifyFromCache(getCliVersion(), 'soft')
|
|
460
|
+
} catch { /* ignore */ }
|
|
461
|
+
}
|
|
440
462
|
return
|
|
441
463
|
}
|
|
442
464
|
|
|
@@ -452,12 +474,26 @@ async function main() {
|
|
|
452
474
|
// Commands that need @uniweb/build will get a helpful error via importProjectCommand().
|
|
453
475
|
}
|
|
454
476
|
|
|
455
|
-
// Start non-blocking update check for global installs
|
|
477
|
+
// Start non-blocking update check for global installs.
|
|
478
|
+
//
|
|
479
|
+
// Two surfaces:
|
|
480
|
+
// - showUpdateNotification (soft, trailing): printed at command end for
|
|
481
|
+
// any verb. Doesn't interrupt the user's workflow.
|
|
482
|
+
// - eager (loud, leading): printed BEFORE staleness-sensitive verbs do
|
|
483
|
+
// their work. Today: only `create` (templates ship with the CLI, so
|
|
484
|
+
// a stale CLI scaffolds stale starter content; the user needs to know
|
|
485
|
+
// before files hit disk). Other verbs are insensitive — `deploy` etc.
|
|
486
|
+
// are project-bound (delegated to local node_modules), and the
|
|
487
|
+
// local-vs-global mismatch warning in delegateToLocal already covers
|
|
488
|
+
// that case.
|
|
456
489
|
let showUpdateNotification = () => {}
|
|
457
490
|
if (global) {
|
|
458
491
|
try {
|
|
459
|
-
const { startUpdateCheck } = await import('./utils/update-check.js')
|
|
492
|
+
const { startUpdateCheck, maybeEagerNotification } = await import('./utils/update-check.js')
|
|
460
493
|
showUpdateNotification = startUpdateCheck(getCliVersion())
|
|
494
|
+
if (command === 'create') {
|
|
495
|
+
maybeEagerNotification(getCliVersion())
|
|
496
|
+
}
|
|
461
497
|
} catch {
|
|
462
498
|
// Update check is optional — don't fail if the module is missing
|
|
463
499
|
}
|
|
@@ -470,6 +506,23 @@ async function main() {
|
|
|
470
506
|
return
|
|
471
507
|
}
|
|
472
508
|
|
|
509
|
+
// Per-command --help: short-circuit BEFORE the command's side effects run.
|
|
510
|
+
// Critical for `deploy --help` (used to open a browser to production for
|
|
511
|
+
// login because deploy.js doesn't parse --help and ensureAuth ran first).
|
|
512
|
+
// Falls back to the global help when a command has no dedicated block.
|
|
513
|
+
if (args.slice(1).some(a => a === '--help' || a === '-h')) {
|
|
514
|
+
const printed = printCommandHelp(command)
|
|
515
|
+
if (printed) {
|
|
516
|
+
await showUpdateNotification()
|
|
517
|
+
return
|
|
518
|
+
}
|
|
519
|
+
// No dedicated block — show global help as a useful fallback rather
|
|
520
|
+
// than executing the command (which often has side effects).
|
|
521
|
+
showHelp()
|
|
522
|
+
await showUpdateNotification()
|
|
523
|
+
return
|
|
524
|
+
}
|
|
525
|
+
|
|
473
526
|
// Handle build command (dynamic import — depends on @uniweb/build)
|
|
474
527
|
if (command === 'build') {
|
|
475
528
|
const { build } = await importProjectCommand('./commands/build.js')
|
|
@@ -478,6 +531,16 @@ async function main() {
|
|
|
478
531
|
return
|
|
479
532
|
}
|
|
480
533
|
|
|
534
|
+
// Handle dev command — thin wrapper that shells to the package manager's
|
|
535
|
+
// workspace-filtered `dev` script (mirrors what `uniweb create` writes
|
|
536
|
+
// into the root package.json::scripts.dev). Lazy import keeps startup
|
|
537
|
+
// fast when the user is not running dev.
|
|
538
|
+
if (command === 'dev') {
|
|
539
|
+
const { dev } = await import('./commands/dev.js')
|
|
540
|
+
await dev(args.slice(1))
|
|
541
|
+
return
|
|
542
|
+
}
|
|
543
|
+
|
|
481
544
|
// Handle docs command (dynamic import — depends on @uniweb/build)
|
|
482
545
|
if (command === 'docs') {
|
|
483
546
|
const { docs } = await importProjectCommand('./commands/docs.js')
|
|
@@ -807,18 +870,321 @@ async function main() {
|
|
|
807
870
|
log(` ${colors.cyan}cd ${projectName}${colors.reset}`)
|
|
808
871
|
log(` ${colors.cyan}${prefix} add project${colors.reset}`)
|
|
809
872
|
log(` ${colors.cyan}${installCmd(pm)}${colors.reset}`)
|
|
810
|
-
log(` ${colors.cyan}${
|
|
873
|
+
log(` ${colors.cyan}${prefix} dev${colors.reset} ${colors.dim}# Start dev server${colors.reset}`)
|
|
811
874
|
} else {
|
|
812
875
|
log(`Next steps:\n`)
|
|
813
876
|
log(` ${colors.cyan}cd ${projectName}${colors.reset}`)
|
|
814
877
|
log(` ${colors.cyan}${installCmd(pm)}${colors.reset}`)
|
|
815
|
-
log(` ${colors.cyan}${
|
|
878
|
+
log(` ${colors.cyan}${prefix} dev${colors.reset} ${colors.dim}# Start dev server${colors.reset}`)
|
|
816
879
|
}
|
|
817
880
|
log('')
|
|
881
|
+
log(`When ready to ship:\n`)
|
|
882
|
+
log(` ${colors.cyan}${prefix} deploy${colors.reset} ${colors.dim}# Uniweb hosting (default; uniweb login first)${colors.reset}`)
|
|
883
|
+
log(` ${colors.cyan}${prefix} deploy --host=<adapter>${colors.reset} ${colors.dim}# cloudflare-pages, netlify, vercel, github-pages, s3-cloudfront${colors.reset}`)
|
|
884
|
+
log(` ${colors.cyan}${prefix} export${colors.reset} ${colors.dim}# Build dist/ for any static host (no Uniweb account)${colors.reset}`)
|
|
885
|
+
log('')
|
|
886
|
+
log(` ${colors.dim}See ${colors.reset}${colors.cyan}${prefix} <command> --help${colors.reset}${colors.dim} for command-specific options.${colors.reset}`)
|
|
887
|
+
log('')
|
|
818
888
|
|
|
819
889
|
await showUpdateNotification()
|
|
820
890
|
}
|
|
821
891
|
|
|
892
|
+
/**
|
|
893
|
+
* Print help for a specific command. Returns true if a dedicated help
|
|
894
|
+
* block exists for the command, false to signal "fall back to global
|
|
895
|
+
* help."
|
|
896
|
+
*
|
|
897
|
+
* Help text intentionally lives next to the dispatcher rather than in
|
|
898
|
+
* the per-command files because most help-seekers haven't run that
|
|
899
|
+
* command yet — keeping it here means `uniweb foo --help` prints
|
|
900
|
+
* without loading @uniweb/build or any project context.
|
|
901
|
+
*/
|
|
902
|
+
function printCommandHelp(command) {
|
|
903
|
+
const blocks = {
|
|
904
|
+
deploy: `
|
|
905
|
+
${colors.cyan}${colors.bright}uniweb deploy${colors.reset} ${colors.dim}— Deploy a site${colors.reset}
|
|
906
|
+
|
|
907
|
+
${colors.bright}Usage:${colors.reset}
|
|
908
|
+
uniweb deploy [options]
|
|
909
|
+
|
|
910
|
+
The host is determined by the resolved deploy.yml target. Defaults to
|
|
911
|
+
${colors.cyan}uniweb${colors.reset} hosting (link-mode, edge JIT prerender) when no deploy.yml exists.
|
|
912
|
+
|
|
913
|
+
${colors.bright}Hosts:${colors.reset}
|
|
914
|
+
uniweb Uniweb hosting (default; requires \`uniweb login\`)
|
|
915
|
+
cloudflare-pages Cloudflare Pages (build artifact + adapter postBuild)
|
|
916
|
+
netlify Netlify (alias of cloudflare-pages adapter)
|
|
917
|
+
vercel Vercel (build-only — deploy via \`npx vercel\`)
|
|
918
|
+
github-pages GitHub Pages (build-only — push dist/ to gh-pages)
|
|
919
|
+
s3-cloudfront AWS S3 + CloudFront (uploads + invalidates via CLI)
|
|
920
|
+
generic-static Plain static-host build, no host-specific helpers
|
|
921
|
+
|
|
922
|
+
${colors.bright}Options:${colors.reset}
|
|
923
|
+
--target <name> Pick a target from deploy.yml (default: deploy.yml's \`default:\`)
|
|
924
|
+
--host <name> Override the resolved target's host (does not persist)
|
|
925
|
+
--host No value → interactive picker (TTY only)
|
|
926
|
+
--dry-run Resolve site.yml + foundation/runtime; print summary; no writes
|
|
927
|
+
--no-auto-publish Don't auto-publish workspace-local foundation as part of deploy
|
|
928
|
+
--no-save Skip the auto-save of lastDeploy in deploy.yml
|
|
929
|
+
--local Internal: target the unicloud mock (see workspace root CLAUDE.md)
|
|
930
|
+
--non-interactive Fail with usage info instead of prompting
|
|
931
|
+
|
|
932
|
+
${colors.bright}Auth:${colors.reset}
|
|
933
|
+
\`host: uniweb\` requires authentication. Run \`uniweb login\` first, set
|
|
934
|
+
\`UNIWEB_TOKEN=<bearer>\` env var, or use a static-host adapter that
|
|
935
|
+
doesn't need a Uniweb account. CI / agents / piped stdin auto-detect
|
|
936
|
+
non-interactive mode and bail with an actionable error instead of
|
|
937
|
+
hanging on a browser callback.
|
|
938
|
+
|
|
939
|
+
${colors.bright}Examples:${colors.reset}
|
|
940
|
+
uniweb deploy # Default (host=uniweb)
|
|
941
|
+
uniweb deploy --dry-run # Print summary, no writes
|
|
942
|
+
uniweb deploy --host=cloudflare-pages # One-off override
|
|
943
|
+
uniweb deploy --target=preview # Pick named target from deploy.yml
|
|
944
|
+
`,
|
|
945
|
+
publish: `
|
|
946
|
+
${colors.cyan}${colors.bright}uniweb publish${colors.reset} ${colors.dim}— Publish a foundation to the catalog${colors.reset}
|
|
947
|
+
|
|
948
|
+
${colors.bright}Usage:${colors.reset}
|
|
949
|
+
uniweb publish [@org/name] [options]
|
|
950
|
+
|
|
951
|
+
For site-bound foundations (one foundation, one site), use \`uniweb deploy\`
|
|
952
|
+
instead — it auto-publishes under a site-scoped slot, no naming ceremony.
|
|
953
|
+
|
|
954
|
+
${colors.bright}Options:${colors.reset}
|
|
955
|
+
--catalog Confirm publish to the public catalog (required in CI)
|
|
956
|
+
--propagate Walk trusting sites' policy waves (default: silent)
|
|
957
|
+
--name <id> Foundation id (overrides package.json::uniweb.id)
|
|
958
|
+
--namespace <ns> Force org-scope namespace (overrides package.json)
|
|
959
|
+
--local Internal: publish to the unicloud mock (see workspace root CLAUDE.md)
|
|
960
|
+
--registry <url> Use a specific registry URL
|
|
961
|
+
--edit-access <p> "open" or "restricted" (default: restricted)
|
|
962
|
+
--dry-run Show what would be published without uploading
|
|
963
|
+
--non-interactive Fail with usage info instead of prompting
|
|
964
|
+
`,
|
|
965
|
+
create: `
|
|
966
|
+
${colors.cyan}${colors.bright}uniweb create${colors.reset} ${colors.dim}— Create a new project${colors.reset}
|
|
967
|
+
|
|
968
|
+
${colors.bright}Usage:${colors.reset}
|
|
969
|
+
uniweb create [name] [options]
|
|
970
|
+
|
|
971
|
+
${colors.bright}Options:${colors.reset}
|
|
972
|
+
--template <type> Project template (default: starter)
|
|
973
|
+
Built-in: starter, none, marketing
|
|
974
|
+
Local: ./path/to/template
|
|
975
|
+
npm: @scope/template-name
|
|
976
|
+
GitHub: github:user/repo or https://github.com/user/repo
|
|
977
|
+
--blank Create an empty workspace (grow with \`uniweb add\`)
|
|
978
|
+
--name <name> Project display name
|
|
979
|
+
--no-git Skip git repository initialization
|
|
980
|
+
|
|
981
|
+
${colors.bright}Examples:${colors.reset}
|
|
982
|
+
uniweb create my-project # Foundation + site + starter content
|
|
983
|
+
uniweb create my-project --template marketing # Official template
|
|
984
|
+
uniweb create my-project --blank # Empty workspace
|
|
985
|
+
`,
|
|
986
|
+
dev: `
|
|
987
|
+
${colors.cyan}${colors.bright}uniweb dev${colors.reset} ${colors.dim}— Start a dev server for a site${colors.reset}
|
|
988
|
+
|
|
989
|
+
${colors.bright}Usage:${colors.reset}
|
|
990
|
+
uniweb dev Start dev server for the (single) site
|
|
991
|
+
uniweb dev <site> Start dev server for a specific site
|
|
992
|
+
uniweb dev --site <name> Same, with explicit flag form
|
|
993
|
+
|
|
994
|
+
Thin wrapper around the package manager's workspace-filtered \`dev\`
|
|
995
|
+
script (\`pnpm --filter <site> dev\` or \`npm -w <site> run dev\`). Picks
|
|
996
|
+
the single site automatically; for multi-site workspaces the first
|
|
997
|
+
site runs by default with a notice pointing at \`--site\` for explicit
|
|
998
|
+
selection.
|
|
999
|
+
`,
|
|
1000
|
+
build: `
|
|
1001
|
+
${colors.cyan}${colors.bright}uniweb build${colors.reset} ${colors.dim}— Build the current project${colors.reset}
|
|
1002
|
+
|
|
1003
|
+
${colors.bright}Usage:${colors.reset}
|
|
1004
|
+
uniweb build [options]
|
|
1005
|
+
|
|
1006
|
+
At workspace root, builds all foundations first, then all sites.
|
|
1007
|
+
Pre-rendering is enabled by default when build.prerender: true in site.yml.
|
|
1008
|
+
|
|
1009
|
+
${colors.bright}Options:${colors.reset}
|
|
1010
|
+
--target <type> Build target (foundation, site) — auto-detected if not specified
|
|
1011
|
+
--prerender Force pre-rendering (overrides site.yml)
|
|
1012
|
+
--no-prerender Skip pre-rendering (overrides site.yml)
|
|
1013
|
+
--foundation-dir Path to foundation directory (for prerendering)
|
|
1014
|
+
--host <name> Apply host-specific postBuild (e.g., cloudflare-pages emits _redirects)
|
|
1015
|
+
--platform <name> (Deprecated alias for --host)
|
|
1016
|
+
`,
|
|
1017
|
+
add: `
|
|
1018
|
+
${colors.cyan}${colors.bright}uniweb add${colors.reset} ${colors.dim}— Add a foundation, site, or extension${colors.reset}
|
|
1019
|
+
|
|
1020
|
+
${colors.bright}Subcommands:${colors.reset}
|
|
1021
|
+
add project [name] Add a co-located foundation + site pair
|
|
1022
|
+
add foundation [name] Add a foundation (--from, --path, --project)
|
|
1023
|
+
add site [name] Add a site (--from, --foundation, --path, --project)
|
|
1024
|
+
add extension <name> Add an extension (--from, --site, --path)
|
|
1025
|
+
add section <name> Add a section type to a foundation (--foundation)
|
|
1026
|
+
|
|
1027
|
+
${colors.bright}Common options:${colors.reset}
|
|
1028
|
+
--from <template> Source content from a template
|
|
1029
|
+
--path <dir> Override default folder location
|
|
1030
|
+
--foundation <name> Wire site/extension to this foundation (CI-friendly)
|
|
1031
|
+
--site <name> Wire extension to this site (CI-friendly)
|
|
1032
|
+
--non-interactive Fail with usage info instead of prompting
|
|
1033
|
+
`,
|
|
1034
|
+
export: `
|
|
1035
|
+
${colors.cyan}${colors.bright}uniweb export${colors.reset} ${colors.dim}— Export a self-contained site for third-party hosting${colors.reset}
|
|
1036
|
+
|
|
1037
|
+
${colors.bright}Usage:${colors.reset}
|
|
1038
|
+
uniweb export [options]
|
|
1039
|
+
|
|
1040
|
+
Builds dist/ and prints upload examples for common static hosts. No login,
|
|
1041
|
+
no deploy step — you push the artifact to your host of choice yourself.
|
|
1042
|
+
For Uniweb-hosted sites, use \`uniweb deploy\`.
|
|
1043
|
+
|
|
1044
|
+
${colors.bright}Options:${colors.reset}
|
|
1045
|
+
--no-prerender Skip per-page prerendered HTML
|
|
1046
|
+
--host <name> Apply host-specific postBuild (cloudflare-pages, github-pages, …)
|
|
1047
|
+
`,
|
|
1048
|
+
doctor: `
|
|
1049
|
+
${colors.cyan}${colors.bright}uniweb doctor${colors.reset} ${colors.dim}— Diagnose project configuration issues${colors.reset}
|
|
1050
|
+
|
|
1051
|
+
${colors.bright}Usage:${colors.reset}
|
|
1052
|
+
uniweb doctor [options]
|
|
1053
|
+
|
|
1054
|
+
${colors.bright}Options:${colors.reset}
|
|
1055
|
+
--fix Apply fixes for safely-fixable issues
|
|
1056
|
+
--fix <issue-id> Apply fix for a specific issue id only
|
|
1057
|
+
--non-interactive Fail with usage info instead of prompting
|
|
1058
|
+
|
|
1059
|
+
Exit code is 1 if errors are found (warnings only → exit 0).
|
|
1060
|
+
`,
|
|
1061
|
+
rename: `
|
|
1062
|
+
${colors.cyan}${colors.bright}uniweb rename${colors.reset} ${colors.dim}— Rename a workspace package${colors.reset}
|
|
1063
|
+
|
|
1064
|
+
${colors.bright}Usage:${colors.reset}
|
|
1065
|
+
uniweb rename foundation <old> <new>
|
|
1066
|
+
|
|
1067
|
+
Today supports renaming foundations only. Updates folder name, foundation
|
|
1068
|
+
package.json::name, every dependent site's site.yml::foundation, every
|
|
1069
|
+
dependent site's package.json::dependencies, pnpm-workspace.yaml, and
|
|
1070
|
+
package.json::workspaces. Transactional — bails on conflict before any
|
|
1071
|
+
filesystem mutation.
|
|
1072
|
+
`,
|
|
1073
|
+
login: `
|
|
1074
|
+
${colors.cyan}${colors.bright}uniweb login${colors.reset} ${colors.dim}— Log in to your Uniweb account${colors.reset}
|
|
1075
|
+
|
|
1076
|
+
${colors.bright}Usage:${colors.reset}
|
|
1077
|
+
uniweb login [options]
|
|
1078
|
+
|
|
1079
|
+
Opens a browser to hub.uniweb.app for OAuth-style login, then captures
|
|
1080
|
+
the token via a loopback callback. Falls back to a paste-token prompt
|
|
1081
|
+
if the browser flow fails.
|
|
1082
|
+
|
|
1083
|
+
${colors.bright}Options:${colors.reset}
|
|
1084
|
+
--backend <url> Override the auth backend (default: https://hub.uniweb.app)
|
|
1085
|
+
|
|
1086
|
+
In non-interactive mode (CI / no TTY / --non-interactive), this command
|
|
1087
|
+
errors out — set the \`UNIWEB_TOKEN\` env var instead, or run \`login\`
|
|
1088
|
+
once on a machine with a browser to seed ~/.uniweb/auth.json.
|
|
1089
|
+
`,
|
|
1090
|
+
invite: `
|
|
1091
|
+
${colors.cyan}${colors.bright}uniweb invite${colors.reset} ${colors.dim}— Create a foundation invite for a client${colors.reset}
|
|
1092
|
+
|
|
1093
|
+
${colors.bright}Usage:${colors.reset}
|
|
1094
|
+
uniweb invite <email> [options]
|
|
1095
|
+
|
|
1096
|
+
${colors.bright}Options:${colors.reset}
|
|
1097
|
+
--uses <n> Max sites per invite (default: 1)
|
|
1098
|
+
--expires <days> Days until expiry (default: 30)
|
|
1099
|
+
--version <n> Major version to license (default: current)
|
|
1100
|
+
--list List invites for your foundation
|
|
1101
|
+
--revoke <id> Revoke an invite
|
|
1102
|
+
--resend <id> Resend an invite
|
|
1103
|
+
`,
|
|
1104
|
+
handoff: `
|
|
1105
|
+
${colors.cyan}${colors.bright}uniweb handoff${colors.reset} ${colors.dim}— Hand off a site to a client${colors.reset}
|
|
1106
|
+
|
|
1107
|
+
${colors.bright}Usage:${colors.reset}
|
|
1108
|
+
uniweb handoff <email> [options]
|
|
1109
|
+
|
|
1110
|
+
${colors.bright}Options:${colors.reset}
|
|
1111
|
+
--site <id> Site identifier (default: auto-generated)
|
|
1112
|
+
--web Show web-based handoff instructions instead
|
|
1113
|
+
`,
|
|
1114
|
+
template: `
|
|
1115
|
+
${colors.cyan}${colors.bright}uniweb template${colors.reset} ${colors.dim}— Manage cloud templates${colors.reset}
|
|
1116
|
+
|
|
1117
|
+
${colors.bright}Subcommands:${colors.reset}
|
|
1118
|
+
template publish Publish a site as a cloud template
|
|
1119
|
+
|
|
1120
|
+
${colors.bright}Publish Options:${colors.reset}
|
|
1121
|
+
--name <name> Template registry name (overrides site.yml template: field)
|
|
1122
|
+
--title <title> Display title (overrides site.yml name: field)
|
|
1123
|
+
--description <t> Description
|
|
1124
|
+
--registry <url> Registry URL (default: http://localhost:4001)
|
|
1125
|
+
`,
|
|
1126
|
+
docs: `
|
|
1127
|
+
${colors.cyan}${colors.bright}uniweb docs${colors.reset} ${colors.dim}— Generate component documentation${colors.reset}
|
|
1128
|
+
|
|
1129
|
+
${colors.bright}Subcommands:${colors.reset}
|
|
1130
|
+
docs Generate COMPONENTS.md from foundation schema
|
|
1131
|
+
docs site Show site.yml configuration reference
|
|
1132
|
+
docs page Show page.yml configuration reference
|
|
1133
|
+
docs meta Show component meta.js reference
|
|
1134
|
+
|
|
1135
|
+
${colors.bright}Options:${colors.reset}
|
|
1136
|
+
--output <file> Output filename (default: COMPONENTS.md)
|
|
1137
|
+
--from-source Read meta.js files directly instead of schema.json
|
|
1138
|
+
`,
|
|
1139
|
+
i18n: `
|
|
1140
|
+
${colors.cyan}${colors.bright}uniweb i18n${colors.reset} ${colors.dim}— Internationalization workflow${colors.reset}
|
|
1141
|
+
|
|
1142
|
+
${colors.bright}Subcommands:${colors.reset}
|
|
1143
|
+
i18n extract Extract translatable strings to manifest
|
|
1144
|
+
i18n sync Update manifest with content changes
|
|
1145
|
+
i18n status Show translation coverage per locale
|
|
1146
|
+
`,
|
|
1147
|
+
inspect: `
|
|
1148
|
+
${colors.cyan}${colors.bright}uniweb inspect${colors.reset} ${colors.dim}— Inspect parsed content shape${colors.reset}
|
|
1149
|
+
|
|
1150
|
+
${colors.bright}Usage:${colors.reset}
|
|
1151
|
+
uniweb inspect <path>
|
|
1152
|
+
|
|
1153
|
+
Prints the parsed content shape of a markdown file or folder — the
|
|
1154
|
+
{ content, params, items, … } object that components actually receive.
|
|
1155
|
+
Useful for debugging "why isn't my section getting X?".
|
|
1156
|
+
`,
|
|
1157
|
+
update: `
|
|
1158
|
+
${colors.cyan}${colors.bright}uniweb update${colors.reset} ${colors.dim}— Update the CLI itself, plus AGENTS.md when in a project${colors.reset}
|
|
1159
|
+
|
|
1160
|
+
${colors.bright}Usage:${colors.reset}
|
|
1161
|
+
uniweb update Self-update + (in project) refresh AGENTS.md
|
|
1162
|
+
uniweb update --agents-only Only refresh AGENTS.md (skip self-update)
|
|
1163
|
+
uniweb update --no-agents Only self-update (skip AGENTS.md)
|
|
1164
|
+
uniweb update --yes Skip the confirmation prompts
|
|
1165
|
+
|
|
1166
|
+
${colors.bright}What it does:${colors.reset}
|
|
1167
|
+
1. Self-update the global install via npm / pnpm / yarn (auto-detected).
|
|
1168
|
+
In TTY, prompts before running. In CI / non-interactive, prints the
|
|
1169
|
+
command and exits without running it.
|
|
1170
|
+
2. If the cwd resolves to a Uniweb project (root package.json declares
|
|
1171
|
+
\`uniweb\` as a dep), refreshes AGENTS.md from the CLI's bundled
|
|
1172
|
+
version. Outside a Uniweb project, this step is skipped — the
|
|
1173
|
+
command will not write AGENTS.md into unrelated directories.
|
|
1174
|
+
|
|
1175
|
+
${colors.bright}Project-local installs:${colors.reset}
|
|
1176
|
+
When the running CLI is project-local (lives in node_modules), self-
|
|
1177
|
+
update is a no-op — the version is pinned by your project's
|
|
1178
|
+
package.json. The verb prints that explanation and proceeds with the
|
|
1179
|
+
AGENTS.md refresh path only.
|
|
1180
|
+
`,
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
if (!blocks[command]) return false
|
|
1184
|
+
log(blocks[command])
|
|
1185
|
+
return true
|
|
1186
|
+
}
|
|
1187
|
+
|
|
822
1188
|
function showHelp() {
|
|
823
1189
|
log(`
|
|
824
1190
|
${colors.cyan}${colors.bright}Uniweb CLI${colors.reset} ${colors.dim}v${getCliVersion()}${colors.reset}
|
|
@@ -830,6 +1196,7 @@ ${colors.bright}Commands:${colors.reset}
|
|
|
830
1196
|
create [name] Create a new project
|
|
831
1197
|
add <type> [name] Add a foundation, site, or extension to a project
|
|
832
1198
|
rename <type> Rename a workspace package (foundation today)
|
|
1199
|
+
dev Start a dev server for a site
|
|
833
1200
|
build Build the current project
|
|
834
1201
|
deploy Deploy a site to Uniweb hosting
|
|
835
1202
|
export Export a self-contained site for third-party hosting
|
|
@@ -896,21 +1263,34 @@ ${colors.bright}Template Options:${colors.reset}
|
|
|
896
1263
|
--registry <url> Registry URL (default: http://localhost:4001)
|
|
897
1264
|
|
|
898
1265
|
${colors.bright}Deploy Options:${colors.reset}
|
|
1266
|
+
--target <name> Pick a target from deploy.yml (default: deploy.yml's \`default:\`)
|
|
1267
|
+
--host <name> Override the resolved target's host (does not persist).
|
|
1268
|
+
Without a value, opens an interactive picker (TTY only).
|
|
1269
|
+
Hosts: uniweb, cloudflare-pages, netlify, vercel,
|
|
1270
|
+
github-pages, s3-cloudfront, generic-static.
|
|
899
1271
|
--dry-run Resolve site.yml + foundation/runtime; print summary; no writes
|
|
900
1272
|
--no-auto-publish Don't auto-publish workspace-local foundation as part of deploy
|
|
1273
|
+
--no-save Skip the auto-save of lastDeploy in deploy.yml
|
|
1274
|
+
|
|
1275
|
+
${colors.bright}Dev Options:${colors.reset}
|
|
1276
|
+
<site> Site name to run (positional)
|
|
1277
|
+
--site <name> Site name to run (explicit form)
|
|
901
1278
|
|
|
902
1279
|
${colors.bright}Export Options:${colors.reset}
|
|
903
1280
|
--no-prerender Skip per-page prerendered HTML
|
|
1281
|
+
--host <name> Apply host-specific postBuild (cloudflare-pages, github-pages, …)
|
|
904
1282
|
|
|
905
1283
|
${colors.bright}Build Options:${colors.reset}
|
|
906
|
-
--target <type> Build target (foundation, site)
|
|
1284
|
+
--target <type> Build target (foundation, site) — auto-detected if not specified
|
|
907
1285
|
--prerender Force pre-rendering (overrides site.yml)
|
|
908
1286
|
--no-prerender Skip pre-rendering (overrides site.yml)
|
|
909
1287
|
--foundation-dir Path to foundation directory (for prerendering)
|
|
910
|
-
--
|
|
1288
|
+
--host <name> Apply host-specific postBuild (cloudflare-pages, s3-cloudfront, …)
|
|
1289
|
+
--platform <name> (Deprecated alias for --host)
|
|
911
1290
|
|
|
912
1291
|
At workspace root, builds all foundations first, then all sites.
|
|
913
|
-
Pre-rendering is enabled by default when build.prerender: true in site.yml
|
|
1292
|
+
Pre-rendering is enabled by default when build.prerender: true in site.yml.
|
|
1293
|
+
See \`uniweb <command> --help\` for command-specific detail and examples.
|
|
914
1294
|
|
|
915
1295
|
${colors.bright}Docs Subcommands:${colors.reset}
|
|
916
1296
|
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,17 +52,55 @@ 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
|
+
}
|
|
64
74
|
}
|
|
65
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Synchronously read the cache and print a 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
|
+
* Two call sites today, with different tone needs:
|
|
82
|
+
* - `create` (tone='eager'): loud leading notice — templates ship with
|
|
83
|
+
* the CLI, the user is about to scaffold files, this matters.
|
|
84
|
+
* - `--version` / `-v` (tone='soft'): brief trailing notice — the user
|
|
85
|
+
* was already asking about version, mention staleness while we're
|
|
86
|
+
* here. Goes to stderr so scripts capturing stdout aren't affected.
|
|
87
|
+
*
|
|
88
|
+
* @param {string} currentVersion
|
|
89
|
+
* @param {'eager'|'soft'} [tone='eager']
|
|
90
|
+
* @returns {boolean} true if a notification was printed
|
|
91
|
+
*/
|
|
92
|
+
export function maybeNotifyFromCache(currentVersion, tone = 'eager') {
|
|
93
|
+
const state = readState()
|
|
94
|
+
if (!state.latestVersion) return false
|
|
95
|
+
if (compareSemver(state.latestVersion, currentVersion) <= 0) return false
|
|
96
|
+
printNotification(currentVersion, state.latestVersion, tone)
|
|
97
|
+
return true
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Old name preserved as alias — `create` calls it without a tone arg
|
|
101
|
+
// and gets the eager default. Keeps that call site unchanged.
|
|
102
|
+
export const maybeEagerNotification = maybeNotifyFromCache
|
|
103
|
+
|
|
66
104
|
/**
|
|
67
105
|
* Start a non-blocking update check.
|
|
68
106
|
*
|