uniweb 0.12.11 → 0.12.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uniweb",
3
- "version": "0.12.11",
3
+ "version": "0.12.12",
4
4
  "description": "Create structured Vite + React sites with content/code separation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -41,14 +41,14 @@
41
41
  "js-yaml": "^4.1.0",
42
42
  "prompts": "^2.4.2",
43
43
  "tar": "^7.0.0",
44
- "@uniweb/runtime": "0.8.13",
45
44
  "@uniweb/core": "0.7.11",
45
+ "@uniweb/runtime": "0.8.13",
46
46
  "@uniweb/kit": "0.9.11"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "@uniweb/build": "0.14.2",
50
- "@uniweb/semantic-parser": "1.1.17",
51
- "@uniweb/content-reader": "1.1.10"
50
+ "@uniweb/content-reader": "1.1.10",
51
+ "@uniweb/semantic-parser": "1.1.17"
52
52
  },
53
53
  "peerDependenciesMeta": {
54
54
  "@uniweb/build": {
@@ -1,16 +1,47 @@
1
1
  /**
2
- * uniweb update - Update generated project files
2
+ * uniweb update Update the CLI itself, and (in a Uniweb project) the
3
+ * project's AGENTS.md.
3
4
  *
4
- * Regenerates AGENTS.md from the installed CLI version.
5
+ * Two responsibilities, in priority order:
6
+ *
7
+ * 1. **Self-update the global install.** Most users running `uniweb update`
8
+ * expect the verb to update the CLI binary itself (this is what `npm
9
+ * update -g`, `gh update`, `claude update`, etc. all do). The CLI
10
+ * detects the relevant package manager and runs the global-install
11
+ * command for it (`npm i -g uniweb@latest`, `pnpm add -g uniweb@latest`,
12
+ * `yarn global add uniweb@latest`). In TTY, prompts before executing.
13
+ * In non-interactive mode, prints the command and exits — never runs an
14
+ * unconfirmed self-update from a script.
15
+ *
16
+ * 2. **Refresh AGENTS.md** (only when the cwd resolves to a *Uniweb*
17
+ * project — checked via `package.json::devDependencies::uniweb` or
18
+ * `dependencies::uniweb` at the workspace root). The previous
19
+ * implementation walked up looking for ANY pnpm-workspace.yaml or
20
+ * `package.json::workspaces` root, which falsely identified unrelated
21
+ * monorepos as Uniweb projects and wrote AGENTS.md into them.
22
+ *
23
+ * Flags:
24
+ * --agents-only Skip self-update; only refresh AGENTS.md.
25
+ * --no-agents Skip AGENTS.md; only self-update.
26
+ * --yes Skip the confirmation prompt before self-update.
27
+ * --non-interactive Auto-detected; never runs unconfirmed self-update.
28
+ *
29
+ * Project-local case (CLI lives in node_modules, not global): self-update
30
+ * isn't possible — that's a project decision (bump the dep in
31
+ * package.json). The verb prints that explanation and proceeds with the
32
+ * AGENTS.md refresh path only.
5
33
  */
6
34
 
7
- import { existsSync, writeFileSync } from 'node:fs'
8
- import { join, resolve } from 'node:path'
35
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs'
36
+ import { join } from 'node:path'
37
+ import { spawn } from 'node:child_process'
38
+ import prompts from 'prompts'
39
+
9
40
  import { findWorkspaceRoot } from '../utils/workspace.js'
10
41
  import { readAgentsVersion, generateAgentsContent } from '../utils/agents-stamp.js'
11
42
  import { getCliVersion } from '../versions.js'
43
+ import { isNonInteractive } from '../utils/interactive.js'
12
44
 
13
- // ANSI colors
14
45
  const colors = {
15
46
  reset: '\x1b[0m',
16
47
  bright: '\x1b[1m',
@@ -18,7 +49,7 @@ const colors = {
18
49
  red: '\x1b[31m',
19
50
  green: '\x1b[32m',
20
51
  yellow: '\x1b[33m',
21
- blue: '\x1b[36m'
52
+ cyan: '\x1b[36m',
22
53
  }
23
54
 
24
55
  const success = (msg) => console.log(`${colors.green}✓${colors.reset} ${msg}`)
@@ -26,30 +57,196 @@ const warn = (msg) => console.log(`${colors.yellow}⚠${colors.reset} ${msg}`)
26
57
  const error = (msg) => console.log(`${colors.red}✗${colors.reset} ${msg}`)
27
58
  const log = console.log
28
59
 
29
- export async function update(args = []) {
30
- const workspaceDir = findWorkspaceRoot(process.cwd())
60
+ /**
61
+ * Detect whether this CLI is running from a global install. Mirrors the
62
+ * logic in index.js::isGlobalInstall — when global, process.argv[1]
63
+ * points outside any node_modules.
64
+ */
65
+ function isGlobalInstall() {
66
+ const scriptPath = process.argv[1]
67
+ if (!scriptPath) return false
68
+ return !scriptPath.split('/').includes('node_modules') &&
69
+ !scriptPath.split('\\').includes('node_modules')
70
+ }
31
71
 
32
- if (!workspaceDir) {
33
- error('Not in a Uniweb workspace')
34
- log(`${colors.dim}Run this command from your project root or a package directory.${colors.reset}`)
35
- process.exit(1)
72
+ /**
73
+ * Find a *Uniweb* workspace root from cwd. Stricter than findWorkspaceRoot
74
+ * also requires that the workspace's root package.json declares uniweb
75
+ * as a dep or devDep. Otherwise the previous behavior (walking up to any
76
+ * pnpm-workspace.yaml) writes AGENTS.md into unrelated monorepos.
77
+ */
78
+ function findUniwebWorkspace(cwd) {
79
+ const workspaceDir = findWorkspaceRoot(cwd)
80
+ if (!workspaceDir) return null
81
+ const pkgPath = join(workspaceDir, 'package.json')
82
+ if (!existsSync(pkgPath)) return null
83
+ try {
84
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
85
+ const hasUniwebDep = !!(pkg.devDependencies?.uniweb || pkg.dependencies?.uniweb)
86
+ return hasUniwebDep ? workspaceDir : null
87
+ } catch {
88
+ return null
36
89
  }
90
+ }
37
91
 
92
+ /**
93
+ * Detect the package manager that owns the global install. Heuristic
94
+ * based on the CLI's filesystem path — pnpm and yarn berry use distinctive
95
+ * directory layouts; npm is the fallback.
96
+ *
97
+ * @returns {'pnpm'|'yarn'|'npm'}
98
+ */
99
+ function detectGlobalPm() {
100
+ const path = (process.argv[1] || '').toLowerCase()
101
+ if (path.includes('/pnpm/') || path.includes('\\pnpm\\')) return 'pnpm'
102
+ if (path.includes('/yarn/') || path.includes('\\yarn\\')) return 'yarn'
103
+ return 'npm'
104
+ }
105
+
106
+ /**
107
+ * Build the global-install command for a given PM.
108
+ */
109
+ function globalInstallCmd(pm) {
110
+ if (pm === 'pnpm') return 'pnpm add -g uniweb@latest'
111
+ if (pm === 'yarn') return 'yarn global add uniweb@latest'
112
+ return 'npm i -g uniweb@latest'
113
+ }
114
+
115
+ /**
116
+ * Fetch the latest published version. Returns null on network error.
117
+ */
118
+ async function fetchLatestVersion() {
119
+ try {
120
+ const res = await fetch('https://registry.npmjs.org/uniweb/latest')
121
+ if (!res.ok) return null
122
+ const data = await res.json()
123
+ return data?.version || null
124
+ } catch {
125
+ return null
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Compare two semver strings: 1 if a>b, -1 if a<b, 0 if equal.
131
+ */
132
+ function compareSemver(a, b) {
133
+ const pa = a.split('.').map(Number)
134
+ const pb = b.split('.').map(Number)
135
+ for (let i = 0; i < 3; i++) {
136
+ if ((pa[i] || 0) > (pb[i] || 0)) return 1
137
+ if ((pa[i] || 0) < (pb[i] || 0)) return -1
138
+ }
139
+ return 0
140
+ }
141
+
142
+ /**
143
+ * Run a shell command, inheriting stdio. Resolves with the exit code.
144
+ */
145
+ function runCommand(cmd) {
146
+ return new Promise((resolve) => {
147
+ const [bin, ...rest] = cmd.split(' ')
148
+ const child = spawn(bin, rest, { stdio: 'inherit' })
149
+ child.on('close', code => resolve(code ?? 0))
150
+ child.on('error', () => resolve(1))
151
+ })
152
+ }
153
+
154
+ export async function update(args = []) {
155
+ const agentsOnly = args.includes('--agents-only')
156
+ const skipAgents = args.includes('--no-agents')
157
+ const skipPrompt = args.includes('--yes') || isNonInteractive(args)
158
+ const isGlobal = isGlobalInstall()
159
+ const workspaceDir = findUniwebWorkspace(process.cwd())
160
+ const inProject = !!workspaceDir
38
161
  const cliVersion = getCliVersion()
39
- const agentsPath = join(workspaceDir, 'AGENTS.md')
40
- const currentVersion = readAgentsVersion(agentsPath)
41
162
 
42
- if (currentVersion === cliVersion) {
43
- success(`AGENTS.md is already up to date (v${cliVersion})`)
163
+ // ─── Step 1: Self-update path ─────────────────────────────────
164
+ if (!agentsOnly) {
165
+ if (!isGlobal) {
166
+ // Project-local: can't self-update meaningfully.
167
+ log(`${colors.dim}Running the project-local CLI (v${cliVersion}). This copy is pinned by your${colors.reset}`)
168
+ log(`${colors.dim}project's package.json. To update it, bump${colors.reset} ${colors.cyan}uniweb${colors.reset}${colors.dim} in${colors.reset} ${colors.cyan}package.json${colors.reset}${colors.dim} and re-install.${colors.reset}`)
169
+ log('')
170
+ } else {
171
+ const latest = await fetchLatestVersion()
172
+ if (latest === null) {
173
+ warn('Could not reach the npm registry to check for updates.')
174
+ log(`${colors.dim}Current: ${cliVersion}. Try later, or run${colors.reset} ${colors.cyan}${globalInstallCmd(detectGlobalPm())}${colors.reset}${colors.dim} manually.${colors.reset}`)
175
+ log('')
176
+ } else if (compareSemver(latest, cliVersion) <= 0) {
177
+ success(`uniweb is up to date (v${cliVersion}).`)
178
+ log('')
179
+ } else {
180
+ const pm = detectGlobalPm()
181
+ const cmd = globalInstallCmd(pm)
182
+ log(`${colors.yellow}Update available:${colors.reset} ${colors.dim}${cliVersion}${colors.reset} → ${colors.cyan}${latest}${colors.reset}`)
183
+ log(`${colors.dim}Detected package manager:${colors.reset} ${pm}`)
184
+ log(`${colors.dim}Will run:${colors.reset} ${colors.cyan}${cmd}${colors.reset}`)
185
+ log('')
186
+
187
+ if (skipPrompt) {
188
+ log(`${colors.dim}Non-interactive — skipping self-update. Run the command above to update.${colors.reset}`)
189
+ log('')
190
+ } else {
191
+ const { go } = await prompts({
192
+ type: 'confirm',
193
+ name: 'go',
194
+ message: `Run \`${cmd}\` now?`,
195
+ initial: true,
196
+ })
197
+ if (go) {
198
+ const code = await runCommand(cmd)
199
+ if (code === 0) {
200
+ success(`Self-update complete.`)
201
+ } else {
202
+ error(`Self-update failed (exit ${code}). Run the command above manually if needed.`)
203
+ }
204
+ log('')
205
+ } else {
206
+ log(`${colors.dim}Skipped self-update.${colors.reset}`)
207
+ log('')
208
+ }
209
+ }
210
+ }
211
+ }
212
+ }
213
+
214
+ // ─── Step 2: AGENTS.md refresh path ───────────────────────────
215
+ if (skipAgents) return
216
+ if (!inProject) {
217
+ if (agentsOnly) {
218
+ error('Not in a Uniweb project (no `uniweb` dep in the workspace root package.json).')
219
+ log(`${colors.dim}Run this command from inside a project created by${colors.reset} ${colors.cyan}uniweb create${colors.reset}${colors.dim}.${colors.reset}`)
220
+ process.exit(1)
221
+ }
222
+ // Self-update-only path. Quietly skip AGENTS.md.
44
223
  return
45
224
  }
46
225
 
226
+ const agentsPath = join(workspaceDir, 'AGENTS.md')
227
+ const currentAgentsVersion = readAgentsVersion(agentsPath)
228
+ if (currentAgentsVersion === cliVersion) {
229
+ success(`AGENTS.md is already up to date (v${cliVersion}).`)
230
+ return
231
+ }
232
+
233
+ // Prompt before writing in TTY, unless --yes / non-interactive (in which
234
+ // case we err on the side of doing the right thing — refresh — since the
235
+ // user explicitly invoked `uniweb update` from a Uniweb project).
236
+ if (!skipPrompt && !agentsOnly) {
237
+ const action = currentAgentsVersion ? `Update AGENTS.md (v${currentAgentsVersion} → v${cliVersion})?` : `Create AGENTS.md (v${cliVersion})?`
238
+ const { yes } = await prompts({ type: 'confirm', name: 'yes', message: action, initial: true })
239
+ if (!yes) {
240
+ log(`${colors.dim}Skipped AGENTS.md.${colors.reset}`)
241
+ return
242
+ }
243
+ }
244
+
47
245
  const content = generateAgentsContent()
48
246
  writeFileSync(agentsPath, content)
49
-
50
- if (currentVersion) {
51
- success(`Updated AGENTS.md (v${currentVersion} → v${cliVersion})`)
247
+ if (currentAgentsVersion) {
248
+ success(`Updated AGENTS.md (v${currentAgentsVersion} → v${cliVersion}).`)
52
249
  } else {
53
- success(`Created AGENTS.md (v${cliVersion})`)
250
+ success(`Created AGENTS.md (v${cliVersion}).`)
54
251
  }
55
252
  }
package/src/index.js CHANGED
@@ -103,8 +103,12 @@ function getCliVersion() {
103
103
  /**
104
104
  * Commands that always run from the global CLI (no project context needed)
105
105
  */
106
+ // Commands that always run from the global CLI, never delegating to a
107
+ // project-local copy. `update` is here because its primary job is to
108
+ // self-update the GLOBAL install — delegating it to project-local would
109
+ // short-circuit that intent.
106
110
  const STANDALONE_COMMANDS = new Set([
107
- 'create', '--help', '-h', '--version', '-v', 'login',
111
+ 'create', '--help', '-h', '--version', '-v', 'login', 'update',
108
112
  ])
109
113
 
110
114
  /**
@@ -441,8 +445,20 @@ async function main() {
441
445
  const pm = detectPackageManager()
442
446
 
443
447
  // Handle --version / -v
448
+ //
449
+ // Output convention: the version goes to stdout (parseable, scriptable —
450
+ // `version=$(uniweb --version)` should keep working). Any staleness
451
+ // notice goes to stderr, so it shows in interactive terminals but
452
+ // doesn't pollute captured output. Cache-only — never makes a network
453
+ // call from this path.
444
454
  if (command === '--version' || command === '-v') {
445
455
  console.log(`uniweb ${getCliVersion()}`)
456
+ if (isGlobalInstall()) {
457
+ try {
458
+ const { maybeNotifyFromCache } = await import('./utils/update-check.js')
459
+ maybeNotifyFromCache(getCliVersion(), 'soft')
460
+ } catch { /* ignore */ }
461
+ }
446
462
  return
447
463
  }
448
464
 
@@ -1139,14 +1155,28 @@ Prints the parsed content shape of a markdown file or folder — the
1139
1155
  Useful for debugging "why isn't my section getting X?".
1140
1156
  `,
1141
1157
  update: `
1142
- ${colors.cyan}${colors.bright}uniweb update${colors.reset} ${colors.dim}— Update AGENTS.md to match installed CLI version${colors.reset}
1158
+ ${colors.cyan}${colors.bright}uniweb update${colors.reset} ${colors.dim}— Update the CLI itself, plus AGENTS.md when in a project${colors.reset}
1143
1159
 
1144
1160
  ${colors.bright}Usage:${colors.reset}
1145
- uniweb update
1146
-
1147
- Refreshes the project's AGENTS.md from the CLI's bundled version. Run
1148
- after upgrading the \`uniweb\` package to pick up new content authoring
1149
- patterns and platform documentation.
1161
+ uniweb update Self-update + (in project) refresh AGENTS.md
1162
+ uniweb update --agents-only Only refresh AGENTS.md (skip self-update)
1163
+ uniweb update --no-agents Only self-update (skip AGENTS.md)
1164
+ uniweb update --yes Skip the confirmation prompts
1165
+
1166
+ ${colors.bright}What it does:${colors.reset}
1167
+ 1. Self-update the global install via npm / pnpm / yarn (auto-detected).
1168
+ In TTY, prompts before running. In CI / non-interactive, prints the
1169
+ command and exits without running it.
1170
+ 2. If the cwd resolves to a Uniweb project (root package.json declares
1171
+ \`uniweb\` as a dep), refreshes AGENTS.md from the CLI's bundled
1172
+ version. Outside a Uniweb project, this step is skipped — the
1173
+ command will not write AGENTS.md into unrelated directories.
1174
+
1175
+ ${colors.bright}Project-local installs:${colors.reset}
1176
+ When the running CLI is project-local (lives in node_modules), self-
1177
+ update is a no-op — the version is pinned by your project's
1178
+ package.json. The verb prints that explanation and proceeds with the
1179
+ AGENTS.md refresh path only.
1150
1180
  `,
1151
1181
  }
1152
1182
 
@@ -74,26 +74,33 @@ function printNotification(current, latest, tone = 'soft') {
74
74
  }
75
75
 
76
76
  /**
77
- * Synchronously read the cache and print an eager notification if a newer
77
+ * Synchronously read the cache and print a notification if a newer
78
78
  * version is known. No network fetch — only reads what `startUpdateCheck`
79
79
  * has previously cached. Returns true if a notification was printed.
80
80
  *
81
- * Use this for staleness-sensitive verbs (`create`) BEFORE the verb does
82
- * its work, so the user sees the warning before any files are written
83
- * from CLI-bundled templates. For other verbs, the trailing soft
84
- * notification from startUpdateCheck() is sufficient.
81
+ * Two call sites today, with different tone needs:
82
+ * - `create` (tone='eager'): loud leading notice templates ship with
83
+ * the CLI, the user is about to scaffold files, this matters.
84
+ * - `--version` / `-v` (tone='soft'): brief trailing notice — the user
85
+ * was already asking about version, mention staleness while we're
86
+ * here. Goes to stderr so scripts capturing stdout aren't affected.
85
87
  *
86
88
  * @param {string} currentVersion
89
+ * @param {'eager'|'soft'} [tone='eager']
87
90
  * @returns {boolean} true if a notification was printed
88
91
  */
89
- export function maybeEagerNotification(currentVersion) {
92
+ export function maybeNotifyFromCache(currentVersion, tone = 'eager') {
90
93
  const state = readState()
91
94
  if (!state.latestVersion) return false
92
95
  if (compareSemver(state.latestVersion, currentVersion) <= 0) return false
93
- printNotification(currentVersion, state.latestVersion, 'eager')
96
+ printNotification(currentVersion, state.latestVersion, tone)
94
97
  return true
95
98
  }
96
99
 
100
+ // Old name preserved as alias — `create` calls it without a tone arg
101
+ // and gets the eager default. Keeps that call site unchanged.
102
+ export const maybeEagerNotification = maybeNotifyFromCache
103
+
97
104
  /**
98
105
  * Start a non-blocking update check.
99
106
  *