uniweb 0.12.14 → 0.12.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,35 +1,32 @@
1
1
  /**
2
- * uniweb update — Update the CLI itself, and (in a Uniweb project) the
3
- * project's AGENTS.md.
2
+ * uniweb update — Reconcile a Uniweb workspace's state with the running
3
+ * CLI's expectations. Three convergence steps, in order:
4
4
  *
5
- * Two responsibilities, in priority order:
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
- * 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.
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 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.
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
- * 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.
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 the previous behavior (walking up to any
76
- * pnpm-workspace.yaml) writes AGENTS.md into unrelated monorepos.
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. Heuristic
94
- * based on the CLI's filesystem path — pnpm and yarn berry use distinctive
95
- * directory layouts; npm is the fallback.
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
- * Compare two semver strings: 1 if a>b, -1 if a<b, 0 if equal.
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 skipAgents = args.includes('--no-agents')
157
- const skipPrompt = args.includes('--yes') || isNonInteractive(args)
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
- // ─── Step 1: Self-update path ─────────────────────────────────
164
- if (!agentsOnly) {
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 (skipPrompt) {
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
- // ─── Step 2: AGENTS.md refresh path ───────────────────────────
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
- 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.
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
- // 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).
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 ? `Update AGENTS.md (v${currentAgentsVersion} → v${cliVersion})?` : `Create AGENTS.md (v${cliVersion})?`
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}`)
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-05-05T20:43:37.314Z",
3
+ "generatedAt": "2026-05-06T17:01:18.376Z",
4
4
  "packages": {
5
5
  "@uniweb/build": {
6
- "version": "0.14.2",
6
+ "version": "0.14.3",
7
7
  "path": "framework/build",
8
8
  "deps": [
9
9
  "@uniweb/content-reader",
@@ -92,7 +92,7 @@
92
92
  "deps": []
93
93
  },
94
94
  "@uniweb/unipress": {
95
- "version": "0.4.7",
95
+ "version": "0.4.8",
96
96
  "path": "framework/unipress",
97
97
  "deps": [
98
98
  "@uniweb/build",
package/src/index.js CHANGED
@@ -1185,28 +1185,39 @@ Prints the parsed content shape of a markdown file or folder — the
1185
1185
  Useful for debugging "why isn't my section getting X?".
1186
1186
  `,
1187
1187
  update: `
1188
- ${colors.cyan}${colors.bright}uniweb update${colors.reset} ${colors.dim}— Update the CLI itself, plus AGENTS.md when in a project${colors.reset}
1188
+ ${colors.cyan}${colors.bright}uniweb update${colors.reset} ${colors.dim}— Reconcile workspace state with the running CLI${colors.reset}
1189
1189
 
1190
1190
  ${colors.bright}Usage:${colors.reset}
1191
- uniweb update Self-update + (in project) refresh AGENTS.md
1192
- uniweb update --agents-only Only refresh AGENTS.md (skip self-update)
1193
- uniweb update --no-agents Only self-update (skip AGENTS.md)
1194
- uniweb update --yes Skip the confirmation prompts
1191
+ uniweb update Self-update + align deps + refresh AGENTS.md
1192
+ uniweb update --deps-only Only align workspace @uniweb/* deps
1193
+ uniweb update --agents-only Only refresh AGENTS.md
1194
+ uniweb update --no-deps Skip the deps-alignment step
1195
+ uniweb update --no-agents Skip the AGENTS.md step
1196
+ uniweb update --dry-run Print survey + would-be writes; no mutations
1197
+ uniweb update --allow-mismatch Refresh AGENTS.md even if declared deps lag
1198
+ uniweb update --yes Skip confirmation prompts
1195
1199
 
1196
1200
  ${colors.bright}What it does:${colors.reset}
1197
- 1. Self-update the global install via npm / pnpm / yarn (auto-detected).
1198
- In TTY, prompts before running. In CI / non-interactive, prints the
1199
- command and exits without running it.
1200
- 2. If the cwd resolves to a Uniweb project (root package.json declares
1201
- \`uniweb\` as a dep), refreshes AGENTS.md from the CLI's bundled
1202
- version. Outside a Uniweb project, this step is skipped — the
1203
- command will not write AGENTS.md into unrelated directories.
1201
+ Prints a version survey first (CLI version, AGENTS.md stamp, every
1202
+ @uniweb/* + uniweb dep declared in workspace package.json files,
1203
+ marked aligned / behind / ahead). Then three steps:
1204
+
1205
+ 1. ${colors.bright}Self-update${colors.reset} the global install via npm / pnpm / yarn
1206
+ (auto-detected). TTY prompts; non-interactive prints the command.
1207
+ 2. ${colors.bright}Align workspace deps${colors.reset} to the CLI's bundled version matrix —
1208
+ edits every workspace package.json (only @uniweb/* + uniweb keys),
1209
+ then offers to run the workspace's package manager (lockfile-detected:
1210
+ pnpm-lock.yaml → pnpm, yarn.lock → yarn, package-lock.json → npm).
1211
+ If the install fails, package.json edits are kept and a revert
1212
+ command is printed.
1213
+ 3. ${colors.bright}Refresh AGENTS.md${colors.reset} from the CLI's bundled partial. Refuses to
1214
+ run while declared deps still lag the CLI (would document features
1215
+ not in your installed packages); pass --allow-mismatch to override.
1204
1216
 
1205
1217
  ${colors.bright}Project-local installs:${colors.reset}
1206
1218
  When the running CLI is project-local (lives in node_modules), self-
1207
1219
  update is a no-op — the version is pinned by your project's
1208
- package.json. The verb prints that explanation and proceeds with the
1209
- AGENTS.md refresh path only.
1220
+ package.json. The deps + AGENTS.md steps still run.
1210
1221
  `,
1211
1222
  }
1212
1223