uniweb 0.61.0 → 0.63.0
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 +7 -7
- package/partials/agents.md +17 -15
- package/src/backend/client.js +231 -20
- package/src/backend/foundation-bring-along.js +71 -65
- package/src/backend/site-sync.js +255 -127
- package/src/commands/add.js +62 -4
- package/src/commands/clone.js +36 -6
- package/src/commands/publish.js +33 -20
- package/src/commands/pull.js +68 -15
- package/src/commands/push.js +23 -17
- package/src/commands/register.js +234 -37
- package/src/commands/rename.js +12 -9
- package/src/commands/status.js +32 -7
- package/src/framework-index.json +8 -8
- package/src/index.js +43 -10
- package/src/utils/args.js +6 -14
- package/src/utils/flag-guard.js +17 -9
- package/src/utils/foundation-name.js +179 -0
- package/src/utils/git.js +4 -1
- package/src/utils/registry-orgs.js +3 -2
- package/src/utils/scaffold.js +7 -5
- package/src/utils/site-data-upload.js +13 -2
- package/starter/foundation/main.js +2 -1
- package/templates/foundation/main.js.hbs +9 -0
- package/templates/site/site.yml.hbs +3 -2
package/src/commands/register.js
CHANGED
|
@@ -3,15 +3,14 @@
|
|
|
3
3
|
* standalone schemas package (the data schemas alone, no foundation), to the
|
|
4
4
|
* registry as one names-only `.uwx` document (uwx-format.md §5).
|
|
5
5
|
*
|
|
6
|
-
* `uniweb login && uniweb register`. Distinct from `uniweb publish
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* `uniweb login && uniweb register`. Distinct from `uniweb publish`, which ships a
|
|
7
|
+
* SITE — and releases the site's local foundation by running this command
|
|
8
|
+
* (`backend/foundation-bring-along.js`).
|
|
9
9
|
*
|
|
10
|
-
* If the foundation's `dist/` is missing or
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* pre-built foundation.
|
|
10
|
+
* If the foundation's `dist/` is missing or stale — no build, a version bump
|
|
11
|
+
* since it, a source file newer than it, no SSR bundle (`foundationNeedsBuild`)
|
|
12
|
+
* — `register` builds it first. Preview paths (`--dry-run`, `-o`) never write to
|
|
13
|
+
* `dist/`; they require a pre-built foundation.
|
|
15
14
|
*
|
|
16
15
|
* Run from a foundation, or from a schemas-only package — a package that exports
|
|
17
16
|
* schemas (e.g. `@uniweb/schemas`, any `@org/schemas`) or a bare `schemas/*.yml`
|
|
@@ -22,8 +21,13 @@
|
|
|
22
21
|
* uniweb register Build the .uwx, submit it, then deliver
|
|
23
22
|
* the foundation's dist/ code (plan +
|
|
24
23
|
* upload — see utils/code-upload.js)
|
|
25
|
-
* uniweb register --scope @org
|
|
26
|
-
*
|
|
24
|
+
* uniweb register --scope @org A foundation whose name is bare: register it under
|
|
25
|
+
* @org (resolves @/x -> @org/x) and write the scope
|
|
26
|
+
* into its name in main.js. Refused when the name
|
|
27
|
+
* already carries another scope — a foundation's
|
|
28
|
+
* scope is the one in its name.
|
|
29
|
+
* A schemas-only package: publish under @org.
|
|
30
|
+
* Default: its package.json "uniweb.scope".
|
|
27
31
|
* uniweb register --schema-only Skip the code delivery (schemas land, no dist upload)
|
|
28
32
|
* uniweb register --dry-run Print the .uwx + the code file plan; submit nothing
|
|
29
33
|
* uniweb register -o foundation.uwx Write the .uwx to a file; submit nothing
|
|
@@ -64,13 +68,21 @@ import {
|
|
|
64
68
|
statSync
|
|
65
69
|
} from 'node:fs'
|
|
66
70
|
import { execSync } from 'node:child_process'
|
|
67
|
-
import { resolve, join } from 'node:path'
|
|
71
|
+
import { resolve, join, relative } from 'node:path'
|
|
72
|
+
import prompts from 'prompts'
|
|
68
73
|
import { buildRegistryPackage, buildSchemaOnlyPackage } from '@uniweb/build/uwx'
|
|
69
74
|
import {
|
|
70
75
|
classifyPackage,
|
|
71
76
|
isSchemasPackage,
|
|
72
|
-
collectStandaloneSchemas
|
|
77
|
+
collectStandaloneSchemas,
|
|
78
|
+
readFoundationName,
|
|
79
|
+
checkFoundationName,
|
|
80
|
+
splitFoundationName
|
|
73
81
|
} from '@uniweb/build'
|
|
82
|
+
import {
|
|
83
|
+
suggestFoundationName,
|
|
84
|
+
writeFoundationName
|
|
85
|
+
} from '../utils/foundation-name.js'
|
|
74
86
|
import { readRegistryAuth } from '../utils/registry-auth.js'
|
|
75
87
|
import {
|
|
76
88
|
collectDistFiles,
|
|
@@ -137,8 +149,10 @@ function cliVersion() {
|
|
|
137
149
|
}
|
|
138
150
|
}
|
|
139
151
|
|
|
140
|
-
//
|
|
141
|
-
// (`{ "uniweb": { "scope": "@acme" } }`) — the default when `--scope` is absent.
|
|
152
|
+
// A SCHEMAS-ONLY package's recorded publish org, from its package.json `uniweb.scope`
|
|
153
|
+
// (`{ "uniweb": { "scope": "@acme" } }`) — the default when `--scope` is absent. It
|
|
154
|
+
// has no main.js whose name could carry it. ⛔ A foundation's scope is part of its name
|
|
155
|
+
// (2026-09-22); its `uniweb.scope` is refused by `readFoundationName`.
|
|
142
156
|
function readPkgScope(foundationDir) {
|
|
143
157
|
try {
|
|
144
158
|
const pkg = JSON.parse(
|
|
@@ -150,9 +164,10 @@ function readPkgScope(foundationDir) {
|
|
|
150
164
|
}
|
|
151
165
|
}
|
|
152
166
|
|
|
153
|
-
// Record the chosen publish scope in
|
|
154
|
-
// with the
|
|
155
|
-
// the file's existing JSON style.
|
|
167
|
+
// Record the chosen publish scope in a schemas-only package's package.json so it
|
|
168
|
+
// travels with the package (read back by readPkgScope on the next register). Preserves
|
|
169
|
+
// the file's existing JSON style. A foundation's goes into its name instead
|
|
170
|
+
// (`settleFoundationScope`).
|
|
156
171
|
async function writePkgScope(foundationDir, scope) {
|
|
157
172
|
const path = join(foundationDir, 'package.json')
|
|
158
173
|
const pkg = JSON.parse(readFileSync(path, 'utf8'))
|
|
@@ -209,6 +224,173 @@ async function resolveFoundationDir(args) {
|
|
|
209
224
|
process.exit(1)
|
|
210
225
|
}
|
|
211
226
|
|
|
227
|
+
/**
|
|
228
|
+
* The foundation's name, settled before anything is built or sent.
|
|
229
|
+
*
|
|
230
|
+
* A foundation registers as `@org/<name>`, and sites pin it by that name. The name
|
|
231
|
+
* is `main.js`'s `name`, else package.json's — `readFoundationName`, the rule the
|
|
232
|
+
* build reads for the schema this command submits. ⛔ `src` and `foundation` are
|
|
233
|
+
* the folder the code sits in, not the code: a project scaffolded before the name
|
|
234
|
+
* moved to main.js has package name `src`, and every such project in an org would
|
|
235
|
+
* register the same `@org/src`. So a name that cannot register is ASKED FOR here,
|
|
236
|
+
* once, and kept in main.js — or, with nobody to ask, refused with the line to add.
|
|
237
|
+
* Before the build, so a written name is built into the schema like any edit.
|
|
238
|
+
*
|
|
239
|
+
* @param {string} targetDir
|
|
240
|
+
* @param {{ args: string[], isPreview: boolean }} o
|
|
241
|
+
* @returns {Promise<boolean>} false → stop; the reason is printed
|
|
242
|
+
*/
|
|
243
|
+
export async function settleFoundationName(targetDir, { args, isPreview }) {
|
|
244
|
+
let read
|
|
245
|
+
try {
|
|
246
|
+
read = await readFoundationName(targetDir)
|
|
247
|
+
} catch (err) {
|
|
248
|
+
error(err.message)
|
|
249
|
+
return false
|
|
250
|
+
}
|
|
251
|
+
const problem = checkFoundationName(read.name)
|
|
252
|
+
if (!problem) return true
|
|
253
|
+
|
|
254
|
+
const where = relative(process.cwd(), read.mainFile) || read.mainFile
|
|
255
|
+
const suggestion = suggestFoundationName(targetDir)
|
|
256
|
+
const reason = `${problem[0].toUpperCase()}${problem.slice(1)}.`
|
|
257
|
+
|
|
258
|
+
// Nobody to ask — a script, a preview (which writes nothing), or --json (whose
|
|
259
|
+
// stdout is a data channel a prompt would corrupt). Name the fix exactly.
|
|
260
|
+
if (isPreview || jsonMode || isNonInteractive(args)) {
|
|
261
|
+
error(`This foundation has no name it can register under. ${reason}`)
|
|
262
|
+
log(` Name it in ${colors.bright}${where}${colors.reset}, in the default export:`)
|
|
263
|
+
log(` ${colors.bright}name: '${suggestion || '<name>'}',${colors.reset}`)
|
|
264
|
+
log(` ${colors.dim}It registers as @<org>/<name>, and sites pin that name.${colors.reset}`)
|
|
265
|
+
return false
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
info(`This foundation needs a name to register under. ${reason}`)
|
|
269
|
+
const { name } = await prompts(
|
|
270
|
+
{
|
|
271
|
+
type: 'text',
|
|
272
|
+
name: 'name',
|
|
273
|
+
message: 'Foundation name (it registers as @<org>/<name>):',
|
|
274
|
+
initial: suggestion || '',
|
|
275
|
+
validate: (v) => checkFoundationName(String(v).trim()) || true
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
onCancel: () => {
|
|
279
|
+
log('\nRegister cancelled.')
|
|
280
|
+
process.exit(0)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
)
|
|
284
|
+
const chosen = String(name || '').trim()
|
|
285
|
+
if (!chosen) return false
|
|
286
|
+
|
|
287
|
+
const written = writeFoundationName(read.mainFile, chosen, {
|
|
288
|
+
replace: read.source === 'main.js' ? read.name : null
|
|
289
|
+
})
|
|
290
|
+
if (!written.ok) {
|
|
291
|
+
error(`Could not write the name: ${written.reason}.`)
|
|
292
|
+
log(` Add it to the default export in ${colors.bright}${where}${colors.reset}: name: '${chosen}',`)
|
|
293
|
+
return false
|
|
294
|
+
}
|
|
295
|
+
// Read it back through the build's own rule — the text edit is only a means.
|
|
296
|
+
const again = await readFoundationName(targetDir).catch(() => null)
|
|
297
|
+
if (again?.name !== chosen) {
|
|
298
|
+
error(`${where} now names the foundation '${chosen}', but it still reads as "${again?.name}".`)
|
|
299
|
+
return false
|
|
300
|
+
}
|
|
301
|
+
success(`Named the foundation ${colors.bright}${chosen}${colors.reset} — kept in ${where}.`)
|
|
302
|
+
return true
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Settle the scope a foundation registers under — the one in its name.
|
|
307
|
+
*
|
|
308
|
+
* ⭐ THE SCOPE IS PART OF THE NAME (2026-09-22): `name: '@acme/marketing'` in main.js.
|
|
309
|
+
* A scoped name decides it, and a `--scope` naming another org is refused — which org
|
|
310
|
+
* was meant is not ours to guess, and today's alternative was a split identity (the
|
|
311
|
+
* foundation under one org, its data schemas under the other). A bare name has not
|
|
312
|
+
* registered yet: it takes `--scope`, else one derived from your orgs, and the chosen
|
|
313
|
+
* scope is WRITTEN INTO THE NAME, so it is chosen once and travels with the code.
|
|
314
|
+
* ⛔ Until then it was written to `package.json::uniweb.scope`, which is refused now
|
|
315
|
+
* (`readFoundationName`, naming the line to write).
|
|
316
|
+
*
|
|
317
|
+
* A preview (`--dry-run`, `-o`) writes nothing: a bare name previews under `--scope`,
|
|
318
|
+
* or unscoped.
|
|
319
|
+
*
|
|
320
|
+
* @returns {Promise<{ scope: string|null, source: string|null } | { cancelled: true } | null>}
|
|
321
|
+
* null when refused (the reason is printed); `cancelled` when no org was chosen
|
|
322
|
+
*/
|
|
323
|
+
export async function settleFoundationScope(targetDir, { args, isPreview, flagScope, client }) {
|
|
324
|
+
let read
|
|
325
|
+
try {
|
|
326
|
+
read = await readFoundationName(targetDir)
|
|
327
|
+
} catch (err) {
|
|
328
|
+
error(err.message)
|
|
329
|
+
return null
|
|
330
|
+
}
|
|
331
|
+
const where = relative(process.cwd(), read.mainFile) || read.mainFile
|
|
332
|
+
const { scope: nameScope, bare } = splitFoundationName(read.name)
|
|
333
|
+
|
|
334
|
+
if (nameScope) {
|
|
335
|
+
if (flagScope && flagScope !== nameScope) {
|
|
336
|
+
error(
|
|
337
|
+
`${where} names this foundation ${read.name}, so it registers under ${nameScope} — ` +
|
|
338
|
+
`--scope ${flagScope} names another org.`
|
|
339
|
+
)
|
|
340
|
+
log(
|
|
341
|
+
` ${colors.dim}To register it under ${flagScope}, change its name in ${where}: ` +
|
|
342
|
+
`name: '${flagScope}/${bare}'${colors.reset}`
|
|
343
|
+
)
|
|
344
|
+
return null
|
|
345
|
+
}
|
|
346
|
+
return { scope: nameScope, source: 'main.js' }
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (isPreview) return { scope: flagScope, source: flagScope ? '--scope' : null }
|
|
350
|
+
|
|
351
|
+
let scope = flagScope
|
|
352
|
+
let source = flagScope ? '--scope' : null
|
|
353
|
+
if (!scope) {
|
|
354
|
+
scope = await deriveScopeFromLogin(client, args)
|
|
355
|
+
if (!scope) return { cancelled: true }
|
|
356
|
+
source = 'login'
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const scoped = `${scope}/${bare}`
|
|
360
|
+
const written = writeFoundationName(read.mainFile, scoped, {
|
|
361
|
+
replace: read.source === 'main.js' ? read.name : null
|
|
362
|
+
})
|
|
363
|
+
if (!written.ok) {
|
|
364
|
+
error(`Could not write the scope into the foundation's name: ${written.reason}.`)
|
|
365
|
+
log(` Name it in the default export in ${colors.bright}${where}${colors.reset}: name: '${scoped}',`)
|
|
366
|
+
return null
|
|
367
|
+
}
|
|
368
|
+
// Read it back through the build's own rule — the text edit is only a means.
|
|
369
|
+
const again = await readFoundationName(targetDir).catch(() => null)
|
|
370
|
+
if (again?.name !== scoped) {
|
|
371
|
+
error(`${where} now names the foundation '${scoped}', but it still reads as "${again?.name}".`)
|
|
372
|
+
return null
|
|
373
|
+
}
|
|
374
|
+
success(`This foundation registers as ${colors.bright}${scoped}${colors.reset} — kept in ${where}.`)
|
|
375
|
+
return { scope, source }
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* A publish scope derived from the login's org memberships (list → 1 use / 0 create /
|
|
380
|
+
* N pick), as `@handle` — or null when none was chosen.
|
|
381
|
+
*/
|
|
382
|
+
async function deriveScopeFromLogin(client, args) {
|
|
383
|
+
const token = await client.token()
|
|
384
|
+
const sess = await readRegistryAuth(client.origin)
|
|
385
|
+
const derived = await deriveScope({
|
|
386
|
+
apiBase: client.origin,
|
|
387
|
+
token,
|
|
388
|
+
accountHandle: sess?.handle || null,
|
|
389
|
+
args
|
|
390
|
+
})
|
|
391
|
+
return derived ? `@${derived}` : null
|
|
392
|
+
}
|
|
393
|
+
|
|
212
394
|
// Directories that are never foundation source. `dist` is the output we compare
|
|
213
395
|
// against; the rest are tooling. Dotted entries are skipped wholesale.
|
|
214
396
|
const NON_SOURCE_DIRS = new Set(['dist', 'node_modules'])
|
|
@@ -403,10 +585,13 @@ async function runRegister(args = []) {
|
|
|
403
585
|
? process.cwd()
|
|
404
586
|
: await resolveFoundationDir(args)
|
|
405
587
|
|
|
406
|
-
// Scope
|
|
407
|
-
//
|
|
408
|
-
//
|
|
409
|
-
|
|
588
|
+
// Scope. ⭐ A FOUNDATION's is the one in its name (`name: '@acme/marketing'` in
|
|
589
|
+
// main.js), settled below before the build; a bare name takes --scope, else one
|
|
590
|
+
// derived from your orgs, and register writes it into the name (2026-09-22).
|
|
591
|
+
// A SCHEMAS-ONLY package has no name to carry one: --scope, else package.json
|
|
592
|
+
// `uniweb.scope`, else (real submit only) derived from login membership. Either
|
|
593
|
+
// spelling, `@acme` or `acme`, and from here on the one form: `@acme` (publishScope).
|
|
594
|
+
const pkgScope = standalone ? readPkgScope(targetDir) : null
|
|
410
595
|
const givenScope = scopeFlag || pkgScope
|
|
411
596
|
let scope = publishScope(givenScope)
|
|
412
597
|
let scopeSource = scopeFlag
|
|
@@ -443,8 +628,24 @@ async function runRegister(args = []) {
|
|
|
443
628
|
return { exitCode: 2 }
|
|
444
629
|
}
|
|
445
630
|
} else {
|
|
446
|
-
|
|
447
|
-
|
|
631
|
+
if (!(await settleFoundationName(targetDir, { args, isPreview }))) {
|
|
632
|
+
return { exitCode: 2 }
|
|
633
|
+
}
|
|
634
|
+
// ⭐ BEFORE THE BUILD, so the build embeds the name the foundation registers as.
|
|
635
|
+
// Settled after it, a scope written into main.js would leave dist/ describing the
|
|
636
|
+
// bare name — stale the moment it was built.
|
|
637
|
+
const settled = await settleFoundationScope(targetDir, {
|
|
638
|
+
args,
|
|
639
|
+
isPreview,
|
|
640
|
+
flagScope: scope,
|
|
641
|
+
client
|
|
642
|
+
})
|
|
643
|
+
if (!settled) return { exitCode: 2 }
|
|
644
|
+
if (settled.cancelled) return { exitCode: 0 }
|
|
645
|
+
scope = settled.scope
|
|
646
|
+
scopeSource = settled.source
|
|
647
|
+
// Build-if-stale (`foundationNeedsBuild`): a missing or stale dist/ gets
|
|
648
|
+
// (re)built before we read its schema. Preview paths
|
|
448
649
|
// (--dry-run / -o) must not write to dist/, so they require a pre-built
|
|
449
650
|
// foundation and say so instead of building.
|
|
450
651
|
const { needs, reason } = foundationNeedsBuild(targetDir)
|
|
@@ -491,24 +692,18 @@ async function runRegister(args = []) {
|
|
|
491
692
|
}
|
|
492
693
|
}
|
|
493
694
|
|
|
494
|
-
//
|
|
495
|
-
// 0 create / N pick)
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
const
|
|
499
|
-
const derived = await deriveScope({
|
|
500
|
-
apiBase: client.origin,
|
|
501
|
-
token,
|
|
502
|
-
accountHandle: sess?.handle || null,
|
|
503
|
-
args
|
|
504
|
-
})
|
|
695
|
+
// A schemas-only package with no scope, on a real submit → derive it from login
|
|
696
|
+
// membership (list → 1 use / 0 create / N pick) and persist it to package.json.
|
|
697
|
+
// (A foundation's was settled before its build — `settleFoundationScope`.)
|
|
698
|
+
if (standalone && !scope && !isPreview) {
|
|
699
|
+
const derived = await deriveScopeFromLogin(client, args)
|
|
505
700
|
if (!derived) return { exitCode: 0 }
|
|
506
|
-
scope =
|
|
701
|
+
scope = derived
|
|
507
702
|
scopeSource = 'login'
|
|
508
703
|
try {
|
|
509
704
|
await writePkgScope(targetDir, scope)
|
|
510
705
|
info(
|
|
511
|
-
`Saved ${colors.bright}${scope}${colors.reset} as this
|
|
706
|
+
`Saved ${colors.bright}${scope}${colors.reset} as this package's publish scope (package.json).`
|
|
512
707
|
)
|
|
513
708
|
} catch {
|
|
514
709
|
log(
|
|
@@ -605,7 +800,9 @@ async function runRegister(args = []) {
|
|
|
605
800
|
// Submit requires a concrete scope — the registry rejects @/… fail-closed.
|
|
606
801
|
if (!scope) {
|
|
607
802
|
error(
|
|
608
|
-
|
|
803
|
+
standalone
|
|
804
|
+
? 'No publish scope — set "uniweb.scope" in package.json, or pass --scope @org.'
|
|
805
|
+
: "No publish scope — name the foundation '@org/<name>' in main.js, or pass --scope @org."
|
|
609
806
|
)
|
|
610
807
|
log(
|
|
611
808
|
` ${colors.dim}Without a scope, names stay @/… and the registry rejects them.${colors.reset}`
|
package/src/commands/rename.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
* Extensions are technically a flavor of foundation (same build, same
|
|
22
22
|
* package.json shape, distinguished only by `extension: true` in
|
|
23
|
-
*
|
|
23
|
+
* main.js or `role: 'extension'` in the built schema). The
|
|
24
24
|
* rename verb still keeps them on a separate subcommand because the
|
|
25
25
|
* touch-point sets differ — `rename foundation` against an extension
|
|
26
26
|
* would update the wrong things. Each subcommand guards its target
|
|
@@ -30,11 +30,13 @@
|
|
|
30
30
|
* (target name already taken, target not found, folder collision,
|
|
31
31
|
* type mismatch) we bail with a clear message and no partial state.
|
|
32
32
|
*
|
|
33
|
-
* Out of scope: registry side.
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
33
|
+
* Out of scope: registry side. What a foundation registers as is `name` in its
|
|
34
|
+
* main.js, which is independent of the workspace name and stays untouched —
|
|
35
|
+
* except for a foundation with no main.js `name`, whose package name IS its name
|
|
36
|
+
* (`readFoundationName`), so renaming the package renames what it would register
|
|
37
|
+
* as. There is no registry-rename flag — a registered version is immutable; to
|
|
38
|
+
* change a foundation's registered name, change main.js's `name` and register
|
|
39
|
+
* again (consuming sites repoint their `foundation:` ref).
|
|
38
40
|
*
|
|
39
41
|
* Usage:
|
|
40
42
|
* uniweb rename foundation <old> <new>
|
|
@@ -632,9 +634,10 @@ ${colors.bright}What rename extension does:${colors.reset}
|
|
|
632
634
|
• Updates pnpm-workspace.yaml + package.json::workspaces.
|
|
633
635
|
|
|
634
636
|
${colors.bright}What rename does NOT do (any subcommand):${colors.reset}
|
|
635
|
-
• Push to the registry
|
|
636
|
-
|
|
637
|
-
|
|
637
|
+
• Push to the registry, or change what a foundation registers as — that is
|
|
638
|
+
\`name\` in its main.js, independent of the package name. To register under
|
|
639
|
+
a new name, change it there and run \`${prefix} register\` (alias
|
|
640
|
+
\`${prefix} release\`) — there is no rename flag.
|
|
638
641
|
|
|
639
642
|
${colors.bright}Examples:${colors.reset}
|
|
640
643
|
${prefix} rename foundation src marketing-src
|
package/src/commands/status.js
CHANGED
|
@@ -25,10 +25,19 @@ import { join } from 'node:path'
|
|
|
25
25
|
import yaml from 'js-yaml'
|
|
26
26
|
|
|
27
27
|
import { resolveSiteDir } from './deploy.js'
|
|
28
|
-
import { probeUnpushed } from '../backend/site-sync.js'
|
|
29
|
-
import {
|
|
28
|
+
import { probeUnpushed, nameSiteWorkspace, readSiteWorkspace } from '../backend/site-sync.js'
|
|
29
|
+
import { readOrgFlag } from '../utils/args.js'
|
|
30
|
+
import {
|
|
31
|
+
BackendClient,
|
|
32
|
+
resolveBackendOrigin,
|
|
33
|
+
workspaceHandle,
|
|
34
|
+
WorkspaceMismatchError
|
|
35
|
+
} from '../backend/client.js'
|
|
30
36
|
import { readBackendState } from '@uniweb/build/uwx'
|
|
31
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
resolveLocalFoundation,
|
|
39
|
+
foundationScopedName
|
|
40
|
+
} from '../backend/foundation-bring-along.js'
|
|
32
41
|
import { computeFoundationDigest } from '../utils/code-upload.js'
|
|
33
42
|
import { checkFlags } from '../utils/flag-guard.js'
|
|
34
43
|
|
|
@@ -112,26 +121,40 @@ export async function status(args = []) {
|
|
|
112
121
|
let site = null
|
|
113
122
|
let fdnLatest = null
|
|
114
123
|
let foundationFresh = null // true/false when both digests are known; else null
|
|
124
|
+
let remoteError = null // a refusal about the workspace — the one remote failure worth saying
|
|
115
125
|
if (remote) {
|
|
116
126
|
try {
|
|
117
127
|
const client = new BackendClient({
|
|
118
128
|
args,
|
|
119
129
|
command: 'Status'
|
|
120
130
|
})
|
|
131
|
+
// The workspace the site's requests name — as pull names it. A stale record is
|
|
132
|
+
// re-adopted from the backend's answer; `--json` keeps stdout to the document.
|
|
133
|
+
const orgFlag = readOrgFlag(args)
|
|
134
|
+
nameSiteWorkspace(client, {
|
|
135
|
+
siteDir,
|
|
136
|
+
workspace: orgFlag ? workspaceHandle(orgFlag) : readSiteWorkspace(siteDir, client.origin),
|
|
137
|
+
explicit: Boolean(orgFlag),
|
|
138
|
+
note: jsonMode ? undefined : say.dim
|
|
139
|
+
})
|
|
121
140
|
if (uuid) site = await client.siteStatus(uuid)
|
|
122
141
|
// Foundation freshness: prefer the LOCAL foundation's scoped name (so a
|
|
123
142
|
// local-foundation site can be checked too); fall back to a scoped
|
|
124
143
|
// site.yml ref. The digest compare is read-only — it never builds, so it
|
|
125
144
|
// only fires when the local foundation is already built (dist present).
|
|
126
145
|
const local = resolveLocalFoundation(siteDir, siteYml)
|
|
127
|
-
const lookupName =
|
|
146
|
+
const lookupName =
|
|
147
|
+
(local && (await foundationScopedName(local.dir))) || fndScope
|
|
128
148
|
if (lookupName) fdnLatest = await client.readFoundationLatest(lookupName)
|
|
129
149
|
if (local?.dir && fdnLatest?.digest) {
|
|
130
150
|
const localDigest = computeFoundationDigest(join(local.dir, 'dist'))
|
|
131
151
|
if (localDigest) foundationFresh = localDigest === fdnLatest.digest
|
|
132
152
|
}
|
|
133
|
-
} catch {
|
|
134
|
-
//
|
|
153
|
+
} catch (err) {
|
|
154
|
+
// Degrade silently — except a refusal about the workspace: `--org` named one the
|
|
155
|
+
// backend does not work on this site from, or the deployment has another. Saying
|
|
156
|
+
// nothing would read as "fine".
|
|
157
|
+
if (err instanceof WorkspaceMismatchError || err?.status === 409) remoteError = err.message
|
|
135
158
|
}
|
|
136
159
|
}
|
|
137
160
|
|
|
@@ -149,7 +172,8 @@ export async function status(args = []) {
|
|
|
149
172
|
remote: {
|
|
150
173
|
site,
|
|
151
174
|
foundation_latest: fdnLatest?.latest_version ?? null,
|
|
152
|
-
foundation_fresh: foundationFresh
|
|
175
|
+
foundation_fresh: foundationFresh,
|
|
176
|
+
...(remoteError ? { error: remoteError } : {})
|
|
153
177
|
}
|
|
154
178
|
}
|
|
155
179
|
: {})
|
|
@@ -198,6 +222,7 @@ export async function status(args = []) {
|
|
|
198
222
|
|
|
199
223
|
// Remote signals
|
|
200
224
|
if (remote) {
|
|
225
|
+
if (remoteError) say.warn(remoteError)
|
|
201
226
|
if (site) {
|
|
202
227
|
if (site.draft_dirty) {
|
|
203
228
|
say.info(
|
package/src/framework-index.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-09-
|
|
3
|
+
"generatedAt": "2026-09-23T12:12:24.996Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/api": {
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.5.0",
|
|
7
7
|
"path": "framework/api",
|
|
8
8
|
"deps": [
|
|
9
9
|
"@uniweb/core"
|
|
10
10
|
]
|
|
11
11
|
},
|
|
12
12
|
"@uniweb/build": {
|
|
13
|
-
"version": "0.
|
|
13
|
+
"version": "0.59.0",
|
|
14
14
|
"path": "framework/build",
|
|
15
15
|
"deps": [
|
|
16
16
|
"@uniweb/content-reader",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"deps": []
|
|
36
36
|
},
|
|
37
37
|
"@uniweb/core": {
|
|
38
|
-
"version": "0.29.
|
|
38
|
+
"version": "0.29.8",
|
|
39
39
|
"path": "framework/core",
|
|
40
40
|
"deps": [
|
|
41
41
|
"@uniweb/semantic-parser",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
]
|
|
84
84
|
},
|
|
85
85
|
"@uniweb/runtime": {
|
|
86
|
-
"version": "0.27.
|
|
86
|
+
"version": "0.27.4",
|
|
87
87
|
"path": "framework/runtime",
|
|
88
88
|
"deps": [
|
|
89
89
|
"@uniweb/core",
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
"deps": []
|
|
97
97
|
},
|
|
98
98
|
"@uniweb/schemas": {
|
|
99
|
-
"version": "0.3.
|
|
99
|
+
"version": "0.3.4",
|
|
100
100
|
"path": "framework/schemas",
|
|
101
101
|
"deps": []
|
|
102
102
|
},
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
"deps": []
|
|
117
117
|
},
|
|
118
118
|
"@uniweb/templates": {
|
|
119
|
-
"version": "0.
|
|
119
|
+
"version": "0.16.0",
|
|
120
120
|
"path": "framework/templates",
|
|
121
121
|
"deps": []
|
|
122
122
|
},
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
"deps": []
|
|
127
127
|
},
|
|
128
128
|
"@uniweb/unipress": {
|
|
129
|
-
"version": "0.10.
|
|
129
|
+
"version": "0.10.2",
|
|
130
130
|
"path": "framework/unipress",
|
|
131
131
|
"deps": [
|
|
132
132
|
"@uniweb/build",
|
package/src/index.js
CHANGED
|
@@ -53,6 +53,10 @@ import {
|
|
|
53
53
|
mergeTemplateDependencies,
|
|
54
54
|
getWorkspaceTemplateOutputs
|
|
55
55
|
} from './utils/scaffold.js'
|
|
56
|
+
import {
|
|
57
|
+
normalizeFoundationName,
|
|
58
|
+
ensureFoundationName
|
|
59
|
+
} from './utils/foundation-name.js'
|
|
56
60
|
import {
|
|
57
61
|
detectPackageManager,
|
|
58
62
|
detectWorkspacePm,
|
|
@@ -354,12 +358,16 @@ async function createFromPackageTemplates(
|
|
|
354
358
|
// 2. Scaffold foundation (folder: src/, package name: src)
|
|
355
359
|
// The folder name 'src' carries the meaning — a foundation is the site's
|
|
356
360
|
// source code. The package name 'src' keeps it unique within the
|
|
357
|
-
// workspace, since 'site' is taken by the site package.
|
|
361
|
+
// workspace, since 'site' is taken by the site package. Neither is the
|
|
362
|
+
// foundation's NAME — what it registers as — which is main.js's `name`:
|
|
363
|
+
// the project's, since a project's code is its foundation.
|
|
358
364
|
onProgress?.('Creating foundation...')
|
|
365
|
+
const registryName = normalizeFoundationName(projectName)
|
|
359
366
|
await scaffoldFoundation(
|
|
360
367
|
join(projectDir, 'src'),
|
|
361
368
|
{
|
|
362
369
|
name: 'src',
|
|
370
|
+
registryName,
|
|
363
371
|
projectName,
|
|
364
372
|
isExtension: false
|
|
365
373
|
},
|
|
@@ -380,10 +388,12 @@ async function createFromPackageTemplates(
|
|
|
380
388
|
{ onProgress, onWarning }
|
|
381
389
|
)
|
|
382
390
|
|
|
383
|
-
// 4. Apply starter content (unless creating a "none" project)
|
|
391
|
+
// 4. Apply starter content (unless creating a "none" project). Its main.js
|
|
392
|
+
// replaces the scaffolded one, so the name is given again.
|
|
384
393
|
if (includeStarter) {
|
|
385
394
|
onProgress?.('Adding starter content...')
|
|
386
395
|
await applyStarter(projectDir, { projectName }, { onProgress, onWarning })
|
|
396
|
+
ensureFoundationName(join(projectDir, 'src'), registryName)
|
|
387
397
|
}
|
|
388
398
|
|
|
389
399
|
success(`Created project: ${projectName}`)
|
|
@@ -475,15 +485,31 @@ async function createFromContentTemplate(
|
|
|
475
485
|
)
|
|
476
486
|
|
|
477
487
|
// 2. Scaffold and apply content for each package
|
|
488
|
+
const claimed = new Set() // names this project's foundations register as
|
|
478
489
|
for (const pkg of placed) {
|
|
479
490
|
const fullPath = join(projectDir, pkg.relativePath)
|
|
480
491
|
|
|
492
|
+
// A foundation's name — what it registers as — is the template's name for
|
|
493
|
+
// the package when that is one, else the project's (`src` and `foundation`
|
|
494
|
+
// name a folder); `<project>-<package>` if another package here took it. A
|
|
495
|
+
// template's own main.js `name` outranks all of them.
|
|
496
|
+
let registryName = null
|
|
497
|
+
if (pkg.type === 'foundation' || pkg.type === 'extension') {
|
|
498
|
+
registryName =
|
|
499
|
+
[
|
|
500
|
+
normalizeFoundationName(pkg.name) || normalizeFoundationName(projectName),
|
|
501
|
+
normalizeFoundationName(`${projectName}-${pkg.name}`)
|
|
502
|
+
].find((n) => n && !claimed.has(n)) || null
|
|
503
|
+
if (registryName) claimed.add(registryName)
|
|
504
|
+
}
|
|
505
|
+
|
|
481
506
|
if (pkg.type === 'foundation' || pkg.type === 'extension') {
|
|
482
507
|
onProgress?.(`Creating ${pkg.type}: ${pkg.name}...`)
|
|
483
508
|
await scaffoldFoundation(
|
|
484
509
|
fullPath,
|
|
485
510
|
{
|
|
486
511
|
name: pkg.name,
|
|
512
|
+
registryName,
|
|
487
513
|
projectName,
|
|
488
514
|
isExtension: pkg.type === 'extension'
|
|
489
515
|
},
|
|
@@ -534,6 +560,7 @@ async function createFromContentTemplate(
|
|
|
534
560
|
renames: contentDir.renames
|
|
535
561
|
}
|
|
536
562
|
)
|
|
563
|
+
ensureFoundationName(fullPath, registryName)
|
|
537
564
|
}
|
|
538
565
|
|
|
539
566
|
// Merge template dependencies into package.json
|
|
@@ -1429,11 +1456,12 @@ ${colors.bright}Options:${colors.reset}
|
|
|
1429
1456
|
bind to, and the app cannot open such a site.
|
|
1430
1457
|
--no-save Do not record this deploy in deploy.yml
|
|
1431
1458
|
--no-validate Skip the content-conformance check (it only warns)
|
|
1432
|
-
--org @org Publish under @org (membership-gated
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1459
|
+
--org @org Publish under @org (membership-gated). On a site's FIRST
|
|
1460
|
+
publish it decides which org owns the site, and whose storage
|
|
1461
|
+
its assets are charged to. Recorded in sync.json and named on
|
|
1462
|
+
every request after, so it is never re-typed. Without it, you
|
|
1463
|
+
are asked once. Later, it must match the workspace the backend
|
|
1464
|
+
works on the site from, or publish stops.
|
|
1437
1465
|
--personal Create the site under your personal account, deliberately.
|
|
1438
1466
|
Only needed on a first publish, and only to answer the owner
|
|
1439
1467
|
question without a prompt (CI, agents, scripts).
|
|
@@ -1698,13 +1726,18 @@ Auto-detects what you run it in:
|
|
|
1698
1726
|
• a schemas-only pkg just its data schemas, no foundation — e.g. @uniweb/schemas,
|
|
1699
1727
|
any @org/schemas package, or a bare schemas/*.yml folder
|
|
1700
1728
|
|
|
1701
|
-
|
|
1702
|
-
|
|
1729
|
+
A foundation's scope is part of its name — name: '@org/<name>' in main.js. A bare
|
|
1730
|
+
name takes --scope (or one of your orgs), and register writes it into the name.
|
|
1731
|
+
|
|
1732
|
+
Schema scopes:
|
|
1733
|
+
@/name your own schema, in the foundation's scope (@/x -> @org/x)
|
|
1703
1734
|
@std/name a shared standard schema (from @uniweb/schemas)
|
|
1704
1735
|
@org/name another org's published schema, referenced by name
|
|
1705
1736
|
|
|
1706
1737
|
${colors.bright}Options:${colors.reset}
|
|
1707
|
-
--scope @org
|
|
1738
|
+
--scope @org A foundation with a bare name: register under @org, and write it into
|
|
1739
|
+
the name (refused when the name has another scope). A schemas-only
|
|
1740
|
+
package: publish under @org; default: package.json uniweb.scope
|
|
1708
1741
|
--dry-run Print the .uwx; submit nothing
|
|
1709
1742
|
-o, --output <f> Write the .uwx to a file; submit nothing
|
|
1710
1743
|
--non-interactive Fail with usage info instead of prompting
|