uniweb 0.12.19 → 0.12.21

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,9 +1,9 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-05-06T22:34:24.827Z",
3
+ "generatedAt": "2026-05-13T05:21:17.590Z",
4
4
  "packages": {
5
5
  "@uniweb/build": {
6
- "version": "0.14.4",
6
+ "version": "0.14.5",
7
7
  "path": "framework/build",
8
8
  "deps": [
9
9
  "@uniweb/content-reader",
@@ -14,7 +14,7 @@
14
14
  ]
15
15
  },
16
16
  "@uniweb/content-reader": {
17
- "version": "1.1.10",
17
+ "version": "1.1.11",
18
18
  "path": "framework/content-reader",
19
19
  "deps": []
20
20
  },
@@ -42,24 +42,24 @@
42
42
  "deps": []
43
43
  },
44
44
  "@uniweb/kit": {
45
- "version": "0.9.11",
45
+ "version": "0.9.13",
46
46
  "path": "framework/kit",
47
47
  "deps": [
48
48
  "@uniweb/core"
49
49
  ]
50
50
  },
51
51
  "@uniweb/loom": {
52
- "version": "0.2.2",
52
+ "version": "0.2.3",
53
53
  "path": "framework/loom",
54
54
  "deps": []
55
55
  },
56
56
  "@uniweb/press": {
57
- "version": "0.4.5",
57
+ "version": "0.4.6",
58
58
  "path": "framework/press",
59
59
  "deps": []
60
60
  },
