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,376 @@
|
|
|
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 — and every record file in the records directory against the
|
|
8
|
+
* schema its folder names, whether or not a section reads it. It answers "does my
|
|
9
|
+
* data match what I promised?" — distinct from `doctor`, which checks your project
|
|
10
|
+
* against framework conventions.
|
|
11
|
+
*
|
|
12
|
+
* ⭐ A violation fails it (exit 1); `--lax` reports without failing. It warned by
|
|
13
|
+
* default until 2026-09-24, with `--strict` to fail — a gate that passes is one
|
|
14
|
+
* nobody reads, and `push` / `publish` now refuse the same findings. The live render
|
|
15
|
+
* path stays tolerant. Remote (`url:`) inputs are not in the project, so they're
|
|
16
|
+
* reported as deferred, never silently skipped. The value of an entity reference is
|
|
17
|
+
* not checked against the entity it names. ⛔ This said references were "reported as
|
|
18
|
+
* deferred" until 2026-09-24; no check ever reported them.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
22
|
+
import { join, resolve, basename } from 'node:path'
|
|
23
|
+
import yaml from 'js-yaml'
|
|
24
|
+
import { validateDataInputs } from '@uniweb/build'
|
|
25
|
+
import { discoverFoundations, discoverSites } from '../utils/discover.js'
|
|
26
|
+
import { findWorkspaceRoot } from '../utils/workspace.js'
|
|
27
|
+
|
|
28
|
+
const colors = {
|
|
29
|
+
reset: '\x1b[0m',
|
|
30
|
+
bright: '\x1b[1m',
|
|
31
|
+
dim: '\x1b[2m',
|
|
32
|
+
red: '\x1b[31m',
|
|
33
|
+
green: '\x1b[32m',
|
|
34
|
+
yellow: '\x1b[33m',
|
|
35
|
+
blue: '\x1b[36m'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const log = console.log
|
|
39
|
+
const success = (msg) => log(`${colors.green}✓${colors.reset} ${msg}`)
|
|
40
|
+
const warn = (msg) => log(`${colors.yellow}⚠${colors.reset} ${msg}`)
|
|
41
|
+
const error = (msg) => console.error(`${colors.red}✗${colors.reset} ${msg}`)
|
|
42
|
+
const info = (msg) => log(`${colors.blue}→${colors.reset} ${msg}`)
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Read a flag that takes a value: `--site foo` or `--site=foo`.
|
|
46
|
+
*/
|
|
47
|
+
function flagValue(args, name) {
|
|
48
|
+
const eq = args.find((a) => a.startsWith(`${name}=`))
|
|
49
|
+
if (eq) return eq.slice(name.length + 1)
|
|
50
|
+
const idx = args.indexOf(name)
|
|
51
|
+
if (idx !== -1 && args[idx + 1] && !args[idx + 1].startsWith('--'))
|
|
52
|
+
return args[idx + 1]
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function loadSiteYml(dir) {
|
|
57
|
+
for (const f of ['site.yml', 'site.yaml']) {
|
|
58
|
+
const p = join(dir, f)
|
|
59
|
+
if (existsSync(p)) {
|
|
60
|
+
try {
|
|
61
|
+
return yaml.load(readFileSync(p, 'utf8'))
|
|
62
|
+
} catch {
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Validate one site against its local foundation.
|
|
72
|
+
*
|
|
73
|
+
* @returns {Promise<Object>} { site, foundation, status, report?, reason? }
|
|
74
|
+
* status: 'checked' | 'skipped'
|
|
75
|
+
*/
|
|
76
|
+
async function validateSite(site, foundations, workspaceDir) {
|
|
77
|
+
const sitePath = join(workspaceDir, site.path)
|
|
78
|
+
const siteYml = loadSiteYml(sitePath)
|
|
79
|
+
const foundationName = siteYml?.foundation
|
|
80
|
+
|
|
81
|
+
// No local foundation to check against → out of static scope (the schemas
|
|
82
|
+
// live in the foundation; a registry-ref / URL foundation isn't on disk).
|
|
83
|
+
if (!foundationName) {
|
|
84
|
+
return {
|
|
85
|
+
site: site.name,
|
|
86
|
+
status: 'skipped',
|
|
87
|
+
reason: 'no foundation declared in site.yml (runtime-loaded?)'
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const match = foundations.find(
|
|
91
|
+
(f) => f.name === foundationName || basename(f.path) === foundationName
|
|
92
|
+
)
|
|
93
|
+
if (!match) {
|
|
94
|
+
return {
|
|
95
|
+
site: site.name,
|
|
96
|
+
status: 'skipped',
|
|
97
|
+
reason: `foundation "${foundationName}" is not a local workspace foundation — its schemas aren't on disk to check against`
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const foundationPath = join(workspaceDir, match.path)
|
|
102
|
+
const report = await validateDataInputs({
|
|
103
|
+
siteRoot: sitePath,
|
|
104
|
+
foundationPath
|
|
105
|
+
})
|
|
106
|
+
return { site: site.name, foundation: match.name, status: 'checked', report }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Print one site's findings in human form. Groups by the (file, schema) pair
|
|
111
|
+
* so a collection feeding many sections lists its findings once, with the
|
|
112
|
+
* sections that use it — every link of the chain (route › section › key › file
|
|
113
|
+
* › item › field) is present, de-duped.
|
|
114
|
+
*/
|
|
115
|
+
function printSiteHuman(result) {
|
|
116
|
+
log('')
|
|
117
|
+
if (result.status === 'skipped') {
|
|
118
|
+
warn(
|
|
119
|
+
`${colors.bright}${result.site}${colors.reset} — skipped: ${result.reason}`
|
|
120
|
+
)
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const { report, foundation } = result
|
|
125
|
+
const { violations, deferred, setupErrors, summary } = report
|
|
126
|
+
const header = `${colors.bright}${result.site}${colors.reset} ${colors.dim}(foundation: ${foundation})${colors.reset}`
|
|
127
|
+
|
|
128
|
+
if (violations.length === 0 && setupErrors.length === 0) {
|
|
129
|
+
success(
|
|
130
|
+
`${header} — ${summary.records} record(s) / ${summary.schemas} schema(s) conform`
|
|
131
|
+
)
|
|
132
|
+
} else {
|
|
133
|
+
info(header)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Group violations by file+schema pair.
|
|
137
|
+
const groups = new Map()
|
|
138
|
+
for (const v of violations) {
|
|
139
|
+
const key = `${v.file} ${v.schema}`
|
|
140
|
+
if (!groups.has(key))
|
|
141
|
+
groups.set(key, {
|
|
142
|
+
file: v.file,
|
|
143
|
+
schema: v.schema,
|
|
144
|
+
users: v.users,
|
|
145
|
+
findings: []
|
|
146
|
+
})
|
|
147
|
+
groups.get(key).findings.push(v)
|
|
148
|
+
}
|
|
149
|
+
for (const g of groups.values()) {
|
|
150
|
+
log(
|
|
151
|
+
` ${colors.red}✗${colors.reset} ${g.file} ${colors.dim}· schema ${g.schema}${colors.reset}`
|
|
152
|
+
)
|
|
153
|
+
for (const u of dedupeUsers(g.users)) {
|
|
154
|
+
log(
|
|
155
|
+
` ${colors.dim}used by${colors.reset} ${u.route} › ${u.section} › data.${u.key}`
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
for (const f of g.findings) {
|
|
159
|
+
log(
|
|
160
|
+
` ${colors.yellow}•${colors.reset} item ${colors.bright}"${f.item}"${colors.reset} › ${f.field} — ${f.message}`
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (const e of setupErrors) {
|
|
166
|
+
log(` ${colors.red}✗${colors.reset} ${e.file} — ${e.message}`)
|
|
167
|
+
for (const u of dedupeUsers(e.users)) {
|
|
168
|
+
log(
|
|
169
|
+
` ${colors.dim}used by${colors.reset} ${u.route} › ${u.section} › data.${u.key}`
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (deferred.length > 0) {
|
|
175
|
+
log(` ${colors.dim}↪ deferred (not checked):${colors.reset}`)
|
|
176
|
+
for (const d of deferred) {
|
|
177
|
+
const extra = d.url
|
|
178
|
+
? ` ${colors.dim}(${d.url})${colors.reset}`
|
|
179
|
+
: d.ref
|
|
180
|
+
? ` ${colors.dim}(${d.ref})${colors.reset}`
|
|
181
|
+
: ''
|
|
182
|
+
log(
|
|
183
|
+
` ${colors.dim}•${colors.reset} ${d.route} › ${d.section} › data.${d.key} — ${d.reason}${extra}`
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
log(
|
|
189
|
+
` ${colors.dim}${summary.records} record(s) · ${summary.schemas} schema(s) · ` +
|
|
190
|
+
`${summary.violations} violation(s) · ${summary.deferred} deferred${colors.reset}`
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function dedupeUsers(users) {
|
|
195
|
+
const seen = new Set()
|
|
196
|
+
const out = []
|
|
197
|
+
for (const u of users || []) {
|
|
198
|
+
const k = `${u.route} ${u.section} ${u.key}`
|
|
199
|
+
if (seen.has(k)) continue
|
|
200
|
+
seen.add(k)
|
|
201
|
+
out.push(u)
|
|
202
|
+
}
|
|
203
|
+
return out
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export async function validate(args = []) {
|
|
207
|
+
const asJson = args.includes('--json')
|
|
208
|
+
// A violation fails the run unless `--lax` says to only report it. (`--strict`, the
|
|
209
|
+
// flag that turned failing on until 2026-09-24, is simply the default now.)
|
|
210
|
+
const strict = !args.includes('--lax')
|
|
211
|
+
const siteFilter = flagValue(args, '--site')
|
|
212
|
+
const positional = args.find(
|
|
213
|
+
(a, i) => !a.startsWith('--') && args[i - 1] !== '--site'
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
const target = positional ? resolve(process.cwd(), positional) : process.cwd()
|
|
217
|
+
const workspaceDir = findWorkspaceRoot(target)
|
|
218
|
+
|
|
219
|
+
if (!workspaceDir) {
|
|
220
|
+
if (asJson)
|
|
221
|
+
log(
|
|
222
|
+
JSON.stringify(
|
|
223
|
+
{ ok: false, error: 'not in a Uniweb workspace' },
|
|
224
|
+
null,
|
|
225
|
+
2
|
|
226
|
+
)
|
|
227
|
+
)
|
|
228
|
+
else
|
|
229
|
+
error(
|
|
230
|
+
'Not in a Uniweb workspace. Run this from a project root or a site directory.'
|
|
231
|
+
)
|
|
232
|
+
return { exitCode: 2 }
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const [sites, foundations] = await Promise.all([
|
|
236
|
+
discoverSites(workspaceDir),
|
|
237
|
+
discoverFoundations(workspaceDir)
|
|
238
|
+
])
|
|
239
|
+
|
|
240
|
+
// Select which sites to check: --site filter, an explicitly targeted site
|
|
241
|
+
// directory, or all sites in the workspace.
|
|
242
|
+
let selected = sites
|
|
243
|
+
if (siteFilter) {
|
|
244
|
+
selected = sites.filter(
|
|
245
|
+
(s) => s.name === siteFilter || basename(s.path) === siteFilter
|
|
246
|
+
)
|
|
247
|
+
if (selected.length === 0) {
|
|
248
|
+
const names = sites.map((s) => s.name).join(', ') || '(none)'
|
|
249
|
+
if (asJson)
|
|
250
|
+
log(
|
|
251
|
+
JSON.stringify(
|
|
252
|
+
{
|
|
253
|
+
ok: false,
|
|
254
|
+
error: `site "${siteFilter}" not found`,
|
|
255
|
+
sites: sites.map((s) => s.name)
|
|
256
|
+
},
|
|
257
|
+
null,
|
|
258
|
+
2
|
|
259
|
+
)
|
|
260
|
+
)
|
|
261
|
+
else error(`Site "${siteFilter}" not found. Available: ${names}`)
|
|
262
|
+
return { exitCode: 2 }
|
|
263
|
+
}
|
|
264
|
+
} else if (target !== workspaceDir) {
|
|
265
|
+
const match = sites.find((s) => join(workspaceDir, s.path) === target)
|
|
266
|
+
if (match) selected = [match]
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (selected.length === 0) {
|
|
270
|
+
if (asJson)
|
|
271
|
+
log(
|
|
272
|
+
JSON.stringify({ ok: true, sites: [], note: 'no sites found' }, null, 2)
|
|
273
|
+
)
|
|
274
|
+
else warn('No sites found in this workspace.')
|
|
275
|
+
return { exitCode: 0 }
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// The data pipeline (collectSiteContent / processQueries) prints progress
|
|
279
|
+
// via console.log. Route that to stderr while the engine runs so stdout stays
|
|
280
|
+
// clean — pure JSON for `--json`, just the report otherwise. `log` captured
|
|
281
|
+
// the original stdout writer at module load, so our own output is unaffected.
|
|
282
|
+
const results = []
|
|
283
|
+
const origConsoleLog = console.log
|
|
284
|
+
console.log = (...a) => process.stderr.write(a.join(' ') + '\n')
|
|
285
|
+
try {
|
|
286
|
+
for (const site of selected) {
|
|
287
|
+
try {
|
|
288
|
+
results.push(await validateSite(site, foundations, workspaceDir))
|
|
289
|
+
} catch (err) {
|
|
290
|
+
results.push({ site: site.name, status: 'error', reason: err.message })
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
} finally {
|
|
294
|
+
console.log = origConsoleLog
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const totalViolations = results.reduce(
|
|
298
|
+
(n, r) => n + (r.report?.violations.length || 0),
|
|
299
|
+
0
|
|
300
|
+
)
|
|
301
|
+
const totalSetupErrors = results.reduce(
|
|
302
|
+
(n, r) => n + (r.report?.setupErrors.length || 0),
|
|
303
|
+
0
|
|
304
|
+
)
|
|
305
|
+
const hadError = results.some((r) => r.status === 'error')
|
|
306
|
+
|
|
307
|
+
if (asJson) {
|
|
308
|
+
const payload = {
|
|
309
|
+
ok: totalViolations === 0 && !hadError,
|
|
310
|
+
strict,
|
|
311
|
+
sites: results.map((r) => ({
|
|
312
|
+
site: r.site,
|
|
313
|
+
foundation: r.foundation || null,
|
|
314
|
+
status: r.status,
|
|
315
|
+
reason: r.reason || null,
|
|
316
|
+
...(r.report || {})
|
|
317
|
+
})),
|
|
318
|
+
summary: {
|
|
319
|
+
sites: results.length,
|
|
320
|
+
violations: totalViolations,
|
|
321
|
+
setupErrors: totalSetupErrors,
|
|
322
|
+
deferred: results.reduce(
|
|
323
|
+
(n, r) => n + (r.report?.deferred.length || 0),
|
|
324
|
+
0
|
|
325
|
+
)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
log(JSON.stringify(payload, null, 2))
|
|
329
|
+
} else {
|
|
330
|
+
log('')
|
|
331
|
+
log(`${colors.blue}${colors.bright}Uniweb Validate${colors.reset}`)
|
|
332
|
+
log(
|
|
333
|
+
`${colors.dim}Checking content against the data schemas your foundation declares…${colors.reset}`
|
|
334
|
+
)
|
|
335
|
+
for (const r of results) {
|
|
336
|
+
if (r.status === 'error') {
|
|
337
|
+
log('')
|
|
338
|
+
error(
|
|
339
|
+
`${colors.bright}${r.site}${colors.reset} — could not check: ${r.reason}`
|
|
340
|
+
)
|
|
341
|
+
} else {
|
|
342
|
+
printSiteHuman(r)
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
log('')
|
|
347
|
+
log('─'.repeat(50))
|
|
348
|
+
if (totalViolations === 0 && !hadError) {
|
|
349
|
+
log('')
|
|
350
|
+
success(`${colors.bright}All data conforms.${colors.reset}`)
|
|
351
|
+
if (!strict && totalSetupErrors === 0) {
|
|
352
|
+
// nothing more to say
|
|
353
|
+
}
|
|
354
|
+
log('')
|
|
355
|
+
} else {
|
|
356
|
+
log('')
|
|
357
|
+
if (totalViolations > 0) {
|
|
358
|
+
const mode = strict
|
|
359
|
+
? `${colors.red}error${colors.reset}`
|
|
360
|
+
: `${colors.yellow}warning${colors.reset}`
|
|
361
|
+
log(
|
|
362
|
+
`${totalViolations} violation(s) — reported as ${mode}${strict ? '' : ` ${colors.dim}(--lax: without it, this fails)${colors.reset}`}`
|
|
363
|
+
)
|
|
364
|
+
}
|
|
365
|
+
if (hadError)
|
|
366
|
+
log(`${colors.red}Some sites could not be checked.${colors.reset}`)
|
|
367
|
+
log('')
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Exit semantics: 2 = couldn't run; 1 = violations (unless --lax); 0 = clean, or
|
|
372
|
+
// violations reported under --lax. Setup/read failures are surfaced but don't fail.
|
|
373
|
+
if (hadError) return { exitCode: 2 }
|
|
374
|
+
if (totalViolations > 0 && strict) return { exitCode: 1 }
|
|
375
|
+
return { exitCode: 0 }
|
|
376
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"generatedAt": "2026-10-13T14:23:15.630Z",
|
|
4
|
+
"packages": {
|
|
5
|
+
"@uniweb/api": {
|
|
6
|
+
"version": "1.0.1"
|
|
7
|
+
},
|
|
8
|
+
"@uniweb/build": {
|
|
9
|
+
"version": "1.0.1"
|
|
10
|
+
},
|
|
11
|
+
"@uniweb/content-reader": {
|
|
12
|
+
"version": "1.2.8"
|
|
13
|
+
},
|
|
14
|
+
"@uniweb/content-writer": {
|
|
15
|
+
"version": "1.0.1"
|
|
16
|
+
},
|
|
17
|
+
"@uniweb/core": {
|
|
18
|
+
"version": "1.0.1"
|
|
19
|
+
},
|
|
20
|
+
"@uniweb/frame-bridge": {
|
|
21
|
+
"version": "1.0.1"
|
|
22
|
+
},
|
|
23
|
+
"@uniweb/icons": {
|
|
24
|
+
"version": "1.0.1"
|
|
25
|
+
},
|
|
26
|
+
"@uniweb/kit": {
|
|
27
|
+
"version": "1.0.1"
|
|
28
|
+
},
|
|
29
|
+
"@uniweb/loom": {
|
|
30
|
+
"version": "1.0.1"
|
|
31
|
+
},
|
|
32
|
+
"@uniweb/press": {
|
|
33
|
+
"version": "1.0.1"
|
|
34
|
+
},
|
|
35
|
+
"@uniweb/projections": {
|
|
36
|
+
"version": "1.0.1"
|
|
37
|
+
},
|
|
38
|
+
"@uniweb/runtime": {
|
|
39
|
+
"version": "1.0.2"
|
|
40
|
+
},
|
|
41
|
+
"@uniweb/scene": {
|
|
42
|
+
"version": "1.0.1"
|
|
43
|
+
},
|
|
44
|
+
"@uniweb/schemas": {
|
|
45
|
+
"version": "1.0.1"
|
|
46
|
+
},
|
|
47
|
+
"@uniweb/scholar": {
|
|
48
|
+
"version": "1.0.1"
|
|
49
|
+
},
|
|
50
|
+
"@uniweb/semantic-parser": {
|
|
51
|
+
"version": "1.5.1"
|
|
52
|
+
},
|
|
53
|
+
"@uniweb/snapshot": {
|
|
54
|
+
"version": "1.0.1"
|
|
55
|
+
},
|
|
56
|
+
"@uniweb/templates": {
|
|
57
|
+
"version": "1.0.1"
|
|
58
|
+
},
|
|
59
|
+
"@uniweb/theming": {
|
|
60
|
+
"version": "1.0.1"
|
|
61
|
+
},
|
|
62
|
+
"@uniweb/unipress": {
|
|
63
|
+
"version": "1.0.2"
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"templates": {
|
|
67
|
+
"marketing": {
|
|
68
|
+
"name": "Marketing",
|
|
69
|
+
"description": "Landing pages and marketing sites with Tailwind CSS v4",
|
|
70
|
+
"tags": [
|
|
71
|
+
"marketing",
|
|
72
|
+
"landing-page",
|
|
73
|
+
"tailwind"
|
|
74
|
+
]
|
|
75
|
+
},
|
|
76
|
+
"academic": {
|
|
77
|
+
"name": "Academic",
|
|
78
|
+
"description": "Academic sites for researchers, labs, and departments",
|
|
79
|
+
"tags": [
|
|
80
|
+
"academic",
|
|
81
|
+
"research",
|
|
82
|
+
"portfolio"
|
|
83
|
+
]
|
|
84
|
+
},
|
|
85
|
+
"docs": {
|
|
86
|
+
"name": "Docs",
|
|
87
|
+
"description": "Documentation sites with navigation levels",
|
|
88
|
+
"tags": [
|
|
89
|
+
"docs",
|
|
90
|
+
"documentation",
|
|
91
|
+
"technical"
|
|
92
|
+
]
|
|
93
|
+
},
|
|
94
|
+
"international": {
|
|
95
|
+
"name": "International",
|
|
96
|
+
"description": "Multilingual corporate website with English, Spanish, and French",
|
|
97
|
+
"tags": [
|
|
98
|
+
"i18n",
|
|
99
|
+
"multilingual",
|
|
100
|
+
"corporate",
|
|
101
|
+
"business",
|
|
102
|
+
"localization"
|
|
103
|
+
]
|
|
104
|
+
},
|
|
105
|
+
"blog": {
|
|
106
|
+
"name": "Blog",
|
|
107
|
+
"description": "A blog built on the @std/article standard schema. Markdown articles render as a card grid and as full posts on a parametric [slug] page. The first template to use a shared standard data schema — no custom schema file needed.",
|
|
108
|
+
"tags": [
|
|
109
|
+
"blog",
|
|
110
|
+
"articles",
|
|
111
|
+
"collections",
|
|
112
|
+
"standard-schema",
|
|
113
|
+
"data-schema"
|
|
114
|
+
]
|
|
115
|
+
},
|
|
116
|
+
"dynamic": {
|
|
117
|
+
"name": "Dynamic",
|
|
118
|
+
"description": "Live API data fetching with loading states, transforms, and the portable data pattern",
|
|
119
|
+
"tags": [
|
|
120
|
+
"data",
|
|
121
|
+
"api",
|
|
122
|
+
"fetch",
|
|
123
|
+
"loading-states",
|
|
124
|
+
"dynamic"
|
|
125
|
+
]
|
|
126
|
+
},
|
|
127
|
+
"store": {
|
|
128
|
+
"name": "Store",
|
|
129
|
+
"description": "Artisan e-commerce with a product catalog, Shopify Buy Button, journal blog, and stone-amber design",
|
|
130
|
+
"tags": [
|
|
131
|
+
"store",
|
|
132
|
+
"e-commerce",
|
|
133
|
+
"shopify",
|
|
134
|
+
"artisan",
|
|
135
|
+
"products"
|
|
136
|
+
]
|
|
137
|
+
},
|
|
138
|
+
"learning": {
|
|
139
|
+
"name": "Learning",
|
|
140
|
+
"description": "Course-based learning site with quizzes, code challenges, and AI grading integration",
|
|
141
|
+
"tags": [
|
|
142
|
+
"learning",
|
|
143
|
+
"course",
|
|
144
|
+
"education",
|
|
145
|
+
"quiz",
|
|
146
|
+
"lms",
|
|
147
|
+
"tutorial"
|
|
148
|
+
]
|
|
149
|
+
},
|
|
150
|
+
"extensions": {
|
|
151
|
+
"name": "Extensions",
|
|
152
|
+
"description": "Primary foundation with a visual effects extension, demonstrating multi-foundation sites",
|
|
153
|
+
"tags": [
|
|
154
|
+
"extensions",
|
|
155
|
+
"multi-foundation",
|
|
156
|
+
"visual-effects",
|
|
157
|
+
"runtime"
|
|
158
|
+
]
|
|
159
|
+
},
|
|
160
|
+
"cv-loom": {
|
|
161
|
+
"name": "CV (Loom + Press)",
|
|
162
|
+
"description": "An academic CV whose sections are written with Loom {placeholder} expressions, filled in from a single profile at render time by the foundation's content handler, and downloadable as a branded Word document through Press. Ships with Charles Darwin's CV as sample data.",
|
|
163
|
+
"tags": [
|
|
164
|
+
"cv",
|
|
165
|
+
"loom",
|
|
166
|
+
"template-engine",
|
|
167
|
+
"content-handler",
|
|
168
|
+
"dynamic-prose",
|
|
169
|
+
"academic"
|
|
170
|
+
]
|
|
171
|
+
},
|
|
172
|
+
"monograph": {
|
|
173
|
+
"name": "Monograph",
|
|
174
|
+
"description": "A richly illustrated long-form document — numbered chapters with nested subsections, figures with captions, data-driven specimen and measurement tables, and a citestyle bibliography. The same JSX drives the themed web preview and a branded .docx with headers, footers, and page numbers. Ships with Darwin's Galapagos observations as sample content.",
|
|
175
|
+
"tags": [
|
|
176
|
+
"monograph",
|
|
177
|
+
"docusite",
|
|
178
|
+
"press",
|
|
179
|
+
"document",
|
|
180
|
+
"figures",
|
|
181
|
+
"tables",
|
|
182
|
+
"bibliography"
|
|
183
|
+
]
|
|
184
|
+
},
|
|
185
|
+
"academic-metrics": {
|
|
186
|
+
"name": "Academic Metrics",
|
|
187
|
+
"description": "A docusite whose content is also a downloadable Excel workbook. Aggregates academic metrics (publications, funding, supervisions) across multiple members of a university unit. Rich web previews + flat tabular xlsx sheets from the same data. Ships with three 19th-century naturalists as sample members.",
|
|
188
|
+
"tags": [
|
|
189
|
+
"academic-metrics",
|
|
190
|
+
"docusite",
|
|
191
|
+
"press",
|
|
192
|
+
"xlsx",
|
|
193
|
+
"report",
|
|
194
|
+
"members",
|
|
195
|
+
"aggregate",
|
|
196
|
+
"academic"
|
|
197
|
+
]
|
|
198
|
+
},
|
|
199
|
+
"business-docs": {
|
|
200
|
+
"name": "Business Documents",
|
|
201
|
+
"description": "A complete business-document site: invoice and SOW records, pages listing them, and a filtered engagement report with XLSX export. Built on @uniweb/business-docs and demonstrates the same foundation that ships the unipress invoice template.",
|
|
202
|
+
"tags": [
|
|
203
|
+
"business-docs",
|
|
204
|
+
"invoice",
|
|
205
|
+
"sow",
|
|
206
|
+
"report",
|
|
207
|
+
"press",
|
|
208
|
+
"xlsx",
|
|
209
|
+
"docx",
|
|
210
|
+
"pagedjs"
|
|
211
|
+
]
|
|
212
|
+
},
|
|
213
|
+
"paste": {
|
|
214
|
+
"name": "Paste a Design (Level 0)",
|
|
215
|
+
"description": "Bring an existing React design (AI-generated or hand-built) into Uniweb with zero rewiring — replace App.jsx and run. Ships lucide-react preinstalled and SPA-first. The Level 0 conversion starting point.",
|
|
216
|
+
"tags": [
|
|
217
|
+
"paste",
|
|
218
|
+
"level-0",
|
|
219
|
+
"convert",
|
|
220
|
+
"ai-generated",
|
|
221
|
+
"starter"
|
|
222
|
+
]
|
|
223
|
+
},
|
|
224
|
+
"services": {
|
|
225
|
+
"name": "Services",
|
|
226
|
+
"description": "Local-services site built around an author-designed quote request form",
|
|
227
|
+
"tags": [
|
|
228
|
+
"services",
|
|
229
|
+
"local-business",
|
|
230
|
+
"forms",
|
|
231
|
+
"quote",
|
|
232
|
+
"trades"
|
|
233
|
+
]
|
|
234
|
+
},
|
|
235
|
+
"conference": {
|
|
236
|
+
"name": "Conference",
|
|
237
|
+
"description": "A site that is also an app — sign-in, per-viewer data, and an editable programme. Ships a local backend so it runs with nothing installed.",
|
|
238
|
+
"tags": [
|
|
239
|
+
"app",
|
|
240
|
+
"api",
|
|
241
|
+
"backend",
|
|
242
|
+
"auth",
|
|
243
|
+
"permissions",
|
|
244
|
+
"conference"
|
|
245
|
+
]
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|