uniweb 0.13.7 → 0.13.9

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uniweb",
3
- "version": "0.13.7",
3
+ "version": "0.13.9",
4
4
  "description": "Create structured Vite + React sites with content/code separation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,13 +42,13 @@
42
42
  "prompts": "^2.4.2",
43
43
  "tar": "^7.0.0",
44
44
  "@uniweb/core": "0.7.31",
45
- "@uniweb/kit": "0.9.39",
46
- "@uniweb/runtime": "0.8.38"
45
+ "@uniweb/runtime": "0.8.38",
46
+ "@uniweb/kit": "0.9.41"
47
47
  },
48
48
  "peerDependencies": {
49
+ "@uniweb/build": "0.15.8",
49
50
  "@uniweb/content-reader": "1.1.17",
50
- "@uniweb/semantic-parser": "1.1.20",
51
- "@uniweb/build": "0.15.7"
51
+ "@uniweb/semantic-parser": "1.1.20"
52
52
  },
53
53
  "peerDependenciesMeta": {
54
54
  "@uniweb/build": {
@@ -31,6 +31,8 @@
31
31
  * --no-agents Skip the AGENTS.md step.
32
32
  * --no-deps Skip the deps-alignment step.
33
33
  * --dry-run Print survey + would-be writes; no mutations.
34
+ * --verbose List every surveyed dep, including aligned ones
35
+ * (the default collapses those to a count).
34
36
  * --allow-mismatch Refresh AGENTS.md even if declared deps lag.
35
37
  * --yes Don't prompt — apply edits and run the install.
36
38
  * --non-interactive Auto-detected; prints the plan, never mutates
@@ -50,6 +52,7 @@ import { detectWorkspacePm, installCmd, detectGlobalCliPm, globalCliUpdateCmd }
50
52
  import { writeJsonPreservingStyle } from '../utils/json-file.js'
51
53
  import { surveyWorkspaceDeps, compareSemver } from '../utils/dep-survey.js'
52
54
  import { checkWorkspaceInstall, readDeclaredFoundation } from '../utils/install-integrity.js'
55
+ import { getLatestVersion } from '../utils/update-check.js'
53
56
 
54
57
  const colors = {
55
58
  reset: '\x1b[0m',
@@ -110,16 +113,45 @@ function findUniwebWorkspace(cwd) {
110
113
  }
111
114
  }
112
115
 
113
- /** Fetch the latest published CLI version. Returns null on network error. */
114
- async function fetchLatestVersion() {
115
- try {
116
- const res = await fetch('https://registry.npmjs.org/uniweb/latest')
117
- if (!res.ok) return null
118
- const data = await res.json()
119
- return data?.version || null
120
- } catch {
121
- return null
116
+ /**
117
+ * Trailing advisory: a newer CLI exists, and this run could not have used it.
118
+ *
119
+ * Placed AFTER the work, not before, because it is a what-to-do-next — the
120
+ * same reason update-check calls its post-command notice the 'soft' tone.
121
+ * It also has to be the last thing on screen when it fires: a run that ends
122
+ * on `✓ deps aligned` + `✓ AGENTS.md up to date` reads as "all good", and
123
+ * those ticks are measured against THIS CLI's matrix. True, and beside the
124
+ * point, when the CLI itself is two releases back.
125
+ *
126
+ * The remedy is per-provenance because the wrong one is useless advice:
127
+ * - project-local — the version is pinned by the project's package.json,
128
+ * so a global install is irrelevant. `npx uniweb@latest update` both
129
+ * aligns the deps AND rewrites that pin, so it is self-healing; say so,
130
+ * or the reader assumes they'll be back here next release.
131
+ * - global — updating the global install is the durable fix.
132
+ * - npx — skipped entirely. The version was chosen explicitly on the
133
+ * command line, and the lead-in already named it.
134
+ *
135
+ * Exported for tests: this notice must fire when behind and stay silent
136
+ * when current, and it's the half of the command that had no coverage.
137
+ *
138
+ * @returns {boolean} true if a notice was printed.
139
+ */
140
+ export function printStaleCliNotice({ cliVersion, latest, isNpx, isGlobal, globalPm }) {
141
+ if (isNpx) return false
142
+ if (!latest || compareSemver(latest, cliVersion) <= 0) return false
143
+
144
+ log('')
145
+ log(`${colors.yellow}⚠${colors.reset} ${colors.bright}A newer uniweb is available:${colors.reset} ${colors.dim}v${cliVersion}${colors.reset} → ${colors.cyan}v${latest}${colors.reset}`)
146
+ if (isGlobal) {
147
+ log(`${colors.dim}This run aligned the project to v${cliVersion}'s matrix. To update the CLI:${colors.reset} ${colors.cyan}${globalCliUpdateCmd(globalPm)}${colors.reset}`)
148
+ log(`${colors.dim}Or align to the latest release without a global install:${colors.reset} ${colors.cyan}npx uniweb@latest update${colors.reset}`)
149
+ } else {
150
+ log(`${colors.dim}This run aligned the project to v${cliVersion}'s matrix — the version your project pins.${colors.reset}`)
151
+ log(`${colors.dim}To move to v${latest}:${colors.reset} ${colors.cyan}npx uniweb@latest update${colors.reset} ${colors.dim}(updates the pin too).${colors.reset}`)
122
152
  }
153
+ log('')
154
+ return true
123
155
  }
124
156
 
125
157
  /** Run a shell command, inheriting stdio. Resolves with the exit code. */
@@ -132,8 +164,22 @@ function runCommand(cmd, cwd) {
132
164
  })
133
165
  }
134
166
 
135
- /** Print the survey report grouped by package directory. */
136
- function printSurvey(report, cliVersion, agentsVersion) {
167
+ /**
168
+ * Print the survey report grouped by package directory.
169
+ *
170
+ * Only rows that need attention get a line. An aligned dep renders as
171
+ * `0.9.37 → 0.9.37 aligned` — an identity mapping that says nothing, and
172
+ * on a typical workspace there are six of them. That is the whole screen
173
+ * on the command's most common outcome (a no-op), and it buries the one
174
+ * line that isn't noise. Aligned deps collapse to a count instead.
175
+ *
176
+ * `--verbose` restores the full table for anyone auditing the matrix.
177
+ *
178
+ * Exported for tests: the collapse is the point of the function, and a
179
+ * regression here is invisible — the command still works, it just stops
180
+ * being readable.
181
+ */
182
+ export function printSurvey(report, cliVersion, agentsVersion, { verbose = false } = {}) {
137
183
  log('')
138
184
  log(`${colors.bright}uniweb CLI:${colors.reset} v${cliVersion}`)
139
185
  log(`${colors.bright}AGENTS.md stamp:${colors.reset} ${agentsVersion ? 'v' + agentsVersion : colors.dim + '(none)' + colors.reset}`)
@@ -145,8 +191,16 @@ function printSurvey(report, cliVersion, agentsVersion) {
145
191
  return
146
192
  }
147
193
 
194
+ const needsAttention = report.rows.filter(r => r.status !== 'aligned')
195
+ const shown = verbose ? report.rows : needsAttention
196
+ const alignedCount = report.rows.length - needsAttention.length
197
+
198
+ // Everything aligned and not auditing: step 1's success line says so in
199
+ // one sentence. A header over an empty table is worse than no header.
200
+ if (shown.length === 0) return
201
+
148
202
  const byDir = {}
149
- for (const row of report.rows) {
203
+ for (const row of shown) {
150
204
  if (!byDir[row.relDir]) byDir[row.relDir] = []
151
205
  byDir[row.relDir].push(row)
152
206
  }
@@ -171,6 +225,9 @@ function printSurvey(report, cliVersion, agentsVersion) {
171
225
  log(` ${icon} ${row.name}${padding} ${row.current.padEnd(10)} → ${row.target.padEnd(10)} ${statusText}`)
172
226
  }
173
227
  }
228
+ if (!verbose && alignedCount > 0) {
229
+ log(` ${colors.dim}(${alignedCount} other${alignedCount === 1 ? '' : 's'} already aligned — ${colors.reset}${colors.cyan}--verbose${colors.reset}${colors.dim} to list)${colors.reset}`)
230
+ }
174
231
  log('')
175
232
  }
176
233
 
@@ -228,6 +285,7 @@ export async function update(args = []) {
228
285
  const skipDeps = args.includes('--no-deps') || agentsOnly
229
286
  const dryRun = args.includes('--dry-run')
230
287
  const allowMismatch = args.includes('--allow-mismatch')
288
+ const verbose = args.includes('--verbose')
231
289
  const hasYes = args.includes('--yes')
232
290
  const nonInteractive = isNonInteractive(args)
233
291
  const isGlobal = isGlobalInstall()
@@ -248,30 +306,39 @@ export async function update(args = []) {
248
306
  if (inProject) {
249
307
  survey = await surveyWorkspaceDeps(workspaceDir)
250
308
  agentsVersion = readAgentsVersion(join(workspaceDir, 'AGENTS.md'))
251
- printSurvey(survey, cliVersion, agentsVersion)
309
+ printSurvey(survey, cliVersion, agentsVersion, { verbose })
252
310
  }
253
311
 
254
312
  // ── This command reconciles the *project*, not the CLI ───────────
255
- // Surface (but don't act on) a newer published CLI: this run aligns
256
- // the project to *this* CLI's matrix.
313
+ // Which CLI is running is a lead-in it belongs before the work. Whether
314
+ // that CLI is STALE is a what-to-do-next, and it is printed after the work
315
+ // by printStaleCliNotice() so it lands last instead of under two green
316
+ // ticks. The lookup happens here because the check must run on every
317
+ // provenance, not just a global install.
318
+ //
319
+ // Why the project-local path needs it MORE than the global one: a global
320
+ // CLI also gets the general notifier in index.js, but that is gated on
321
+ // `if (global)` — so a project-local run gets no staleness signal from
322
+ // anywhere. That is exactly the case where the user cannot work it out
323
+ // themselves, because the project pins the version. It was the quiet path
324
+ // and it should have been the loud one.
257
325
  let installPm = inProject ? detectWorkspacePm(workspaceDir) : null
326
+ const globalPm = isGlobal ? detectGlobalCliPm() : null
327
+ // Non-TTY reads cache only: scripted runs stay fast and offline-safe, the
328
+ // same convention `--version` follows in index.js. Skipped entirely when
329
+ // there is nothing to reconcile — the notice is never reached from those
330
+ // paths, and looking it up anyway would spend a network call on nothing.
331
+ const latestCli = (isNpx || !inProject)
332
+ ? null
333
+ : await getLatestVersion({ allowNetwork: !!process.stdout.isTTY })
334
+
258
335
  if (isNpx) {
259
336
  log(`${colors.dim}Running${colors.reset} ${colors.cyan}uniweb@${cliVersion}${colors.reset} ${colors.dim}via npx — aligning this project to v${cliVersion}'s matrix.${colors.reset}`)
260
337
  log(`${colors.dim}(To install the CLI:${colors.reset} ${colors.cyan}npm i -g uniweb${colors.reset}${colors.dim}.)${colors.reset}`)
261
338
  log('')
262
- } else if (isGlobal) {
263
- const latest = await fetchLatestVersion()
264
- if (latest && compareSemver(latest, cliVersion) > 0) {
265
- const pm = detectGlobalCliPm()
266
- log(`${colors.yellow}A newer uniweb is available:${colors.reset} ${colors.dim}v${cliVersion}${colors.reset} → ${colors.cyan}v${latest}${colors.reset}`)
267
- log(`${colors.dim}This run aligns the project to v${cliVersion}. To update the CLI:${colors.reset} ${colors.cyan}${globalCliUpdateCmd(pm)}${colors.reset}`)
268
- log(`${colors.dim}Or, to align to the latest release without a global install:${colors.reset} ${colors.cyan}npx uniweb@latest update${colors.reset}`)
269
- log('')
270
- }
271
- } else {
339
+ } else if (!isGlobal) {
272
340
  // Project-local copy (lives in this project's node_modules).
273
341
  log(`${colors.dim}Running the project-local CLI (v${cliVersion}) — pinned by your project's${colors.reset} ${colors.cyan}package.json${colors.reset}${colors.dim}.${colors.reset}`)
274
- log(`${colors.dim}To use a newer CLI, bump${colors.reset} ${colors.cyan}uniweb${colors.reset}${colors.dim} in${colors.reset} ${colors.cyan}package.json${colors.reset}${colors.dim} and re-install, or run${colors.reset} ${colors.cyan}npx uniweb@latest update${colors.reset}${colors.dim}.${colors.reset}`)
275
342
  log('')
276
343
  }
277
344
 
@@ -289,7 +356,8 @@ export async function update(args = []) {
289
356
 
290
357
  if (!skipDeps && survey) {
291
358
  if (!survey.anyDrift) {
292
- success('Workspace deps are aligned with the CLI.')
359
+ // The count carries the work the collapsed table no longer shows.
360
+ success(`Workspace deps are aligned with the CLI (${survey.rows.length} checked).`)
293
361
  if (survey.anyAhead) {
294
362
  log(`${colors.dim}(Some deps are ahead of the CLI's bundled matrix — left untouched.)${colors.reset}`)
295
363
  }
@@ -438,6 +506,11 @@ export async function update(args = []) {
438
506
  if (!dryRun && (depsEdited || agentsResult === 'created' || agentsResult === 'updated')) {
439
507
  printSummary({ editedPaths, depsEdited, installRan, installPm, agentsResult, cliVersion })
440
508
  }
509
+
510
+ // Last, deliberately — see printStaleCliNotice. Everything above reports
511
+ // against THIS CLI's matrix; if the CLI itself is behind, that is the note
512
+ // the reader should leave with.
513
+ printStaleCliNotice({ cliVersion, latest: latestCli, isNpx, isGlobal, globalPm })
441
514
  }
442
515
 
443
516
  /**
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-07-27T11:29:06.768Z",
3
+ "generatedAt": "2026-07-27T16:00:55.172Z",
4
4
  "packages": {
5
5
  "@uniweb/build": {
6
- "version": "0.15.7",
6
+ "version": "0.15.8",
7
7
  "path": "framework/build",
8
8
  "deps": [
9
9
  "@uniweb/content-reader",
@@ -44,7 +44,7 @@
44
44
  "deps": []
45
45
  },
46
46
  "@uniweb/kit": {
47
- "version": "0.9.39",
47
+ "version": "0.9.41",
48
48
  "path": "framework/kit",
49
49
  "deps": [
50
50
  "@uniweb/core",
@@ -108,7 +108,7 @@
108
108
  "deps": []
109
109
  },
110
110
  "@uniweb/unipress": {
111
- "version": "0.5.6",
111
+ "version": "0.5.8",
112
112
  "path": "framework/unipress",
113
113
  "deps": [
114
114
  "@uniweb/build",
@@ -103,6 +103,61 @@ export function maybeNotifyFromCache(currentVersion, tone = 'eager') {
103
103
  // and gets the eager default. Keeps that call site unchanged.
104
104
  export const maybeEagerNotification = maybeNotifyFromCache
105
105
 
106
+ /**
107
+ * Resolve the latest published CLI version WITHOUT printing anything.
108
+ *
109
+ * The notify helpers in this module own both the lookup and the message.
110
+ * That's right for callers who want the house notice, and wrong for callers
111
+ * whose remedy differs — `uniweb update` running from a project's
112
+ * node_modules can't tell the user to `npm i -g uniweb`, because the version
113
+ * they're on is pinned by their own package.json. Those callers need the
114
+ * fact, not the sentence.
115
+ *
116
+ * Caching and the timeout live here rather than at the call site so every
117
+ * lookup gets them. `update.js` previously had its own copy with neither: an
118
+ * unbounded `await fetch()` before the command did any work, which turns a
119
+ * flaky network into a hang on a routine verb.
120
+ *
121
+ * @param {object} [opts]
122
+ * @param {number} [opts.timeoutMs=1500] Network cap. Slow/offline returns
123
+ * whatever the cache holds rather than blocking.
124
+ * @param {boolean} [opts.allowNetwork=true] False = cache-only. Pass false
125
+ * for non-TTY callers; scripts must stay fast and offline-safe (the same
126
+ * convention `--version` follows).
127
+ * @param {number} [opts.maxAgeMs] How old a cache entry may be before a
128
+ * refetch is attempted. Defaults to the module's 1-day interval.
129
+ * @returns {Promise<string|null>} The latest version, or null if unknown.
130
+ */
131
+ export async function getLatestVersion({
132
+ timeoutMs = 1500,
133
+ allowNetwork = true,
134
+ maxAgeMs = CHECK_INTERVAL,
135
+ } = {}) {
136
+ const state = readState()
137
+ const cached = state.latestVersion || null
138
+ const fresh = state.lastCheck && (Date.now() - state.lastCheck) < maxAgeMs
139
+ if (fresh && cached) return cached
140
+ // A stale cache entry still beats nothing for an advisory, so it's the
141
+ // fallback for every failure path below rather than a hard null.
142
+ if (!allowNetwork) return cached
143
+
144
+ const controller = new AbortController()
145
+ const timer = setTimeout(() => controller.abort(), timeoutMs)
146
+ try {
147
+ const res = await fetch('https://registry.npmjs.org/uniweb/latest', { signal: controller.signal })
148
+ if (!res.ok) return cached
149
+ const data = await res.json()
150
+ const latest = data?.version || null
151
+ if (!latest) return cached
152
+ writeState({ lastCheck: Date.now(), latestVersion: latest })
153
+ return latest
154
+ } catch {
155
+ return cached
156
+ } finally {
157
+ clearTimeout(timer)
158
+ }
159
+ }
160
+
106
161
  /**
107
162
  * Fetch the latest version (with a tight timeout) and print a notice if
108
163
  * a newer version is found. Updates the on-disk cache as a side effect