uniweb 1.0.0 → 1.0.2
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/LICENSE +201 -0
- package/README.md +532 -3
- package/package.json +60 -11
- package/partials/adding-pages.hbs +77 -0
- package/partials/agents.md +3000 -0
- package/partials/ai-assistance.hbs +28 -0
- package/partials/components-docs.hbs +13 -0
- package/partials/config-reference.hbs +66 -0
- package/partials/deployment.hbs +13 -0
- package/partials/learn-more.hbs +6 -0
- package/partials/quick-start.hbs +8 -0
- package/partials/search-docs.hbs +110 -0
- package/src/backend/asset-download.js +222 -0
- package/src/backend/client.js +986 -0
- package/src/backend/foundation-bring-along.js +692 -0
- package/src/backend/payment-handoff.js +362 -0
- package/src/backend/service-request.js +210 -0
- package/src/backend/site-media.js +255 -0
- package/src/backend/site-preview.js +77 -0
- package/src/backend/site-sync.js +2515 -0
- package/src/backend/workspace.js +234 -0
- package/src/commands/add.js +1900 -0
- package/src/commands/build.js +1214 -0
- package/src/commands/clone.js +466 -0
- package/src/commands/content.js +202 -0
- package/src/commands/deploy.js +537 -0
- package/src/commands/dev.js +202 -0
- package/src/commands/docs.js +530 -0
- package/src/commands/doctor.js +1982 -0
- package/src/commands/export.js +116 -0
- package/src/commands/families.js +82 -0
- package/src/commands/forget.js +197 -0
- package/src/commands/handoff.js +21 -0
- package/src/commands/i18n.js +1707 -0
- package/src/commands/inspect.js +375 -0
- package/src/commands/invite.js +24 -0
- package/src/commands/org.js +96 -0
- package/src/commands/publish.js +1087 -0
- package/src/commands/pull.js +1257 -0
- package/src/commands/push.js +663 -0
- package/src/commands/refresh.js +212 -0
- package/src/commands/register.js +1092 -0
- package/src/commands/rename.js +647 -0
- package/src/commands/site.js +358 -0
- package/src/commands/snapshot.js +461 -0
- package/src/commands/starter.js +206 -0
- package/src/commands/status.js +302 -0
- package/src/commands/sync.js +93 -0
- package/src/commands/template.js +107 -0
- package/src/commands/update.js +887 -0
- package/src/commands/validate.js +376 -0
- package/src/framework-index.json +248 -0
- package/src/index.js +2241 -0
- package/src/templates/fetchers/github.js +113 -0
- package/src/templates/fetchers/npm.js +85 -0
- package/src/templates/fetchers/release.js +92 -0
- package/src/templates/index.js +190 -0
- package/src/templates/processor.js +333 -0
- package/src/templates/resolver.js +232 -0
- package/src/templates/validator.js +155 -0
- package/src/utils/agents-stamp.js +110 -0
- package/src/utils/args.js +126 -0
- package/src/utils/asset-upload.js +270 -0
- package/src/utils/bytes.js +30 -0
- package/src/utils/code-upload.js +394 -0
- package/src/utils/config.js +417 -0
- package/src/utils/conformance.js +274 -0
- package/src/utils/dep-survey.js +176 -0
- package/src/utils/destination-prompt.js +146 -0
- package/src/utils/discover.js +75 -0
- package/src/utils/env.js +22 -0
- package/src/utils/exit.js +30 -0
- package/src/utils/fetch-retry.js +109 -0
- package/src/utils/flag-guard.js +246 -0
- package/src/utils/foundation-name.js +179 -0
- package/src/utils/git.js +342 -0
- package/src/utils/host-prompt.js +51 -0
- package/src/utils/install-integrity.js +210 -0
- package/src/utils/interactive.js +118 -0
- package/src/utils/json-file.js +72 -0
- package/src/utils/names.js +91 -0
- package/src/utils/placement.js +151 -0
- package/src/utils/pm.js +165 -0
- package/src/utils/preview.js +20 -0
- package/src/utils/pull-written.js +132 -0
- package/src/utils/records-guard.js +88 -0
- package/src/utils/registry-auth.js +994 -0
- package/src/utils/registry-orgs.js +290 -0
- package/src/utils/scaffold.js +483 -0
- package/src/utils/schemaless-report.js +30 -0
- package/src/utils/semver-precedence.js +80 -0
- package/src/utils/session-file.js +95 -0
- package/src/utils/site-content-refs.js +23 -0
- package/src/utils/site-copies.js +123 -0
- package/src/utils/site-data-upload.js +302 -0
- package/src/utils/site-identity.js +137 -0
- package/src/utils/snapshot-settings.js +112 -0
- package/src/utils/update-check.js +302 -0
- package/src/utils/uwx-read.js +157 -0
- package/src/utils/workspace.js +223 -0
- package/src/versions.js +467 -0
- package/starter/foundation/main.js +50 -0
- package/starter/foundation/sections/Section/index.jsx +95 -0
- package/starter/foundation/sections/Section/meta.js +44 -0
- package/starter/site/pages/about/1-about.md.hbs +13 -0
- package/starter/site/pages/about/page.yml +3 -0
- package/starter/site/pages/home/1-welcome.md.hbs +14 -0
- package/starter/site/pages/home/page.yml +3 -0
- package/templates/foundation/_gitignore +5 -0
- package/templates/foundation/components/.gitkeep +0 -0
- package/templates/foundation/main.js.hbs +33 -0
- package/templates/foundation/package.json.hbs +51 -0
- package/templates/foundation/sections/.gitkeep +0 -0
- package/templates/foundation/styles.css +6 -0
- package/templates/foundation/vite.config.js +3 -0
- package/templates/site/_gitignore +12 -0
- package/templates/site/entry.js +7 -0
- package/templates/site/index.html.hbs +13 -0
- package/templates/site/package.json.hbs +27 -0
- package/templates/site/public/favicon.svg +7 -0
- package/templates/site/site.yml.hbs +91 -0
- package/templates/site/theme.yml +99 -0
- package/templates/site/vite.config.js +7 -0
- package/templates/workspace/AGENTS.md.hbs +2 -0
- package/templates/workspace/README.md.hbs +87 -0
- package/templates/workspace/_gitignore +29 -0
- package/templates/workspace/_vscode/settings.json +6 -0
- package/templates/workspace/package.json.hbs +20 -0
- package/templates/workspace/pnpm-workspace.yaml.hbs +26 -0
|
@@ -0,0 +1,887 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* uniweb update — Reconcile a Uniweb workspace's state with the CLI that's
|
|
3
|
+
* running this command. One job, two convergence steps:
|
|
4
|
+
*
|
|
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.
|
|
10
|
+
*
|
|
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.
|
|
27
|
+
*
|
|
28
|
+
* Flags:
|
|
29
|
+
* --agents-only Only refresh AGENTS.md (skip the deps step).
|
|
30
|
+
* --deps-only Only align deps (skip the AGENTS.md step).
|
|
31
|
+
* --no-agents Skip the AGENTS.md step.
|
|
32
|
+
* --no-deps Skip the deps-alignment step.
|
|
33
|
+
* --dry-run Print survey + would-be writes; no mutations.
|
|
34
|
+
* --verbose List every surveyed dep, including aligned ones
|
|
35
|
+
* (the default collapses those to a count).
|
|
36
|
+
* --allow-mismatch Refresh AGENTS.md even if declared deps lag.
|
|
37
|
+
* --yes Don't prompt — apply edits and run the install.
|
|
38
|
+
* --non-interactive Auto-detected; prints the plan, never mutates
|
|
39
|
+
* (combine with --yes to apply non-interactively).
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
43
|
+
import { join } from 'node:path'
|
|
44
|
+
import { spawn } from 'node:child_process'
|
|
45
|
+
import prompts from 'prompts'
|
|
46
|
+
|
|
47
|
+
import { findWorkspaceRoot } from '../utils/workspace.js'
|
|
48
|
+
import {
|
|
49
|
+
readAgentsVersion,
|
|
50
|
+
refreshAgentsContent,
|
|
51
|
+
PROJECT_NOTES_START,
|
|
52
|
+
PROJECT_NOTES_END
|
|
53
|
+
} from '../utils/agents-stamp.js'
|
|
54
|
+
import { getCliVersion } from '../versions.js'
|
|
55
|
+
import { isNonInteractive } from '../utils/interactive.js'
|
|
56
|
+
import {
|
|
57
|
+
detectWorkspacePm,
|
|
58
|
+
installCmd,
|
|
59
|
+
detectGlobalCliPm,
|
|
60
|
+
globalCliUpdateCmd
|
|
61
|
+
} from '../utils/pm.js'
|
|
62
|
+
import { writeJsonPreservingStyle } from '../utils/json-file.js'
|
|
63
|
+
import { surveyWorkspaceDeps, compareSemver } from '../utils/dep-survey.js'
|
|
64
|
+
import {
|
|
65
|
+
checkWorkspaceInstall,
|
|
66
|
+
readDeclaredFoundation
|
|
67
|
+
} from '../utils/install-integrity.js'
|
|
68
|
+
import { getLatestVersion } from '../utils/update-check.js'
|
|
69
|
+
|
|
70
|
+
const colors = {
|
|
71
|
+
reset: '\x1b[0m',
|
|
72
|
+
bright: '\x1b[1m',
|
|
73
|
+
dim: '\x1b[2m',
|
|
74
|
+
red: '\x1b[31m',
|
|
75
|
+
green: '\x1b[32m',
|
|
76
|
+
yellow: '\x1b[33m',
|
|
77
|
+
cyan: '\x1b[36m'
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const success = (msg) => console.log(`${colors.green}✓${colors.reset} ${msg}`)
|
|
81
|
+
const warn = (msg) => console.log(`${colors.yellow}⚠${colors.reset} ${msg}`)
|
|
82
|
+
const error = (msg) => console.log(`${colors.red}✗${colors.reset} ${msg}`)
|
|
83
|
+
const info = (msg) => console.log(`${colors.cyan}ℹ${colors.reset} ${msg}`)
|
|
84
|
+
const log = console.log
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Detect whether this CLI is running from a global install — when global,
|
|
88
|
+
* process.argv[1] points outside any node_modules. Mirrors
|
|
89
|
+
* index.js::isGlobalInstall.
|
|
90
|
+
*/
|
|
91
|
+
function isGlobalInstall() {
|
|
92
|
+
const scriptPath = process.argv[1]
|
|
93
|
+
if (!scriptPath) return false
|
|
94
|
+
return (
|
|
95
|
+
!scriptPath.split('/').includes('node_modules') &&
|
|
96
|
+
!scriptPath.split('\\').includes('node_modules')
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Detect whether this CLI was launched via `npx uniweb …` (or `npm exec`).
|
|
102
|
+
* npx materializes the package under `…/_npx/<hash>/node_modules/uniweb/…`,
|
|
103
|
+
* which `isGlobalInstall()` can't distinguish from a real project-local
|
|
104
|
+
* dependency (both contain `node_modules`).
|
|
105
|
+
*/
|
|
106
|
+
function isNpxInvocation() {
|
|
107
|
+
const p = (process.argv[1] || '').replace(/\\/g, '/')
|
|
108
|
+
return p.includes('/_npx/')
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Find a *Uniweb* workspace root from cwd. Stricter than findWorkspaceRoot
|
|
113
|
+
* — also requires that the workspace's root package.json declares uniweb
|
|
114
|
+
* as a dep or devDep. Otherwise we'd write AGENTS.md and edit package.json
|
|
115
|
+
* files in unrelated monorepos.
|
|
116
|
+
*/
|
|
117
|
+
function findUniwebWorkspace(cwd) {
|
|
118
|
+
const workspaceDir = findWorkspaceRoot(cwd)
|
|
119
|
+
if (!workspaceDir) return null
|
|
120
|
+
const pkgPath = join(workspaceDir, 'package.json')
|
|
121
|
+
if (!existsSync(pkgPath)) return null
|
|
122
|
+
try {
|
|
123
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
124
|
+
const hasUniwebDep = !!(
|
|
125
|
+
pkg.devDependencies?.uniweb || pkg.dependencies?.uniweb
|
|
126
|
+
)
|
|
127
|
+
return hasUniwebDep ? workspaceDir : null
|
|
128
|
+
} catch {
|
|
129
|
+
return null
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Trailing advisory: a newer CLI exists, and this run could not have used it.
|
|
135
|
+
*
|
|
136
|
+
* Placed AFTER the work, not before, because it is a what-to-do-next — the
|
|
137
|
+
* same reason update-check calls its post-command notice the 'soft' tone.
|
|
138
|
+
* It also has to be the last thing on screen when it fires: a run that ends
|
|
139
|
+
* on `✓ deps aligned` + `✓ AGENTS.md up to date` reads as "all good", and
|
|
140
|
+
* those ticks are measured against THIS CLI's matrix. True, and beside the
|
|
141
|
+
* point, when the CLI itself is two releases back.
|
|
142
|
+
*
|
|
143
|
+
* The remedy is per-provenance because the wrong one is useless advice:
|
|
144
|
+
* - project-local — the version is pinned by the project's package.json,
|
|
145
|
+
* so a global install is irrelevant. `npx uniweb@latest update` both
|
|
146
|
+
* aligns the deps AND rewrites that pin, so it is self-healing; say so,
|
|
147
|
+
* or the reader assumes they'll be back here next release.
|
|
148
|
+
* - global — updating the global install is the durable fix.
|
|
149
|
+
* - npx — skipped entirely. The version was chosen explicitly on the
|
|
150
|
+
* command line, and the lead-in already named it.
|
|
151
|
+
*
|
|
152
|
+
* Exported for tests: this notice must fire when behind and stay silent
|
|
153
|
+
* when current, and it's the half of the command that had no coverage.
|
|
154
|
+
*
|
|
155
|
+
* @returns {boolean} true if a notice was printed.
|
|
156
|
+
*/
|
|
157
|
+
export function printStaleCliNotice({
|
|
158
|
+
cliVersion,
|
|
159
|
+
latest,
|
|
160
|
+
isNpx,
|
|
161
|
+
isGlobal,
|
|
162
|
+
globalPm
|
|
163
|
+
}) {
|
|
164
|
+
if (isNpx) return false
|
|
165
|
+
if (!latest || compareSemver(latest, cliVersion) <= 0) return false
|
|
166
|
+
|
|
167
|
+
log('')
|
|
168
|
+
log(
|
|
169
|
+
`${colors.yellow}⚠${colors.reset} ${colors.bright}A newer uniweb is available:${colors.reset} ${colors.dim}v${cliVersion}${colors.reset} → ${colors.cyan}v${latest}${colors.reset}`
|
|
170
|
+
)
|
|
171
|
+
if (isGlobal) {
|
|
172
|
+
log(
|
|
173
|
+
`${colors.dim}This run aligned the project to v${cliVersion}'s matrix. To update the CLI:${colors.reset} ${colors.cyan}${globalCliUpdateCmd(globalPm)}${colors.reset}`
|
|
174
|
+
)
|
|
175
|
+
log(
|
|
176
|
+
`${colors.dim}Or align to the latest release without a global install:${colors.reset} ${colors.cyan}npx uniweb@latest update${colors.reset}`
|
|
177
|
+
)
|
|
178
|
+
} else {
|
|
179
|
+
log(
|
|
180
|
+
`${colors.dim}This run aligned the project to v${cliVersion}'s matrix — the version your project pins.${colors.reset}`
|
|
181
|
+
)
|
|
182
|
+
log(
|
|
183
|
+
`${colors.dim}To move to v${latest}:${colors.reset} ${colors.cyan}npx uniweb@latest update${colors.reset} ${colors.dim}(updates the pin too).${colors.reset}`
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
log('')
|
|
187
|
+
return true
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Run a shell command, inheriting stdio. Resolves with the exit code. */
|
|
191
|
+
function runCommand(cmd, cwd) {
|
|
192
|
+
return new Promise((resolve) => {
|
|
193
|
+
const [bin, ...rest] = cmd.split(' ')
|
|
194
|
+
const child = spawn(bin, rest, { stdio: 'inherit', cwd })
|
|
195
|
+
child.on('close', (code) => resolve(code ?? 0))
|
|
196
|
+
child.on('error', () => resolve(1))
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Print the survey report grouped by package directory.
|
|
202
|
+
*
|
|
203
|
+
* Only rows that need attention get a line. An aligned dep renders as
|
|
204
|
+
* `0.9.37 → 0.9.37 aligned` — an identity mapping that says nothing, and
|
|
205
|
+
* on a typical workspace there are six of them. That is the whole screen
|
|
206
|
+
* on the command's most common outcome (a no-op), and it buries the one
|
|
207
|
+
* line that isn't noise. Aligned deps collapse to a count instead.
|
|
208
|
+
*
|
|
209
|
+
* `--verbose` restores the full table for anyone auditing the matrix.
|
|
210
|
+
*
|
|
211
|
+
* Exported for tests: the collapse is the point of the function, and a
|
|
212
|
+
* regression here is invisible — the command still works, it just stops
|
|
213
|
+
* being readable.
|
|
214
|
+
*/
|
|
215
|
+
export function printSurvey(
|
|
216
|
+
report,
|
|
217
|
+
cliVersion,
|
|
218
|
+
agentsVersion,
|
|
219
|
+
{ verbose = false } = {}
|
|
220
|
+
) {
|
|
221
|
+
log('')
|
|
222
|
+
log(`${colors.bright}uniweb CLI:${colors.reset} v${cliVersion}`)
|
|
223
|
+
log(
|
|
224
|
+
`${colors.bright}AGENTS.md stamp:${colors.reset} ${agentsVersion ? 'v' + agentsVersion : colors.dim + '(none)' + colors.reset}`
|
|
225
|
+
)
|
|
226
|
+
log('')
|
|
227
|
+
|
|
228
|
+
if (report.rows.length === 0) {
|
|
229
|
+
log(
|
|
230
|
+
`${colors.dim}No @uniweb/* deps found in workspace package.json files.${colors.reset}`
|
|
231
|
+
)
|
|
232
|
+
log('')
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const needsAttention = report.rows.filter((r) => r.status !== 'aligned')
|
|
237
|
+
const shown = verbose ? report.rows : needsAttention
|
|
238
|
+
const alignedCount = report.rows.length - needsAttention.length
|
|
239
|
+
|
|
240
|
+
// Everything aligned and not auditing: step 1's success line says so in
|
|
241
|
+
// one sentence. A header over an empty table is worse than no header.
|
|
242
|
+
if (shown.length === 0) return
|
|
243
|
+
|
|
244
|
+
const byDir = {}
|
|
245
|
+
for (const row of shown) {
|
|
246
|
+
if (!byDir[row.relDir]) byDir[row.relDir] = []
|
|
247
|
+
byDir[row.relDir].push(row)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
log(`${colors.bright}Workspace deps (declared):${colors.reset}`)
|
|
251
|
+
for (const [dir, dirRows] of Object.entries(byDir)) {
|
|
252
|
+
log(` ${colors.dim}${dir}/${colors.reset}`)
|
|
253
|
+
const maxName = Math.max(...dirRows.map((r) => r.name.length))
|
|
254
|
+
for (const row of dirRows) {
|
|
255
|
+
const padding = ' '.repeat(maxName - row.name.length)
|
|
256
|
+
let icon, statusText
|
|
257
|
+
if (row.status === 'aligned') {
|
|
258
|
+
icon = `${colors.green}✓${colors.reset}`
|
|
259
|
+
statusText = `${colors.dim}aligned${colors.reset}`
|
|
260
|
+
} else if (row.status === 'behind') {
|
|
261
|
+
icon = `${colors.yellow}✗${colors.reset}`
|
|
262
|
+
// ⛔ `behind` alone was the whole message, in one colour, for every
|
|
263
|
+
// distance. The minor slot is where our breaking changes live, in 1.x as in
|
|
264
|
+
// 0.x (`publish.js` derives it from a breaking marker and nothing else), so
|
|
265
|
+
// `^0.14.1 → ^0.16.0` is two releases a consumer must act on and
|
|
266
|
+
// `^0.15.0 → ^0.15.2` is not — and the table said the same thing about
|
|
267
|
+
// both. Naming the class is the difference between a list and a signal.
|
|
268
|
+
statusText = row.breaking
|
|
269
|
+
? `${colors.red}${row.bump} · BREAKING${colors.reset}`
|
|
270
|
+
: `${colors.yellow}${row.bump}${colors.reset}`
|
|
271
|
+
} else {
|
|
272
|
+
icon = `${colors.cyan}↑${colors.reset}`
|
|
273
|
+
statusText = `${colors.cyan}ahead of CLI${colors.reset}`
|
|
274
|
+
}
|
|
275
|
+
log(
|
|
276
|
+
` ${icon} ${row.name}${padding} ${row.current.padEnd(10)} → ${row.target.padEnd(10)} ${statusText}`
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (!verbose && alignedCount > 0) {
|
|
281
|
+
log(
|
|
282
|
+
` ${colors.dim}(${alignedCount} other${alignedCount === 1 ? '' : 's'} already aligned — ${colors.reset}${colors.cyan}--verbose${colors.reset}${colors.dim} to list)${colors.reset}`
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ⭐ **A summary, because the table scrolls and `--yes` does not stop.** The
|
|
287
|
+
// per-row label above is invisible to the case that matters most: a CI or an
|
|
288
|
+
// agent running `update --yes`, where nobody reads a table and the only
|
|
289
|
+
// artifact is a log. This block names the packages, so "what did that
|
|
290
|
+
// upgrade cross?" is answerable afterwards from the log alone.
|
|
291
|
+
//
|
|
292
|
+
// ⚖️ It reports; it does not gate. Crossing a minor IS the ordinary way
|
|
293
|
+
// to take a Uniweb update — gating it would gate nearly every real upgrade
|
|
294
|
+
// and make the verb useless. The decision stays the operator's; what changed
|
|
295
|
+
// is that they can now make it knowingly.
|
|
296
|
+
const breakingRows = report.rows.filter((r) => r.breaking)
|
|
297
|
+
if (breakingRows.length > 0) {
|
|
298
|
+
const names = [...new Set(breakingRows.map((r) => r.name))]
|
|
299
|
+
log('')
|
|
300
|
+
log(
|
|
301
|
+
`${colors.red}⚠${colors.reset} ${colors.bright}${names.length} package${names.length === 1 ? '' : 's'} cross${names.length === 1 ? 'es' : ''} a breaking boundary:${colors.reset}`
|
|
302
|
+
)
|
|
303
|
+
for (const name of names) {
|
|
304
|
+
const r = breakingRows.find((x) => x.name === name)
|
|
305
|
+
log(` ${colors.red}${name}${colors.reset} ${r.current} → ${r.target} ${colors.dim}(${r.bump})${colors.reset}`)
|
|
306
|
+
}
|
|
307
|
+
log(
|
|
308
|
+
` ${colors.dim}A minor version is where Uniweb's breaking changes go, so these are releases${colors.reset}`
|
|
309
|
+
)
|
|
310
|
+
log(
|
|
311
|
+
` ${colors.dim}you may need to act on. Read their changelogs before shipping.${colors.reset}`
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
log('')
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Apply the CLI's matrix to workspace package.json files — but only to
|
|
319
|
+
* deps that *lag* the matrix (survey status `behind`). Deps that are
|
|
320
|
+
* `ahead` are left alone: `update` never downgrades. Indentation and the
|
|
321
|
+
* trailing newline of each file are preserved (a one-key bump shouldn't
|
|
322
|
+
* reflow the whole file). Returns the list of paths that actually changed.
|
|
323
|
+
*/
|
|
324
|
+
function applyDepUpdates(workspaceDir, surveyRows, dryRun) {
|
|
325
|
+
const behind = surveyRows.filter((r) => r.status === 'behind')
|
|
326
|
+
const byDir = {}
|
|
327
|
+
for (const row of behind) {
|
|
328
|
+
const dir = row.relDir === '(root)' ? '' : row.relDir
|
|
329
|
+
if (!byDir[dir]) byDir[dir] = []
|
|
330
|
+
byDir[dir].push(row)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const edited = []
|
|
334
|
+
for (const [relDir, dirRows] of Object.entries(byDir)) {
|
|
335
|
+
const pkgDir = relDir ? join(workspaceDir, relDir) : workspaceDir
|
|
336
|
+
const pkgPath = join(pkgDir, 'package.json')
|
|
337
|
+
if (!existsSync(pkgPath)) continue
|
|
338
|
+
const original = readFileSync(pkgPath, 'utf8')
|
|
339
|
+
let pkg
|
|
340
|
+
try {
|
|
341
|
+
pkg = JSON.parse(original)
|
|
342
|
+
} catch {
|
|
343
|
+
continue
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
let changed = false
|
|
347
|
+
for (const row of dirRows) {
|
|
348
|
+
const section = pkg[row.section]
|
|
349
|
+
if (
|
|
350
|
+
section &&
|
|
351
|
+
section[row.name] !== undefined &&
|
|
352
|
+
section[row.name] !== row.target
|
|
353
|
+
) {
|
|
354
|
+
section[row.name] = row.target
|
|
355
|
+
changed = true
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (changed) {
|
|
360
|
+
edited.push(pkgPath)
|
|
361
|
+
if (!dryRun) writeJsonPreservingStyle(pkgPath, pkg, original)
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return edited
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function relativize(path, root) {
|
|
369
|
+
return path.startsWith(root) ? path.slice(root.length + 1) : path
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export async function update(args = []) {
|
|
373
|
+
const agentsOnly = args.includes('--agents-only')
|
|
374
|
+
const depsOnly = args.includes('--deps-only')
|
|
375
|
+
const skipAgents = args.includes('--no-agents') || depsOnly
|
|
376
|
+
const skipDeps = args.includes('--no-deps') || agentsOnly
|
|
377
|
+
const dryRun = args.includes('--dry-run')
|
|
378
|
+
const allowMismatch = args.includes('--allow-mismatch')
|
|
379
|
+
const verbose = args.includes('--verbose')
|
|
380
|
+
const hasYes = args.includes('--yes')
|
|
381
|
+
const nonInteractive = isNonInteractive(args)
|
|
382
|
+
const isGlobal = isGlobalInstall()
|
|
383
|
+
const isNpx = isNpxInvocation()
|
|
384
|
+
const workspaceDir = findUniwebWorkspace(process.cwd())
|
|
385
|
+
const inProject = !!workspaceDir
|
|
386
|
+
const cliVersion = getCliVersion()
|
|
387
|
+
|
|
388
|
+
if ((agentsOnly || depsOnly) && !inProject) {
|
|
389
|
+
error(
|
|
390
|
+
`${agentsOnly ? '--agents-only' : '--deps-only'} requires a Uniweb project (no \`uniweb\` dep in the workspace root).`
|
|
391
|
+
)
|
|
392
|
+
log(
|
|
393
|
+
`${colors.dim}Run this command from inside a project created by${colors.reset} ${colors.cyan}uniweb create${colors.reset}${colors.dim}.${colors.reset}`
|
|
394
|
+
)
|
|
395
|
+
process.exit(1)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ── Survey first (always, when in a Uniweb project) ──────────────
|
|
399
|
+
let survey = null
|
|
400
|
+
let agentsVersion = null
|
|
401
|
+
if (inProject) {
|
|
402
|
+
survey = await surveyWorkspaceDeps(workspaceDir)
|
|
403
|
+
agentsVersion = readAgentsVersion(join(workspaceDir, 'AGENTS.md'))
|
|
404
|
+
printSurvey(survey, cliVersion, agentsVersion, { verbose })
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ── This command reconciles the *project*, not the CLI ───────────
|
|
408
|
+
// Which CLI is running is a lead-in — it belongs before the work. Whether
|
|
409
|
+
// that CLI is STALE is a what-to-do-next, and it is printed after the work
|
|
410
|
+
// by printStaleCliNotice() so it lands last instead of under two green
|
|
411
|
+
// ticks. The lookup happens here because the check must run on every
|
|
412
|
+
// provenance, not just a global install.
|
|
413
|
+
//
|
|
414
|
+
// Why the project-local path needs it MORE than the global one: a global
|
|
415
|
+
// CLI also gets the general notifier in index.js, but that is gated on
|
|
416
|
+
// `if (global)` — so a project-local run gets no staleness signal from
|
|
417
|
+
// anywhere. That is exactly the case where the user cannot work it out
|
|
418
|
+
// themselves, because the project pins the version. It was the quiet path
|
|
419
|
+
// and it should have been the loud one.
|
|
420
|
+
let installPm = inProject ? detectWorkspacePm(workspaceDir) : null
|
|
421
|
+
const globalPm = isGlobal ? detectGlobalCliPm() : null
|
|
422
|
+
// Non-TTY reads cache only: scripted runs stay fast and offline-safe, the
|
|
423
|
+
// same convention `--version` follows in index.js. Skipped entirely when
|
|
424
|
+
// there is nothing to reconcile — the notice is never reached from those
|
|
425
|
+
// paths, and looking it up anyway would spend a network call on nothing.
|
|
426
|
+
const latestCli =
|
|
427
|
+
isNpx || !inProject
|
|
428
|
+
? null
|
|
429
|
+
: await getLatestVersion({ allowNetwork: !!process.stdout.isTTY })
|
|
430
|
+
|
|
431
|
+
if (isNpx) {
|
|
432
|
+
log(
|
|
433
|
+
`${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}`
|
|
434
|
+
)
|
|
435
|
+
log(
|
|
436
|
+
`${colors.dim}(To install the CLI:${colors.reset} ${colors.cyan}npm i -g uniweb${colors.reset}${colors.dim}.)${colors.reset}`
|
|
437
|
+
)
|
|
438
|
+
log('')
|
|
439
|
+
} else if (!isGlobal) {
|
|
440
|
+
// Project-local copy (lives in this project's node_modules).
|
|
441
|
+
log(
|
|
442
|
+
`${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}`
|
|
443
|
+
)
|
|
444
|
+
log('')
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (!inProject) {
|
|
448
|
+
log(
|
|
449
|
+
`${colors.dim}Not inside a Uniweb project — nothing to reconcile.${colors.reset}`
|
|
450
|
+
)
|
|
451
|
+
log(
|
|
452
|
+
`${colors.dim}Run this from a project created by${colors.reset} ${colors.cyan}uniweb create${colors.reset}${colors.dim}.${colors.reset}`
|
|
453
|
+
)
|
|
454
|
+
log('')
|
|
455
|
+
return
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ── Step 1: Deps alignment ───────────────────────────────────────
|
|
459
|
+
let depsEdited = false // package.json files were rewritten
|
|
460
|
+
let installRan = false // `<pm> install` ran and succeeded
|
|
461
|
+
let editedPaths = []
|
|
462
|
+
|
|
463
|
+
if (!skipDeps && survey) {
|
|
464
|
+
if (!survey.anyDrift) {
|
|
465
|
+
// The count carries the work the collapsed table no longer shows.
|
|
466
|
+
success(
|
|
467
|
+
`Workspace deps are aligned with the CLI (${survey.rows.length} checked).`
|
|
468
|
+
)
|
|
469
|
+
if (survey.anyAhead) {
|
|
470
|
+
log(
|
|
471
|
+
`${colors.dim}(Some deps are ahead of the CLI's bundled matrix — left untouched.)${colors.reset}`
|
|
472
|
+
)
|
|
473
|
+
}
|
|
474
|
+
if (!existsSync(join(workspaceDir, 'node_modules'))) {
|
|
475
|
+
warn(
|
|
476
|
+
`No ${colors.bright}node_modules${colors.reset} in the workspace — run ${colors.cyan}${installCmd(installPm || 'pnpm')}${colors.reset} to install.`
|
|
477
|
+
)
|
|
478
|
+
}
|
|
479
|
+
log('')
|
|
480
|
+
} else {
|
|
481
|
+
log(
|
|
482
|
+
`${colors.yellow}⚠${colors.reset} Some workspace deps lag the CLI's bundled matrix.`
|
|
483
|
+
)
|
|
484
|
+
log('')
|
|
485
|
+
|
|
486
|
+
// Decide whether to write the package.json edits.
|
|
487
|
+
let proceed
|
|
488
|
+
if (dryRun) {
|
|
489
|
+
proceed = false
|
|
490
|
+
} else if (hasYes) {
|
|
491
|
+
proceed = true
|
|
492
|
+
} else if (nonInteractive) {
|
|
493
|
+
proceed = false
|
|
494
|
+
info(
|
|
495
|
+
`${colors.dim}Non-interactive — printing the alignment plan; not editing files.${colors.reset}`
|
|
496
|
+
)
|
|
497
|
+
log(
|
|
498
|
+
`${colors.dim}To apply, re-run with${colors.reset} ${colors.cyan}--yes${colors.reset}${colors.dim}, or align manually:${colors.reset}`
|
|
499
|
+
)
|
|
500
|
+
log(` ${colors.cyan}pnpm update "@uniweb/*" uniweb -r${colors.reset}`)
|
|
501
|
+
log('')
|
|
502
|
+
} else {
|
|
503
|
+
const { go } = await prompts({
|
|
504
|
+
type: 'confirm',
|
|
505
|
+
name: 'go',
|
|
506
|
+
message: `Edit workspace package.json files to align with v${cliVersion}?`,
|
|
507
|
+
initial: true
|
|
508
|
+
})
|
|
509
|
+
proceed = !!go
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (dryRun) {
|
|
513
|
+
const wouldEdit = applyDepUpdates(workspaceDir, survey.rows, true)
|
|
514
|
+
if (wouldEdit.length > 0) {
|
|
515
|
+
info('Dry-run: would update package.json in:')
|
|
516
|
+
for (const path of wouldEdit)
|
|
517
|
+
log(
|
|
518
|
+
` ${colors.dim}- ${relativize(path, workspaceDir)}${colors.reset}`
|
|
519
|
+
)
|
|
520
|
+
if (installPm) {
|
|
521
|
+
log(
|
|
522
|
+
`${colors.dim}Then would run:${colors.reset} ${colors.cyan}${installCmd(installPm)}${colors.reset}`
|
|
523
|
+
)
|
|
524
|
+
} else {
|
|
525
|
+
log(
|
|
526
|
+
`${colors.dim}Then would prompt for an install command (no lockfile detected).${colors.reset}`
|
|
527
|
+
)
|
|
528
|
+
}
|
|
529
|
+
log('')
|
|
530
|
+
}
|
|
531
|
+
} else if (proceed) {
|
|
532
|
+
editedPaths = applyDepUpdates(workspaceDir, survey.rows, false)
|
|
533
|
+
depsEdited = editedPaths.length > 0
|
|
534
|
+
if (!depsEdited) {
|
|
535
|
+
info('No package.json files needed changes.')
|
|
536
|
+
log('')
|
|
537
|
+
} else {
|
|
538
|
+
for (const path of editedPaths)
|
|
539
|
+
success(`Updated ${relativize(path, workspaceDir)}`)
|
|
540
|
+
log('')
|
|
541
|
+
|
|
542
|
+
// Resolve the workspace PM (lockfile-driven). If absent, ask.
|
|
543
|
+
if (!installPm) {
|
|
544
|
+
if (nonInteractive) {
|
|
545
|
+
warn(
|
|
546
|
+
'No lockfile in workspace root — cannot pick an install command for you.'
|
|
547
|
+
)
|
|
548
|
+
log(
|
|
549
|
+
`${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}`
|
|
550
|
+
)
|
|
551
|
+
log('')
|
|
552
|
+
} else {
|
|
553
|
+
const { picked } = await prompts({
|
|
554
|
+
type: 'select',
|
|
555
|
+
name: 'picked',
|
|
556
|
+
message:
|
|
557
|
+
'No lockfile found. Which package manager does this workspace use?',
|
|
558
|
+
choices: [
|
|
559
|
+
{ title: 'pnpm', value: 'pnpm' },
|
|
560
|
+
{ title: 'yarn', value: 'yarn' },
|
|
561
|
+
{ title: 'npm', value: 'npm' },
|
|
562
|
+
{ title: "skip — I'll install manually", value: null }
|
|
563
|
+
]
|
|
564
|
+
})
|
|
565
|
+
installPm = picked || null
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (installPm) {
|
|
570
|
+
const cmd = installCmd(installPm)
|
|
571
|
+
let runInstall
|
|
572
|
+
if (hasYes) {
|
|
573
|
+
runInstall = true
|
|
574
|
+
} else if (nonInteractive) {
|
|
575
|
+
runInstall = false
|
|
576
|
+
info(
|
|
577
|
+
`${colors.dim}Non-interactive — run the install yourself:${colors.reset} ${colors.cyan}${cmd}${colors.reset}`
|
|
578
|
+
)
|
|
579
|
+
log('')
|
|
580
|
+
} else {
|
|
581
|
+
const { go } = await prompts({
|
|
582
|
+
type: 'confirm',
|
|
583
|
+
name: 'go',
|
|
584
|
+
message: `Run \`${cmd}\` now?`,
|
|
585
|
+
initial: true
|
|
586
|
+
})
|
|
587
|
+
runInstall = !!go
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (runInstall) {
|
|
591
|
+
const code = await runCommand(cmd, workspaceDir)
|
|
592
|
+
if (code === 0) {
|
|
593
|
+
installRan = true
|
|
594
|
+
success('Install complete.')
|
|
595
|
+
log('')
|
|
596
|
+
} else {
|
|
597
|
+
error(
|
|
598
|
+
`Install failed (exit ${code}). package.json edits are intact.`
|
|
599
|
+
)
|
|
600
|
+
const editedRel = editedPaths
|
|
601
|
+
.map((p) => relativize(p, workspaceDir))
|
|
602
|
+
.join(' ')
|
|
603
|
+
log(
|
|
604
|
+
`${colors.dim}To revert:${colors.reset} ${colors.cyan}git checkout -- ${editedRel}${colors.reset}`
|
|
605
|
+
)
|
|
606
|
+
log(
|
|
607
|
+
`${colors.dim}To retry: ${colors.reset} ${colors.cyan}${cmd}${colors.reset}`
|
|
608
|
+
)
|
|
609
|
+
log('')
|
|
610
|
+
process.exit(code)
|
|
611
|
+
}
|
|
612
|
+
} else {
|
|
613
|
+
log(
|
|
614
|
+
`${colors.dim}Skipped install. Edits saved; run${colors.reset} ${colors.cyan}${cmd}${colors.reset} ${colors.dim}to apply them.${colors.reset}`
|
|
615
|
+
)
|
|
616
|
+
log('')
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
} else {
|
|
621
|
+
log(`${colors.dim}Skipped deps alignment.${colors.reset}`)
|
|
622
|
+
log('')
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// ── Step 2: AGENTS.md ────────────────────────────────────────────
|
|
628
|
+
let agentsResult = null // 'created' | 'updated' | 'current' | 'skipped'
|
|
629
|
+
|
|
630
|
+
if (!skipAgents) {
|
|
631
|
+
agentsResult = await refreshAgents({
|
|
632
|
+
workspaceDir,
|
|
633
|
+
cliVersion,
|
|
634
|
+
allowMismatch,
|
|
635
|
+
dryRun,
|
|
636
|
+
hasYes,
|
|
637
|
+
nonInteractive,
|
|
638
|
+
agentsOnly,
|
|
639
|
+
depsEdited,
|
|
640
|
+
installRan,
|
|
641
|
+
installPm
|
|
642
|
+
})
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// ── Verify the install actually took ─────────────────────────────
|
|
646
|
+
// An install that was interrupted, or that satisfied a `file:` foundation
|
|
647
|
+
// with a copy instead of a link, leaves a tree that no longer matches what
|
|
648
|
+
// was just written. Builds keep working — they read the workspace source —
|
|
649
|
+
// so the mismatch only shows up later, as a dev server serving stale code,
|
|
650
|
+
// by which point this command is nobody's first suspect. It is the most
|
|
651
|
+
// likely command to produce that state, so it is the right one to check.
|
|
652
|
+
if (!dryRun && installRan) {
|
|
653
|
+
await reportInstallIntegrity(workspaceDir)
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// ── Closing summary ──────────────────────────────────────────────
|
|
657
|
+
if (
|
|
658
|
+
!dryRun &&
|
|
659
|
+
(depsEdited || agentsResult === 'created' || agentsResult === 'updated')
|
|
660
|
+
) {
|
|
661
|
+
printSummary({
|
|
662
|
+
editedPaths,
|
|
663
|
+
depsEdited,
|
|
664
|
+
installRan,
|
|
665
|
+
installPm,
|
|
666
|
+
agentsResult,
|
|
667
|
+
cliVersion
|
|
668
|
+
})
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Last, deliberately — see printStaleCliNotice. Everything above reports
|
|
672
|
+
// against THIS CLI's matrix; if the CLI itself is behind, that is the note
|
|
673
|
+
// the reader should leave with.
|
|
674
|
+
printStaleCliNotice({
|
|
675
|
+
cliVersion,
|
|
676
|
+
latest: latestCli,
|
|
677
|
+
isNpx,
|
|
678
|
+
isGlobal,
|
|
679
|
+
globalPm
|
|
680
|
+
})
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Report any drift between the workspace and the tree that was just installed.
|
|
685
|
+
*/
|
|
686
|
+
async function reportInstallIntegrity(workspaceDir) {
|
|
687
|
+
try {
|
|
688
|
+
// Imported here, not at the top: discover.js reaches @uniweb/build, an
|
|
689
|
+
// optional peer that must stay off the CLI's startup path so `npx uniweb
|
|
690
|
+
// create` works in an empty directory. update.js is loaded at startup.
|
|
691
|
+
const { discoverSites, discoverFoundations } =
|
|
692
|
+
await import('../utils/discover.js')
|
|
693
|
+
|
|
694
|
+
const sites = (await discoverSites(workspaceDir)).map((s) => ({
|
|
695
|
+
name: s.name,
|
|
696
|
+
path: join(workspaceDir, s.path),
|
|
697
|
+
foundation: readDeclaredFoundation(join(workspaceDir, s.path))
|
|
698
|
+
}))
|
|
699
|
+
const foundations = (await discoverFoundations(workspaceDir)).map((f) => ({
|
|
700
|
+
name: f.name,
|
|
701
|
+
path: join(workspaceDir, f.path)
|
|
702
|
+
}))
|
|
703
|
+
|
|
704
|
+
const findings = checkWorkspaceInstall(sites, foundations, workspaceDir)
|
|
705
|
+
if (!findings.length) return
|
|
706
|
+
|
|
707
|
+
log('')
|
|
708
|
+
warn('The install finished, but dev will not resolve what is declared:')
|
|
709
|
+
for (const finding of findings) {
|
|
710
|
+
log(` ${finding.message}`)
|
|
711
|
+
}
|
|
712
|
+
log(
|
|
713
|
+
` ${colors.dim}${findings[0].remedy}. \`uniweb doctor\` explains in full.${colors.reset}`
|
|
714
|
+
)
|
|
715
|
+
} catch {
|
|
716
|
+
// A post-flight check must never turn a successful update into a failure.
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Step 2 — regenerate AGENTS.md from the CLI's bundled partial, stamped
|
|
722
|
+
* with the CLI version. Guarded: won't run if the deps were edited but
|
|
723
|
+
* not installed (the doc would describe code that isn't on disk yet), and
|
|
724
|
+
* won't run if declared deps still lag the CLI unless --allow-mismatch.
|
|
725
|
+
*
|
|
726
|
+
* @returns {'created'|'updated'|'current'|'skipped'} what happened.
|
|
727
|
+
*/
|
|
728
|
+
async function refreshAgents(ctx) {
|
|
729
|
+
const {
|
|
730
|
+
workspaceDir,
|
|
731
|
+
cliVersion,
|
|
732
|
+
allowMismatch,
|
|
733
|
+
dryRun,
|
|
734
|
+
hasYes,
|
|
735
|
+
nonInteractive,
|
|
736
|
+
agentsOnly,
|
|
737
|
+
depsEdited,
|
|
738
|
+
installRan,
|
|
739
|
+
installPm
|
|
740
|
+
} = ctx
|
|
741
|
+
|
|
742
|
+
// Deps were rewritten but not installed → node_modules is now behind
|
|
743
|
+
// package.json. Refreshing the doc here would document features the
|
|
744
|
+
// installed code doesn't have. No override — run the install first.
|
|
745
|
+
if (depsEdited && !installRan) {
|
|
746
|
+
const cmd = installCmd(installPm || 'pnpm')
|
|
747
|
+
warn(
|
|
748
|
+
'AGENTS.md refresh skipped: package.json was updated but not installed.'
|
|
749
|
+
)
|
|
750
|
+
log(
|
|
751
|
+
`${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}`
|
|
752
|
+
)
|
|
753
|
+
log('')
|
|
754
|
+
return 'skipped'
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Re-survey: deps may have just been edited+installed, which clears the
|
|
758
|
+
// declared-deps gate.
|
|
759
|
+
const finalSurvey = await surveyWorkspaceDeps(workspaceDir)
|
|
760
|
+
if (finalSurvey.anyDrift && !allowMismatch) {
|
|
761
|
+
warn('AGENTS.md refresh skipped: workspace deps still lag the CLI.')
|
|
762
|
+
log(
|
|
763
|
+
`${colors.dim}AGENTS.md from v${cliVersion} would document features not in your installed packages.${colors.reset}`
|
|
764
|
+
)
|
|
765
|
+
log(
|
|
766
|
+
`${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}`
|
|
767
|
+
)
|
|
768
|
+
log('')
|
|
769
|
+
if (agentsOnly) process.exit(1)
|
|
770
|
+
return 'skipped'
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const agentsPath = join(workspaceDir, 'AGENTS.md')
|
|
774
|
+
const currentAgentsVersion = readAgentsVersion(agentsPath)
|
|
775
|
+
if (currentAgentsVersion === cliVersion) {
|
|
776
|
+
success(`AGENTS.md is already up to date (v${cliVersion}).`)
|
|
777
|
+
return 'current'
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// The project's notes travel to the new file (`refreshAgentsContent`). Markers that do not pair
|
|
781
|
+
// up leave AGENTS.md as it is: where a block ends would be a guess, and a wrong one loses notes.
|
|
782
|
+
const refreshed = refreshAgentsContent(
|
|
783
|
+
existsSync(agentsPath) ? readFileSync(agentsPath, 'utf8') : null
|
|
784
|
+
)
|
|
785
|
+
if (refreshed.error) {
|
|
786
|
+
warn(`AGENTS.md left as it is: ${refreshed.error}.`)
|
|
787
|
+
log(
|
|
788
|
+
`${colors.dim}Your project notes are kept between ${PROJECT_NOTES_START} and ${PROJECT_NOTES_END}, each on a line of its own. Fix the markers, then re-run${colors.reset} ${colors.cyan}uniweb update${colors.reset}${colors.dim}.${colors.reset}`
|
|
789
|
+
)
|
|
790
|
+
log('')
|
|
791
|
+
if (agentsOnly) process.exit(1)
|
|
792
|
+
return 'skipped'
|
|
793
|
+
}
|
|
794
|
+
const keeping = refreshed.kept
|
|
795
|
+
? `, keeping ${refreshed.kept === 1 ? 'its project notes' : `its ${refreshed.kept} project-notes blocks`}`
|
|
796
|
+
: ''
|
|
797
|
+
|
|
798
|
+
if (dryRun) {
|
|
799
|
+
info(
|
|
800
|
+
`Dry-run: would ${currentAgentsVersion ? `update AGENTS.md (v${currentAgentsVersion} → v${cliVersion})` : `create AGENTS.md (v${cliVersion})`}${keeping}.`
|
|
801
|
+
)
|
|
802
|
+
return 'skipped'
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
if (!hasYes && !nonInteractive && !agentsOnly) {
|
|
806
|
+
const action = currentAgentsVersion
|
|
807
|
+
? `Update AGENTS.md (v${currentAgentsVersion} → v${cliVersion})?`
|
|
808
|
+
: `Create AGENTS.md (v${cliVersion})?`
|
|
809
|
+
const { yes } = await prompts({
|
|
810
|
+
type: 'confirm',
|
|
811
|
+
name: 'yes',
|
|
812
|
+
message: action,
|
|
813
|
+
initial: true
|
|
814
|
+
})
|
|
815
|
+
if (!yes) {
|
|
816
|
+
log(`${colors.dim}Skipped AGENTS.md.${colors.reset}`)
|
|
817
|
+
return 'skipped'
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
writeFileSync(agentsPath, refreshed.content)
|
|
822
|
+
if (currentAgentsVersion) {
|
|
823
|
+
success(`Updated AGENTS.md (v${currentAgentsVersion} → v${cliVersion})${keeping}.`)
|
|
824
|
+
return 'updated'
|
|
825
|
+
}
|
|
826
|
+
success(`Created AGENTS.md (v${cliVersion}).`)
|
|
827
|
+
return 'created'
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Print a compact recap of what changed, plus a review hint.
|
|
832
|
+
*
|
|
833
|
+
* Exported for tests: the dev-server restart notice must fire exactly when the
|
|
834
|
+
* installed packages changed, and stay quiet otherwise (this runs on every
|
|
835
|
+
* `uniweb update`, including no-op ones).
|
|
836
|
+
*/
|
|
837
|
+
export function printSummary({
|
|
838
|
+
editedPaths,
|
|
839
|
+
depsEdited,
|
|
840
|
+
installRan,
|
|
841
|
+
installPm,
|
|
842
|
+
agentsResult,
|
|
843
|
+
cliVersion
|
|
844
|
+
}) {
|
|
845
|
+
log(`${colors.bright}Summary${colors.reset}`)
|
|
846
|
+
if (depsEdited) {
|
|
847
|
+
log(
|
|
848
|
+
` ${colors.green}✓${colors.reset} package.json updated in ${editedPaths.length} file${editedPaths.length === 1 ? '' : 's'}`
|
|
849
|
+
)
|
|
850
|
+
if (installRan) {
|
|
851
|
+
log(
|
|
852
|
+
` ${colors.green}✓${colors.reset} ${installCmd(installPm || 'pnpm')} completed`
|
|
853
|
+
)
|
|
854
|
+
} else {
|
|
855
|
+
log(
|
|
856
|
+
` ${colors.yellow}⚠${colors.reset} install NOT run — run ${colors.cyan}${installCmd(installPm || 'pnpm')}${colors.reset} to apply`
|
|
857
|
+
)
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
if (agentsResult === 'created')
|
|
861
|
+
log(` ${colors.green}✓${colors.reset} AGENTS.md created (v${cliVersion})`)
|
|
862
|
+
else if (agentsResult === 'updated')
|
|
863
|
+
log(` ${colors.green}✓${colors.reset} AGENTS.md updated (v${cliVersion})`)
|
|
864
|
+
else if (agentsResult === 'skipped' && depsEdited)
|
|
865
|
+
log(` ${colors.dim}·${colors.reset} AGENTS.md not refreshed (see above)`)
|
|
866
|
+
log(
|
|
867
|
+
`${colors.dim}Review changes with${colors.reset} ${colors.cyan}git diff${colors.reset}${colors.dim}, then commit.${colors.reset}`
|
|
868
|
+
)
|
|
869
|
+
|
|
870
|
+
// Only when the installed packages actually changed under a process that may
|
|
871
|
+
// still be running. This failure is nastier than it sounds: hot reload picks
|
|
872
|
+
// up your source but not a swapped dependency, so a dev server started before
|
|
873
|
+
// the update keeps serving the OLD framework against your NEW project code.
|
|
874
|
+
// The result looks like a bug you just introduced, and the one place nobody
|
|
875
|
+
// thinks to look is the server that has been running fine all along.
|
|
876
|
+
if (depsEdited && installRan) {
|
|
877
|
+
log('')
|
|
878
|
+
log(
|
|
879
|
+
`${colors.yellow}↻${colors.reset} ${colors.bright}Restart any running dev server.${colors.reset}`
|
|
880
|
+
)
|
|
881
|
+
log(
|
|
882
|
+
`${colors.dim}Hot reload picks up your source, not swapped dependencies — a running${colors.reset} ${colors.cyan}uniweb dev${colors.reset} ${colors.dim}keeps using the framework version it started with.${colors.reset}`
|
|
883
|
+
)
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
log('')
|
|
887
|
+
}
|