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.
- package/README.md +30 -1
- package/package.json +7 -7
- package/partials/agents.md +41 -11
- package/partials/config-reference.hbs +1 -2
- package/src/commands/add.js +1 -87
- package/src/commands/build.js +2 -2
- package/src/commands/clone.js +337 -0
- package/src/commands/content.js +199 -0
- package/src/commands/deploy.js +27 -6
- package/src/commands/docs.js +2 -3
- package/src/commands/doctor.js +24 -5
- package/src/commands/org.js +66 -0
- package/src/commands/publish.js +4 -3
- package/src/commands/pull.js +238 -0
- package/src/commands/push.js +400 -0
- package/src/commands/register.js +274 -0
- package/src/commands/rename.js +10 -5
- package/src/commands/update.js +211 -245
- package/src/commands/validate.js +288 -0
- package/src/framework-index.json +11 -10
- package/src/index.js +155 -26
- package/src/templates/processor.js +41 -19
- package/src/utils/config.js +30 -2
- package/src/utils/dep-survey.js +99 -0
- package/src/utils/json-file.js +68 -0
- package/src/utils/placement.js +100 -0
- package/src/utils/pm.js +29 -0
- package/src/utils/registry-auth.js +380 -0
- package/src/utils/registry-orgs.js +179 -0
- package/src/utils/scaffold.js +18 -5
- package/src/utils/site-content-refs.js +21 -0
- package/src/utils/update-check.js +4 -2
- package/src/versions.js +11 -4
- package/templates/foundation/_gitignore +5 -0
- package/templates/site/_gitignore +5 -0
- package/templates/site/package.json.hbs +2 -2
- package/templates/workspace/_gitignore +33 -0
|
@@ -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
|
+
}
|
package/src/framework-index.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-
|
|
3
|
+
"generatedAt": "2026-06-03T19:50:40.168Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/build": {
|
|
6
|
-
"version": "0.14.
|
|
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.
|
|
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.
|
|
23
|
+
"version": "0.2.5",
|
|
23
24
|
"path": "framework/content-writer",
|
|
24
25
|
"deps": []
|
|
25
26
|
},
|
|
26
27
|
"@uniweb/core": {
|
|
27
|
-
"version": "0.7.
|
|
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.
|
|
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.
|
|
53
|
+
"version": "0.2.3",
|
|
53
54
|
"path": "framework/loom",
|
|
54
55
|
"deps": []
|
|
55
56
|
},
|
|
56
57
|
"@uniweb/press": {
|
|
57
|
-
"version": "0.4.
|
|
58
|
+
"version": "0.4.7",
|
|
58
59
|
"path": "framework/press",
|
|
59
60
|
"deps": []
|
|
60
61
|
},
|
|
61
62
|
"@uniweb/runtime": {
|
|
62
|
-
"version": "0.8.
|
|
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.
|
|
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
|
|
132
|
-
//
|
|
133
|
-
//
|
|
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
|
|
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
|
-
|
|
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}—
|
|
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
|
|
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
|
|
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
|
|
1303
|
-
|
|
1304
|
-
1. ${colors.bright}
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
pnpm
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
not in your
|
|
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
|
|
1318
|
-
|
|
1319
|
-
|
|
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
|
-
|
|
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
|