uniweb 0.12.20 → 0.12.22

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.
@@ -0,0 +1,288 @@
1
+ /**
2
+ * uniweb validate - Check your content against the data schemas your
3
+ * foundation declares.
4
+ *
5
+ * For each section that consumes file-based data, this resolves the schema the
6
+ * foundation bound to that input (via `meta.js` `data:`) and checks the data
7
+ * items against it. It answers "does my data match what I promised?" — distinct
8
+ * from `doctor`, which checks your project against framework conventions.
9
+ *
10
+ * It warns by default; `--strict` turns findings into a non-zero exit for CI.
11
+ * The live render path stays tolerant — this gate runs before a site is live,
12
+ * by choice. Dynamic (remote) inputs and entity references can't be resolved
13
+ * without a running backend, so they're reported as deferred, never silently
14
+ * skipped.
15
+ */
16
+
17
+ import { existsSync, readFileSync } from 'node:fs'
18
+ import { join, resolve, basename } from 'node:path'
19
+ import yaml from 'js-yaml'
20
+ import { validateDataInputs } from '@uniweb/build'
21
+ import { discoverFoundations, discoverSites } from '../utils/discover.js'
22
+ import { findWorkspaceRoot } from '../utils/workspace.js'
23
+
24
+ const colors = {
25
+ reset: '\x1b[0m',
26
+ bright: '\x1b[1m',
27
+ dim: '\x1b[2m',
28
+ red: '\x1b[31m',
29
+ green: '\x1b[32m',
30
+ yellow: '\x1b[33m',
31
+ blue: '\x1b[36m',
32
+ }
33
+
34
+ const log = console.log
35
+ const success = (msg) => log(`${colors.green}✓${colors.reset} ${msg}`)
36
+ const warn = (msg) => log(`${colors.yellow}⚠${colors.reset} ${msg}`)
37
+ const error = (msg) => console.error(`${colors.red}✗${colors.reset} ${msg}`)
38
+ const info = (msg) => log(`${colors.blue}→${colors.reset} ${msg}`)
39
+
40
+ /**
41
+ * Read a flag that takes a value: `--site foo` or `--site=foo`.
42
+ */
43
+ function flagValue(args, name) {
44
+ const eq = args.find((a) => a.startsWith(`${name}=`))
45
+ if (eq) return eq.slice(name.length + 1)
46
+ const idx = args.indexOf(name)
47
+ if (idx !== -1 && args[idx + 1] && !args[idx + 1].startsWith('--')) return args[idx + 1]
48
+ return null
49
+ }
50
+
51
+ function loadSiteYml(dir) {
52
+ for (const f of ['site.yml', 'site.yaml']) {
53
+ const p = join(dir, f)
54
+ if (existsSync(p)) {
55
+ try {
56
+ return yaml.load(readFileSync(p, 'utf8'))
57
+ } catch {
58
+ return null
59
+ }
60
+ }
61
+ }
62
+ return null
63
+ }
64
+
65
+ /**
66
+ * Validate one site against its local foundation.
67
+ *
68
+ * @returns {Promise<Object>} { site, foundation, status, report?, reason? }
69
+ * status: 'checked' | 'skipped'
70
+ */
71
+ async function validateSite(site, foundations, workspaceDir) {
72
+ const sitePath = join(workspaceDir, site.path)
73
+ const siteYml = loadSiteYml(sitePath)
74
+ const foundationName = siteYml?.foundation
75
+
76
+ // No local foundation to check against → out of static scope (the schemas
77
+ // live in the foundation; a registry-ref / URL foundation isn't on disk).
78
+ if (!foundationName) {
79
+ return { site: site.name, status: 'skipped', reason: 'no foundation declared in site.yml (runtime-loaded?)' }
80
+ }
81
+ const match = foundations.find((f) => f.name === foundationName || basename(f.path) === foundationName)
82
+ if (!match) {
83
+ return {
84
+ site: site.name,
85
+ status: 'skipped',
86
+ reason: `foundation "${foundationName}" is not a local workspace foundation — its schemas aren't on disk to check against`,
87
+ }
88
+ }
89
+
90
+ const foundationPath = join(workspaceDir, match.path)
91
+ const report = await validateDataInputs({ siteRoot: sitePath, foundationPath })
92
+ return { site: site.name, foundation: match.name, status: 'checked', report }
93
+ }
94
+
95
+ /**
96
+ * Print one site's findings in human form. Groups by the (file, schema) pair
97
+ * so a collection feeding many sections lists its findings once, with the
98
+ * sections that use it — every link of the chain (route › section › key › file
99
+ * › item › field) is present, de-duped.
100
+ */
101
+ function printSiteHuman(result) {
102
+ log('')
103
+ if (result.status === 'skipped') {
104
+ warn(`${colors.bright}${result.site}${colors.reset} — skipped: ${result.reason}`)
105
+ return
106
+ }
107
+
108
+ const { report, foundation } = result
109
+ const { violations, deferred, setupErrors, summary } = report
110
+ const header = `${colors.bright}${result.site}${colors.reset} ${colors.dim}(foundation: ${foundation})${colors.reset}`
111
+
112
+ if (violations.length === 0 && setupErrors.length === 0) {
113
+ success(`${header} — ${summary.records} record(s) / ${summary.schemas} schema(s) conform`)
114
+ } else {
115
+ info(header)
116
+ }
117
+
118
+ // Group violations by file+schema pair.
119
+ const groups = new Map()
120
+ for (const v of violations) {
121
+ const key = `${v.file} ${v.schema}`
122
+ if (!groups.has(key)) groups.set(key, { file: v.file, schema: v.schema, users: v.users, findings: [] })
123
+ groups.get(key).findings.push(v)
124
+ }
125
+ for (const g of groups.values()) {
126
+ log(` ${colors.red}✗${colors.reset} ${g.file} ${colors.dim}· schema ${g.schema}${colors.reset}`)
127
+ for (const u of dedupeUsers(g.users)) {
128
+ log(` ${colors.dim}used by${colors.reset} ${u.route} › ${u.section} › data.${u.key}`)
129
+ }
130
+ for (const f of g.findings) {
131
+ log(` ${colors.yellow}•${colors.reset} item ${colors.bright}"${f.item}"${colors.reset} › ${f.field} — ${f.message}`)
132
+ }
133
+ }
134
+
135
+ for (const e of setupErrors) {
136
+ log(` ${colors.red}✗${colors.reset} ${e.file} — ${e.message}`)
137
+ for (const u of dedupeUsers(e.users)) {
138
+ log(` ${colors.dim}used by${colors.reset} ${u.route} › ${u.section} › data.${u.key}`)
139
+ }
140
+ }
141
+
142
+ if (deferred.length > 0) {
143
+ log(` ${colors.dim}↪ deferred (not statically checkable):${colors.reset}`)
144
+ for (const d of deferred) {
145
+ const extra = d.url ? ` ${colors.dim}(${d.url})${colors.reset}` : d.ref ? ` ${colors.dim}(${d.ref})${colors.reset}` : ''
146
+ log(` ${colors.dim}•${colors.reset} ${d.route} › ${d.section} › data.${d.key} — ${d.reason}${extra}`)
147
+ }
148
+ }
149
+
150
+ log(
151
+ ` ${colors.dim}${summary.records} record(s) · ${summary.schemas} schema(s) · ` +
152
+ `${summary.violations} violation(s) · ${summary.deferred} deferred${colors.reset}`
153
+ )
154
+ }
155
+
156
+ function dedupeUsers(users) {
157
+ const seen = new Set()
158
+ const out = []
159
+ for (const u of users || []) {
160
+ const k = `${u.route} ${u.section} ${u.key}`
161
+ if (seen.has(k)) continue
162
+ seen.add(k)
163
+ out.push(u)
164
+ }
165
+ return out
166
+ }
167
+
168
+ export async function validate(args = []) {
169
+ const asJson = args.includes('--json')
170
+ const strict = args.includes('--strict')
171
+ const siteFilter = flagValue(args, '--site')
172
+ const positional = args.find((a, i) => !a.startsWith('--') && args[i - 1] !== '--site')
173
+
174
+ const target = positional ? resolve(process.cwd(), positional) : process.cwd()
175
+ const workspaceDir = findWorkspaceRoot(target)
176
+
177
+ if (!workspaceDir) {
178
+ if (asJson) log(JSON.stringify({ ok: false, error: 'not in a Uniweb workspace' }, null, 2))
179
+ else error('Not in a Uniweb workspace. Run this from a project root or a site directory.')
180
+ return { exitCode: 2 }
181
+ }
182
+
183
+ const [sites, foundations] = await Promise.all([discoverSites(workspaceDir), discoverFoundations(workspaceDir)])
184
+
185
+ // Select which sites to check: --site filter, an explicitly targeted site
186
+ // directory, or all sites in the workspace.
187
+ let selected = sites
188
+ if (siteFilter) {
189
+ selected = sites.filter((s) => s.name === siteFilter || basename(s.path) === siteFilter)
190
+ if (selected.length === 0) {
191
+ const names = sites.map((s) => s.name).join(', ') || '(none)'
192
+ if (asJson) log(JSON.stringify({ ok: false, error: `site "${siteFilter}" not found`, sites: sites.map((s) => s.name) }, null, 2))
193
+ else error(`Site "${siteFilter}" not found. Available: ${names}`)
194
+ return { exitCode: 2 }
195
+ }
196
+ } else if (target !== workspaceDir) {
197
+ const match = sites.find((s) => join(workspaceDir, s.path) === target)
198
+ if (match) selected = [match]
199
+ }
200
+
201
+ if (selected.length === 0) {
202
+ if (asJson) log(JSON.stringify({ ok: true, sites: [], note: 'no sites found' }, null, 2))
203
+ else warn('No sites found in this workspace.')
204
+ return { exitCode: 0 }
205
+ }
206
+
207
+ // The data pipeline (collectSiteContent / processCollections) prints progress
208
+ // via console.log. Route that to stderr while the engine runs so stdout stays
209
+ // clean — pure JSON for `--json`, just the report otherwise. `log` captured
210
+ // the original stdout writer at module load, so our own output is unaffected.
211
+ const results = []
212
+ const origConsoleLog = console.log
213
+ console.log = (...a) => process.stderr.write(a.join(' ') + '\n')
214
+ try {
215
+ for (const site of selected) {
216
+ try {
217
+ results.push(await validateSite(site, foundations, workspaceDir))
218
+ } catch (err) {
219
+ results.push({ site: site.name, status: 'error', reason: err.message })
220
+ }
221
+ }
222
+ } finally {
223
+ console.log = origConsoleLog
224
+ }
225
+
226
+ const totalViolations = results.reduce((n, r) => n + (r.report?.violations.length || 0), 0)
227
+ const totalSetupErrors = results.reduce((n, r) => n + (r.report?.setupErrors.length || 0), 0)
228
+ const hadError = results.some((r) => r.status === 'error')
229
+
230
+ if (asJson) {
231
+ const payload = {
232
+ ok: totalViolations === 0 && !hadError,
233
+ strict,
234
+ sites: results.map((r) => ({
235
+ site: r.site,
236
+ foundation: r.foundation || null,
237
+ status: r.status,
238
+ reason: r.reason || null,
239
+ ...(r.report || {}),
240
+ })),
241
+ summary: {
242
+ sites: results.length,
243
+ violations: totalViolations,
244
+ setupErrors: totalSetupErrors,
245
+ deferred: results.reduce((n, r) => n + (r.report?.deferred.length || 0), 0),
246
+ },
247
+ }
248
+ log(JSON.stringify(payload, null, 2))
249
+ } else {
250
+ log('')
251
+ log(`${colors.blue}${colors.bright}Uniweb Validate${colors.reset}`)
252
+ log(`${colors.dim}Checking content against the data schemas your foundation declares…${colors.reset}`)
253
+ for (const r of results) {
254
+ if (r.status === 'error') {
255
+ log('')
256
+ error(`${colors.bright}${r.site}${colors.reset} — could not check: ${r.reason}`)
257
+ } else {
258
+ printSiteHuman(r)
259
+ }
260
+ }
261
+
262
+ log('')
263
+ log('─'.repeat(50))
264
+ if (totalViolations === 0 && !hadError) {
265
+ log('')
266
+ success(`${colors.bright}All data conforms.${colors.reset}`)
267
+ if (!strict && totalSetupErrors === 0) {
268
+ // nothing more to say
269
+ }
270
+ log('')
271
+ } else {
272
+ log('')
273
+ if (totalViolations > 0) {
274
+ const mode = strict ? `${colors.red}error${colors.reset}` : `${colors.yellow}warning${colors.reset}`
275
+ log(`${totalViolations} violation(s) — reported as ${mode}${strict ? '' : ` ${colors.dim}(pass --strict to fail CI)${colors.reset}`}`)
276
+ }
277
+ if (hadError) log(`${colors.red}Some sites could not be checked.${colors.reset}`)
278
+ log('')
279
+ }
280
+ }
281
+
282
+ // Exit semantics: 2 = couldn't run; 1 = violations under --strict; 0 = clean
283
+ // or warn-only (the live path stays tolerant, so findings don't fail by
284
+ // default). Setup/read failures are surfaced but don't fail the build.
285
+ if (hadError) return { exitCode: 2 }
286
+ if (totalViolations > 0 && strict) return { exitCode: 1 }
287
+ return { exitCode: 0 }
288
+ }
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-05-12T15:22:16.890Z",
3
+ "generatedAt": "2026-06-03T19:50:40.168Z",
4
4
  "packages": {
5
5
  "@uniweb/build": {
6
- "version": "0.14.5",
6
+ "version": "0.14.6",
7
7
  "path": "framework/build",
8
8
  "deps": [
9
9
  "@uniweb/content-reader",
10
+ "@uniweb/content-writer",
10
11
  "@uniweb/core",
11
12
  "@uniweb/runtime",
12
13
  "@uniweb/schemas",
@@ -14,17 +15,17 @@
14
15
  ]
15
16
  },
16
17
  "@uniweb/content-reader": {
17
- "version": "1.1.11",
18
+ "version": "1.1.12",
18
19
  "path": "framework/content-reader",
19
20
  "deps": []
20
21
  },
21
22
  "@uniweb/content-writer": {
22
- "version": "0.2.4",
23
+ "version": "0.2.5",
23
24
  "path": "framework/content-writer",
24
25
  "deps": []
25
26
  },
26
27
  "@uniweb/core": {
27
- "version": "0.7.11",
28
+ "version": "0.7.12",
28
29
  "path": "framework/core",
29
30
  "deps": [
30
31
  "@uniweb/semantic-parser",
@@ -42,24 +43,24 @@
42
43
  "deps": []
43
44
  },
44
45
  "@uniweb/kit": {
45
- "version": "0.9.13",
46
+ "version": "0.9.14",
46
47
  "path": "framework/kit",
47
48
  "deps": [
48
49
  "@uniweb/core"
49
50
  ]
50
51
  },
51
52
  "@uniweb/loom": {
52
- "version": "0.2.2",
53
+ "version": "0.2.3",
53
54
  "path": "framework/loom",
54
55
  "deps": []
55
56
  },
56
57
  "@uniweb/press": {
57
- "version": "0.4.6",
58
+ "version": "0.4.7",
58
59
  "path": "framework/press",
59
60
  "deps": []
60
61
  },
61
62
  "@uniweb/runtime": {
62
- "version": "0.8.14",
63
+ "version": "0.8.15",
63
64
  "path": "framework/runtime",
64
65
  "deps": [
65
66
  "@uniweb/core",
@@ -92,7 +93,7 @@
92
93
  "deps": []
93
94
  },
94
95
  "@uniweb/unipress": {
95
- "version": "0.4.9",
96
+ "version": "0.4.10",
96
97
  "path": "framework/unipress",
97
98
  "deps": [
98
99
  "@uniweb/build",
package/src/index.js CHANGED
@@ -37,6 +37,7 @@ import { login } from './commands/login.js'
37
37
  import { invite } from './commands/invite.js'
38
38
  import { handoff } from './commands/handoff.js'
39
39
  import { update } from './commands/update.js'
40
+ import { clone } from './commands/clone.js'
40
41
  import { template } from './commands/template.js'
41
42
  import {
42
43
  resolveTemplate,
@@ -128,11 +129,12 @@ function getCliVersion() {
128
129
  * Commands that always run from the global CLI (no project context needed)
129
130
  */
130
131
  // Commands that always run from the global CLI, never delegating to a
131
- // project-local copy. `update` is here because its primary job is to
132
- // self-update the GLOBAL installdelegating it to project-local would
133
- // short-circuit that intent.
132
+ // project-local copy. `update` is here because it reconciles the project
133
+ // against *this* CLI's version matrix when run from a (newer) global
134
+ // install that's the whole point; delegating to the project-local copy
135
+ // would align the project to the version it already has, i.e. a no-op.
134
136
  const STANDALONE_COMMANDS = new Set([
135
- 'create', '--help', '-h', '--version', '-v', 'login', 'update',
137
+ 'create', 'clone', '--help', '-h', '--version', '-v', 'login', 'update',
136
138
  ])
137
139
 
138
140
  /**
@@ -598,6 +600,44 @@ async function main() {
598
600
  process.exit(result?.errors > 0 ? 1 : 0)
599
601
  }
600
602
 
603
+ // Handle validate command (dynamic import — depends on @uniweb/build)
604
+ if (command === 'validate') {
605
+ const { validate } = await importProjectCommand('./commands/validate.js')
606
+ const result = await validate(args.slice(1))
607
+ process.exit(result?.exitCode ?? 0)
608
+ }
609
+
610
+ // Handle register command (dynamic import — depends on @uniweb/build)
611
+ if (command === 'register') {
612
+ const { register } = await importProjectCommand('./commands/register.js')
613
+ const result = await register(args.slice(1))
614
+ process.exit(result?.exitCode ?? 0)
615
+ }
616
+
617
+ // Handle push command (dynamic import — depends on @uniweb/build)
618
+ if (command === 'push') {
619
+ const { push } = await importProjectCommand('./commands/push.js')
620
+ const result = await push(args.slice(1))
621
+ process.exit(result?.exitCode ?? 0)
622
+ }
623
+
624
+ // Handle pull command (dynamic import — depends on @uniweb/build)
625
+ if (command === 'pull') {
626
+ const { pull } = await importProjectCommand('./commands/pull.js')
627
+ const result = await pull(args.slice(1))
628
+ process.exit(result?.exitCode ?? 0)
629
+ }
630
+
631
+ // Handle clone command (global — bootstraps a new project from a backend site;
632
+ // STANDALONE, so a global `uniweb clone` runs here instead of delegating to a
633
+ // project-local CLI that doesn't exist yet. clone.js avoids any static
634
+ // @uniweb/build import, then delegates the projection to the project-local
635
+ // `uniweb pull` after install.)
636
+ if (command === 'clone') {
637
+ const result = await clone(args.slice(1))
638
+ process.exit(result?.exitCode ?? 0)
639
+ }
640
+
601
641
  // Handle update command
602
642
  if (command === 'update') {
603
643
  await update(args.slice(1))
@@ -645,9 +685,32 @@ async function main() {
645
685
  return
646
686
  }
647
687
 
648
- // Handle login command
688
+ // Handle content command (dynamic import — depends on @uniweb/build/uwx)
689
+ if (command === 'content') {
690
+ const { content } = await importProjectCommand('./commands/content.js')
691
+ await content(args.slice(1))
692
+ return
693
+ }
694
+
695
+ // Handle login command. Default targets the NEW backend (username/password);
696
+ // `--legacy` runs the old browser/social flow (still used by publish/deploy
697
+ // internally via ensureAuth, so it stays reachable).
649
698
  if (command === 'login') {
650
- await login(args.slice(1))
699
+ const loginArgs = args.slice(1)
700
+ if (loginArgs.includes('--legacy')) {
701
+ await login(loginArgs.filter((a) => a !== '--legacy'))
702
+ } else {
703
+ const { getRegistryApiBaseUrl } = await import('./utils/config.js')
704
+ const { runRegistryLogin } = await import('./utils/registry-auth.js')
705
+ await runRegistryLogin({ apiBase: getRegistryApiBaseUrl(), args: loginArgs })
706
+ }
707
+ return
708
+ }
709
+
710
+ // Handle org command (new-backend orgs/units — publish-scope management)
711
+ if (command === 'org') {
712
+ const { org } = await import('./commands/org.js')
713
+ await org(args.slice(1))
651
714
  return
652
715
  }
653
716
 
@@ -1168,6 +1231,59 @@ ${colors.bright}Options:${colors.reset}
1168
1231
  --non-interactive Fail with usage info instead of prompting
1169
1232
 
1170
1233
  Exit code is 1 if errors are found (warnings only → exit 0).
1234
+ `,
1235
+ validate: `
1236
+ ${colors.cyan}${colors.bright}uniweb validate${colors.reset} ${colors.dim}— Check your content against your foundation's data schemas${colors.reset}
1237
+
1238
+ ${colors.bright}Usage:${colors.reset}
1239
+ uniweb validate [path] [options]
1240
+
1241
+ Checks each section's file-based data inputs against the schema your
1242
+ foundation declared for that input (meta.js \`data:\`). Answers "does my
1243
+ data match what I promised?" — distinct from \`doctor\`, which checks your
1244
+ project against framework conventions.
1245
+
1246
+ Warns by default; the live render path stays tolerant, so this is a
1247
+ pre-live / CI gate. Dynamic (\`url:\`) inputs and entity references can't be
1248
+ resolved without a running backend, so they're reported as deferred,
1249
+ never silently skipped.
1250
+
1251
+ ${colors.bright}Options:${colors.reset}
1252
+ --strict Treat findings as errors (non-zero exit for CI)
1253
+ --json Machine-readable output (for CI annotations)
1254
+ --site <name> Check one site in a multi-site workspace
1255
+
1256
+ Exit codes: 0 clean (or warn-only), 1 violations under --strict, 2 setup error.
1257
+ `,
1258
+ register: `
1259
+ ${colors.cyan}${colors.bright}uniweb register${colors.reset} ${colors.dim}— Register a foundation + its data schemas with the backend registry${colors.reset}
1260
+
1261
+ ${colors.bright}Usage:${colors.reset}
1262
+ uniweb register [options]
1263
+
1264
+ Builds one \`.uwx\` document and submits it to the registry over HTTP. Run
1265
+ \`uniweb login\` first (or pass \`--token\`). Distinct from \`uniweb publish\` (legacy
1266
+ hosting platform).
1267
+
1268
+ Auto-detects what you run it in:
1269
+ • a foundation the foundation + the data schemas it defines/renders
1270
+ • a schemas-only pkg just its data schemas, no foundation — e.g. @uniweb/schemas,
1271
+ any @org/schemas package, or a bare schemas/*.yml folder
1272
+
1273
+ Schema scopes (set the org with --scope, or package.json uniweb.scope):
1274
+ @/name your own schema, scoped to the publish org (@/x -> @org/x)
1275
+ @std/name a shared standard schema (from @uniweb/schemas)
1276
+ @org/name another org's published schema, referenced by name
1277
+
1278
+ ${colors.bright}Options:${colors.reset}
1279
+ --scope @org Publish under @org (resolves @/x -> @org/x); default: package.json uniweb.scope
1280
+ --dry-run Print the .uwx; submit nothing
1281
+ -o, --output <f> Write the .uwx to a file; submit nothing
1282
+ --registry <url> Submit endpoint (default: \$UNIWEB_REGISTER_URL or a local URL)
1283
+ --token <bearer> Submit with this bearer; skips \`uniweb login\` (or set UNIWEB_TOKEN)
1284
+ --non-interactive Fail with usage info instead of prompting
1285
+
1286
+ Run from a foundation, a schemas-only package, or a workspace with a single foundation.
1171
1287
  `,
1172
1288
  rename: `
1173
1289
  ${colors.cyan}${colors.bright}uniweb rename${colors.reset} ${colors.dim}— Rename a workspace package${colors.reset}
@@ -1284,39 +1400,47 @@ Prints the parsed content shape of a markdown file or folder — the
1284
1400
  Useful for debugging "why isn't my section getting X?".
1285
1401
  `,
1286
1402
  update: `
1287
- ${colors.cyan}${colors.bright}uniweb update${colors.reset} ${colors.dim}— Reconcile workspace state with the running CLI${colors.reset}
1403
+ ${colors.cyan}${colors.bright}uniweb update${colors.reset} ${colors.dim}— Align this project with the running CLI${colors.reset}
1288
1404
 
1289
1405
  ${colors.bright}Usage:${colors.reset}
1290
- uniweb update Self-update + align deps + refresh AGENTS.md
1406
+ uniweb update Align deps + refresh AGENTS.md
1291
1407
  uniweb update --deps-only Only align workspace @uniweb/* deps
1292
1408
  uniweb update --agents-only Only refresh AGENTS.md
1293
1409
  uniweb update --no-deps Skip the deps-alignment step
1294
1410
  uniweb update --no-agents Skip the AGENTS.md step
1295
1411
  uniweb update --dry-run Print survey + would-be writes; no mutations
1296
1412
  uniweb update --allow-mismatch Refresh AGENTS.md even if declared deps lag
1297
- uniweb update --yes Skip confirmation prompts
1413
+ uniweb update --yes Don't prompt — apply edits and run the install
1298
1414
 
1299
1415
  ${colors.bright}What it does:${colors.reset}
1300
1416
  Prints a version survey first (CLI version, AGENTS.md stamp, every
1301
1417
  @uniweb/* + uniweb dep declared in workspace package.json files,
1302
- marked aligned / behind / ahead). Then three steps:
1303
-
1304
- 1. ${colors.bright}Self-update${colors.reset} the global install via npm / pnpm / yarn
1305
- (auto-detected). TTY prompts; non-interactive prints the command.
1306
- 2. ${colors.bright}Align workspace deps${colors.reset} to the CLI's bundled version matrix
1307
- edits every workspace package.json (only @uniweb/* + uniweb keys),
1308
- then offers to run the workspace's package manager (lockfile-detected:
1309
- pnpm-lock.yaml → pnpm, yarn.lock → yarn, package-lock.json → npm).
1310
- If the install fails, package.json edits are kept and a revert
1311
- command is printed.
1312
- 3. ${colors.bright}Refresh AGENTS.md${colors.reset} from the CLI's bundled partial. Refuses to
1313
- run while declared deps still lag the CLI (would document features
1314
- not in your installed packages); pass --allow-mismatch to override.
1418
+ marked aligned / behind / ahead). Then two steps:
1419
+
1420
+ 1. ${colors.bright}Align workspace deps${colors.reset} to this CLI's bundled version matrix
1421
+ rewrites the @uniweb/* + uniweb keys in each package.json that lags
1422
+ (deps ahead of the matrix are left alone never downgraded;
1423
+ existing indentation is preserved), then offers to run the
1424
+ workspace's package manager (lockfile-detected: pnpm-lock.yaml →
1425
+ pnpm, yarn.lock → yarn, package-lock.json → npm). If the install
1426
+ fails, the package.json edits are kept and a revert command printed.
1427
+ 2. ${colors.bright}Refresh AGENTS.md${colors.reset} from this CLI's bundled partial. Won't run
1428
+ if deps were edited but not installed (node_modules would be behind
1429
+ package.json), or while declared deps still lag the CLI (would
1430
+ document features not in your packages) pass --allow-mismatch for
1431
+ the latter.
1432
+
1433
+ ${colors.bright}Which matrix?${colors.reset}
1434
+ \`update\` pins to the version matrix *this* CLI shipped with — not
1435
+ necessarily the latest release. To reconcile against the latest release
1436
+ without touching a global install, run \`npx uniweb@latest update\`. This
1437
+ command does NOT update the CLI itself; use your package manager
1438
+ (\`npm i -g uniweb@latest\`, \`pnpm add -g uniweb@latest\`, …).
1315
1439
 
1316
1440
  ${colors.bright}Project-local installs:${colors.reset}
1317
- When the running CLI is project-local (lives in node_modules), self-
1318
- update is a no-opthe version is pinned by your project's
1319
- package.json. The deps + AGENTS.md steps still run.
1441
+ When run from a project-local CLI (in node_modules), it aligns the
1442
+ project to that pinned version bump \`uniweb\` in package.json (or use
1443
+ \`npx uniweb@latest update\`) to align to something newer.
1320
1444
  `,
1321
1445
  }
1322
1446
 
@@ -1334,6 +1458,7 @@ ${colors.bright}Usage:${colors.reset}
1334
1458
 
1335
1459
  ${colors.bright}Commands:${colors.reset}
1336
1460
  create [name] Create a new project
1461
+ clone <site-uuid> Clone a backend site into a local file project
1337
1462
  add <type> [name] Add a foundation, site, or extension to a project
1338
1463
  rename <type> Rename a foundation, site, or extension across the workspace
1339
1464
  dev Start a dev server for a site
@@ -1341,12 +1466,16 @@ ${colors.bright}Commands:${colors.reset}
1341
1466
  deploy Deploy a site to Uniweb hosting
1342
1467
  export Export a self-contained site for third-party hosting
1343
1468
  publish Publish a foundation to the Uniweb registry
1469
+ register Register a foundation + its data schemas with the backend registry
1470
+ push Push a site's content to the backend
1471
+ pull Pull a site's content from the backend
1344
1472
  invite <email> Create a foundation invite for a client
1345
1473
  handoff <email> Hand off a site to a client
1346
1474
  inspect <path> Inspect parsed content shape of a markdown file or folder
1347
1475
  docs Generate component documentation
1348
1476
  doctor Diagnose project configuration issues
1349
- update Update AGENTS.md to match installed CLI version
1477
+ validate Check your content against your foundation's data schemas
1478
+ update Align workspace deps + AGENTS.md to the running CLI
1350
1479
  i18n <cmd> Internationalization (extract, sync, status)
1351
1480
  template publish Publish a site as a cloud template
1352
1481
  login Log in to your Uniweb account