61
61
  "@uniweb/runtime": {
62
- "version": "0.8.13",
62
+ "version": "0.8.14",
63
63
  "path": "framework/runtime",
64
64
  "deps": [
65
65
  "@uniweb/core",
@@ -92,7 +92,7 @@
92
92
  "deps": []
93
93
  },
94
94
  "@uniweb/unipress": {
95
- "version": "0.4.9",
95
+ "version": "0.4.10",
96
96
  "path": "framework/unipress",
97
97
  "deps": [
98
98
  "@uniweb/build",
package/src/index.js CHANGED
@@ -128,9 +128,10 @@ function getCliVersion() {
128
128
  * Commands that always run from the global CLI (no project context needed)
129
129
  */
130
130
  // Commands that always run from the global CLI, never delegating to a
131
- // project-local copy. `update` is here because its primary job is to
132
- // self-update the GLOBAL installdelegating it to project-local would
133
- // short-circuit that intent.
131
+ // project-local copy. `update` is here because it reconciles the project
132
+ // against *this* CLI's version matrix when run from a (newer) global
133
+ // install that's the whole point; delegating to the project-local copy
134
+ // would align the project to the version it already has, i.e. a no-op.
134
135
  const STANDALONE_COMMANDS = new Set([
135
136
  'create', '--help', '-h', '--version', '-v', 'login', 'update',
136
137
  ])
@@ -1205,12 +1206,12 @@ ${colors.cyan}${colors.bright}uniweb login${colors.reset} ${colors.dim}— Log i
1205
1206
  ${colors.bright}Usage:${colors.reset}
1206
1207
  uniweb login [options]
1207
1208
 
1208
- Opens a browser to hub.uniweb.app for OAuth-style login, then captures
1209
+ Opens a browser to www.uniweb.app for OAuth-style login, then captures
1209
1210
  the token via a loopback callback. Falls back to a paste-token prompt
1210
1211
  if the browser flow fails.
1211
1212
 
1212
1213
  ${colors.bright}Options:${colors.reset}
1213
- --backend <url> Override the auth backend (default: https://hub.uniweb.app)
1214
+ --backend <url> Override the auth backend (default: https://www.uniweb.app)
1214
1215
 
1215
1216
  In non-interactive mode (CI / no TTY / --non-interactive), this command
1216
1217
  errors out — set the \`UNIWEB_TOKEN\` env var instead, or run \`login\`
@@ -1284,39 +1285,47 @@ Prints the parsed content shape of a markdown file or folder — the
1284
1285
  Useful for debugging "why isn't my section getting X?".
1285
1286
  `,
1286
1287
  update: `
1287
- ${colors.cyan}${colors.bright}uniweb update${colors.reset} ${colors.dim}— Reconcile workspace state with the running CLI${colors.reset}
1288
+ ${colors.cyan}${colors.bright}uniweb update${colors.reset} ${colors.dim}— Align this project with the running CLI${colors.reset}
1288
1289
 
1289
1290
  ${colors.bright}Usage:${colors.reset}
1290
- uniweb update Self-update + align deps + refresh AGENTS.md
1291
+ uniweb update Align deps + refresh AGENTS.md
1291
1292
  uniweb update --deps-only Only align workspace @uniweb/* deps
1292
1293
  uniweb update --agents-only Only refresh AGENTS.md
1293
1294
  uniweb update --no-deps Skip the deps-alignment step
1294
1295
  uniweb update --no-agents Skip the AGENTS.md step
1295
1296
  uniweb update --dry-run Print survey + would-be writes; no mutations
1296
1297
  uniweb update --allow-mismatch Refresh AGENTS.md even if declared deps lag
1297
- uniweb update --yes Skip confirmation prompts
1298
+ uniweb update --yes Don't prompt — apply edits and run the install
1298
1299
 
1299
1300
  ${colors.bright}What it does:${colors.reset}
1300
1301
  Prints a version survey first (CLI version, AGENTS.md stamp, every
1301
1302
  @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.
1303
+ marked aligned / behind / ahead). Then two steps:
1304
+
1305
+ 1. ${colors.bright}Align workspace deps${colors.reset} to this CLI's bundled version matrix
1306
+ rewrites the @uniweb/* + uniweb keys in each package.json that lags
1307
+ (deps ahead of the matrix are left alone never downgraded;
1308
+ existing indentation is preserved), then offers to run the
1309
+ workspace's package manager (lockfile-detected: pnpm-lock.yaml →
1310
+ pnpm, yarn.lock → yarn, package-lock.json → npm). If the install
1311
+ fails, the package.json edits are kept and a revert command printed.
1312
+ 2. ${colors.bright}Refresh AGENTS.md${colors.reset} from this CLI's bundled partial. Won't run
1313
+ if deps were edited but not installed (node_modules would be behind
1314
+ package.json), or while declared deps still lag the CLI (would
1315
+ document features not in your packages) pass --allow-mismatch for
1316
+ the latter.
1317
+
1318
+ ${colors.bright}Which matrix?${colors.reset}
1319
+ \`update\` pins to the version matrix *this* CLI shipped with — not
1320
+ necessarily the latest release. To reconcile against the latest release
1321
+ without touching a global install, run \`npx uniweb@latest update\`. This
1322
+ command does NOT update the CLI itself; use your package manager
1323
+ (\`npm i -g uniweb@latest\`, \`pnpm add -g uniweb@latest\`, …).
1315
1324
 
1316
1325
  ${colors.bright}Project-local installs:${colors.reset}
1317
- When the running CLI is project-local (lives in node_modules), self-
1318
- update is a no-opthe version is pinned by your project's
1319
- package.json. The deps + AGENTS.md steps still run.
1326
+ When run from a project-local CLI (in node_modules), it aligns the
1327
+ project to that pinned version bump \`uniweb\` in package.json (or use
1328
+ \`npx uniweb@latest update\`) to align to something newer.
1320
1329
  `,
1321
1330
  }
1322
1331
 
@@ -1346,7 +1355,7 @@ ${colors.bright}Commands:${colors.reset}
1346
1355
  inspect <path> Inspect parsed content shape of a markdown file or folder
1347
1356
  docs Generate component documentation
1348
1357
  doctor Diagnose project configuration issues
1349
- update Update AGENTS.md to match installed CLI version
1358
+ update Align workspace deps + AGENTS.md to the running CLI
1350
1359
  i18n <cmd> Internationalization (extract, sync, status)
1351
1360
  template publish Publish a site as a cloud template
1352
1361
  login Log in to your Uniweb account
@@ -20,6 +20,7 @@ import { join } from 'node:path'
20
20
  import { homedir } from 'node:os'
21
21
  import yaml from 'js-yaml'
22
22
  import { filterCmd } from './pm.js'
23
+ import { writeJsonPreservingStyleAsync } from './json-file.js'
23
24
 
24
25
  // ── Platform URLs ──────────────────────────────────────────────
25
26
 
@@ -27,10 +28,8 @@ import { filterCmd } from './pm.js'
27
28
  // REGISTRY hosts platform operations (publish, foundations, runtime, admin):
28
29
  // moved to hosting.uniweb.app in the CDN migration (Phase 4c, 2026-05-04).
29
30
  // BACKEND hosts the PHP user-facing surface (login, account, orgs, billing,
30
- // publish-authorize): owned by the v4 single-domain plan
31
- // (kb/platform/plans/uniweb-domain-plan-v4.md), which will move it to
32
- // uniweb.app/api/* when v4 ships. Until then, it stays at hub.uniweb.app.
33
- const PRODUCTION_BACKEND_URL = 'https://hub.uniweb.app'
31
+ // publish-authorize).
32
+ const PRODUCTION_BACKEND_URL = 'https://www.uniweb.app'
34
33
  const PRODUCTION_REGISTRY_URL = 'https://hosting.uniweb.app'
35
34
 
36
35
  /**
@@ -156,13 +155,19 @@ export async function readRootPackageJson(rootDir) {
156
155
  }
157
156
 
158
157
  /**
159
- * Write root package.json (2-space indent)
158
+ * Write root package.json, preserving the file's existing indentation
159
+ * (newly scaffolded workspaces use 2-space; we don't reflow whatever's
160
+ * already there).
160
161
  * @param {string} rootDir - Workspace root directory
161
162
  * @param {Object} pkg - Package.json object
162
163
  */
163
164
  export async function writeRootPackageJson(rootDir, pkg) {
164
165
  const pkgPath = join(rootDir, 'package.json')
165
- await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
166
+ if (existsSync(pkgPath)) {
167
+ await writeJsonPreservingStyleAsync(pkgPath, pkg)
168
+ } else {
169
+ await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
170
+ }
166
171
  }
167
172
 
168
173
  /**
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Workspace `@uniweb/*` dependency survey.
3
+ *
4
+ * Compares the `@uniweb/*` + `uniweb` versions *declared* in every
5
+ * package.json across a workspace against the running CLI's bundled
6
+ * version matrix (`getResolvedVersions`). Shared by `uniweb update`
7
+ * (which fixes the drift) and `uniweb doctor` (which only reports it) so
8
+ * the two never disagree about what "out of date" means.
9
+ *
10
+ * Comparison is on declared specs, not installed (node_modules) versions
11
+ * — that's what's committed and what `git diff` will show after a fix.
12
+ */
13
+
14
+ import { existsSync, readFileSync } from 'node:fs'
15
+ import { join } from 'node:path'
16
+ import { getResolvedVersions } from '../versions.js'
17
+ import { getWorkspacePackages } from './workspace.js'
18
+
19
+ /**
20
+ * Strip a leading semver range operator (^, ~, >=, <, …) so two specs can
21
+ * be compared by their underlying version. Range expressions like
22
+ * ">=0.5 <0.7" aren't fully parsed — the first version-shaped token wins.
23
+ * Sufficient for `@uniweb/*` deps, which use `^x.y.z` / `x.y.z`.
24
+ * @param {string} spec
25
+ * @returns {string}
26
+ */
27
+ export function stripVersionRange(spec) {
28
+ return (spec || '').replace(/^[\^~>=<\s]+/, '').trim().split(/\s+/)[0] || ''
29
+ }
30
+
31
+ /**
32
+ * Compare two version specs (range prefix tolerated). Returns 1 / -1 / 0.
33
+ * @param {string} a
34
+ * @param {string} b
35
+ * @returns {number}
36
+ */
37
+ export function compareSemver(a, b) {
38
+ const pa = stripVersionRange(a).split('.').map(Number)
39
+ const pb = stripVersionRange(b).split('.').map(Number)
40
+ for (let i = 0; i < 3; i++) {
41
+ if ((pa[i] || 0) > (pb[i] || 0)) return 1
42
+ if ((pa[i] || 0) < (pb[i] || 0)) return -1
43
+ }
44
+ return 0
45
+ }
46
+
47
+ /**
48
+ * @typedef {object} DepRow
49
+ * @property {string} relDir Workspace-relative dir, or '(root)'.
50
+ * @property {string} section 'dependencies' | 'devDependencies' | 'peerDependencies'
51
+ * @property {string} name Package name (e.g. '@uniweb/core' or 'uniweb').
52
+ * @property {string} current The spec declared in package.json.
53
+ * @property {string} target The spec the running CLI's matrix wants.
54
+ * @property {'aligned'|'behind'|'ahead'} status current vs target.
55
+ */
56
+
57
+ /**
58
+ * Survey a workspace's declared `@uniweb/*` + `uniweb` deps against the
59
+ * running CLI's bundled matrix.
60
+ *
61
+ * @param {string} workspaceDir Absolute path to the workspace root.
62
+ * @returns {Promise<{ targets: Record<string,string>, rows: DepRow[], anyDrift: boolean, anyAhead: boolean }>}
63
+ * `anyDrift` — at least one dep lags the matrix. `anyAhead` — at least
64
+ * one dep is newer than the matrix.
65
+ */
66
+ export async function surveyWorkspaceDeps(workspaceDir) {
67
+ const targets = getResolvedVersions()
68
+ const packages = await getWorkspacePackages(workspaceDir)
69
+ const dirs = ['', ...packages]
70
+ const rows = []
71
+ let anyDrift = false
72
+ let anyAhead = false
73
+
74
+ for (const relDir of dirs) {
75
+ const pkgDir = relDir ? join(workspaceDir, relDir) : workspaceDir
76
+ const pkgPath = join(pkgDir, 'package.json')
77
+ if (!existsSync(pkgPath)) continue
78
+ let pkg
79
+ try { pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) } catch { continue }
80
+
81
+ for (const sectionName of ['dependencies', 'devDependencies', 'peerDependencies']) {
82
+ const section = pkg[sectionName]
83
+ if (!section) continue
84
+ for (const [name, current] of Object.entries(section)) {
85
+ if (!(name.startsWith('@uniweb/') || name === 'uniweb')) continue
86
+ const target = targets[name]
87
+ if (!target) continue
88
+ const cmp = compareSemver(target, current)
89
+ let status
90
+ if (cmp > 0) { status = 'behind'; anyDrift = true }
91
+ else if (cmp < 0) { status = 'ahead'; anyAhead = true }
92
+ else { status = 'aligned' }
93
+ rows.push({ relDir: relDir || '(root)', section: sectionName, name, current, target, status })
94
+ }
95
+ }
96
+ }
97
+
98
+ return { targets, rows, anyDrift, anyAhead }
99
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Style-preserving JSON file writes.
3
+ *
4
+ * The CLI rewrites `package.json` in several places (`update`, `doctor`,
5
+ * `rename`, `add`). A naive `JSON.stringify(obj, null, 2)` reflows the
6
+ * *entire* file whenever the project happened to use tabs or 4-space
7
+ * indentation — turning a one-key version bump into a hundred-line diff
8
+ * (and a needless merge-conflict surface). `framework/CLAUDE.md` calls
9
+ * this out as an anti-pattern for human commits; the tooling shouldn't do
10
+ * it either. These helpers detect the file's existing indentation and
11
+ * trailing-newline convention and preserve both.
12
+ */
13
+
14
+ import { readFileSync, writeFileSync } from 'node:fs'
15
+ import { readFile, writeFile } from 'node:fs/promises'
16
+
17
+ /**
18
+ * Detect the indentation unit a JSON source string uses.
19
+ * @param {string} src
20
+ * @returns {number|string} A space count, or '\t' for tab-indented files.
21
+ * Defaults to 2 when the file has no indented lines (e.g. `{}`).
22
+ */
23
+ export function detectJsonIndent(src) {
24
+ const m = src.match(/\n([ \t]+)\S/)
25
+ if (!m) return 2
26
+ const lead = m[1]
27
+ if (lead.includes('\t')) return '\t'
28
+ return lead.length
29
+ }
30
+
31
+ /**
32
+ * Serialize `obj` to JSON using the indentation and trailing-newline
33
+ * convention of `originalSrc` (or of the file currently at `filePath`).
34
+ * @param {object} obj
35
+ * @param {string} originalSrc - The file's current text.
36
+ * @returns {string}
37
+ */
38
+ export function stringifyJsonLike(obj, originalSrc) {
39
+ const indent = detectJsonIndent(originalSrc)
40
+ const body = JSON.stringify(obj, null, indent)
41
+ return originalSrc.endsWith('\n') ? body + '\n' : body
42
+ }
43
+
44
+ /**
45
+ * Write `obj` to `filePath` as JSON, preserving the file's existing
46
+ * indentation and trailing-newline convention. Pass `originalSrc` when
47
+ * the caller already has the file contents in hand (avoids a re-read);
48
+ * otherwise the file is read to sniff its style.
49
+ * @param {string} filePath
50
+ * @param {object} obj
51
+ * @param {string|null} [originalSrc]
52
+ */
53
+ export function writeJsonPreservingStyle(filePath, obj, originalSrc = null) {
54
+ const src = originalSrc ?? readFileSync(filePath, 'utf8')
55
+ writeFileSync(filePath, stringifyJsonLike(obj, src))
56
+ }
57
+
58
+ /**
59
+ * Async counterpart to {@link writeJsonPreservingStyle}, for the CLI's
60
+ * many `node:fs/promises`-based call sites.
61
+ * @param {string} filePath
62
+ * @param {object} obj
63
+ * @param {string|null} [originalSrc]
64
+ */
65
+ export async function writeJsonPreservingStyleAsync(filePath, obj, originalSrc = null) {
66
+ const src = originalSrc ?? await readFile(filePath, 'utf8')
67
+ await writeFile(filePath, stringifyJsonLike(obj, src))
68
+ }
package/src/utils/pm.js CHANGED
@@ -40,6 +40,35 @@ export function detectWorkspacePm(workspaceRoot) {
40
40
  return null
41
41
  }
42
42
 
43
+ /**
44
+ * Detect which package manager owns a *globally installed* `uniweb` CLI
45
+ * binary, by inspecting its install path (`process.argv[1]`). pnpm and
46
+ * yarn keep global packages under recognizable directory segments;
47
+ * everything else is assumed to be npm. Only meaningful when the CLI is
48
+ * actually a global install (see index.js::isGlobalInstall) — a
49
+ * project-local or npx-launched copy is updated differently.
50
+ *
51
+ * @returns {'pnpm' | 'yarn' | 'npm'}
52
+ */
53
+ export function detectGlobalCliPm() {
54
+ const path = (process.argv[1] || '').toLowerCase().replace(/\\/g, '/')
55
+ if (path.includes('/pnpm/')) return 'pnpm'
56
+ if (path.includes('/yarn/')) return 'yarn'
57
+ return 'npm'
58
+ }
59
+
60
+ /**
61
+ * The command to (re)install the latest `uniweb` CLI globally with a
62
+ * given package manager.
63
+ * @param {'pnpm' | 'yarn' | 'npm'} pm
64
+ * @returns {string}
65
+ */
66
+ export function globalCliUpdateCmd(pm) {
67
+ if (pm === 'pnpm') return 'pnpm add -g uniweb@latest'
68
+ if (pm === 'yarn') return 'yarn global add uniweb@latest'
69
+ return 'npm i -g uniweb@latest'
70
+ }
71
+
43
72
  /**
44
73
  * Generate a workspace-filtered command.
45
74
  * pnpm: "pnpm --filter site dev"
@@ -13,6 +13,7 @@ import yaml from 'js-yaml'
13
13
  import Handlebars from 'handlebars'
14
14
  import { copyTemplateDirectory, enumerateTemplateOutputs, registerVersions } from '../templates/processor.js'
15
15
  import { getVersionsForTemplates, getCliVersion } from '../versions.js'
16
+ import { writeJsonPreservingStyleAsync } from './json-file.js'
16
17
 
17
18
  const __dirname = dirname(fileURLToPath(import.meta.url))
18
19
  const TEMPLATES_DIR = join(__dirname, '..', '..', 'templates')
@@ -380,12 +381,13 @@ function resolveDependencyVersion(rawValue) {
380
381
  */
381
382
  export async function mergeTemplateDependencies(packageJsonPath, deps) {
382
383
  if (!deps || Object.keys(deps).length === 0) return
383
- const pkg = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'))
384
+ const src = await fs.readFile(packageJsonPath, 'utf-8')
385
+ const pkg = JSON.parse(src)
384
386
  if (!pkg.dependencies) pkg.dependencies = {}
385
387
  for (const [name, version] of Object.entries(deps)) {
386
388
  if (!pkg.dependencies[name] && !pkg.devDependencies?.[name]) {
387
389
  pkg.dependencies[name] = resolveDependencyVersion(version)
388
390
  }
389
391
  }
390
- await fs.writeFile(packageJsonPath, JSON.stringify(pkg, null, 2) + '\n')
392
+ await writeJsonPreservingStyleAsync(packageJsonPath, pkg, src)
391
393
  }
@@ -9,6 +9,7 @@
9
9
  import { homedir } from 'node:os'
10
10
  import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs'
11
11
  import { join } from 'node:path'
12
+ import { detectGlobalCliPm, globalCliUpdateCmd } from './pm.js'
12
13
 
13
14
  const CHECK_INTERVAL = 24 * 60 * 60 * 1000 // 1 day
14
15
  const STATE_DIR = join(homedir(), '.uniweb')
@@ -62,14 +63,15 @@ function printNotification(current, latest, tone = 'soft') {
62
63
  const cyan = '\x1b[36m'
63
64
  const dim = '\x1b[2m'
64
65
  const reset = '\x1b[0m'
66
+ const updateCmd = globalCliUpdateCmd(detectGlobalCliPm())
65
67
  console.error('')
66
68
  if (tone === 'eager') {
67
69
  console.error(`${yellow}Heads up:${reset} this CLI is ${dim}${current}${reset}; latest is ${cyan}${latest}${reset}.`)
68
- console.error(`${dim}Templates ship with the CLI — consider updating first:${reset} npm i -g uniweb`)
70
+ console.error(`${dim}Templates ship with the CLI — consider updating first:${reset} ${updateCmd}`)
69
71
  console.error(`${dim}Or run a one-shot fresh:${reset} npx uniweb@latest <command>`)
70
72
  } else {
71
73
  console.error(`${yellow}Update available:${reset} ${dim}${current}${reset} → ${cyan}${latest}${reset}`)
72
- console.error(`${dim}Run${reset} npm i -g uniweb ${dim}to update${reset}`)
74
+ console.error(`${dim}Run${reset} ${updateCmd} ${dim}to update the CLI${reset}`)
73
75
  }
74
76
  }
75
77
 
package/src/versions.js CHANGED
@@ -76,9 +76,16 @@ function getFrameworkRoot() {
76
76
 
77
77
  /**
78
78
  * Read the current on-disk version of a specific `@uniweb/*` package by
79
- * looking up `framework/<last-segment>/package.json`. Returns a caret
80
- * range string like `^0.6.0`, or null if the package isn't present on
81
- * disk (i.e. the CLI is running from npm, not from the workspace).
79
+ * looking up `framework/<last-segment>/package.json`. Returns the version
80
+ * string verbatim (e.g. `0.7.11`), or null if the package isn't present
81
+ * on disk (i.e. the CLI is running from npm, not from the workspace).
82
+ *
83
+ * Returns the version *exact*, not as a caret range, to match the shape
84
+ * published-CLI direct deps take after pnpm resolves `workspace:*` at
85
+ * publish time. Both modes converge on identical specs in
86
+ * `getResolvedVersions()`, so `uniweb update` and the scaffolder produce
87
+ * the same `package.json` whether the CLI ran from npm or from this
88
+ * monorepo.
82
89
  */
83
90
  function readWorkspaceVersion(packageName) {
84
91
  const root = getFrameworkRoot()
@@ -90,7 +97,7 @@ function readWorkspaceVersion(packageName) {
90
97
  try {
91
98
  const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
92
99
  if (pkg.name === packageName && pkg.version) {
93
- return `^${pkg.version}`
100
+ return pkg.version
94
101
  }
95
102
  } catch {}
96
103
  return null