uniweb 0.12.15 → 0.12.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -5
- package/package.json +2 -2
- package/partials/agents.md +5 -0
- package/src/commands/update.js +352 -54
- package/src/framework-index.json +2 -2
- package/src/index.js +134 -24
- package/src/templates/processor.js +47 -0
- package/src/utils/pm.js +28 -2
- package/src/utils/scaffold.js +23 -1
package/README.md
CHANGED
|
@@ -40,12 +40,18 @@ Edit files in `site/pages/` and `src/sections/` to see changes instantly.
|
|
|
40
40
|
|
|
41
41
|
Use `--blank` for an empty workspace (no packages) — grow with `uniweb add`.
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
**Two starting points.** Either let the CLI create a new directory:
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
46
|
pnpm create uniweb my-site --template docs
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
…or scaffold inside an existing directory (e.g., a freshly-cloned GitHub repo):
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pnpm create uniweb . --template docs
|
|
53
|
+
```
|
|
54
|
+
|
|
49
55
|
### Local Scripts
|
|
50
56
|
|
|
51
57
|
Run these from the **project root**:
|
|
@@ -243,7 +249,7 @@ The parser extracts semantic elements from markdown—`title` from the first hea
|
|
|
243
249
|
|
|
244
250
|
## Foundations Are Portable
|
|
245
251
|
|
|
246
|
-
The `src/` folder (your project's foundation) ships with your project as a convenience, but a foundation is
|
|
252
|
+
The `src/` folder (your project's foundation) ships with your project as a convenience, but a foundation is dynamically loaded — sites reference it by configuration, not by folder proximity.
|
|
247
253
|
|
|
248
254
|
**Two ways to use a foundation:**
|
|
249
255
|
|
|
@@ -308,8 +314,7 @@ You (or your dev team) write the markdown. Deploy site + foundation together.
|
|
|
308
314
|
The shortest path to a live site is free, on GitHub Pages, with a custom domain:
|
|
309
315
|
|
|
310
316
|
```bash
|
|
311
|
-
|
|
312
|
-
cd my-site
|
|
317
|
+
uniweb create . # from within a freshly-cloned GitHub repo
|
|
313
318
|
uniweb add ci --host=github-pages
|
|
314
319
|
# Commit, push to GitHub, enable Pages in repo settings → live site
|
|
315
320
|
```
|
|
@@ -346,6 +351,7 @@ A future version will let markdown in a git repo and content in the Uniweb apps
|
|
|
346
351
|
| `uniweb export` | Produce a self-contained `dist/` for any static host. You upload it yourself. `--host=<adapter>` adds host-specific helper files. |
|
|
347
352
|
| `uniweb publish @org/name` | Publish a foundation to the registry (path 2). |
|
|
348
353
|
| `uniweb build` | Inspect a build locally. For shipping, use `deploy` or `export`. |
|
|
354
|
+
| `uniweb update` | Reconcile workspace state with the CLI: self-update the global install, align `@uniweb/*` deps in every `package.json` to the CLI's matrix, refresh `AGENTS.md`. Refuses to outpace declared deps with a stale doc. |
|
|
349
355
|
|
|
350
356
|
`--host=<adapter>` is the same option across `deploy`, `export`, and `add ci`. Built-in adapters: `cloudflare-pages`, `netlify`, `github-pages`, `vercel`, `s3-cloudfront`, `generic-static`. Each adapter implements only the operations it supports — `add ci` is currently `github-pages`-only because it's the only one that needs a workflow file in the repo.
|
|
351
357
|
|
|
@@ -373,7 +379,7 @@ Full documentation is available at **[github.com/uniweb/docs](https://github.com
|
|
|
373
379
|
| Content Structure | [How markdown becomes component props](https://github.com/uniweb/docs/blob/main/reference/content-structure.md) |
|
|
374
380
|
| Component Metadata | [The meta.js schema](https://github.com/uniweb/docs/blob/main/reference/component-metadata.md) |
|
|
375
381
|
| Site Configuration | [site.yml reference](https://github.com/uniweb/docs/blob/main/reference/site-configuration.md) |
|
|
376
|
-
| CLI Commands | [
|
|
382
|
+
| CLI Commands | [All CLI commands and flags](https://github.com/uniweb/docs/blob/main/reference/cli-commands.md) |
|
|
377
383
|
| Templates | [Built-in, official, and external templates](https://github.com/uniweb/docs/blob/main/getting-started/templates.md) |
|
|
378
384
|
| Deployment | [Two artifacts, two verbs — bundled, linked, and per-host recipes](https://github.com/uniweb/docs/blob/main/development/deploying.md) |
|
|
379
385
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.17",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"@uniweb/kit": "0.9.11"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"@uniweb/content-reader": "1.1.10",
|
|
50
49
|
"@uniweb/build": "0.14.3",
|
|
50
|
+
"@uniweb/content-reader": "1.1.10",
|
|
51
51
|
"@uniweb/semantic-parser": "1.1.17"
|
|
52
52
|
},
|
|
53
53
|
"peerDependenciesMeta": {
|
package/partials/agents.md
CHANGED
|
@@ -150,6 +150,9 @@ uniweb deploy --dry-run # Resolve foundation/runtime + print summary;
|
|
|
150
150
|
uniweb export # Build dist/ for any static host (no Uniweb account)
|
|
151
151
|
uniweb publish # Publish a foundation to the Uniweb registry
|
|
152
152
|
uniweb doctor # Diagnose project configuration issues (--fix to auto-repair)
|
|
153
|
+
uniweb update # Reconcile workspace state with the CLI: align @uniweb/*
|
|
154
|
+
# deps in every package.json + refresh this AGENTS.md.
|
|
155
|
+
# Use --dry-run to preview, --deps-only to skip the doc.
|
|
153
156
|
|
|
154
157
|
# Help
|
|
155
158
|
uniweb --help # Top-level help
|
|
@@ -158,6 +161,8 @@ uniweb <command> --help # Per-command help (no side effects)
|
|
|
158
161
|
|
|
159
162
|
`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.
|
|
160
163
|
|
|
164
|
+
If this AGENTS.md was stamped against an older CLI than the workspace's installed `@uniweb/*` packages, run `uniweb update --dry-run` to see the gap. The verb refuses to refresh the doc while declared deps lag the CLI — a stale doc that documents features the installed code doesn't have is worse than no refresh.
|
|
165
|
+
|
|
161
166
|
---
|
|
162
167
|
|
|
163
168
|
## `package.json` `uniweb` configuration
|
package/src/commands/update.js
CHANGED
|
@@ -1,35 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* uniweb update —
|
|
3
|
-
*
|
|
2
|
+
* uniweb update — Reconcile a Uniweb workspace's state with the running
|
|
3
|
+
* CLI's expectations. Three convergence steps, in order:
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* 1. Self-update the global CLI install (npm / pnpm / yarn auto-detected).
|
|
6
|
+
* 2. Align workspace `@uniweb/*` + `uniweb` deps to the CLI's bundled
|
|
7
|
+
* version matrix (`getResolvedVersions`), then run `<pm> install`.
|
|
8
|
+
* 3. Refresh AGENTS.md from the CLI's bundled partial.
|
|
6
9
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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.
|
|
10
|
+
* Why steps 2 and 3 belong together: AGENTS.md is regenerated from the
|
|
11
|
+
* CLI's *current* partials and stamped with `cliVersion`. Refreshing it
|
|
12
|
+
* while declared deps in `package.json` lag the CLI silently produces a
|
|
13
|
+
* doc that documents features the installed code doesn't have. The
|
|
14
|
+
* verb's drift gate refuses that combination unless `--allow-mismatch`
|
|
15
|
+
* is explicit.
|
|
22
16
|
*
|
|
23
17
|
* Flags:
|
|
24
|
-
* --agents-only
|
|
25
|
-
* --
|
|
26
|
-
* --
|
|
27
|
-
* --
|
|
18
|
+
* --agents-only Skip self-update + deps; only refresh AGENTS.md.
|
|
19
|
+
* --deps-only Skip self-update + AGENTS.md; only align deps.
|
|
20
|
+
* --no-agents Skip the AGENTS.md step.
|
|
21
|
+
* --no-deps Skip the deps-alignment step.
|
|
22
|
+
* --dry-run Print survey + would-be writes; no mutations.
|
|
23
|
+
* --allow-mismatch Permit AGENTS.md refresh when declared deps lag.
|
|
24
|
+
* --yes Skip confirmation prompts (still respects gates).
|
|
25
|
+
* --non-interactive Auto-detected; never auto-installs from a script.
|
|
28
26
|
*
|
|
29
27
|
* Project-local case (CLI lives in node_modules, not global): self-update
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* AGENTS.md refresh path only.
|
|
28
|
+
* is a no-op (the version is pinned by package.json). Deps + AGENTS.md
|
|
29
|
+
* paths still run.
|
|
33
30
|
*/
|
|
34
31
|
|
|
35
32
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
@@ -37,10 +34,11 @@ import { join } from 'node:path'
|
|
|
37
34
|
import { spawn } from 'node:child_process'
|
|
38
35
|
import prompts from 'prompts'
|
|
39
36
|
|
|
40
|
-
import { findWorkspaceRoot } from '../utils/workspace.js'
|
|
37
|
+
import { findWorkspaceRoot, getWorkspacePackages } from '../utils/workspace.js'
|
|
41
38
|
import { readAgentsVersion, generateAgentsContent } from '../utils/agents-stamp.js'
|
|
42
|
-
import { getCliVersion } from '../versions.js'
|
|
39
|
+
import { getCliVersion, getResolvedVersions, updatePackageVersions } from '../versions.js'
|
|
43
40
|
import { isNonInteractive } from '../utils/interactive.js'
|
|
41
|
+
import { detectWorkspacePm, installCmd } from '../utils/pm.js'
|
|
44
42
|
|
|
45
43
|
const colors = {
|
|
46
44
|
reset: '\x1b[0m',
|
|
@@ -55,6 +53,7 @@ const colors = {
|
|
|
55
53
|
const success = (msg) => console.log(`${colors.green}✓${colors.reset} ${msg}`)
|
|
56
54
|
const warn = (msg) => console.log(`${colors.yellow}⚠${colors.reset} ${msg}`)
|
|
57
55
|
const error = (msg) => console.log(`${colors.red}✗${colors.reset} ${msg}`)
|
|
56
|
+
const info = (msg) => console.log(`${colors.cyan}ℹ${colors.reset} ${msg}`)
|
|
58
57
|
const log = console.log
|
|
59
58
|
|
|
60
59
|
/**
|
|
@@ -72,8 +71,8 @@ function isGlobalInstall() {
|
|
|
72
71
|
/**
|
|
73
72
|
* Find a *Uniweb* workspace root from cwd. Stricter than findWorkspaceRoot
|
|
74
73
|
* — also requires that the workspace's root package.json declares uniweb
|
|
75
|
-
* as a dep or devDep. Otherwise
|
|
76
|
-
*
|
|
74
|
+
* as a dep or devDep. Otherwise we'd write AGENTS.md and edit package.json
|
|
75
|
+
* files in unrelated monorepos.
|
|
77
76
|
*/
|
|
78
77
|
function findUniwebWorkspace(cwd) {
|
|
79
78
|
const workspaceDir = findWorkspaceRoot(cwd)
|
|
@@ -90,9 +89,9 @@ function findUniwebWorkspace(cwd) {
|
|
|
90
89
|
}
|
|
91
90
|
|
|
92
91
|
/**
|
|
93
|
-
* Detect the package manager that owns the global install.
|
|
94
|
-
* based
|
|
95
|
-
*
|
|
92
|
+
* Detect the package manager that owns the *global* CLI install.
|
|
93
|
+
* Path-based (different signal than detectWorkspacePm, which reads
|
|
94
|
+
* lockfiles in the workspace).
|
|
96
95
|
*
|
|
97
96
|
* @returns {'pnpm'|'yarn'|'npm'}
|
|
98
97
|
*/
|
|
@@ -127,11 +126,21 @@ async function fetchLatestVersion() {
|
|
|
127
126
|
}
|
|
128
127
|
|
|
129
128
|
/**
|
|
130
|
-
*
|
|
129
|
+
* Strip a leading semver range operator (^, ~, >=, <, etc.) so two specs
|
|
130
|
+
* can be compared by their underlying version. Range expressions like
|
|
131
|
+
* ">=0.5 <0.7" aren't fully parsed — we take the first version-shaped
|
|
132
|
+
* token. Sufficient for `@uniweb/*` deps which use `^x.y.z` consistently.
|
|
133
|
+
*/
|
|
134
|
+
function stripRange(spec) {
|
|
135
|
+
return (spec || '').replace(/^[\^~>=<\s]+/, '').trim().split(/\s+/)[0] || ''
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Compare two version specs (range prefix tolerated). Returns 1/-1/0.
|
|
131
140
|
*/
|
|
132
141
|
function compareSemver(a, b) {
|
|
133
|
-
const pa = a.split('.').map(Number)
|
|
134
|
-
const pb = b.split('.').map(Number)
|
|
142
|
+
const pa = stripRange(a).split('.').map(Number)
|
|
143
|
+
const pb = stripRange(b).split('.').map(Number)
|
|
135
144
|
for (let i = 0; i < 3; i++) {
|
|
136
145
|
if ((pa[i] || 0) > (pb[i] || 0)) return 1
|
|
137
146
|
if ((pa[i] || 0) < (pb[i] || 0)) return -1
|
|
@@ -142,28 +151,177 @@ function compareSemver(a, b) {
|
|
|
142
151
|
/**
|
|
143
152
|
* Run a shell command, inheriting stdio. Resolves with the exit code.
|
|
144
153
|
*/
|
|
145
|
-
function runCommand(cmd) {
|
|
154
|
+
function runCommand(cmd, cwd) {
|
|
146
155
|
return new Promise((resolve) => {
|
|
147
156
|
const [bin, ...rest] = cmd.split(' ')
|
|
148
|
-
const child = spawn(bin, rest, { stdio: 'inherit' })
|
|
157
|
+
const child = spawn(bin, rest, { stdio: 'inherit', cwd })
|
|
149
158
|
child.on('close', code => resolve(code ?? 0))
|
|
150
159
|
child.on('error', () => resolve(1))
|
|
151
160
|
})
|
|
152
161
|
}
|
|
153
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Survey workspace `@uniweb/*` and `uniweb` deps against the CLI's
|
|
165
|
+
* bundled version matrix. Returns a structured report with one row per
|
|
166
|
+
* (package directory, dep section, dep name).
|
|
167
|
+
*
|
|
168
|
+
* Comparison is on *declared* versions (package.json), not installed
|
|
169
|
+
* (node_modules) — that's what the user committed and what they'll
|
|
170
|
+
* `git diff` after `applyDepUpdates`.
|
|
171
|
+
*/
|
|
172
|
+
async function surveyVersions(workspaceDir) {
|
|
173
|
+
const targets = getResolvedVersions()
|
|
174
|
+
const packages = await getWorkspacePackages(workspaceDir)
|
|
175
|
+
const dirs = ['', ...packages]
|
|
176
|
+
const rows = []
|
|
177
|
+
let anyDrift = false
|
|
178
|
+
let anyAhead = false
|
|
179
|
+
|
|
180
|
+
for (const relDir of dirs) {
|
|
181
|
+
const pkgDir = relDir ? join(workspaceDir, relDir) : workspaceDir
|
|
182
|
+
const pkgPath = join(pkgDir, 'package.json')
|
|
183
|
+
if (!existsSync(pkgPath)) continue
|
|
184
|
+
let pkg
|
|
185
|
+
try { pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) } catch { continue }
|
|
186
|
+
|
|
187
|
+
for (const sectionName of ['dependencies', 'devDependencies', 'peerDependencies']) {
|
|
188
|
+
const section = pkg[sectionName]
|
|
189
|
+
if (!section) continue
|
|
190
|
+
for (const [name, current] of Object.entries(section)) {
|
|
191
|
+
if (!(name.startsWith('@uniweb/') || name === 'uniweb')) continue
|
|
192
|
+
const target = targets[name]
|
|
193
|
+
if (!target) continue
|
|
194
|
+
const cmp = compareSemver(target, current)
|
|
195
|
+
let status
|
|
196
|
+
if (cmp > 0) { status = 'behind'; anyDrift = true }
|
|
197
|
+
else if (cmp < 0) { status = 'ahead'; anyAhead = true }
|
|
198
|
+
else { status = 'aligned' }
|
|
199
|
+
rows.push({
|
|
200
|
+
relDir: relDir || '(root)',
|
|
201
|
+
section: sectionName,
|
|
202
|
+
name,
|
|
203
|
+
current,
|
|
204
|
+
target,
|
|
205
|
+
status,
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return { targets, rows, anyDrift, anyAhead }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Print the survey report grouped by package directory.
|
|
216
|
+
*/
|
|
217
|
+
function printSurvey(report, cliVersion, agentsVersion) {
|
|
218
|
+
log('')
|
|
219
|
+
log(`${colors.bright}uniweb CLI:${colors.reset} v${cliVersion}`)
|
|
220
|
+
log(`${colors.bright}AGENTS.md stamp:${colors.reset} ${agentsVersion ? 'v' + agentsVersion : colors.dim + '(none)' + colors.reset}`)
|
|
221
|
+
log('')
|
|
222
|
+
|
|
223
|
+
if (report.rows.length === 0) {
|
|
224
|
+
log(`${colors.dim}No @uniweb/* deps found in workspace package.json files.${colors.reset}`)
|
|
225
|
+
log('')
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const byDir = {}
|
|
230
|
+
for (const row of report.rows) {
|
|
231
|
+
if (!byDir[row.relDir]) byDir[row.relDir] = []
|
|
232
|
+
byDir[row.relDir].push(row)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
log(`${colors.bright}Workspace deps (declared):${colors.reset}`)
|
|
236
|
+
for (const [dir, dirRows] of Object.entries(byDir)) {
|
|
237
|
+
log(` ${colors.dim}${dir}/${colors.reset}`)
|
|
238
|
+
const maxName = Math.max(...dirRows.map(r => r.name.length))
|
|
239
|
+
for (const row of dirRows) {
|
|
240
|
+
const padding = ' '.repeat(maxName - row.name.length)
|
|
241
|
+
let icon, statusText
|
|
242
|
+
if (row.status === 'aligned') {
|
|
243
|
+
icon = `${colors.green}✓${colors.reset}`
|
|
244
|
+
statusText = `${colors.dim}aligned${colors.reset}`
|
|
245
|
+
} else if (row.status === 'behind') {
|
|
246
|
+
icon = `${colors.yellow}✗${colors.reset}`
|
|
247
|
+
statusText = `${colors.yellow}behind${colors.reset}`
|
|
248
|
+
} else {
|
|
249
|
+
icon = `${colors.cyan}↑${colors.reset}`
|
|
250
|
+
statusText = `${colors.cyan}ahead of CLI${colors.reset}`
|
|
251
|
+
}
|
|
252
|
+
log(` ${icon} ${row.name}${padding} ${row.current.padEnd(10)} → ${row.target.padEnd(10)} ${statusText}`)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
log('')
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Apply the CLI's bundled matrix to every workspace package.json.
|
|
260
|
+
* `updatePackageVersions` only touches `@uniweb/*` + `uniweb` keys, so
|
|
261
|
+
* unrelated deps (`react`, `vite`, `file:../foundation`, etc.) are left
|
|
262
|
+
* untouched. Returns the list of paths that actually changed.
|
|
263
|
+
*/
|
|
264
|
+
async function applyDepUpdates(workspaceDir, dryRun) {
|
|
265
|
+
const packages = await getWorkspacePackages(workspaceDir)
|
|
266
|
+
const dirs = ['', ...packages]
|
|
267
|
+
const edited = []
|
|
268
|
+
|
|
269
|
+
for (const relDir of dirs) {
|
|
270
|
+
const pkgDir = relDir ? join(workspaceDir, relDir) : workspaceDir
|
|
271
|
+
const pkgPath = join(pkgDir, 'package.json')
|
|
272
|
+
if (!existsSync(pkgPath)) continue
|
|
273
|
+
const original = readFileSync(pkgPath, 'utf8')
|
|
274
|
+
let pkg
|
|
275
|
+
try { pkg = JSON.parse(original) } catch { continue }
|
|
276
|
+
|
|
277
|
+
const updated = updatePackageVersions(pkg)
|
|
278
|
+
const newContent = JSON.stringify(updated, null, 2) + (original.endsWith('\n') ? '\n' : '')
|
|
279
|
+
|
|
280
|
+
if (newContent !== original) {
|
|
281
|
+
edited.push(pkgPath)
|
|
282
|
+
if (!dryRun) writeFileSync(pkgPath, newContent)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return edited
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function relativize(path, root) {
|
|
290
|
+
return path.startsWith(root) ? path.slice(root.length + 1) : path
|
|
291
|
+
}
|
|
292
|
+
|
|
154
293
|
export async function update(args = []) {
|
|
155
294
|
const agentsOnly = args.includes('--agents-only')
|
|
156
|
-
const
|
|
157
|
-
const
|
|
295
|
+
const depsOnly = args.includes('--deps-only')
|
|
296
|
+
const skipAgents = args.includes('--no-agents') || depsOnly
|
|
297
|
+
const skipDeps = args.includes('--no-deps') || agentsOnly
|
|
298
|
+
const dryRun = args.includes('--dry-run')
|
|
299
|
+
const allowMismatch = args.includes('--allow-mismatch')
|
|
300
|
+
const nonInteractive = isNonInteractive(args)
|
|
301
|
+
const skipPrompt = args.includes('--yes') || nonInteractive || dryRun
|
|
158
302
|
const isGlobal = isGlobalInstall()
|
|
159
303
|
const workspaceDir = findUniwebWorkspace(process.cwd())
|
|
160
304
|
const inProject = !!workspaceDir
|
|
161
305
|
const cliVersion = getCliVersion()
|
|
162
306
|
|
|
163
|
-
|
|
164
|
-
|
|
307
|
+
if ((agentsOnly || depsOnly) && !inProject) {
|
|
308
|
+
error(`${agentsOnly ? '--agents-only' : '--deps-only'} requires a Uniweb project (no \`uniweb\` dep in the workspace root).`)
|
|
309
|
+
log(`${colors.dim}Run this command from inside a project created by${colors.reset} ${colors.cyan}uniweb create${colors.reset}${colors.dim}.${colors.reset}`)
|
|
310
|
+
process.exit(1)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ── Survey first (always, when in a Uniweb project) ──────────────
|
|
314
|
+
let survey = null
|
|
315
|
+
let agentsVersion = null
|
|
316
|
+
if (inProject) {
|
|
317
|
+
survey = await surveyVersions(workspaceDir)
|
|
318
|
+
agentsVersion = readAgentsVersion(join(workspaceDir, 'AGENTS.md'))
|
|
319
|
+
printSurvey(survey, cliVersion, agentsVersion)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ── Step 1: Self-update path ─────────────────────────────────────
|
|
323
|
+
if (!agentsOnly && !depsOnly) {
|
|
165
324
|
if (!isGlobal) {
|
|
166
|
-
// Project-local: can't self-update meaningfully.
|
|
167
325
|
log(`${colors.dim}Running the project-local CLI (v${cliVersion}). This copy is pinned by your${colors.reset}`)
|
|
168
326
|
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
327
|
log('')
|
|
@@ -184,7 +342,10 @@ export async function update(args = []) {
|
|
|
184
342
|
log(`${colors.dim}Will run:${colors.reset} ${colors.cyan}${cmd}${colors.reset}`)
|
|
185
343
|
log('')
|
|
186
344
|
|
|
187
|
-
if (
|
|
345
|
+
if (dryRun) {
|
|
346
|
+
info(`${colors.dim}--dry-run: would run \`${cmd}\`.${colors.reset}`)
|
|
347
|
+
log('')
|
|
348
|
+
} else if (skipPrompt) {
|
|
188
349
|
log(`${colors.dim}Non-interactive — skipping self-update. Run the command above to update.${colors.reset}`)
|
|
189
350
|
log('')
|
|
190
351
|
} else {
|
|
@@ -211,15 +372,148 @@ export async function update(args = []) {
|
|
|
211
372
|
}
|
|
212
373
|
}
|
|
213
374
|
|
|
214
|
-
//
|
|
375
|
+
// ── Step 2: Deps alignment ───────────────────────────────────────
|
|
376
|
+
if (!skipDeps && inProject && survey) {
|
|
377
|
+
if (!survey.anyDrift) {
|
|
378
|
+
success('Workspace deps are aligned with the CLI.')
|
|
379
|
+
if (survey.anyAhead) {
|
|
380
|
+
log(`${colors.dim}(Some deps are ahead of the CLI's bundled matrix — left untouched.)${colors.reset}`)
|
|
381
|
+
}
|
|
382
|
+
log('')
|
|
383
|
+
} else {
|
|
384
|
+
log(`${colors.yellow}⚠${colors.reset} Some workspace deps lag the CLI's bundled matrix.`)
|
|
385
|
+
log('')
|
|
386
|
+
|
|
387
|
+
let proceed
|
|
388
|
+
if (dryRun) {
|
|
389
|
+
proceed = false
|
|
390
|
+
} else if (skipPrompt) {
|
|
391
|
+
proceed = !nonInteractive || args.includes('--yes')
|
|
392
|
+
// In CI without --yes, refuse to mutate. The survey above is the report.
|
|
393
|
+
if (!proceed) {
|
|
394
|
+
info(`${colors.dim}Non-interactive — printing the alignment plan; not editing files.${colors.reset}`)
|
|
395
|
+
log(`${colors.dim}To apply, re-run with${colors.reset} ${colors.cyan}--yes${colors.reset}${colors.dim}, or align manually:${colors.reset}`)
|
|
396
|
+
log(` ${colors.cyan}pnpm update "@uniweb/*" uniweb -r${colors.reset}`)
|
|
397
|
+
log('')
|
|
398
|
+
}
|
|
399
|
+
} else {
|
|
400
|
+
const { go } = await prompts({
|
|
401
|
+
type: 'confirm',
|
|
402
|
+
name: 'go',
|
|
403
|
+
message: `Edit workspace package.json files to align with v${cliVersion}?`,
|
|
404
|
+
initial: true,
|
|
405
|
+
})
|
|
406
|
+
proceed = !!go
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (dryRun) {
|
|
410
|
+
const wouldEdit = await applyDepUpdates(workspaceDir, true)
|
|
411
|
+
if (wouldEdit.length > 0) {
|
|
412
|
+
info('Dry-run: would update package.json in:')
|
|
413
|
+
for (const path of wouldEdit) log(` ${colors.dim}- ${relativize(path, workspaceDir)}${colors.reset}`)
|
|
414
|
+
const pm = detectWorkspacePm(workspaceDir)
|
|
415
|
+
if (pm) {
|
|
416
|
+
log(`${colors.dim}Then would run:${colors.reset} ${colors.cyan}${installCmd(pm)}${colors.reset}`)
|
|
417
|
+
} else {
|
|
418
|
+
log(`${colors.dim}Then would prompt for an install command (no lockfile detected).${colors.reset}`)
|
|
419
|
+
}
|
|
420
|
+
log('')
|
|
421
|
+
}
|
|
422
|
+
} else if (proceed) {
|
|
423
|
+
const edited = await applyDepUpdates(workspaceDir, false)
|
|
424
|
+
if (edited.length === 0) {
|
|
425
|
+
info('No package.json files needed changes.')
|
|
426
|
+
log('')
|
|
427
|
+
} else {
|
|
428
|
+
for (const path of edited) {
|
|
429
|
+
success(`Updated ${relativize(path, workspaceDir)}`)
|
|
430
|
+
}
|
|
431
|
+
log('')
|
|
432
|
+
|
|
433
|
+
// Resolve the workspace PM (lockfile-driven). If absent, ask.
|
|
434
|
+
let pm = detectWorkspacePm(workspaceDir)
|
|
435
|
+
if (!pm) {
|
|
436
|
+
if (nonInteractive) {
|
|
437
|
+
warn('No lockfile in workspace root — cannot pick an install command for you.')
|
|
438
|
+
log(`${colors.dim}Run one of:${colors.reset} ${colors.cyan}pnpm install${colors.reset} ${colors.dim}/${colors.reset} ${colors.cyan}yarn install${colors.reset} ${colors.dim}/${colors.reset} ${colors.cyan}npm install${colors.reset}`)
|
|
439
|
+
log('')
|
|
440
|
+
} else {
|
|
441
|
+
const { picked } = await prompts({
|
|
442
|
+
type: 'select',
|
|
443
|
+
name: 'picked',
|
|
444
|
+
message: 'No lockfile found. Which package manager does this workspace use?',
|
|
445
|
+
choices: [
|
|
446
|
+
{ title: 'pnpm', value: 'pnpm' },
|
|
447
|
+
{ title: 'yarn', value: 'yarn' },
|
|
448
|
+
{ title: 'npm', value: 'npm' },
|
|
449
|
+
{ title: 'skip — I\'ll install manually', value: null },
|
|
450
|
+
],
|
|
451
|
+
})
|
|
452
|
+
pm = picked || null
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (pm) {
|
|
457
|
+
const cmd = installCmd(pm)
|
|
458
|
+
let runInstall
|
|
459
|
+
if (nonInteractive) {
|
|
460
|
+
runInstall = false
|
|
461
|
+
info(`${colors.dim}Non-interactive — printing install command:${colors.reset}`)
|
|
462
|
+
log(` ${colors.cyan}${cmd}${colors.reset}`)
|
|
463
|
+
log('')
|
|
464
|
+
} else if (skipPrompt) {
|
|
465
|
+
runInstall = true
|
|
466
|
+
} else {
|
|
467
|
+
const { go } = await prompts({
|
|
468
|
+
type: 'confirm',
|
|
469
|
+
name: 'go',
|
|
470
|
+
message: `Run \`${cmd}\` now?`,
|
|
471
|
+
initial: true,
|
|
472
|
+
})
|
|
473
|
+
runInstall = !!go
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (runInstall) {
|
|
477
|
+
const code = await runCommand(cmd, workspaceDir)
|
|
478
|
+
if (code === 0) {
|
|
479
|
+
success('Install complete.')
|
|
480
|
+
log('')
|
|
481
|
+
} else {
|
|
482
|
+
error(`Install failed (exit ${code}). package.json edits are intact.`)
|
|
483
|
+
const editedRel = edited.map(p => relativize(p, workspaceDir)).join(' ')
|
|
484
|
+
log(`${colors.dim}To revert:${colors.reset} ${colors.cyan}git checkout -- ${editedRel}${colors.reset}`)
|
|
485
|
+
log(`${colors.dim}To retry: ${colors.reset} ${colors.cyan}${cmd}${colors.reset}`)
|
|
486
|
+
log('')
|
|
487
|
+
process.exit(code)
|
|
488
|
+
}
|
|
489
|
+
} else {
|
|
490
|
+
log(`${colors.dim}Skipped install. Edits saved; run${colors.reset} ${colors.cyan}${cmd}${colors.reset} ${colors.dim}to apply.${colors.reset}`)
|
|
491
|
+
log('')
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
} else {
|
|
496
|
+
log(`${colors.dim}Skipped deps alignment.${colors.reset}`)
|
|
497
|
+
log('')
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// ── Step 3: AGENTS.md ────────────────────────────────────────────
|
|
215
503
|
if (skipAgents) return
|
|
216
504
|
if (!inProject) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
505
|
+
// Self-update-only invocation outside a Uniweb project: quietly skip.
|
|
506
|
+
return
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Re-survey: deps may have just been edited, which clears the gate.
|
|
510
|
+
const finalSurvey = await surveyVersions(workspaceDir)
|
|
511
|
+
if (finalSurvey.anyDrift && !allowMismatch) {
|
|
512
|
+
warn('AGENTS.md refresh skipped: workspace deps still lag the CLI.')
|
|
513
|
+
log(`${colors.dim}AGENTS.md from v${cliVersion} would document features not in your installed packages.${colors.reset}`)
|
|
514
|
+
log(`${colors.dim}Re-run without ${colors.reset}${colors.cyan}--no-deps${colors.reset}${colors.dim}, or pass ${colors.reset}${colors.cyan}--allow-mismatch${colors.reset}${colors.dim} to override.${colors.reset}`)
|
|
515
|
+
log('')
|
|
516
|
+
if (agentsOnly) process.exit(1)
|
|
223
517
|
return
|
|
224
518
|
}
|
|
225
519
|
|
|
@@ -230,11 +524,15 @@ export async function update(args = []) {
|
|
|
230
524
|
return
|
|
231
525
|
}
|
|
232
526
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
527
|
+
if (dryRun) {
|
|
528
|
+
info(`Dry-run: would ${currentAgentsVersion ? `update AGENTS.md (v${currentAgentsVersion} → v${cliVersion})` : `create AGENTS.md (v${cliVersion})`}.`)
|
|
529
|
+
return
|
|
530
|
+
}
|
|
531
|
+
|
|
236
532
|
if (!skipPrompt && !agentsOnly) {
|
|
237
|
-
const action = currentAgentsVersion
|
|
533
|
+
const action = currentAgentsVersion
|
|
534
|
+
? `Update AGENTS.md (v${currentAgentsVersion} → v${cliVersion})?`
|
|
535
|
+
: `Create AGENTS.md (v${cliVersion})?`
|
|
238
536
|
const { yes } = await prompts({ type: 'confirm', name: 'yes', message: action, initial: true })
|
|
239
537
|
if (!yes) {
|
|
240
538
|
log(`${colors.dim}Skipped AGENTS.md.${colors.reset}`)
|
package/src/framework-index.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-05-
|
|
3
|
+
"generatedAt": "2026-05-06T17:01:18.376Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/build": {
|
|
6
6
|
"version": "0.14.3",
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
"deps": []
|
|
93
93
|
},
|
|
94
94
|
"@uniweb/unipress": {
|
|
95
|
-
"version": "0.4.
|
|
95
|
+
"version": "0.4.8",
|
|
96
96
|
"path": "framework/unipress",
|
|
97
97
|
"deps": [
|
|
98
98
|
"@uniweb/build",
|
package/src/index.js
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
import { existsSync, readFileSync } from 'node:fs'
|
|
25
25
|
import { execSync, spawn as spawnChild } from 'node:child_process'
|
|
26
|
-
import { resolve, join, relative, dirname } from 'node:path'
|
|
26
|
+
import { resolve, join, relative, dirname, basename } from 'node:path'
|
|
27
27
|
import { fileURLToPath } from 'node:url'
|
|
28
28
|
import prompts from 'prompts'
|
|
29
29
|
// `doctor`, `add`, `publish`, and `deploy` are loaded lazily via
|
|
@@ -43,7 +43,7 @@ import {
|
|
|
43
43
|
parseTemplateId,
|
|
44
44
|
} from './templates/index.js'
|
|
45
45
|
import { validateTemplate } from './templates/validator.js'
|
|
46
|
-
import { scaffoldWorkspace, scaffoldFoundation, scaffoldSite, applyContent, applyStarter, mergeTemplateDependencies } from './utils/scaffold.js'
|
|
46
|
+
import { scaffoldWorkspace, scaffoldFoundation, scaffoldSite, applyContent, applyStarter, mergeTemplateDependencies, getWorkspaceTemplateOutputs } from './utils/scaffold.js'
|
|
47
47
|
import { detectPackageManager, filterCmd, installCmd, runCmd } from './utils/pm.js'
|
|
48
48
|
import { isNonInteractive, getCliPrefix, stripNonInteractiveFlag, formatOptions } from './utils/interactive.js'
|
|
49
49
|
import { findWorkspaceRoot } from './utils/workspace.js'
|
|
@@ -73,6 +73,30 @@ const TEMPLATE_CHOICES = [
|
|
|
73
73
|
{ title: 'Blank workspace', value: 'blank', description: 'Empty workspace — grow with uniweb add' },
|
|
74
74
|
]
|
|
75
75
|
|
|
76
|
+
// Files that may pre-exist in the target dir during `uniweb create .` and
|
|
77
|
+
// will be silently overwritten by the scaffold. Anything else colliding
|
|
78
|
+
// causes the verb to abort. README and .gitignore are the only two files
|
|
79
|
+
// the workspace template writes that overlap with what `gh repo create`
|
|
80
|
+
// puts in a fresh repo, and the scaffold's versions are more useful in
|
|
81
|
+
// this context (Vite/Node-aware .gitignore, project-shaped README).
|
|
82
|
+
const IN_PLACE_OVERWRITE_ALLOWED = new Set(['README.md', '.gitignore'])
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Slugify a directory name into a valid project slug — lowercase,
|
|
86
|
+
* `[a-z0-9-]+`, no leading/trailing/duplicated hyphens. Matches the
|
|
87
|
+
* validation regex used for the interactive name prompt.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} name
|
|
90
|
+
* @returns {string} Slugified name; empty if no valid characters remain.
|
|
91
|
+
*/
|
|
92
|
+
function slugifyName(name) {
|
|
93
|
+
return String(name)
|
|
94
|
+
.toLowerCase()
|
|
95
|
+
.replace(/[^a-z0-9-]+/g, '-')
|
|
96
|
+
.replace(/^-+|-+$/g, '')
|
|
97
|
+
.replace(/-{2,}/g, '-')
|
|
98
|
+
}
|
|
99
|
+
|
|
76
100
|
function log(message) {
|
|
77
101
|
console.log(message)
|
|
78
102
|
}
|
|
@@ -669,6 +693,18 @@ async function main() {
|
|
|
669
693
|
let projectName = args[1]
|
|
670
694
|
let templateType = null // null = use new package template flow
|
|
671
695
|
|
|
696
|
+
// In-place mode: `uniweb create .` scaffolds into the current working
|
|
697
|
+
// directory instead of creating a new one. Pairs with the GitHub-first
|
|
698
|
+
// workflow where the user already cloned an empty repo (README.md and
|
|
699
|
+
// optionally .gitignore present) and wants to scaffold inside it.
|
|
700
|
+
const inPlace = projectName === '.'
|
|
701
|
+
if (inPlace) {
|
|
702
|
+
// Clear the positional so downstream logic (template prompt, name
|
|
703
|
+
// prompt, etc.) doesn't see `.` as a literal name. The actual project
|
|
704
|
+
// name is derived below from the cwd basename or `--name`.
|
|
705
|
+
projectName = null
|
|
706
|
+
}
|
|
707
|
+
|
|
672
708
|
// Check for --template flag
|
|
673
709
|
const templateIndex = args.indexOf('--template')
|
|
674
710
|
if (templateIndex !== -1 && args[templateIndex + 1]) {
|
|
@@ -682,11 +718,14 @@ async function main() {
|
|
|
682
718
|
}
|
|
683
719
|
}
|
|
684
720
|
|
|
685
|
-
// Check for --name flag
|
|
721
|
+
// Check for --name flag. Accepts both `--name foo` and `--name=foo`.
|
|
686
722
|
let displayName = null
|
|
687
723
|
const nameIndex = args.indexOf('--name')
|
|
688
724
|
if (nameIndex !== -1 && args[nameIndex + 1]) {
|
|
689
725
|
displayName = args[nameIndex + 1]
|
|
726
|
+
} else {
|
|
727
|
+
const nameEq = args.find(a => a.startsWith('--name='))
|
|
728
|
+
if (nameEq) displayName = nameEq.slice('--name='.length)
|
|
690
729
|
}
|
|
691
730
|
|
|
692
731
|
// Check for --blank flag
|
|
@@ -708,6 +747,28 @@ async function main() {
|
|
|
708
747
|
|
|
709
748
|
const prefix = getCliPrefix()
|
|
710
749
|
|
|
750
|
+
// In-place: derive the project name from the cwd basename (slugified),
|
|
751
|
+
// or use --name when provided. Skip the interactive name prompt below.
|
|
752
|
+
if (inPlace) {
|
|
753
|
+
if (displayName) {
|
|
754
|
+
projectName = displayName
|
|
755
|
+
} else {
|
|
756
|
+
const dirName = basename(process.cwd())
|
|
757
|
+
const slug = slugifyName(dirName)
|
|
758
|
+
if (!slug) {
|
|
759
|
+
error(`Could not derive a valid project name from the current directory ("${dirName}").`)
|
|
760
|
+
log(`Re-run with ${colors.cyan}--name=<your-name>${colors.reset}.`)
|
|
761
|
+
process.exit(1)
|
|
762
|
+
}
|
|
763
|
+
projectName = slug
|
|
764
|
+
if (slug !== dirName) {
|
|
765
|
+
log(`${colors.dim}Project name:${colors.reset} ${slug} ${colors.dim}(slugified from "${dirName}")${colors.reset}`)
|
|
766
|
+
} else {
|
|
767
|
+
log(`${colors.dim}Project name:${colors.reset} ${slug}`)
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
711
772
|
// Non-interactive: fail with actionable message instead of prompting
|
|
712
773
|
if (nonInteractive && !projectName) {
|
|
713
774
|
error(`Missing project name.\n`)
|
|
@@ -720,7 +781,7 @@ async function main() {
|
|
|
720
781
|
templateType = 'starter'
|
|
721
782
|
}
|
|
722
783
|
|
|
723
|
-
// Interactive prompts
|
|
784
|
+
// Interactive prompts (skipped in in-place mode — name was derived above)
|
|
724
785
|
const response = await prompts([
|
|
725
786
|
{
|
|
726
787
|
type: projectName ? null : 'text',
|
|
@@ -773,14 +834,40 @@ async function main() {
|
|
|
773
834
|
|
|
774
835
|
const effectiveName = displayName || projectName
|
|
775
836
|
|
|
776
|
-
//
|
|
777
|
-
|
|
837
|
+
// Resolve target directory. In-place mode scaffolds into the cwd;
|
|
838
|
+
// otherwise create `./<projectName>`.
|
|
839
|
+
const projectDir = inPlace ? process.cwd() : resolve(process.cwd(), projectName)
|
|
778
840
|
|
|
779
|
-
if (existsSync(projectDir)) {
|
|
841
|
+
if (!inPlace && existsSync(projectDir)) {
|
|
780
842
|
error(`Directory already exists: ${projectName}`)
|
|
781
843
|
process.exit(1)
|
|
782
844
|
}
|
|
783
845
|
|
|
846
|
+
if (inPlace) {
|
|
847
|
+
// Conflict check: enumerate the workspace template's would-write paths
|
|
848
|
+
// (the only stage that touches the project root) and bail if any
|
|
849
|
+
// collide with existing files outside the small allowlist.
|
|
850
|
+
//
|
|
851
|
+
// Allowlist: README.md and .gitignore overwrite cleanly. README is
|
|
852
|
+
// typically GitHub-generated boilerplate; .gitignore should be ours
|
|
853
|
+
// since the scaffold ships sensible Vite/Node ignores.
|
|
854
|
+
const outputs = await getWorkspaceTemplateOutputs({ blank: isBlank })
|
|
855
|
+
const conflicts = []
|
|
856
|
+
for (const rel of outputs) {
|
|
857
|
+
const full = resolve(projectDir, rel)
|
|
858
|
+
if (existsSync(full) && !IN_PLACE_OVERWRITE_ALLOWED.has(rel)) {
|
|
859
|
+
conflicts.push(rel)
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
if (conflicts.length > 0) {
|
|
863
|
+
error(`Cannot scaffold in place — these files would be overwritten:`)
|
|
864
|
+
for (const c of conflicts) log(` ${colors.yellow}${c}${colors.reset}`)
|
|
865
|
+
log('')
|
|
866
|
+
log(`Move or remove them, then re-run ${colors.cyan}uniweb create .${colors.reset}.`)
|
|
867
|
+
process.exit(1)
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
784
871
|
// Template routing logic
|
|
785
872
|
const progressCb = (msg) => log(` ${colors.dim}${msg}${colors.reset}`)
|
|
786
873
|
const warningCb = (msg) => log(` ${colors.yellow}Warning: ${msg}${colors.reset}`)
|
|
@@ -879,13 +966,13 @@ async function main() {
|
|
|
879
966
|
|
|
880
967
|
if (isBlank) {
|
|
881
968
|
log(`Next steps:\n`)
|
|
882
|
-
log(` ${colors.cyan}cd ${projectName}${colors.reset}`)
|
|
969
|
+
if (!inPlace) log(` ${colors.cyan}cd ${projectName}${colors.reset}`)
|
|
883
970
|
log(` ${colors.cyan}${prefix} add project${colors.reset}`)
|
|
884
971
|
log(` ${colors.cyan}${installCmd(pm)}${colors.reset}`)
|
|
885
972
|
log(` ${colors.cyan}${prefix} dev${colors.reset} ${colors.dim}# Start dev server${colors.reset}`)
|
|
886
973
|
} else {
|
|
887
974
|
log(`Next steps:\n`)
|
|
888
|
-
log(` ${colors.cyan}cd ${projectName}${colors.reset}`)
|
|
975
|
+
if (!inPlace) log(` ${colors.cyan}cd ${projectName}${colors.reset}`)
|
|
889
976
|
log(` ${colors.cyan}${installCmd(pm)}${colors.reset}`)
|
|
890
977
|
log(` ${colors.cyan}${prefix} dev${colors.reset} ${colors.dim}# Start dev server${colors.reset}`)
|
|
891
978
|
}
|
|
@@ -979,6 +1066,7 @@ ${colors.cyan}${colors.bright}uniweb create${colors.reset} ${colors.dim}— Crea
|
|
|
979
1066
|
|
|
980
1067
|
${colors.bright}Usage:${colors.reset}
|
|
981
1068
|
uniweb create [name] [options]
|
|
1069
|
+
uniweb create . Scaffold into the current directory
|
|
982
1070
|
|
|
983
1071
|
${colors.bright}Options:${colors.reset}
|
|
984
1072
|
--template <type> Project template (default: starter)
|
|
@@ -987,13 +1075,24 @@ ${colors.bright}Options:${colors.reset}
|
|
|
987
1075
|
npm: @scope/template-name
|
|
988
1076
|
GitHub: github:user/repo or https://github.com/user/repo
|
|
989
1077
|
--blank Create an empty workspace (grow with \`uniweb add\`)
|
|
990
|
-
--name <name> Project
|
|
1078
|
+
--name <name> Project name (overrides slugified basename when used with \`.\`)
|
|
991
1079
|
--no-git Skip git repository initialization
|
|
992
1080
|
|
|
1081
|
+
${colors.bright}In-place mode (\`uniweb create .\`):${colors.reset}
|
|
1082
|
+
Pairs with the GitHub-first workflow — clone an empty repo locally
|
|
1083
|
+
(README, optional .gitignore), then scaffold inside it. Project name
|
|
1084
|
+
is the cwd basename, slugified to a valid npm name. Pass \`--name\` to
|
|
1085
|
+
override. Pre-existing \`README.md\` and \`.gitignore\` are overwritten;
|
|
1086
|
+
any other collision aborts with the list of conflicting files. Skips
|
|
1087
|
+
\`git init\` when a \`.git/\` directory already exists.
|
|
1088
|
+
|
|
993
1089
|
${colors.bright}Examples:${colors.reset}
|
|
994
1090
|
uniweb create my-project # Foundation + site + starter content
|
|
995
1091
|
uniweb create my-project --template marketing # Official template
|
|
996
1092
|
uniweb create my-project --blank # Empty workspace
|
|
1093
|
+
uniweb create . # Scaffold into current dir
|
|
1094
|
+
uniweb create . --template docs # In place + a content template
|
|
1095
|
+
uniweb create . --name=my-app # In place, explicit slug
|
|
997
1096
|
`,
|
|
998
1097
|
dev: `
|
|
999
1098
|
${colors.cyan}${colors.bright}uniweb dev${colors.reset} ${colors.dim}— Start a dev server for a site${colors.reset}
|
|
@@ -1185,28 +1284,39 @@ Prints the parsed content shape of a markdown file or folder — the
|
|
|
1185
1284
|
Useful for debugging "why isn't my section getting X?".
|
|
1186
1285
|
`,
|
|
1187
1286
|
update: `
|
|
1188
|
-
${colors.cyan}${colors.bright}uniweb update${colors.reset} ${colors.dim}—
|
|
1287
|
+
${colors.cyan}${colors.bright}uniweb update${colors.reset} ${colors.dim}— Reconcile workspace state with the running CLI${colors.reset}
|
|
1189
1288
|
|
|
1190
1289
|
${colors.bright}Usage:${colors.reset}
|
|
1191
|
-
uniweb update
|
|
1192
|
-
uniweb update --
|
|
1193
|
-
uniweb update --
|
|
1194
|
-
uniweb update --
|
|
1290
|
+
uniweb update Self-update + align deps + refresh AGENTS.md
|
|
1291
|
+
uniweb update --deps-only Only align workspace @uniweb/* deps
|
|
1292
|
+
uniweb update --agents-only Only refresh AGENTS.md
|
|
1293
|
+
uniweb update --no-deps Skip the deps-alignment step
|
|
1294
|
+
uniweb update --no-agents Skip the AGENTS.md step
|
|
1295
|
+
uniweb update --dry-run Print survey + would-be writes; no mutations
|
|
1296
|
+
uniweb update --allow-mismatch Refresh AGENTS.md even if declared deps lag
|
|
1297
|
+
uniweb update --yes Skip confirmation prompts
|
|
1195
1298
|
|
|
1196
1299
|
${colors.bright}What it does:${colors.reset}
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1300
|
+
Prints a version survey first (CLI version, AGENTS.md stamp, every
|
|
1301
|
+
@uniweb/* + uniweb dep declared in workspace package.json files,
|
|
1302
|
+
marked aligned / behind / ahead). Then three steps:
|
|
1303
|
+
|
|
1304
|
+
1. ${colors.bright}Self-update${colors.reset} the global install via npm / pnpm / yarn
|
|
1305
|
+
(auto-detected). TTY prompts; non-interactive prints the command.
|
|
1306
|
+
2. ${colors.bright}Align workspace deps${colors.reset} to the CLI's bundled version matrix —
|
|
1307
|
+
edits every workspace package.json (only @uniweb/* + uniweb keys),
|
|
1308
|
+
then offers to run the workspace's package manager (lockfile-detected:
|
|
1309
|
+
pnpm-lock.yaml → pnpm, yarn.lock → yarn, package-lock.json → npm).
|
|
1310
|
+
If the install fails, package.json edits are kept and a revert
|
|
1311
|
+
command is printed.
|
|
1312
|
+
3. ${colors.bright}Refresh AGENTS.md${colors.reset} from the CLI's bundled partial. Refuses to
|
|
1313
|
+
run while declared deps still lag the CLI (would document features
|
|
1314
|
+
not in your installed packages); pass --allow-mismatch to override.
|
|
1204
1315
|
|
|
1205
1316
|
${colors.bright}Project-local installs:${colors.reset}
|
|
1206
1317
|
When the running CLI is project-local (lives in node_modules), self-
|
|
1207
1318
|
update is a no-op — the version is pinned by your project's
|
|
1208
|
-
package.json. The
|
|
1209
|
-
AGENTS.md refresh path only.
|
|
1319
|
+
package.json. The deps + AGENTS.md steps still run.
|
|
1210
1320
|
`,
|
|
1211
1321
|
}
|
|
1212
1322
|
|
|
@@ -224,6 +224,53 @@ export async function copyTemplateDirectory(sourcePath, targetPath, data, option
|
|
|
224
224
|
}
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Enumerate the output paths a template directory would write, without
|
|
229
|
+
* touching disk. Mirrors `copyTemplateDirectory`'s naming rules:
|
|
230
|
+
* - `.hbs` extension is stripped
|
|
231
|
+
* - `_dir` is renamed to `.dir` (but `__dir` is preserved as `_dir` would be)
|
|
232
|
+
* - `template.json` is excluded
|
|
233
|
+
* - `skip` filenames are excluded by their post-rename name
|
|
234
|
+
*
|
|
235
|
+
* Returns relative paths (POSIX-style separators on POSIX, native on Windows
|
|
236
|
+
* — `path.join` semantics). Used by the in-place create flow to detect
|
|
237
|
+
* conflicts before any I/O begins.
|
|
238
|
+
*
|
|
239
|
+
* @param {string} sourcePath - Source template directory
|
|
240
|
+
* @param {Object} [options]
|
|
241
|
+
* @param {string[]} [options.skip] - Output filenames to exclude
|
|
242
|
+
* @returns {Promise<string[]>}
|
|
243
|
+
*/
|
|
244
|
+
export async function enumerateTemplateOutputs(sourcePath, options = {}) {
|
|
245
|
+
const { skip = [] } = options
|
|
246
|
+
const outputs = []
|
|
247
|
+
await enumerateInto(sourcePath, '', outputs, skip)
|
|
248
|
+
return outputs
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function enumerateInto(sourcePath, relPath, outputs, skip) {
|
|
252
|
+
const entries = await fs.readdir(sourcePath, { withFileTypes: true })
|
|
253
|
+
for (const entry of entries) {
|
|
254
|
+
const sourceName = entry.name
|
|
255
|
+
if (entry.isDirectory()) {
|
|
256
|
+
const targetName = sourceName.startsWith('_') && !sourceName.startsWith('__')
|
|
257
|
+
? `.${sourceName.slice(1)}`
|
|
258
|
+
: sourceName
|
|
259
|
+
await enumerateInto(
|
|
260
|
+
path.join(sourcePath, sourceName),
|
|
261
|
+
relPath ? path.join(relPath, targetName) : targetName,
|
|
262
|
+
outputs,
|
|
263
|
+
skip,
|
|
264
|
+
)
|
|
265
|
+
} else {
|
|
266
|
+
if (sourceName === 'template.json') continue
|
|
267
|
+
const outputName = sourceName.endsWith('.hbs') ? sourceName.slice(0, -4) : sourceName
|
|
268
|
+
if (skip.includes(outputName)) continue
|
|
269
|
+
outputs.push(relPath ? path.join(relPath, outputName) : outputName)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
227
274
|
/**
|
|
228
275
|
* Clear the template cache
|
|
229
276
|
*/
|
package/src/utils/pm.js
CHANGED
|
@@ -5,9 +5,17 @@
|
|
|
5
5
|
* and generate PM-appropriate commands for output messages.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { existsSync } from 'node:fs'
|
|
9
|
+
import { join } from 'node:path'
|
|
10
|
+
|
|
8
11
|
/**
|
|
9
12
|
* Detect which package manager invoked the CLI.
|
|
10
13
|
* Uses the standard npm_config_user_agent env var (same technique as create-vite, create-next-app).
|
|
14
|
+
*
|
|
15
|
+
* Note: this returns the *invoker* PM, which is empty when the user runs
|
|
16
|
+
* a global CLI binary directly from a shell. For the workspace's own PM
|
|
17
|
+
* (driven by lockfile presence), use `detectWorkspacePm`.
|
|
18
|
+
*
|
|
11
19
|
* @returns {'pnpm' | 'npm'}
|
|
12
20
|
*/
|
|
13
21
|
export function detectPackageManager() {
|
|
@@ -16,6 +24,22 @@ export function detectPackageManager() {
|
|
|
16
24
|
return 'npm'
|
|
17
25
|
}
|
|
18
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Detect the workspace's package manager by inspecting lockfiles at the
|
|
29
|
+
* workspace root. This is the right signal for "what PM should I use to
|
|
30
|
+
* install in this workspace" — independent of how the CLI was invoked.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} workspaceRoot - Absolute path to workspace root
|
|
33
|
+
* @returns {'pnpm' | 'yarn' | 'npm' | null} - null when no lockfile is present
|
|
34
|
+
*/
|
|
35
|
+
export function detectWorkspacePm(workspaceRoot) {
|
|
36
|
+
if (!workspaceRoot) return null
|
|
37
|
+
if (existsSync(join(workspaceRoot, 'pnpm-lock.yaml'))) return 'pnpm'
|
|
38
|
+
if (existsSync(join(workspaceRoot, 'yarn.lock'))) return 'yarn'
|
|
39
|
+
if (existsSync(join(workspaceRoot, 'package-lock.json'))) return 'npm'
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
|
|
19
43
|
/**
|
|
20
44
|
* Generate a workspace-filtered command.
|
|
21
45
|
* pnpm: "pnpm --filter site dev"
|
|
@@ -33,11 +57,13 @@ export function filterCmd(pm, pkg, cmd) {
|
|
|
33
57
|
|
|
34
58
|
/**
|
|
35
59
|
* Generate an install command.
|
|
36
|
-
* @param {'pnpm' | 'npm'} pm
|
|
60
|
+
* @param {'pnpm' | 'yarn' | 'npm'} pm
|
|
37
61
|
* @returns {string}
|
|
38
62
|
*/
|
|
39
63
|
export function installCmd(pm) {
|
|
40
|
-
|
|
64
|
+
if (pm === 'pnpm') return 'pnpm install'
|
|
65
|
+
if (pm === 'yarn') return 'yarn install'
|
|
66
|
+
return 'npm install'
|
|
41
67
|
}
|
|
42
68
|
|
|
43
69
|
/**
|
package/src/utils/scaffold.js
CHANGED
|
@@ -11,7 +11,7 @@ import { join, dirname } from 'node:path'
|
|
|
11
11
|
import { fileURLToPath } from 'node:url'
|
|
12
12
|
import yaml from 'js-yaml'
|
|
13
13
|
import Handlebars from 'handlebars'
|
|
14
|
-
import { copyTemplateDirectory, registerVersions } from '../templates/processor.js'
|
|
14
|
+
import { copyTemplateDirectory, enumerateTemplateOutputs, registerVersions } from '../templates/processor.js'
|
|
15
15
|
import { getVersionsForTemplates, getCliVersion } from '../versions.js'
|
|
16
16
|
|
|
17
17
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
@@ -48,6 +48,28 @@ export async function scaffoldWorkspace(targetDir, context, options = {}) {
|
|
|
48
48
|
})
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Return the relative paths the workspace template would write into the
|
|
53
|
+
* project root, given the same skip rules `scaffoldWorkspace` applies.
|
|
54
|
+
* Used by the in-place create flow (`uniweb create .`) to detect conflicts
|
|
55
|
+
* before any I/O begins.
|
|
56
|
+
*
|
|
57
|
+
* Only the workspace template's outputs are enumerated. The foundation,
|
|
58
|
+
* site, and starter stages write into newly-created subdirectories
|
|
59
|
+
* (`src/`, `site/`) that don't pre-exist in a target like a fresh GitHub
|
|
60
|
+
* clone, so they can't conflict.
|
|
61
|
+
*
|
|
62
|
+
* @param {Object} [options]
|
|
63
|
+
* @param {boolean} [options.blank] - True when scaffolding a blank workspace
|
|
64
|
+
* (no packages yet) — skips `pnpm-workspace.yaml`, mirroring scaffoldWorkspace.
|
|
65
|
+
* @returns {Promise<string[]>}
|
|
66
|
+
*/
|
|
67
|
+
export async function getWorkspaceTemplateOutputs({ blank = false } = {}) {
|
|
68
|
+
const skip = blank ? ['pnpm-workspace.yaml'] : []
|
|
69
|
+
const templatePath = join(TEMPLATES_DIR, 'workspace')
|
|
70
|
+
return enumerateTemplateOutputs(templatePath, { skip })
|
|
71
|
+
}
|
|
72
|
+
|
|
51
73
|
/**
|
|
52
74
|
* Scaffold a foundation from the foundation package template
|
|
53
75
|
*
|