uniweb 0.84.0 → 0.86.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.
@@ -22,9 +22,9 @@
22
22
  * Distinct from `uniweb deploy` (third-party hosts) and `uniweb register`
23
23
  * (foundation code → catalog). For a self-contained artifact, see `uniweb export`.
24
24
  *
25
- * Backend: the one you are logged in to — UNIWEB_REGISTER_URL overrides it for a script
25
+ * Backend: the one you are logged in to — UNIWEB_SERVER overrides it for a script
26
26
  * (resolveBackendOrigin). Auth: UNIWEB_TOKEN > the stored session > `uniweb login`.
27
- * No `--backend` or `--token`: switching and signing in are `uniweb login`.
27
+ * No `--server` or `--token`: switching and signing in are `uniweb login`.
28
28
  *
29
29
  * Usage:
30
30
  * uniweb publish Bring the foundation along, sync, and go live
@@ -57,13 +57,16 @@ import { emitSyncPackages } from '@uniweb/build/uwx'
57
57
  import {
58
58
  bankLanguages,
59
59
  reconcile,
60
- settleServices
60
+ announceServices,
61
+ foundationSupports,
62
+ recordsNotAsked
61
63
  } from '../backend/service-request.js'
62
64
  import { isSiteRelativeExtensionUrl } from '@uniweb/build'
63
65
  import { resolveDefaultLocale } from '@uniweb/core/locale-config'
64
66
 
65
67
  import {
66
68
  BackendClient,
69
+ BackendUnreachableError,
67
70
  describeRequestError,
68
71
  WorkspaceMismatchError
69
72
  } from '../backend/client.js'
@@ -268,10 +271,10 @@ export async function publish(args = []) {
268
271
  }
269
272
  const siteYml = readSiteYml(join(siteDir, 'site.yml'))
270
273
  // ⭐ THE BACKEND YOU ARE LOGGED IN TO is where this goes *[Diego, 2026-09-21]*, for every
271
- // backend verb (resolveBackendOrigin). Only UNIWEB_REGISTER_URL — the automation
274
+ // backend verb (resolveBackendOrigin). Only UNIWEB_SERVER — the automation
272
275
  // override — outranks it, and nothing talks to a backend the user is not logged in to:
273
276
  // logged in nowhere, the origin is the default backend and the first request asks for
274
- // that login. ⛔ There is no `--backend` here: switching is `uniweb login --backend`.
277
+ // that login. ⛔ There is no `--server` here: switching is `uniweb login --server`.
275
278
  // ⛔ The project's own record — its synced backend, deploy.yml's default target — routes
276
279
  // NOTHING. For part of 2026-09-21 it answered a "logged in nowhere" tier, which cannot
277
280
  // be reached: *"We do not allow any communication with backend if the user is not
