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.
- package/README.md +1 -1
- package/package.json +5 -5
- package/src/commands/doctor.js +24 -5
- package/src/commands/publish.js +12 -6
- package/src/commands/rename.js +10 -5
- package/src/commands/update.js +211 -245
- package/src/framework-index.json +8 -8
- package/src/index.js +34 -25
- package/src/utils/config.js +11 -6
- 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/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
|
}
|