uniweb 0.12.20 → 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.
- package/README.md +1 -1
- package/package.json +2 -2
- package/src/commands/doctor.js +24 -5
- package/src/commands/publish.js +4 -3
- package/src/commands/rename.js +10 -5
- package/src/commands/update.js +211 -245
- package/src/framework-index.json +3 -3
- package/src/index.js +32 -23
- package/src/utils/config.js +9 -2
- package/src/utils/dep-survey.js +99 -0
- package/src/utils/json-file.js +68 -0
- package/src/utils/pm.js +29 -0
- package/src/utils/scaffold.js +4 -2
- package/src/utils/update-check.js +4 -2
- package/src/versions.js +11 -4
package/README.md
CHANGED
|
@@ -351,7 +351,7 @@ A future version will let markdown in a git repo and content in the Uniweb apps
|
|
|
351
351
|
| `uniweb export` | Produce a self-contained `dist/` for any static host. You upload it yourself. `--host=<adapter>` adds host-specific helper files. |
|
|
352
352
|
| `uniweb publish @org/name` | Publish a foundation to the registry (path 2). |
|
|
353
353
|
| `uniweb build` | Inspect a build locally. For shipping, use `deploy` or `export`. |
|
|
354
|
-
| `uniweb update` |
|
|
354
|
+
| `uniweb update` | Align this project with the CLI you're running: bump `@uniweb/*` deps in every `package.json` to the CLI's matrix (then install), and refresh `AGENTS.md`. Pins to *this* CLI's matrix — run `npx uniweb@latest update` to align to the latest release. Updating the CLI itself is your package manager's job (`npm i -g uniweb@latest`). |
|
|
355
355
|
|
|
356
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.
|
|
357
357
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.21",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"js-yaml": "^4.1.0",
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
|
-
"@uniweb/core": "0.7.11",
|
|
45
44
|
"@uniweb/runtime": "0.8.14",
|
|
45
|
+
"@uniweb/core": "0.7.11",
|
|
46
46
|
"@uniweb/kit": "0.9.13"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
package/src/commands/doctor.js
CHANGED
|
@@ -10,6 +10,8 @@ import { loadDeployYml } from '@uniweb/build/site'
|
|
|
10
10
|
import { listAdapters } from '@uniweb/build/hosts'
|
|
11
11
|
import { getCliVersion } from '../versions.js'
|
|
12
12
|
import { readAgentsVersion } from '../utils/agents-stamp.js'
|
|
13
|
+
import { writeJsonPreservingStyle } from '../utils/json-file.js'
|
|
14
|
+
import { surveyWorkspaceDeps } from '../utils/dep-survey.js'
|
|
13
15
|
import { discoverFoundations, discoverSites } from '../utils/discover.js'
|
|
14
16
|
import { findWorkspaceRoot } from '../utils/workspace.js'
|
|
15
17
|
|
|
@@ -204,9 +206,10 @@ export async function doctor(args = []) {
|
|
|
204
206
|
const union = Array.from(new Set([...ymlPackages, ...pkgWorkspaces])).sort()
|
|
205
207
|
writeFileSync(ymlPath, yaml.dump({ packages: union }, { flowLevel: -1, quotingType: '"' }))
|
|
206
208
|
const rootPkgPath = join(workspaceDir, 'package.json')
|
|
207
|
-
const
|
|
209
|
+
const rootPkgSrc = readFileSync(rootPkgPath, 'utf-8')
|
|
210
|
+
const rootPkg = JSON.parse(rootPkgSrc)
|
|
208
211
|
rootPkg.workspaces = union
|
|
209
|
-
|
|
212
|
+
writeJsonPreservingStyle(rootPkgPath, rootPkg, rootPkgSrc)
|
|
210
213
|
issue.fixed = true
|
|
211
214
|
fixed(`wrote union [${union.join(', ')}] to both manifests`)
|
|
212
215
|
} else {
|
|
@@ -364,7 +367,7 @@ export async function doctor(args = []) {
|
|
|
364
367
|
const sitePkgPath = join(sitePath, 'package.json')
|
|
365
368
|
const updatedPkg = { ...sitePkg }
|
|
366
369
|
updatedPkg.dependencies = { ...(updatedPkg.dependencies || {}), [foundationName]: expectedPath }
|
|
367
|
-
|
|
370
|
+
writeJsonPreservingStyle(sitePkgPath, updatedPkg)
|
|
368
371
|
issue.fixed = true
|
|
369
372
|
fixed(`added "${foundationName}": "${expectedPath}" to ${relative(workspaceDir, sitePkgPath)}`)
|
|
370
373
|
} else {
|
|
@@ -391,7 +394,7 @@ export async function doctor(args = []) {
|
|
|
391
394
|
const sitePkgPath = join(sitePath, 'package.json')
|
|
392
395
|
const updatedPkg = { ...sitePkg }
|
|
393
396
|
updatedPkg.dependencies = { ...(updatedPkg.dependencies || {}), [foundationName]: expectedPath }
|
|
394
|
-
|
|
397
|
+
writeJsonPreservingStyle(sitePkgPath, updatedPkg)
|
|
395
398
|
issue.fixed = true
|
|
396
399
|
fixed(`updated "${foundationName}" to "${expectedPath}" in ${relative(workspaceDir, sitePkgPath)}`)
|
|
397
400
|
} else {
|
|
@@ -672,11 +675,27 @@ export async function doctor(args = []) {
|
|
|
672
675
|
}
|
|
673
676
|
}
|
|
674
677
|
|
|
678
|
+
const cliVersion = getCliVersion()
|
|
679
|
+
|
|
680
|
+
// Check @uniweb/* dep alignment with the running CLI
|
|
681
|
+
log('')
|
|
682
|
+
const depSurvey = await surveyWorkspaceDeps(workspaceDir)
|
|
683
|
+
const behindDeps = depSurvey.rows.filter(r => r.status === 'behind')
|
|
684
|
+
if (behindDeps.length > 0) {
|
|
685
|
+
const names = [...new Set(behindDeps.map(r => r.name))].sort()
|
|
686
|
+
warn(`${behindDeps.length} workspace dep declaration${behindDeps.length === 1 ? '' : 's'} lag the CLI (v${cliVersion}): ${names.join(', ')}`)
|
|
687
|
+
info(`Run: uniweb update`)
|
|
688
|
+
issues.push({ type: 'warn', message: `${behindDeps.length} @uniweb/* dep declaration(s) behind CLI v${cliVersion}` })
|
|
689
|
+
} else if (depSurvey.anyAhead) {
|
|
690
|
+
success(`@uniweb/* deps are aligned or ahead of the CLI (v${cliVersion})`)
|
|
691
|
+
} else {
|
|
692
|
+
success(`@uniweb/* deps are aligned with the CLI (v${cliVersion})`)
|
|
693
|
+
}
|
|
694
|
+
|
|
675
695
|
// Check AGENTS.md freshness
|
|
676
696
|
log('')
|
|
677
697
|
const agentsPath = join(workspaceDir, 'AGENTS.md')
|
|
678
698
|
const agentsVersion = readAgentsVersion(agentsPath)
|
|
679
|
-
const cliVersion = getCliVersion()
|
|
680
699
|
|
|
681
700
|
if (!existsSync(agentsPath)) {
|
|
682
701
|
warn('AGENTS.md not found')
|
package/src/commands/publish.js
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
36
|
import { existsSync } from 'node:fs'
|
|
37
|
-
import { readFile,
|
|
37
|
+
import { readFile, mkdir } from 'node:fs/promises'
|
|
38
38
|
import { resolve, join } from 'node:path'
|
|
39
39
|
import { execSync } from 'node:child_process'
|
|
40
40
|
|
|
@@ -42,6 +42,7 @@ import { resolveFoundationSrcPath, classifyPackage } from '@uniweb/build'
|
|
|
42
42
|
import { createLocalRegistry, RemoteRegistry } from '../utils/registry.js'
|
|
43
43
|
import { ensureAuth, readAuth, writeAuth, decodeJwtPayload } from '../utils/auth.js'
|
|
44
44
|
import { getRegistryUrl, getBackendUrl } from '../utils/config.js'
|
|
45
|
+
import { writeJsonPreservingStyleAsync } from '../utils/json-file.js'
|
|
45
46
|
import { findWorkspaceRoot, findFoundations, findSites, promptSelect } from '../utils/workspace.js'
|
|
46
47
|
import { isNonInteractive, getCliPrefix } from '../utils/interactive.js'
|
|
47
48
|
|
|
@@ -681,7 +682,7 @@ export async function publish(args = []) {
|
|
|
681
682
|
if (writeBackId) {
|
|
682
683
|
pkg.uniweb = pkg.uniweb || {}
|
|
683
684
|
pkg.uniweb.id = foundationName
|
|
684
|
-
await
|
|
685
|
+
await writeJsonPreservingStyleAsync(pkgPath, pkg)
|
|
685
686
|
info(`Wrote ${colors.cyan}uniweb.id: "${foundationName}"${colors.reset} to ${colors.dim}package.json${colors.reset}`)
|
|
686
687
|
}
|
|
687
688
|
|
|
@@ -851,7 +852,7 @@ export async function publish(args = []) {
|
|
|
851
852
|
if (writeBackId) {
|
|
852
853
|
pkg.uniweb = pkg.uniweb || {}
|
|
853
854
|
pkg.uniweb.id = foundationName
|
|
854
|
-
await
|
|
855
|
+
await writeJsonPreservingStyleAsync(pkgPath, pkg)
|
|
855
856
|
info(`Wrote ${colors.cyan}uniweb.id: "${foundationName}"${colors.reset} to ${colors.dim}package.json${colors.reset}`)
|
|
856
857
|
}
|
|
857
858
|
console.log('')
|
package/src/commands/rename.js
CHANGED
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
updateRootScripts,
|
|
55
55
|
} from '../utils/config.js'
|
|
56
56
|
import { discoverFoundations, discoverSites } from '../utils/discover.js'
|
|
57
|
+
import { writeJsonPreservingStyleAsync } from '../utils/json-file.js'
|
|
57
58
|
import { getExistingPackageNames, validatePackageName } from '../utils/names.js'
|
|
58
59
|
import { detectPackageManager, installCmd } from '../utils/pm.js'
|
|
59
60
|
import { getCliPrefix } from '../utils/interactive.js'
|
|
@@ -184,9 +185,10 @@ async function validateRenameName(rootDir, newName) {
|
|
|
184
185
|
* Rewrite the `name` field in a package.json file.
|
|
185
186
|
*/
|
|
186
187
|
async function rewritePackageJsonName(pkgPath, newName) {
|
|
187
|
-
const
|
|
188
|
+
const src = await readFile(pkgPath, 'utf-8')
|
|
189
|
+
const pkg = JSON.parse(src)
|
|
188
190
|
pkg.name = newName
|
|
189
|
-
await
|
|
191
|
+
await writeJsonPreservingStyleAsync(pkgPath, pkg, src)
|
|
190
192
|
}
|
|
191
193
|
|
|
192
194
|
// ─── Foundation rename ───────────────────────────────────────────
|
|
@@ -234,11 +236,13 @@ async function renameFoundation(rootDir, oldName, newName, prefix) {
|
|
|
234
236
|
for (const site of sites) {
|
|
235
237
|
const sitePkgPath = join(rootDir, site.path, 'package.json')
|
|
236
238
|
const siteYmlPath = join(rootDir, site.path, 'site.yml')
|
|
237
|
-
let pkg, ymlData
|
|
239
|
+
let pkg, pkgSrc, ymlData
|
|
238
240
|
try {
|
|
239
|
-
|
|
241
|
+
pkgSrc = await readFile(sitePkgPath, 'utf-8')
|
|
242
|
+
pkg = JSON.parse(pkgSrc)
|
|
240
243
|
} catch {
|
|
241
244
|
pkg = null
|
|
245
|
+
pkgSrc = null
|
|
242
246
|
}
|
|
243
247
|
try {
|
|
244
248
|
ymlData = yaml.load(await readFile(siteYmlPath, 'utf-8')) || {}
|
|
@@ -252,6 +256,7 @@ async function renameFoundation(rootDir, oldName, newName, prefix) {
|
|
|
252
256
|
path: site.path,
|
|
253
257
|
name: site.name,
|
|
254
258
|
pkg,
|
|
259
|
+
pkgSrc,
|
|
255
260
|
sitePkgPath,
|
|
256
261
|
siteYmlPath,
|
|
257
262
|
ymlData,
|
|
@@ -295,7 +300,7 @@ async function renameFoundation(rootDir, oldName, newName, prefix) {
|
|
|
295
300
|
s.pkg.dependencies[newName] = oldValue.startsWith('file:')
|
|
296
301
|
? `file:${newRel}`
|
|
297
302
|
: oldValue // npm-pinned, leave it; rename-then-republish is out of scope.
|
|
298
|
-
await
|
|
303
|
+
await writeJsonPreservingStyleAsync(s.sitePkgPath, s.pkg, s.pkgSrc)
|
|
299
304
|
}
|
|
300
305
|
if (s.ymlMatches) {
|
|
301
306
|
const newYmlData = { ...s.ymlData, foundation: newName }
|
package/src/commands/update.js
CHANGED
|
@@ -1,32 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* uniweb update — Reconcile a Uniweb workspace's state with the
|
|
3
|
-
*
|
|
2
|
+
* uniweb update — Reconcile a Uniweb workspace's state with the CLI that's
|
|
3
|
+
* running this command. One job, two convergence steps:
|
|
4
4
|
*
|
|
5
|
-
* 1.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* 1. Align workspace `@uniweb/*` + `uniweb` deps to this CLI's bundled
|
|
6
|
+
* version matrix (`getResolvedVersions`) — only deps that *lag* the
|
|
7
|
+
* matrix are touched; deps that are *ahead* are left alone (no
|
|
8
|
+
* downgrades) — then run `<pm> install`.
|
|
9
|
+
* 2. Refresh AGENTS.md from this CLI's bundled partial.
|
|
9
10
|
*
|
|
10
|
-
* Why
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* doc that documents features the installed code doesn't have.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
11
|
+
* Why those two belong together: AGENTS.md is regenerated from the CLI's
|
|
12
|
+
* *current* partials and stamped with `cliVersion`. Refreshing it while
|
|
13
|
+
* the installed `@uniweb/*` packages still lag the CLI silently produces a
|
|
14
|
+
* doc that documents features the installed code doesn't have. So step 2
|
|
15
|
+
* refuses to run unless step 1 actually completed (deps aligned *and*
|
|
16
|
+
* installed). `--allow-mismatch` overrides the declared-deps half of that
|
|
17
|
+
* gate; nothing overrides "you skipped the install" — run the install and
|
|
18
|
+
* re-run `update`.
|
|
19
|
+
*
|
|
20
|
+
* What this command does NOT do: update the CLI itself. A globally
|
|
21
|
+
* installed `uniweb` is updated through its package manager
|
|
22
|
+
* (`npm i -g uniweb@latest`, `pnpm add -g uniweb@latest`, …); the version
|
|
23
|
+
* notification machinery (`utils/update-check.js`, `uniweb --version`)
|
|
24
|
+
* surfaces when that's needed. To reconcile a project against the *latest*
|
|
25
|
+
* release without touching a global install, run `npx uniweb@latest
|
|
26
|
+
* update` — npx fetches the latest CLI, which carries its own matrix.
|
|
16
27
|
*
|
|
17
28
|
* Flags:
|
|
18
|
-
* --agents-only
|
|
19
|
-
* --deps-only
|
|
29
|
+
* --agents-only Only refresh AGENTS.md (skip the deps step).
|
|
30
|
+
* --deps-only Only align deps (skip the AGENTS.md step).
|
|
20
31
|
* --no-agents Skip the AGENTS.md step.
|
|
21
32
|
* --no-deps Skip the deps-alignment step.
|
|
22
33
|
* --dry-run Print survey + would-be writes; no mutations.
|
|
23
|
-
* --allow-mismatch
|
|
24
|
-
* --yes
|
|
25
|
-
* --non-interactive Auto-detected;
|
|
26
|
-
*
|
|
27
|
-
* Project-local case (CLI lives in node_modules, not global): self-update
|
|
28
|
-
* is a no-op (the version is pinned by package.json). Deps + AGENTS.md
|
|
29
|
-
* paths still run.
|
|
34
|
+
* --allow-mismatch Refresh AGENTS.md even if declared deps lag.
|
|
35
|
+
* --yes Don't prompt — apply edits and run the install.
|
|
36
|
+
* --non-interactive Auto-detected; prints the plan, never mutates
|
|
37
|
+
* (combine with --yes to apply non-interactively).
|
|
30
38
|
*/
|
|
31
39
|
|
|
32
40
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
@@ -34,11 +42,13 @@ import { join } from 'node:path'
|
|
|
34
42
|
import { spawn } from 'node:child_process'
|
|
35
43
|
import prompts from 'prompts'
|
|
36
44
|
|
|
37
|
-
import { findWorkspaceRoot
|
|
45
|
+
import { findWorkspaceRoot } from '../utils/workspace.js'
|
|
38
46
|
import { readAgentsVersion, generateAgentsContent } from '../utils/agents-stamp.js'
|
|
39
|
-
import { getCliVersion
|
|
47
|
+
import { getCliVersion } from '../versions.js'
|
|
40
48
|
import { isNonInteractive } from '../utils/interactive.js'
|
|
41
|
-
import { detectWorkspacePm, installCmd } from '../utils/pm.js'
|
|
49
|
+
import { detectWorkspacePm, installCmd, detectGlobalCliPm, globalCliUpdateCmd } from '../utils/pm.js'
|
|
50
|
+
import { writeJsonPreservingStyle } from '../utils/json-file.js'
|
|
51
|
+
import { surveyWorkspaceDeps, compareSemver } from '../utils/dep-survey.js'
|
|
42
52
|
|
|
43
53
|
const colors = {
|
|
44
54
|
reset: '\x1b[0m',
|
|
@@ -57,9 +67,9 @@ const info = (msg) => console.log(`${colors.cyan}ℹ${colors.reset} ${msg}`)
|
|
|
57
67
|
const log = console.log
|
|
58
68
|
|
|
59
69
|
/**
|
|
60
|
-
* Detect whether this CLI is running from a global install
|
|
61
|
-
*
|
|
62
|
-
*
|
|
70
|
+
* Detect whether this CLI is running from a global install — when global,
|
|
71
|
+
* process.argv[1] points outside any node_modules. Mirrors
|
|
72
|
+
* index.js::isGlobalInstall.
|
|
63
73
|
*/
|
|
64
74
|
function isGlobalInstall() {
|
|
65
75
|
const scriptPath = process.argv[1]
|
|
@@ -68,6 +78,17 @@ function isGlobalInstall() {
|
|
|
68
78
|
!scriptPath.split('\\').includes('node_modules')
|
|
69
79
|
}
|
|
70
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Detect whether this CLI was launched via `npx uniweb …` (or `npm exec`).
|
|
83
|
+
* npx materializes the package under `…/_npx/<hash>/node_modules/uniweb/…`,
|
|
84
|
+
* which `isGlobalInstall()` can't distinguish from a real project-local
|
|
85
|
+
* dependency (both contain `node_modules`).
|
|
86
|
+
*/
|
|
87
|
+
function isNpxInvocation() {
|
|
88
|
+
const p = (process.argv[1] || '').replace(/\\/g, '/')
|
|
89
|
+
return p.includes('/_npx/')
|
|
90
|
+
}
|
|
91
|
+
|
|
71
92
|
/**
|
|
72
93
|
* Find a *Uniweb* workspace root from cwd. Stricter than findWorkspaceRoot
|
|
73
94
|
* — also requires that the workspace's root package.json declares uniweb
|
|
@@ -88,32 +109,7 @@ function findUniwebWorkspace(cwd) {
|
|
|
88
109
|
}
|
|
89
110
|
}
|
|
90
111
|
|
|
91
|
-
/**
|
|
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).
|
|
95
|
-
*
|
|
96
|
-
* @returns {'pnpm'|'yarn'|'npm'}
|
|
97
|
-
*/
|
|
98
|
-
function detectGlobalPm() {
|
|
99
|
-
const path = (process.argv[1] || '').toLowerCase()
|
|
100
|
-
if (path.includes('/pnpm/') || path.includes('\\pnpm\\')) return 'pnpm'
|
|
101
|
-
if (path.includes('/yarn/') || path.includes('\\yarn\\')) return 'yarn'
|
|
102
|
-
return 'npm'
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Build the global-install command for a given PM.
|
|
107
|
-
*/
|
|
108
|
-
function globalInstallCmd(pm) {
|
|
109
|
-
if (pm === 'pnpm') return 'pnpm add -g uniweb@latest'
|
|
110
|
-
if (pm === 'yarn') return 'yarn global add uniweb@latest'
|
|
111
|
-
return 'npm i -g uniweb@latest'
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Fetch the latest published version. Returns null on network error.
|
|
116
|
-
*/
|
|
112
|
+
/** Fetch the latest published CLI version. Returns null on network error. */
|
|
117
113
|
async function fetchLatestVersion() {
|
|
118
114
|
try {
|
|
119
115
|
const res = await fetch('https://registry.npmjs.org/uniweb/latest')
|
|
@@ -125,32 +121,7 @@ async function fetchLatestVersion() {
|
|
|
125
121
|
}
|
|
126
122
|
}
|
|
127
123
|
|
|
128
|
-
/**
|
|
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.
|
|
140
|
-
*/
|
|
141
|
-
function compareSemver(a, b) {
|
|
142
|
-
const pa = stripRange(a).split('.').map(Number)
|
|
143
|
-
const pb = stripRange(b).split('.').map(Number)
|
|
144
|
-
for (let i = 0; i < 3; i++) {
|
|
145
|
-
if ((pa[i] || 0) > (pb[i] || 0)) return 1
|
|
146
|
-
if ((pa[i] || 0) < (pb[i] || 0)) return -1
|
|
147
|
-
}
|
|
148
|
-
return 0
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Run a shell command, inheriting stdio. Resolves with the exit code.
|
|
153
|
-
*/
|
|
124
|
+
/** Run a shell command, inheriting stdio. Resolves with the exit code. */
|
|
154
125
|
function runCommand(cmd, cwd) {
|
|
155
126
|
return new Promise((resolve) => {
|
|
156
127
|
const [bin, ...rest] = cmd.split(' ')
|
|
@@ -160,60 +131,7 @@ function runCommand(cmd, cwd) {
|
|
|
160
131
|
})
|
|
161
132
|
}
|
|
162
133
|
|
|
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
|
-
*/
|
|
134
|
+
/** Print the survey report grouped by package directory. */
|
|
217
135
|
function printSurvey(report, cliVersion, agentsVersion) {
|
|
218
136
|
log('')
|
|
219
137
|
log(`${colors.bright}uniweb CLI:${colors.reset} v${cliVersion}`)
|
|
@@ -256,17 +174,23 @@ function printSurvey(report, cliVersion, agentsVersion) {
|
|
|
256
174
|
}
|
|
257
175
|
|
|
258
176
|
/**
|
|
259
|
-
* Apply the CLI's
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
177
|
+
* Apply the CLI's matrix to workspace package.json files — but only to
|
|
178
|
+
* deps that *lag* the matrix (survey status `behind`). Deps that are
|
|
179
|
+
* `ahead` are left alone: `update` never downgrades. Indentation and the
|
|
180
|
+
* trailing newline of each file are preserved (a one-key bump shouldn't
|
|
181
|
+
* reflow the whole file). Returns the list of paths that actually changed.
|
|
263
182
|
*/
|
|
264
|
-
|
|
265
|
-
const
|
|
266
|
-
const
|
|
267
|
-
const
|
|
183
|
+
function applyDepUpdates(workspaceDir, surveyRows, dryRun) {
|
|
184
|
+
const behind = surveyRows.filter(r => r.status === 'behind')
|
|
185
|
+
const byDir = {}
|
|
186
|
+
for (const row of behind) {
|
|
187
|
+
const dir = row.relDir === '(root)' ? '' : row.relDir
|
|
188
|
+
if (!byDir[dir]) byDir[dir] = []
|
|
189
|
+
byDir[dir].push(row)
|
|
190
|
+
}
|
|
268
191
|
|
|
269
|
-
|
|
192
|
+
const edited = []
|
|
193
|
+
for (const [relDir, dirRows] of Object.entries(byDir)) {
|
|
270
194
|
const pkgDir = relDir ? join(workspaceDir, relDir) : workspaceDir
|
|
271
195
|
const pkgPath = join(pkgDir, 'package.json')
|
|
272
196
|
if (!existsSync(pkgPath)) continue
|
|
@@ -274,12 +198,18 @@ async function applyDepUpdates(workspaceDir, dryRun) {
|
|
|
274
198
|
let pkg
|
|
275
199
|
try { pkg = JSON.parse(original) } catch { continue }
|
|
276
200
|
|
|
277
|
-
|
|
278
|
-
|
|
201
|
+
let changed = false
|
|
202
|
+
for (const row of dirRows) {
|
|
203
|
+
const section = pkg[row.section]
|
|
204
|
+
if (section && section[row.name] !== undefined && section[row.name] !== row.target) {
|
|
205
|
+
section[row.name] = row.target
|
|
206
|
+
changed = true
|
|
207
|
+
}
|
|
208
|
+
}
|
|
279
209
|
|
|
280
|
-
if (
|
|
210
|
+
if (changed) {
|
|
281
211
|
edited.push(pkgPath)
|
|
282
|
-
if (!dryRun)
|
|
212
|
+
if (!dryRun) writeJsonPreservingStyle(pkgPath, pkg, original)
|
|
283
213
|
}
|
|
284
214
|
}
|
|
285
215
|
|
|
@@ -297,9 +227,10 @@ export async function update(args = []) {
|
|
|
297
227
|
const skipDeps = args.includes('--no-deps') || agentsOnly
|
|
298
228
|
const dryRun = args.includes('--dry-run')
|
|
299
229
|
const allowMismatch = args.includes('--allow-mismatch')
|
|
230
|
+
const hasYes = args.includes('--yes')
|
|
300
231
|
const nonInteractive = isNonInteractive(args)
|
|
301
|
-
const skipPrompt = args.includes('--yes') || nonInteractive || dryRun
|
|
302
232
|
const isGlobal = isGlobalInstall()
|
|
233
|
+
const isNpx = isNpxInvocation()
|
|
303
234
|
const workspaceDir = findUniwebWorkspace(process.cwd())
|
|
304
235
|
const inProject = !!workspaceDir
|
|
305
236
|
const cliVersion = getCliVersion()
|
|
@@ -314,88 +245,73 @@ export async function update(args = []) {
|
|
|
314
245
|
let survey = null
|
|
315
246
|
let agentsVersion = null
|
|
316
247
|
if (inProject) {
|
|
317
|
-
survey = await
|
|
248
|
+
survey = await surveyWorkspaceDeps(workspaceDir)
|
|
318
249
|
agentsVersion = readAgentsVersion(join(workspaceDir, 'AGENTS.md'))
|
|
319
250
|
printSurvey(survey, cliVersion, agentsVersion)
|
|
320
251
|
}
|
|
321
252
|
|
|
322
|
-
// ──
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
253
|
+
// ── This command reconciles the *project*, not the CLI ───────────
|
|
254
|
+
// Surface (but don't act on) a newer published CLI: this run aligns
|
|
255
|
+
// the project to *this* CLI's matrix.
|
|
256
|
+
let installPm = inProject ? detectWorkspacePm(workspaceDir) : null
|
|
257
|
+
if (isNpx) {
|
|
258
|
+
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}`)
|
|
259
|
+
log(`${colors.dim}(To install the CLI:${colors.reset} ${colors.cyan}npm i -g uniweb${colors.reset}${colors.dim}.)${colors.reset}`)
|
|
260
|
+
log('')
|
|
261
|
+
} else if (isGlobal) {
|
|
262
|
+
const latest = await fetchLatestVersion()
|
|
263
|
+
if (latest && compareSemver(latest, cliVersion) > 0) {
|
|
264
|
+
const pm = detectGlobalCliPm()
|
|
265
|
+
log(`${colors.yellow}A newer uniweb is available:${colors.reset} ${colors.dim}v${cliVersion}${colors.reset} → ${colors.cyan}v${latest}${colors.reset}`)
|
|
266
|
+
log(`${colors.dim}This run aligns the project to v${cliVersion}. To update the CLI:${colors.reset} ${colors.cyan}${globalCliUpdateCmd(pm)}${colors.reset}`)
|
|
267
|
+
log(`${colors.dim}Or, to align to the latest release without a global install:${colors.reset} ${colors.cyan}npx uniweb@latest update${colors.reset}`)
|
|
327
268
|
log('')
|
|
328
|
-
} else {
|
|
329
|
-
const latest = await fetchLatestVersion()
|
|
330
|
-
if (latest === null) {
|
|
331
|
-
warn('Could not reach the npm registry to check for updates.')
|
|
332
|
-
log(`${colors.dim}Current: ${cliVersion}. Try later, or run${colors.reset} ${colors.cyan}${globalInstallCmd(detectGlobalPm())}${colors.reset}${colors.dim} manually.${colors.reset}`)
|
|
333
|
-
log('')
|
|
334
|
-
} else if (compareSemver(latest, cliVersion) <= 0) {
|
|
335
|
-
success(`uniweb is up to date (v${cliVersion}).`)
|
|
336
|
-
log('')
|
|
337
|
-
} else {
|
|
338
|
-
const pm = detectGlobalPm()
|
|
339
|
-
const cmd = globalInstallCmd(pm)
|
|
340
|
-
log(`${colors.yellow}Update available:${colors.reset} ${colors.dim}${cliVersion}${colors.reset} → ${colors.cyan}${latest}${colors.reset}`)
|
|
341
|
-
log(`${colors.dim}Detected package manager:${colors.reset} ${pm}`)
|
|
342
|
-
log(`${colors.dim}Will run:${colors.reset} ${colors.cyan}${cmd}${colors.reset}`)
|
|
343
|
-
log('')
|
|
344
|
-
|
|
345
|
-
if (dryRun) {
|
|
346
|
-
info(`${colors.dim}--dry-run: would run \`${cmd}\`.${colors.reset}`)
|
|
347
|
-
log('')
|
|
348
|
-
} else if (skipPrompt) {
|
|
349
|
-
log(`${colors.dim}Non-interactive — skipping self-update. Run the command above to update.${colors.reset}`)
|
|
350
|
-
log('')
|
|
351
|
-
} else {
|
|
352
|
-
const { go } = await prompts({
|
|
353
|
-
type: 'confirm',
|
|
354
|
-
name: 'go',
|
|
355
|
-
message: `Run \`${cmd}\` now?`,
|
|
356
|
-
initial: true,
|
|
357
|
-
})
|
|
358
|
-
if (go) {
|
|
359
|
-
const code = await runCommand(cmd)
|
|
360
|
-
if (code === 0) {
|
|
361
|
-
success(`Self-update complete.`)
|
|
362
|
-
} else {
|
|
363
|
-
error(`Self-update failed (exit ${code}). Run the command above manually if needed.`)
|
|
364
|
-
}
|
|
365
|
-
log('')
|
|
366
|
-
} else {
|
|
367
|
-
log(`${colors.dim}Skipped self-update.${colors.reset}`)
|
|
368
|
-
log('')
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
269
|
}
|
|
270
|
+
} else {
|
|
271
|
+
// Project-local copy (lives in this project's node_modules).
|
|
272
|
+
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}`)
|
|
273
|
+
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}`)
|
|
274
|
+
log('')
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (!inProject) {
|
|
278
|
+
log(`${colors.dim}Not inside a Uniweb project — nothing to reconcile.${colors.reset}`)
|
|
279
|
+
log(`${colors.dim}Run this from a project created by${colors.reset} ${colors.cyan}uniweb create${colors.reset}${colors.dim}.${colors.reset}`)
|
|
280
|
+
log('')
|
|
281
|
+
return
|
|
373
282
|
}
|
|
374
283
|
|
|
375
|
-
// ── Step
|
|
376
|
-
|
|
284
|
+
// ── Step 1: Deps alignment ───────────────────────────────────────
|
|
285
|
+
let depsEdited = false // package.json files were rewritten
|
|
286
|
+
let installRan = false // `<pm> install` ran and succeeded
|
|
287
|
+
let editedPaths = []
|
|
288
|
+
|
|
289
|
+
if (!skipDeps && survey) {
|
|
377
290
|
if (!survey.anyDrift) {
|
|
378
291
|
success('Workspace deps are aligned with the CLI.')
|
|
379
292
|
if (survey.anyAhead) {
|
|
380
293
|
log(`${colors.dim}(Some deps are ahead of the CLI's bundled matrix — left untouched.)${colors.reset}`)
|
|
381
294
|
}
|
|
295
|
+
if (!existsSync(join(workspaceDir, 'node_modules'))) {
|
|
296
|
+
warn(`No ${colors.bright}node_modules${colors.reset} in the workspace — run ${colors.cyan}${installCmd(installPm || 'pnpm')}${colors.reset} to install.`)
|
|
297
|
+
}
|
|
382
298
|
log('')
|
|
383
299
|
} else {
|
|
384
300
|
log(`${colors.yellow}⚠${colors.reset} Some workspace deps lag the CLI's bundled matrix.`)
|
|
385
301
|
log('')
|
|
386
302
|
|
|
303
|
+
// Decide whether to write the package.json edits.
|
|
387
304
|
let proceed
|
|
388
305
|
if (dryRun) {
|
|
389
306
|
proceed = false
|
|
390
|
-
} else if (
|
|
391
|
-
proceed =
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
}
|
|
307
|
+
} else if (hasYes) {
|
|
308
|
+
proceed = true
|
|
309
|
+
} else if (nonInteractive) {
|
|
310
|
+
proceed = false
|
|
311
|
+
info(`${colors.dim}Non-interactive — printing the alignment plan; not editing files.${colors.reset}`)
|
|
312
|
+
log(`${colors.dim}To apply, re-run with${colors.reset} ${colors.cyan}--yes${colors.reset}${colors.dim}, or align manually:${colors.reset}`)
|
|
313
|
+
log(` ${colors.cyan}pnpm update "@uniweb/*" uniweb -r${colors.reset}`)
|
|
314
|
+
log('')
|
|
399
315
|
} else {
|
|
400
316
|
const { go } = await prompts({
|
|
401
317
|
type: 'confirm',
|
|
@@ -407,32 +323,29 @@ export async function update(args = []) {
|
|
|
407
323
|
}
|
|
408
324
|
|
|
409
325
|
if (dryRun) {
|
|
410
|
-
const wouldEdit =
|
|
326
|
+
const wouldEdit = applyDepUpdates(workspaceDir, survey.rows, true)
|
|
411
327
|
if (wouldEdit.length > 0) {
|
|
412
328
|
info('Dry-run: would update package.json in:')
|
|
413
329
|
for (const path of wouldEdit) log(` ${colors.dim}- ${relativize(path, workspaceDir)}${colors.reset}`)
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
log(`${colors.dim}Then would run:${colors.reset} ${colors.cyan}${installCmd(pm)}${colors.reset}`)
|
|
330
|
+
if (installPm) {
|
|
331
|
+
log(`${colors.dim}Then would run:${colors.reset} ${colors.cyan}${installCmd(installPm)}${colors.reset}`)
|
|
417
332
|
} else {
|
|
418
333
|
log(`${colors.dim}Then would prompt for an install command (no lockfile detected).${colors.reset}`)
|
|
419
334
|
}
|
|
420
335
|
log('')
|
|
421
336
|
}
|
|
422
337
|
} else if (proceed) {
|
|
423
|
-
|
|
424
|
-
|
|
338
|
+
editedPaths = applyDepUpdates(workspaceDir, survey.rows, false)
|
|
339
|
+
depsEdited = editedPaths.length > 0
|
|
340
|
+
if (!depsEdited) {
|
|
425
341
|
info('No package.json files needed changes.')
|
|
426
342
|
log('')
|
|
427
343
|
} else {
|
|
428
|
-
for (const path of
|
|
429
|
-
success(`Updated ${relativize(path, workspaceDir)}`)
|
|
430
|
-
}
|
|
344
|
+
for (const path of editedPaths) success(`Updated ${relativize(path, workspaceDir)}`)
|
|
431
345
|
log('')
|
|
432
346
|
|
|
433
347
|
// Resolve the workspace PM (lockfile-driven). If absent, ask.
|
|
434
|
-
|
|
435
|
-
if (!pm) {
|
|
348
|
+
if (!installPm) {
|
|
436
349
|
if (nonInteractive) {
|
|
437
350
|
warn('No lockfile in workspace root — cannot pick an install command for you.')
|
|
438
351
|
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}`)
|
|
@@ -449,20 +362,19 @@ export async function update(args = []) {
|
|
|
449
362
|
{ title: 'skip — I\'ll install manually', value: null },
|
|
450
363
|
],
|
|
451
364
|
})
|
|
452
|
-
|
|
365
|
+
installPm = picked || null
|
|
453
366
|
}
|
|
454
367
|
}
|
|
455
368
|
|
|
456
|
-
if (
|
|
457
|
-
const cmd = installCmd(
|
|
369
|
+
if (installPm) {
|
|
370
|
+
const cmd = installCmd(installPm)
|
|
458
371
|
let runInstall
|
|
459
|
-
if (
|
|
372
|
+
if (hasYes) {
|
|
373
|
+
runInstall = true
|
|
374
|
+
} else if (nonInteractive) {
|
|
460
375
|
runInstall = false
|
|
461
|
-
info(`${colors.dim}Non-interactive —
|
|
462
|
-
log(` ${colors.cyan}${cmd}${colors.reset}`)
|
|
376
|
+
info(`${colors.dim}Non-interactive — run the install yourself:${colors.reset} ${colors.cyan}${cmd}${colors.reset}`)
|
|
463
377
|
log('')
|
|
464
|
-
} else if (skipPrompt) {
|
|
465
|
-
runInstall = true
|
|
466
378
|
} else {
|
|
467
379
|
const { go } = await prompts({
|
|
468
380
|
type: 'confirm',
|
|
@@ -476,18 +388,19 @@ export async function update(args = []) {
|
|
|
476
388
|
if (runInstall) {
|
|
477
389
|
const code = await runCommand(cmd, workspaceDir)
|
|
478
390
|
if (code === 0) {
|
|
391
|
+
installRan = true
|
|
479
392
|
success('Install complete.')
|
|
480
393
|
log('')
|
|
481
394
|
} else {
|
|
482
395
|
error(`Install failed (exit ${code}). package.json edits are intact.`)
|
|
483
|
-
const editedRel =
|
|
396
|
+
const editedRel = editedPaths.map(p => relativize(p, workspaceDir)).join(' ')
|
|
484
397
|
log(`${colors.dim}To revert:${colors.reset} ${colors.cyan}git checkout -- ${editedRel}${colors.reset}`)
|
|
485
398
|
log(`${colors.dim}To retry: ${colors.reset} ${colors.cyan}${cmd}${colors.reset}`)
|
|
486
399
|
log('')
|
|
487
400
|
process.exit(code)
|
|
488
401
|
}
|
|
489
402
|
} else {
|
|
490
|
-
log(`${colors.dim}Skipped install. Edits saved; run${colors.reset} ${colors.cyan}${cmd}${colors.reset} ${colors.dim}to apply.${colors.reset}`)
|
|
403
|
+
log(`${colors.dim}Skipped install. Edits saved; run${colors.reset} ${colors.cyan}${cmd}${colors.reset} ${colors.dim}to apply them.${colors.reset}`)
|
|
491
404
|
log('')
|
|
492
405
|
}
|
|
493
406
|
}
|
|
@@ -499,52 +412,105 @@ export async function update(args = []) {
|
|
|
499
412
|
}
|
|
500
413
|
}
|
|
501
414
|
|
|
502
|
-
// ── Step
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
415
|
+
// ── Step 2: AGENTS.md ────────────────────────────────────────────
|
|
416
|
+
let agentsResult = null // 'created' | 'updated' | 'current' | 'skipped'
|
|
417
|
+
|
|
418
|
+
if (!skipAgents) {
|
|
419
|
+
agentsResult = await refreshAgents({
|
|
420
|
+
workspaceDir, cliVersion, allowMismatch, dryRun,
|
|
421
|
+
hasYes, nonInteractive, agentsOnly, depsEdited, installRan, installPm,
|
|
422
|
+
})
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ── Closing summary ──────────────────────────────────────────────
|
|
426
|
+
if (!dryRun && (depsEdited || agentsResult === 'created' || agentsResult === 'updated')) {
|
|
427
|
+
printSummary({ editedPaths, depsEdited, installRan, installPm, agentsResult, cliVersion })
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Step 2 — regenerate AGENTS.md from the CLI's bundled partial, stamped
|
|
433
|
+
* with the CLI version. Guarded: won't run if the deps were edited but
|
|
434
|
+
* not installed (the doc would describe code that isn't on disk yet), and
|
|
435
|
+
* won't run if declared deps still lag the CLI unless --allow-mismatch.
|
|
436
|
+
*
|
|
437
|
+
* @returns {'created'|'updated'|'current'|'skipped'} what happened.
|
|
438
|
+
*/
|
|
439
|
+
async function refreshAgents(ctx) {
|
|
440
|
+
const {
|
|
441
|
+
workspaceDir, cliVersion, allowMismatch, dryRun,
|
|
442
|
+
hasYes, nonInteractive, agentsOnly, depsEdited, installRan, installPm,
|
|
443
|
+
} = ctx
|
|
444
|
+
|
|
445
|
+
// Deps were rewritten but not installed → node_modules is now behind
|
|
446
|
+
// package.json. Refreshing the doc here would document features the
|
|
447
|
+
// installed code doesn't have. No override — run the install first.
|
|
448
|
+
if (depsEdited && !installRan) {
|
|
449
|
+
const cmd = installCmd(installPm || 'pnpm')
|
|
450
|
+
warn('AGENTS.md refresh skipped: package.json was updated but not installed.')
|
|
451
|
+
log(`${colors.dim}Your${colors.reset} ${colors.bright}node_modules${colors.reset} ${colors.dim}is behind your${colors.reset} ${colors.bright}package.json${colors.reset}${colors.dim}. Run${colors.reset} ${colors.cyan}${cmd}${colors.reset}${colors.dim}, then re-run${colors.reset} ${colors.cyan}uniweb update${colors.reset}${colors.dim}.${colors.reset}`)
|
|
452
|
+
log('')
|
|
453
|
+
return 'skipped'
|
|
507
454
|
}
|
|
508
455
|
|
|
509
|
-
// Re-survey: deps may have just been edited, which clears the
|
|
510
|
-
|
|
456
|
+
// Re-survey: deps may have just been edited+installed, which clears the
|
|
457
|
+
// declared-deps gate.
|
|
458
|
+
const finalSurvey = await surveyWorkspaceDeps(workspaceDir)
|
|
511
459
|
if (finalSurvey.anyDrift && !allowMismatch) {
|
|
512
460
|
warn('AGENTS.md refresh skipped: workspace deps still lag the CLI.')
|
|
513
461
|
log(`${colors.dim}AGENTS.md from v${cliVersion} would document features not in your installed packages.${colors.reset}`)
|
|
514
462
|
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
463
|
log('')
|
|
516
464
|
if (agentsOnly) process.exit(1)
|
|
517
|
-
return
|
|
465
|
+
return 'skipped'
|
|
518
466
|
}
|
|
519
467
|
|
|
520
468
|
const agentsPath = join(workspaceDir, 'AGENTS.md')
|
|
521
469
|
const currentAgentsVersion = readAgentsVersion(agentsPath)
|
|
522
470
|
if (currentAgentsVersion === cliVersion) {
|
|
523
471
|
success(`AGENTS.md is already up to date (v${cliVersion}).`)
|
|
524
|
-
return
|
|
472
|
+
return 'current'
|
|
525
473
|
}
|
|
526
474
|
|
|
527
475
|
if (dryRun) {
|
|
528
476
|
info(`Dry-run: would ${currentAgentsVersion ? `update AGENTS.md (v${currentAgentsVersion} → v${cliVersion})` : `create AGENTS.md (v${cliVersion})`}.`)
|
|
529
|
-
return
|
|
477
|
+
return 'skipped'
|
|
530
478
|
}
|
|
531
479
|
|
|
532
|
-
if (!
|
|
480
|
+
if (!hasYes && !nonInteractive && !agentsOnly) {
|
|
533
481
|
const action = currentAgentsVersion
|
|
534
482
|
? `Update AGENTS.md (v${currentAgentsVersion} → v${cliVersion})?`
|
|
535
483
|
: `Create AGENTS.md (v${cliVersion})?`
|
|
536
484
|
const { yes } = await prompts({ type: 'confirm', name: 'yes', message: action, initial: true })
|
|
537
485
|
if (!yes) {
|
|
538
486
|
log(`${colors.dim}Skipped AGENTS.md.${colors.reset}`)
|
|
539
|
-
return
|
|
487
|
+
return 'skipped'
|
|
540
488
|
}
|
|
541
489
|
}
|
|
542
490
|
|
|
543
|
-
|
|
544
|
-
writeFileSync(agentsPath, content)
|
|
491
|
+
writeFileSync(agentsPath, generateAgentsContent())
|
|
545
492
|
if (currentAgentsVersion) {
|
|
546
493
|
success(`Updated AGENTS.md (v${currentAgentsVersion} → v${cliVersion}).`)
|
|
547
|
-
|
|
548
|
-
success(`Created AGENTS.md (v${cliVersion}).`)
|
|
494
|
+
return 'updated'
|
|
549
495
|
}
|
|
496
|
+
success(`Created AGENTS.md (v${cliVersion}).`)
|
|
497
|
+
return 'created'
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** Print a compact recap of what changed, plus a review hint. */
|
|
501
|
+
function printSummary({ editedPaths, depsEdited, installRan, installPm, agentsResult, cliVersion }) {
|
|
502
|
+
log(`${colors.bright}Summary${colors.reset}`)
|
|
503
|
+
if (depsEdited) {
|
|
504
|
+
log(` ${colors.green}✓${colors.reset} package.json updated in ${editedPaths.length} file${editedPaths.length === 1 ? '' : 's'}`)
|
|
505
|
+
if (installRan) {
|
|
506
|
+
log(` ${colors.green}✓${colors.reset} ${installCmd(installPm || 'pnpm')} completed`)
|
|
507
|
+
} else {
|
|
508
|
+
log(` ${colors.yellow}⚠${colors.reset} install NOT run — run ${colors.cyan}${installCmd(installPm || 'pnpm')}${colors.reset} to apply`)
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (agentsResult === 'created') log(` ${colors.green}✓${colors.reset} AGENTS.md created (v${cliVersion})`)
|
|
512
|
+
else if (agentsResult === 'updated') log(` ${colors.green}✓${colors.reset} AGENTS.md updated (v${cliVersion})`)
|
|
513
|
+
else if (agentsResult === 'skipped' && depsEdited) log(` ${colors.dim}·${colors.reset} AGENTS.md not refreshed (see above)`)
|
|
514
|
+
log(`${colors.dim}Review changes with${colors.reset} ${colors.cyan}git diff${colors.reset}${colors.dim}, then commit.${colors.reset}`)
|
|
515
|
+
log('')
|
|
550
516
|
}
|
package/src/framework-index.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-05-
|
|
3
|
+
"generatedAt": "2026-05-13T05:21:17.590Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/build": {
|
|
6
6
|
"version": "0.14.5",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
]
|
|
50
50
|
},
|
|
51
51
|
"@uniweb/loom": {
|
|
52
|
-
"version": "0.2.
|
|
52
|
+
"version": "0.2.3",
|
|
53
53
|
"path": "framework/loom",
|
|
54
54
|
"deps": []
|
|
55
55
|
},
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
"deps": []
|
|
93
93
|
},
|
|
94
94
|
"@uniweb/unipress": {
|
|
95
|
-
"version": "0.4.
|
|
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
|
|
132
|
-
//
|
|
133
|
-
//
|
|
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
|
])
|
|
@@ -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}—
|
|
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
|
|
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
|
|
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
|
|
1303
|
-
|
|
1304
|
-
1. ${colors.bright}
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
pnpm
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
not in your
|
|
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
|
|
1318
|
-
|
|
1319
|
-
|
|
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
|
|
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
|
package/src/utils/config.js
CHANGED
|
@@ -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
|
|
|
@@ -154,13 +155,19 @@ export async function readRootPackageJson(rootDir) {
|
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
/**
|
|
157
|
-
* Write root package.json
|
|
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).
|
|
158
161
|
* @param {string} rootDir - Workspace root directory
|
|
159
162
|
* @param {Object} pkg - Package.json object
|
|
160
163
|
*/
|
|
161
164
|
export async function writeRootPackageJson(rootDir, pkg) {
|
|
162
165
|
const pkgPath = join(rootDir, 'package.json')
|
|
163
|
-
|
|
166
|
+
if (existsSync(pkgPath)) {
|
|
167
|
+
await writeJsonPreservingStyleAsync(pkgPath, pkg)
|
|
168
|
+
} else {
|
|
169
|
+
await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
|
|
170
|
+
}
|
|
164
171
|
}
|
|
165
172
|
|
|
166
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"
|
package/src/utils/scaffold.js
CHANGED
|
@@ -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
|
|
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
|
|
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}
|
|
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}
|
|
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
|
|
80
|
-
*
|
|
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
|
|
100
|
+
return pkg.version
|
|
94
101
|
}
|
|
95
102
|
} catch {}
|
|
96
103
|
return null
|