@@ -301,8 +304,8 @@ export async function publish(args = []) {
301
304
  // ⚠️ Logged in to a backend where this project has no site, while it has one
302
305
  // elsewhere: following the login CREATES a second site. Say so before the owner
303
306
  // question, which otherwise arrives with no reason attached. Not when
304
- // UNIWEB_REGISTER_URL chose the backend — that is already a decision.
305
- if (!process.env.UNIWEB_REGISTER_URL) {
307
+ // UNIWEB_SERVER chose the backend — that is already a decision.
308
+ if (!process.env.UNIWEB_SERVER) {
306
309
  const known = syncedElsewhere(siteDir, client.origin)
307
310
  if (known) {
308
311
  const [headline, ...rest] = describeSyncedElsewhere(known, client.origin, 'publish')
@@ -478,6 +481,12 @@ export async function publish(args = []) {
478
481
  cliBin: process.argv[1]
479
482
  })
480
483
  } catch (err) {
484
+ // A backend that did not answer is said as that — not as a release that failed.
485
+ if (err instanceof BackendUnreachableError) {
486
+ say.err(err.message)
487
+ say.dim('Nothing was released or sent. Check that the backend is running, then re-run `uniweb publish`.')
488
+ return { exitCode: 1 }
489
+ }
481
490
  // A scope you are not a member of is said as that, with the ways on (`explainReleaseFailure`).
482
491
  say.err(err.notMember ? err.message : `Foundation release failed: ${err.message}`)
483
492
  for (const line of err.notMember ? err.ways : ['Fix the foundation, then re-run `uniweb publish`.']) say.dim(line)
@@ -804,26 +813,16 @@ export async function publish(args = []) {
804
813
  const injectInfo = {
805
814
  ...(fnd.ref ? { foundation: fnd.ref } : {})
806
815
  }
807
- // ⭐ WHAT THE SITE HAS — read before the push, for the two requests a publish
808
- // carries: the services (`site.yml::services`) and the language selection. Only
809
- // this read sees a decision the owner made in the app since this clone last synced.
810
- // It reads this backend's site, from sync.json; a never-synced site has none.
816
+ // ⭐ WHAT THE SITE HAS — read before the push, for the language selection. Only this
817
+ // read sees a decision the owner made in the app since this clone last synced. It
818
+ // reads this backend's site, from sync.json; a never-synced site has none.
811
819
  const boundUuid = readBackendState(siteDir, client.origin).site?.uuid || null
812
820
  const status = boundUuid ? await client.siteStatus(boundUuid) : null
813
821
 
814
- // ⭐ THE SERVICES: what the owner changed in `site.yml::services` is sent, applied
815
- // over the site's own list; what the site changed is kept and offered into the file;
816
- // where both changed, the owner is asked (`settleServices`). An ask whose decision is
817
- // still open is never sent over the decision the site holds.
818
- const services = await settleServices({
819
- client,
820
- siteDir,
821
- siteYml,
822
- status,
823
- interactive: !isNonInteractive(args),
824
- confirm,
825
- say
826
- })
822
+ // ⭐ THE SERVICES are stated by the push itself — the file's, and off for each held
823
+ // one it no longer lists (`statedServices`) — and the backend decides per service.
824
+ // Said here: what the file asks that will not be sent as written.
825
+ announceServices({ siteYml, say, supports: await foundationSupports(siteDir, siteYml) })
827
826
 
828
827
  // ⭐ THE LANGUAGE SELECTION IS A REQUEST TOO, and it is the one that costs.
829
828
  //
@@ -873,8 +872,6 @@ export async function publish(args = []) {
873
872
  backend: client.origin,
874
873
  // The keys this deployment's Sections take — see deploymentFields.
875
874
  ...fields,
876
- // The `services` Section as settled above — or withheld.
877
- ...services.emit,
878
875
  // Placement identity for the folder — see writeFolderItemUuids.
879
876
  folderItemUuids: readFolderItemUuids(siteDir, client.origin),
880
877
  // Identity for the records' list items — see readRecordItemUuids.
@@ -908,6 +905,10 @@ export async function publish(args = []) {
908
905
  if (refuseUnsendableRecords(pkg.refusals, { error: say.err, note: say.dim })) {
909
906
  return { exitCode: 1 }
910
907
  }
908
+ // ⭐ `records` is what delivers the records the pages show on the published site — said
909
+ // here, at publish, and never at push: syncing records does not depend on it.
910
+ const notAsked = recordsNotAsked({ siteYml, shown: pkg.recordsShown })
911
+ if (notAsked) say.warn(notAsked)
911
912
  const report = {
912
913
  info: (m) => say.info(m),
913
914
  note: (m) => say.dim(m),
@@ -921,9 +922,6 @@ export async function publish(args = []) {
921
922
  report
922
923
  })
923
924
  if (pushResult.exitCode !== 0) return { exitCode: pushResult.exitCode }
924
- // The push stored what it sent: the services agreed on are recorded now, whether or
925
- // not the site then goes live.
926
- services.after()
927
925
  const siteUuid = pushResult.boundSiteUuid
928
926
  if (!siteUuid) {
929
927
  say.err('Push did not yield a site uuid — cannot go live.')
@@ -942,7 +940,7 @@ export async function publish(args = []) {
942
940
  } catch (err) {
943
941
  say.err(describeRequestError(err, client.origin))
944
942
  if (!(err instanceof WorkspaceMismatchError))
945
- say.dim('Is that the backend you meant? Switch with: uniweb login --backend <url>')
943
+ say.dim('Is that the backend you meant? Switch with: uniweb login --server <url>')
946
944
  return { exitCode: 1 }
947
945
  }
948
946
  // The body of a publish that did not go through — read once, and kept for the report below.
@@ -44,9 +44,9 @@
44
44
  *
45
45
  * The content + folder pull lanes are both keyed by the site's uuid on that backend
46
46
  * (sync.json).
47
- * Backend: the one you are logged in to — UNIWEB_REGISTER_URL overrides it for a script
47
+ * Backend: the one you are logged in to — UNIWEB_SERVER overrides it for a script
48
48
  * (resolveBackendOrigin). Auth: UNIWEB_TOKEN > the stored session > `uniweb login`.
49
- * No `--backend` or `--token`: switching and signing in are `uniweb login`.
49
+ * No `--server` or `--token`: switching and signing in are `uniweb login`.
50
50
  *
51
51
  * A project that never pushed has no `$uuid` to pull by — pull is a no-op with a
52
52
  * clear message. The backend serves each lane as a `.uwx` (ZIP: `manifest.json` +
@@ -95,7 +95,8 @@ import {
95
95
  rebankSyncHashes,
96
96
  writeQueryUuids,
97
97
  mergeBaseVersions,
98
- mergeItemBaseVersions,
98
+ writeItemBaseVersions,
99
+ pulledItemVersions,
99
100
  writeUnitBases,
100
101
  writeItemUuids,
101
102
  writeFolderItemUuids
@@ -701,10 +702,10 @@ export async function pull(args = [], deps = {}) {
701
702
  if (blocked) return blocked
702
703
  }
703
704
  // ⭐ THE BACKEND YOU ARE LOGGED IN TO is where this goes *[Diego, 2026-09-21]*, for every
704
- // backend verb (resolveBackendOrigin). Only UNIWEB_REGISTER_URL — the automation
705
+ // backend verb (resolveBackendOrigin). Only UNIWEB_SERVER — the automation
705
706
  // override — outranks it, and nothing talks to a backend the user is not logged in to:
706
707
  // logged in nowhere, the origin is the default backend and the first request asks for
707
- // that login. ⛔ There is no `--backend` here: switching is `uniweb login --backend`.
708
+ // that login. ⛔ There is no `--server` here: switching is `uniweb login --server`.
708
709
  // ⛔ The project's own record — its synced backend, deploy.yml's default target — routes
709
710
  // NOTHING. For part of 2026-09-21 it answered a "logged in nowhere" tier, which cannot
710
711
  // be reached: *"We do not allow any communication with backend if the user is not
@@ -746,14 +747,14 @@ export async function pull(args = [], deps = {}) {
746
747
  if (!siteContentUuid) {
747
748
  // Following the login can land on a backend with no site for this project while it
748
749
  // has one elsewhere — say where, rather than only "push first", which would create a
749
- // second site. Not when UNIWEB_REGISTER_URL chose the backend: that is a decision.
750
- const known = !process.env.UNIWEB_REGISTER_URL
750
+ // second site. Not when UNIWEB_SERVER chose the backend: that is a decision.
751
+ const known = !process.env.UNIWEB_SERVER
751
752
  ? syncedElsewhere(siteDir, client.origin)
752
753
  : null
753
754
  if (known) {
754
755
  info(`Nothing to pull — this project has no site on ${client.origin}.`)
755
756
  note(
756
- `Its ${known.length === 1 ? `site is on ${known[0]}` : `sites are on ${known.join(', ')}`}. To pull from there: uniweb login --backend <url>`
757
+ `Its ${known.length === 1 ? `site is on ${known[0]}` : `sites are on ${known.join(', ')}`}. To pull from there: uniweb login --server <url>`
757
758
  )
758
759
  return { exitCode: 0 }
759
760
  }
@@ -798,7 +799,7 @@ export async function pull(args = [], deps = {}) {
798
799
  } catch (err) {
799
800
  error(describeRequestError(err, client.origin))
800
801
  if (err instanceof WorkspaceMismatchError) return { refused: true }
801
- note('Is that the backend you meant? Switch with: uniweb login --backend <url>')
802
+ note('Is that the backend you meant? Switch with: uniweb login --server <url>')
802
803
  return null
803
804
  }
804
805
  if (res.status === 404) {
@@ -815,7 +816,7 @@ export async function pull(args = [], deps = {}) {
815
816
  if (detail) note(detail)
816
817
  if (res.status === 401 || res.status === 403)
817
818
  note(
818
- "Credentials weren't accepted — log in again (`uniweb login --backend <url>`), or check UNIWEB_TOKEN."
819
+ "Credentials weren't accepted — log in again (`uniweb login --server <url>`), or check UNIWEB_TOKEN."
819
820
  )
820
821
  return null
821
822
  }
@@ -887,6 +888,9 @@ export async function pull(args = [], deps = {}) {
887
888
  // The Models the pulled site's queries name, as the push qualified them — the re-bank
888
889
  // below resolves with them too (see there).
889
890
  let pulledQueryModels = []
891
+ // The versions this copy holds after the pull: every site-content item the pulled
892
+ // document carries (`pulledItemVersions`), banked below once the files have taken it.
893
+ let pulledVersions = null
890
894
  if (content && !content.notModified) {
891
895
  const siteDoc =
892
896
  content.docs &&
@@ -905,6 +909,7 @@ export async function pull(args = [], deps = {}) {
905
909
  // The next push re-establishes it; until then our side reports as unknown,
906
910
  // which is honest rather than wrong.
907
911
  writeUnitBases(siteDir, client.origin, { remote: computeUnitHashes(siteDoc), local: {} })
912
+ pulledVersions = pulledItemVersions(siteDoc, content.itemVersions)
908
913
  // Per-item identity for the next push. Without it the backend reads our
909
914
  // records as new and re-mints every page and section row.
910
915
  writeItemUuids(siteDir, client.origin, collectUnitUuids(siteDoc))
@@ -1216,7 +1221,13 @@ export async function pull(args = [], deps = {}) {
1216
1221
  // Records this pull did not place were not taken, so neither is their lane.
1217
1222
  if (lane === folderLane && recordsNotPlaced) continue
1218
1223
  mergeBaseVersions(siteDir, client.origin, lane.versions)
1219
- mergeItemBaseVersions(siteDir, client.origin, lane.itemVersions)
1224
+ }
1225
+ // ⭐ The site-content items' versions REPLACE what this copy held: a pull is how it
1226
+ // sees the site, and a version kept for an item the site no longer has would be sent
1227
+ // again, asking the backend to delete it. The folder lane's are not kept — that lane
1228
+ // is gated by its entity version. (Both were merged into one map until 2026-10-07.)
1229
+ if (pulledVersions && content && !content.notModified && !content.refused) {
1230
+ writeItemBaseVersions(siteDir, client.origin, pulledVersions)
1220
1231
  }
1221
1232
  // Persist the ETags so the next pull is conditional (304 when unchanged). The
1222
1233
  // folder's is DROPPED when records were not placed, so the next pull fetches
@@ -48,9 +48,9 @@
48
48
  * backend's reconcile deletes items absent from the package, so an author's NEW page
49
49
  * would be hard-deleted). `--force` omits the token and restores last-push-wins.
50
50
  *
51
- * Backend: the one you are logged in to — UNIWEB_REGISTER_URL overrides it for a script
51
+ * Backend: the one you are logged in to — UNIWEB_SERVER overrides it for a script
52
52
  * (resolveBackendOrigin). Auth: UNIWEB_TOKEN > the stored session > `uniweb login`.
53
- * No `--backend` or `--token`: switching and signing in are `uniweb login`.
53
+ * No `--server` or `--token`: switching and signing in are `uniweb login`.
54
54
  *
55
55
  * The two-lane SUBMISSION (POST both lanes, back-fill uuids, persist the
56
56
  * send-only-changed cache) lives in `../backend/site-sync.js` so `uniweb publish`
@@ -65,7 +65,7 @@ import { emitSyncPackages, readBackendState } from '@uniweb/build/uwx'
65
65
  import { findSiteCopies, describeSiteCopies } from '../utils/site-copies.js'
66
66
  import { uploadSiteMedia, describeAssetRefusal } from '../backend/site-media.js'
67
67
  import { updateBackendMap, carryServed, SYNC_STORE_FILE } from '@uniweb/build/uwx'
68
- import { BackendClient } from '../backend/client.js'
68
+ import { BackendClient, BackendUnreachableError } from '../backend/client.js'
69
69
  import { resolveSiteDir } from './deploy.js'
70
70
  import { refuseIfContentDoesNotConform } from '../utils/conformance.js'
71
71
  import { reportSchemalessQueries } from '../utils/schemaless-report.js'
@@ -75,7 +75,7 @@ import {
75
75
  describeSyncedElsewhere
76
76
  } from '../utils/site-identity.js'
77
77
  import { confirm, isNonInteractive } from '../utils/interactive.js'
78
- import { settleServices } from '../backend/service-request.js'
78
+ import { announceServices, foundationSupports } from '../backend/service-request.js'
79
79
  import { guardEmptyRecords } from '../utils/records-guard.js'
80
80
  import { bringFoundationAlong } from '../backend/foundation-bring-along.js'
81
81
  import {
@@ -162,10 +162,10 @@ export async function push(args = [], deps = {}) {
162
162
  // arrival or lose a value on the way. See utils/conformance.js.
163
163
  if (await refuseIfContentDoesNotConform(siteDir, { args })) return { exitCode: 1 }
164
164
  // ⭐ THE BACKEND YOU ARE LOGGED IN TO is where this goes *[Diego, 2026-09-21]*, for every
165
- // backend verb (resolveBackendOrigin). Only UNIWEB_REGISTER_URL — the automation
165
+ // backend verb (resolveBackendOrigin). Only UNIWEB_SERVER — the automation
166
166
  // override — outranks it, and nothing talks to a backend the user is not logged in to:
167
167
  // logged in nowhere, the origin is the default backend and the first request asks for
168
- // that login. ⛔ There is no `--backend` here: switching is `uniweb login --backend`.
168
+ // that login. ⛔ There is no `--server` here: switching is `uniweb login --server`.
169
169
  // ⛔ The project's own record — its synced backend, deploy.yml's default target — routes
170
170
  // NOTHING. For part of 2026-09-21 it answered a "logged in nowhere" tier, which cannot
171
171
  // be reached: *"We do not allow any communication with backend if the user is not
@@ -209,8 +209,8 @@ export async function push(args = [], deps = {}) {
209
209
  // ⚠️ Logged in to a backend where this project has no site, while it has one
210
210
  // elsewhere: following the login CREATES a second site. Say so before the owner
211
211
  // question, which otherwise arrives with no reason attached. Not for `-o` (nothing is
212
- // created) and not when UNIWEB_REGISTER_URL chose the backend — that is a decision.
213
- if (!output && !process.env.UNIWEB_REGISTER_URL) {
212
+ // created) and not when UNIWEB_SERVER chose the backend — that is a decision.
213
+ if (!output && !process.env.UNIWEB_SERVER) {
214
214
  const known = syncedElsewhere(siteDir, client.origin)
215
215
  if (known) {
216
216
  const [headline, ...rest] = describeSyncedElsewhere(known, client.origin, 'push')
@@ -278,6 +278,12 @@ export async function push(args = [], deps = {}) {
278
278
  verb: 'push'
279
279
  })
280
280
  } catch (err) {
281
+ // A backend that did not answer is said as that — not as a release that failed.
282
+ if (err instanceof BackendUnreachableError) {
283
+ error(err.message)
284
+ note('Nothing was released or sent. Check that the backend is running, then re-run `uniweb push`.')
285
+ return { exitCode: 1 }
286
+ }
281
287
  // A scope you are not a member of is said as that, with the ways on (`explainReleaseFailure`).
282
288
  error(err.notMember ? err.message : `Foundation release failed: ${err.message}`)
283
289
  for (const line of err.notMember ? err.ways : ['Fix the foundation, then re-run `uniweb push`.']) note(line)
@@ -521,20 +527,10 @@ export async function push(args = [], deps = {}) {
521
527
  ref: siteYml?.foundation
522
528
  })
523
529
  }
524
- // ⭐ THE SERVICES `site.yml` ASKS FOR — what the owner changed is sent over the site's
525
- // own list; what the site changed is kept and offered into the file; where both
526
- // changed, the owner is asked. Shared with `uniweb publish` (`settleServices`).
527
- // Offline for `-o` and `--dry-run`: nothing is read, and nothing is recorded.
528
- const offline = Boolean(output) || dryRun
529
- const services = await settleServices({
530
- client,
531
- siteDir,
532
- siteYml,
533
- offline,
534
- interactive: !offline && !isNonInteractive(args),
535
- confirm,
536
- say: { info, warn, dim: note, ok: success }
537
- })
530
+ // ⭐ THE SERVICES `site.yml` ASKS FOR are stated by the emit — the file's, and off for
531
+ // each held one it no longer lists (`statedServices`) — and the backend decides per
532
+ // service. Said here: what the file asks that will not be sent as written.
533
+ announceServices({ siteYml, say: { warn }, supports: await foundationSupports(siteDir, siteYml) })
538
534
  const emitOptions = {
539
535
  backend: client.origin,
540
536
  // Placement identity for the folder — see writeFolderItemUuids.
@@ -570,9 +566,7 @@ export async function push(args = [], deps = {}) {
570
566
  itemBaseVersions: readItemBaseVersions(siteDir, client.origin)
571
567
  }),
572
568
  ...(assetRewrite ? { assetRewrite } : {}),
573
- ...(assetIds ? { assetIds } : {}),
574
- // The `services` Section as settled above — or withheld.
575
- ...services.emit
569
+ ...(assetIds ? { assetIds } : {})
576
570
  }
577
571
  let pkg
578
572
  try {
@@ -601,8 +595,6 @@ export async function push(args = [], deps = {}) {
601
595
 
602
596
  // Nothing changed since the last push — the backend is already up to date.
603
597
  if (totalEntities === 0) {
604
- // The site already holds what this copy has, so what was settled is agreed.
605
- if (!offline) services.after()
606
598
  success(
607
599
  `Nothing to push — ${skipped} entit${skipped === 1 ? 'y' : 'ies'} unchanged since the last push.`
608
600
  )
@@ -663,7 +655,6 @@ export async function push(args = [], deps = {}) {
663
655
  }
664
656
  })
665
657
  if (result.exitCode !== 0) return { exitCode: result.exitCode }
666
- services.after()
667
658
  success(
668
659
  `Pushed ${result.finalizedTotal} entit${result.finalizedTotal === 1 ? 'y' : 'ies'}` +
669
660
  (result.wrote.length ? ` — ${result.wrote.join(', ')}` : '')
@@ -31,7 +31,8 @@
31
31
  * uniweb refresh git pull, then merge the backend's content
32
32
  * uniweb refresh --no-git skip the git remote; backend only
33
33
  * uniweb refresh --no-backend skip the backend; git only
34
- * uniweb refresh --backend <url> Override the backend origin
34
+ *
35
+ * The backend is the one you are logged in to, as for every backend verb (no flag picks it).
35
36
  */
36
37
 
37
38
  import { readFileSync } from 'node:fs'
@@ -35,9 +35,9 @@
35
35
  * ({ok,scope,origin,entities:[{name,uuid,version,unchanged}]}),
36
36
  * all human output to stderr — for scripted callers
37
37
  *
38
- * Endpoint: UNIWEB_REGISTER_URL > the backend you are logged in to > the default
39
- * (~/.uniweb/config.json, else uniweb.app) — resolveBackendOrigin. No `--backend`:
40
- * switching is `uniweb login --backend <url>`.
38
+ * Endpoint: UNIWEB_SERVER > the backend you are logged in to > the default
39
+ * (~/.uniweb/config.json, else uniweb.app) — resolveBackendOrigin. No backend flag:
40
+ * switching is `uniweb login --server <url>`.
41
41
  * Auth (submit only): UNIWEB_TOKEN > the stored session > `uniweb login`.
42
42
  */
43
43
 
@@ -595,7 +595,7 @@ async function runRegister(args = []) {
595
595
  const dryRun = args.includes('--dry-run')
596
596
  const output = flagValue(args, '-o') || flagValue(args, '--output')
597
597
  const scopeFlag = flagValue(args, '--scope')
598
- // The backend you are logged in to (UNIWEB_REGISTER_URL overrides it for a script) —
598
+ // The backend you are logged in to (UNIWEB_SERVER overrides it for a script) —
599
599
  // resolveBackendOrigin, like every backend command.
600
600
  const client = new BackendClient({
601
601
  args,
@@ -845,7 +845,7 @@ async function runRegister(args = []) {
845
845
  } catch (err) {
846
846
  error(`Could not reach the registry at ${client.origin}: ${err.message}`)
847
847
  log(
848
- ` ${colors.dim}Is that the backend you meant? Switch with: uniweb login --backend <url>${colors.reset}`
848
+ ` ${colors.dim}Is that the backend you meant? Switch with: uniweb login --server <url>${colors.reset}`
849
849
  )
850
850
  return { exitCode: 2 }
851
851
  }
@@ -905,7 +905,7 @@ async function runRegister(args = []) {
905
905
  ` ${colors.dim}The registry didn't accept your credentials — it may use different ones than \`uniweb login\`.${colors.reset}`
906
906
  )
907
907
  log(
908
- ` ${colors.dim}Log in again (uniweb login --backend <url>), or check UNIWEB_TOKEN — an existing one may be wrong or expired.${colors.reset}`
908
+ ` ${colors.dim}Log in again (uniweb login --server <url>), or check UNIWEB_TOKEN — an existing one may be wrong or expired.${colors.reset}`
909
909
  )
910
910
  }
911
911
  // ⭐ **Surface `detail` as a sentence, not as a JSON dump.** The registry
@@ -62,7 +62,7 @@ import { writeJsonPreservingStyleAsync } from '../utils/json-file.js'
62
62
  import { getExistingPackageNames, validatePackageName } from '../utils/names.js'
63
63
  import { detectPackageManager, installCmd } from '../utils/pm.js'
64
64
  import { getCliPrefix } from '../utils/interactive.js'
65
- import { replaceInTopLevelList, setTopLevelScalar } from '../utils/yaml-edit.js'
65
+ import { replaceInYamlList, setYamlKey } from '@uniweb/build/uwx'
66
66
 
67
67
  const colors = {
68
68
  reset: '\x1b[0m',
@@ -307,9 +307,9 @@ async function renameFoundation(rootDir, oldName, newName, prefix) {
307
307
  pkgSrc,
308
308
  sitePkgPath,
309
309
  siteYmlPath,
310
- // Edited in place — see utils/yaml-edit.js — and computed HERE, before
310
+ // Edited in place — `setYamlKey`, @uniweb/build — and computed HERE, before
311
311
  // anything moves, so a file it cannot edit stops the rename cleanly.
312
- newYmlText: ymlMatches ? setTopLevelScalar(ymlText, 'foundation', newName) : null,
312
+ newYmlText: ymlMatches ? setYamlKey(ymlText, 'foundation', newName) : null,
313
313
  hasDep,
314
314
  ymlMatches
315
315
  })
@@ -318,7 +318,7 @@ async function renameFoundation(rootDir, oldName, newName, prefix) {
318
318
  refuseUneditableSiteYml(
319
319
  affectedSites.filter((s) => s.ymlMatches && s.newYmlText === null).map((s) => s.path),
320
320
  'foundation',
321
- `Write it on one line: \`foundation: ${oldName}\`.`
321
+ `Write \`foundation: ${oldName}\` as a plain value — not through a YAML alias (\`*name\`) or merge key (\`<<:\`).`
322
322
  )
323
323
 
324
324
  // ─── Print plan, then execute ────────────────────────────────
@@ -530,14 +530,14 @@ async function renameExtension(rootDir, oldName, newName, prefix) {
530
530
  siteYmlPath,
531
531
  hits,
532
532
  // Edited in place, and computed before anything moves — as for foundations.
533
- newYmlText: replaceInTopLevelList(ymlText, 'extensions', replacements)
533
+ newYmlText: replaceInYamlList(ymlText, 'extensions', replacements)
534
534
  })
535
535
  }
536
536
  }
537
537
  refuseUneditableSiteYml(
538
538
  affectedSites.filter((a) => a.newYmlText === null).map((a) => a.site.path),
539
539
  'extensions',
540
- `Write each entry on a line of its own, e.g. \`- ${oldUrlPrefix}dist/entry.js\`.`
540
+ 'Write `extensions:` as a plain list — not through a YAML alias (`*name`) or merge key (`<<:`).'
541
541
  )
542
542
 
543
543
  log('')