uniweb 0.59.2 → 0.60.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.
@@ -5,10 +5,11 @@
5
5
  * - folder lane → one `@uniweb/folder` + the collection-record entities it
6
6
  * references (the dynamic half; the `$ref` closure rides together).
7
7
  *
8
- * Each entity is an entity-content document (`$id` + `$model` + sections). The site
9
- * holds exactly one identity: `site.yml::$uuid` (the site-content entity). A first
10
- * push has none — it CREATEs the site (uuid-less), the backend mints + adopts it
11
- * and returns the new uuid, which `push` records into `site.yml`. Later pushes
8
+ * Each entity is an entity-content document (`$id` + `$model` + sections). The site's
9
+ * identity on a backend is its site-content uuid, in that backend's section of
10
+ * `sync.json`. A first push to a backend has none — it CREATEs the site (uuid-less),
11
+ * the backend mints + adopts it and returns the new uuid, which `push` records in
12
+ * `sync.json`. Later pushes
12
13
  * UPDATE by that uuid. The folder lane is keyed by the SAME site-content uuid —
13
14
  * the backend owns the site's `@uniweb/folder`, so the framework never holds a
14
15
  * folder uuid. Records still round-trip their own `$uuid`
@@ -37,7 +38,6 @@
37
38
  * this prompt existed. First push only.
38
39
  * uniweb push --dry-run Report what would be pushed; submit nothing
39
40
  * uniweb push -o out.uwx Write the .uwx file(s) per lane; submit nothing
40
- * uniweb push --token <bearer> Submit with this bearer; skips `uniweb login`
41
41
  * uniweb push --foundation <dir> Use this local foundation for the Model schema
42
42
  * uniweb push --all Send every record (bypass the changed-only cache)
43
43
  * uniweb push --force Overwrite upstream changes (drop the staleness gate)
@@ -49,9 +49,9 @@
49
49
  * backend's reconcile deletes items absent from the package, so an author's NEW page
50
50
  * would be hard-deleted). `--force` omits the token and restores last-push-wins.
51
51
  *
52
- * Backend: via BackendClient (the content + folder sync lanes). Origin from
53
- * UNIWEB_REGISTER_URL > the local default.
54
- * Auth: --token > UNIWEB_TOKEN > `uniweb login` session.
52
+ * Backend: the one you are logged in to — UNIWEB_REGISTER_URL overrides it for a script
53
+ * (resolveBackendOrigin). Auth: UNIWEB_TOKEN > the stored session > `uniweb login`.
54
+ * No `--backend` or `--token`: switching and signing in are `uniweb login`.
55
55
  *
56
56
  * The two-lane SUBMISSION (POST both lanes, back-fill uuids, persist the
57
57
  * send-only-changed cache) lives in `../backend/site-sync.js` so `uniweb publish`
@@ -63,17 +63,18 @@ import { readFileSync, writeFileSync } from 'node:fs'
63
63
  import { join, resolve } from 'node:path'
64
64
  import yaml from 'js-yaml'
65
65
  import { emitSyncPackages } from '@uniweb/build/uwx'
66
+ import { findSiteCopies, describeSiteCopies } from '../utils/site-copies.js'
66
67
  import { uploadSiteMedia, describeAssetRefusal } from '../backend/site-media.js'
67
- import { updateAssetMap, ASSET_MAP_FILE } from '@uniweb/build/uwx'
68
+ import { updateBackendMap, carryServed, SYNC_STORE_FILE } from '@uniweb/build/uwx'
68
69
  import { BackendClient } from '../backend/client.js'
69
- import { resolveSiteDir, resolveSiteBackend } from './deploy.js'
70
+ import { resolveSiteDir } from './deploy.js'
70
71
  import { warnIfContentDoesNotConform } from '../utils/conformance.js'
71
72
  import { reportSchemalessQueries } from '../utils/schemaless-report.js'
72
73
  import { readOrgFlag } from '../utils/args.js'
73
74
  import { checkFlags } from '../utils/flag-guard.js'
74
75
  import {
75
- assertSiteBackendScope,
76
- readSiteIdentity
76
+ syncedElsewhere,
77
+ describeSyncedElsewhere
77
78
  } from '../utils/site-identity.js'
78
79
  import { confirm } from '../utils/interactive.js'
79
80
  import { guardEmptyRecords } from '../utils/records-guard.js'
@@ -141,7 +142,6 @@ export async function push(args = [], deps = {}) {
141
142
  }
142
143
  const dryRun = args.includes('--dry-run')
143
144
  const output = flagValue(args, '-o') || flagValue(args, '--output')
144
- const tokenFlag = flagValue(args, '--token')
145
145
  const foundationDir = flagValue(args, '--foundation')
146
146
  const sendAll = args.includes('--all') // bypass the send-only-changed cache
147
147
  // --force drops the optimistic-concurrency precondition, making the push
@@ -156,50 +156,64 @@ export async function push(args = [], deps = {}) {
156
156
  // Advisory only — warns and pushes. A malformed data block otherwise rides
157
157
  // the sync wire unchecked; see utils/conformance.js.
158
158
  await warnIfContentDoesNotConform(siteDir, { args })
159
- // The project's own statement of where its identity lives. Feeds the origin ladder
160
- // ABOVE the session (see resolveBackendOrigin), so a teammate who cloned this project
161
- // targets the backend it is bound to instead of whatever they last logged into.
162
- // ⛔ The RAW value, never `resolveSiteScope` — null must mean "defer to the next tier".
163
- const siteScope = readSiteIdentity(siteDir).backend
164
- const siteBackend = await resolveSiteBackend(siteDir)
159
+ // ⭐ THE BACKEND YOU ARE LOGGED IN TO is where this goes *[Diego, 2026-09-21]*, for every
160
+ // backend verb (resolveBackendOrigin). Only UNIWEB_REGISTER_URL — the automation
161
+ // override — outranks it, and nothing talks to a backend the user is not logged in to:
162
+ // logged in nowhere, the origin is the default backend and the first request asks for
163
+ // that login. ⛔ There is no `--backend` here: switching is `uniweb login --backend`.
164
+ // ⛔ The project's own record — its synced backend, deploy.yml's default target — routes
165
+ // NOTHING. For part of 2026-09-21 it answered a "logged in nowhere" tier, which cannot
166
+ // be reached: *"We do not allow any communication with backend if the user is not
167
+ // logged into a backend."*
165
168
  // One front door. The bearer is resolved lazily on first need (a non-local Model
166
169
  // read during the build, or the submit). Offline emit (--dry-run / -o) is fully
167
170
  // offline: it never submits, and its Model resolver never reads from the backend
168
171
  // (the `offline` flag below), so it never authenticates — even when a collection
169
172
  // references a Model the local foundation doesn't define.
170
173
  const client = new BackendClient({
171
- originFlag: flagValue(args, '--backend'),
172
- siteScope,
173
- siteBackend,
174
- token: tokenFlag,
175
174
  args,
176
175
  command: 'Syncing'
177
176
  })
178
177
 
179
- // ⛔ SCOPE CHECK — before anything is sent. A project whose stored identity was minted
180
- // by a different backend cannot be pushed here: the uuids, the asset ids and the sync
181
- // cache are all foreign at once. Runs after the client so it sees the RESOLVED origin
182
- // (flag > env > deploy.yml > session), not the one we guessed.
178
+ // ⛔ A COPY OF ANOTHER PROJECT — before anything is sent. A plain directory copy
179
+ // carries sync.json, so it holds the ORIGINAL's site on this backend and its push
180
+ // would update that site (see utils/site-copies.js for why the signal is exact).
181
+ // Refused, not warned: a warning printed while the push goes ahead is printed over
182
+ // the damage. Runs after the client so it checks the RESOLVED origin.
183
183
  //
184
- // ⚠️ NOT for `-o`, which is a LOCAL EMIT and reaches no backend at all. Its output is
185
- // built from files on disk; the resolved origin is not an input to it, so a mismatch
186
- // cannot make the artifact wrong — and refusing would break an operation this command
187
- // deliberately keeps offline (the `!output && !dryRun` guards below are the same rule).
188
- // The refusal even says "Sending them elsewhere is refused" over a run that sends
189
- // nothing.
184
+ // ⚠️ NOT for `-o`, which is a LOCAL EMIT and reaches no backend at all — there is no
185
+ // site for it to update, and refusing would break an operation this command keeps
186
+ // offline (the `!output && !dryRun` guards below are the same rule).
190
187
  //
191
188
  // `--dry-run` IS checked, and the asymmetry is the point: a dry run previews a real
192
- // push, so when that push would be refused, saying so is the honest preview. Printing
193
- // "would update content at <origin>" instead would preview something that cannot happen.
189
+ // push, so when that push would be refused, saying so is the honest preview.
190
+ //
191
+ // *(This spot held the backend SCOPE CHECK, `assertSiteBackendScope`, until
192
+ // 2026-09-20 — deleted when identity became per-backend and a foreign backend's
193
+ // uuids stopped being reachable at all.)*
194
194
  if (!output) {
195
- const scope = assertSiteBackendScope(siteDir, client.origin)
196
- if (!scope.ok) {
197
- error(scope.message)
198
- scope.hint.forEach(note)
195
+ const copies = findSiteCopies(siteDir, client.origin)
196
+ if (copies.length) {
197
+ const said = describeSiteCopies(copies, client.origin, 'push')
198
+ error(said.headline)
199
+ for (const line of said.lines) note(line)
199
200
  return { exitCode: 1 }
200
201
  }
201
202
  }
202
203
 
204
+ // ⚠️ Logged in to a backend where this project has no site, while it has one
205
+ // elsewhere: following the login CREATES a second site. Say so before the owner
206
+ // question, which otherwise arrives with no reason attached. Not for `-o` (nothing is
207
+ // created) and not when UNIWEB_REGISTER_URL chose the backend — that is a decision.
208
+ if (!output && !process.env.UNIWEB_REGISTER_URL) {
209
+ const known = syncedElsewhere(siteDir, client.origin)
210
+ if (known) {
211
+ const [headline, ...rest] = describeSyncedElsewhere(known, client.origin, 'push')
212
+ info(headline)
213
+ for (const line of rest) note(line)
214
+ }
215
+ }
216
+
203
217
  // WHO will own this site, if this push is the one that creates it. Resolved
204
218
  // before any lane runs, because both create paths below consume it and neither
205
219
  // should be reached with the question still open. A site that already exists
@@ -298,18 +312,18 @@ export async function push(args = [], deps = {}) {
298
312
  //
299
313
  // Still skipped for an offline emit: `-o` / `--dry-run` must not mutate project state.
300
314
  if (!output && !dryRun) {
301
- const dropped = clearRemoteSyncStateIfUnbound(siteDir)
315
+ const dropped = clearRemoteSyncStateIfUnbound(siteDir, client.origin)
302
316
  if (dropped.length) {
303
317
  note(
304
318
  `Cleared stale sync state from a previous site (${dropped.join(', ')}).`
305
319
  )
306
320
  }
307
- const stale = dropSiteBoundValues(siteDir)
321
+ const stale = dropSiteBoundValues(siteDir, client.origin)
308
322
  if (stale.length) {
309
323
  note(`Dropped the previous site's ${stale.join(' and ')} from site.yml.`)
310
324
  }
311
325
  }
312
- const priorHashes = readSyncCache(siteDir)
326
+ const priorHashes = readSyncCache(siteDir, client.origin)
313
327
  // Per-item identity, without which the backend reads every record as new and
314
328
  // recreates every page and section row.
315
329
  //
@@ -329,8 +343,8 @@ export async function push(args = [], deps = {}) {
329
343
  // The upload runs before `ensureItemUuids`, and the reason once recorded here —
330
344
  // "BEFORE `ensureItemUuids`, which mints uuids on the backend, so a refusal
331
345
  // leaves nothing minted" — is FALSE. `ensureItemUuids` mints nothing: it reads a
332
- // local cache, and if that is empty it reads `site.yml::$uuid` and returns
333
- // immediately when there is none (the first-push case). Its only backend call is
346
+ // local map, and if that is empty it reads this backend's site uuid from sync.json
347
+ // and returns immediately when there is none (the first-push case). Its only backend call is
334
348
  // a GET (`pullSiteContent`); its only write is a local file. So this ordering is
335
349
  // not protecting what that comment claimed, and is under review — an upload is a
336
350
  // durable, metered write, which makes uploading first the thing that leaves bytes
@@ -379,7 +393,7 @@ export async function push(args = [], deps = {}) {
379
393
  // is reported and confirmed before anything is sent. The format stays honest;
380
394
  // the asking happens here.
381
395
  if (!dryRun) {
382
- const guard = await guardEmptyRecords({ siteDir, args, warn, note })
396
+ const guard = await guardEmptyRecords({ siteDir, backend: client.origin, args, warn, note })
383
397
  if (!guard.ok) return { exitCode: 1 }
384
398
  }
385
399
 
@@ -389,8 +403,9 @@ export async function push(args = [], deps = {}) {
389
403
  let mediaRefs = []
390
404
  try {
391
405
  const probe = await emitSyncPackages(siteDir, {
406
+ backend: client.origin,
392
407
  // Placement identity for the folder — see writeFolderItemUuids.
393
- folderItemUuids: readFolderItemUuids(siteDir),
408
+ folderItemUuids: readFolderItemUuids(siteDir, client.origin),
394
409
  // Resolves a foundation-relative `@/x` model ref into `@org/x`.
395
410
  ...(asOrg ? { org: asOrg } : {}),
396
411
  ...(foundationDir ? { foundationDir } : {}),
@@ -458,10 +473,10 @@ export async function push(args = [], deps = {}) {
458
473
  )
459
474
  // Record identity in the COMMITTED map. Merge, never replace: this push
460
475
  // carries only the refs its content touched.
461
- const rec = updateAssetMap(siteDir, ids)
476
+ const rec = updateBackendMap(siteDir, client.origin, 'assets', ids, carryServed)
462
477
  if (rec.written) {
463
478
  note(
464
- `${ASSET_MAP_FILE}: ${rec.added.length} added, ${rec.changed.length} changed — commit it`
479
+ `${SYNC_STORE_FILE}: ${rec.added.length} asset(s) added, ${rec.changed.length} changed — commit it`
465
480
  )
466
481
  }
467
482
  } catch (err) {
@@ -482,16 +497,17 @@ export async function push(args = [], deps = {}) {
482
497
 
483
498
  const itemUuids =
484
499
  output || dryRun
485
- ? readItemUuids(siteDir)
500
+ ? readItemUuids(siteDir, client.origin)
486
501
  : await ensureItemUuids({ client, siteDir, note })
487
502
  let pkg
488
503
  try {
489
504
  pkg = await emitSyncPackages(siteDir, {
505
+ backend: client.origin,
490
506
  // Placement identity for the folder — see writeFolderItemUuids.
491
- folderItemUuids: readFolderItemUuids(siteDir),
507
+ folderItemUuids: readFolderItemUuids(siteDir, client.origin),
492
508
  // Identity for the `queries` section — see readQueryUuids. Keyed by
493
509
  // name, because a declaration has no file for a path-keyed map to hold.
494
- queryUuids: readQueryUuids(siteDir),
510
+ queryUuids: readQueryUuids(siteDir, client.origin),
495
511
  // Resolves a foundation-relative `@/x` model ref into `@org/x`.
496
512
  ...(asOrg ? { org: asOrg } : {}),
497
513
  ...(foundationDir ? { foundationDir } : {}),
@@ -513,8 +529,8 @@ export async function push(args = [], deps = {}) {
513
529
  ...(force
514
530
  ? {}
515
531
  : {
516
- baseVersions: readBaseVersions(siteDir),
517
- itemBaseVersions: readItemBaseVersions(siteDir)
532
+ baseVersions: readBaseVersions(siteDir, client.origin),
533
+ itemBaseVersions: readItemBaseVersions(siteDir, client.origin)
518
534
  }),
519
535
  ...(assetRewrite ? { assetRewrite } : {}),
520
536
  ...(assetIds ? { assetIds } : {})
@@ -46,6 +46,8 @@ import {
46
46
  headProvenance
47
47
  } from '../utils/git.js'
48
48
  import { probeUnpushed } from '../backend/site-sync.js'
49
+ import { resolveBackendOrigin } from '../backend/client.js'
50
+ import { readBackendState } from '@uniweb/build/uwx'
49
51
  import { checkFlags } from '../utils/flag-guard.js'
50
52
 
51
53
  const c = {
@@ -65,28 +67,16 @@ const say = {
65
67
  dim: (m) => console.log(` ${c.dim}${m}${c.reset}`)
66
68
  }
67
69
 
68
- // Forward a flag AND its value to the delegated verb. `--backend http://x` is two
69
- // argv entries, so a naive filter passes the flag and drops the URL — leaving the
70
- // delegated pull pointed at the default backend while the user believes they
71
- // overrode it. `--backend=http://x` is one entry and passes through as-is.
72
- function collectPassthrough(args, names) {
73
- const out = []
74
- for (let i = 0; i < args.length; i++) {
75
- const a = args[i]
76
- const name = names.find((n) => a === n || a.startsWith(`${n}=`))
77
- if (!name) continue
78
- out.push(a)
79
- if (a === name && args[i + 1] && !args[i + 1].startsWith('-'))
80
- out.push(args[++i])
81
- }
82
- return out
83
- }
84
-
85
- // A site is backend-synced once it has an identity to pull by.
86
- function siteContentUuid(siteDir) {
70
+ // A site is backend-synced once it has an identity to pull by — on THIS backend,
71
+ // from sync.json. It read `site.yml::$uuid`, so after step 4 refresh treated every
72
+ // project as unsynced and skipped the backend half of its check.
73
+ //
74
+ // ⛔ `backend` is the one the delegated pull will use: the backend the user is logged in
75
+ // to. (For part of 2026-09-21, `refresh --backend X` checked one backend's sync state and
76
+ // pulled from another; the flag is gone from the backend verbs since.)
77
+ function siteContentUuid(siteDir, backend) {
87
78
  try {
88
- const y = yaml.load(readFileSync(join(siteDir, 'site.yml'), 'utf8'))
89
- return typeof y?.$uuid === 'string' ? y.$uuid : null
79
+ return readBackendState(siteDir, backend).site?.uuid || null
90
80
  } catch {
91
81
  return null
92
82
  }
@@ -155,20 +145,21 @@ export async function refresh(args = [], deps = {}) {
155
145
  }
156
146
 
157
147
  // ── 2. the backend ────────────────────────────────────────────────────────
148
+ // The backend the delegated pull talks to — the one you are logged in to
149
+ // (resolveBackendOrigin). Everything below asks about THAT one.
150
+ const backend = resolveBackendOrigin()
158
151
  if (skipBackend) {
159
152
  skipped.push('backend (--no-backend)')
160
- } else if (!siteContentUuid(siteDir)) {
153
+ } else if (!siteContentUuid(siteDir, backend)) {
161
154
  skipped.push('backend (this site has never been synced)')
162
155
  } else {
163
156
  say.info("Merging the backend's content…")
164
157
  const pull = deps.pull || (await import('./pull.js')).pull
165
158
  // `--merge` rather than a plain pull: an author editing a different part of the
166
159
  // same section is not a conflict, and should not be presented as one.
167
- const passthrough = collectPassthrough(args, [
168
- '--backend',
169
- '--token'
170
- ])
171
- const res = await pull(['--merge', ...passthrough])
160
+ // Nothing of refresh's own argv is forwarded: the pull goes to the backend you are
161
+ // logged in to, as refresh does, and authenticates the same way.
162
+ const res = await pull(['--merge'])
172
163
  conflicts = res?.merge?.conflicted?.length ?? 0
173
164
  if (res?.exitCode && !conflicts) {
174
165
  // Failed for a reason other than conflicts — say so plainly rather than
@@ -192,9 +183,11 @@ export async function refresh(args = [], deps = {}) {
192
183
 
193
184
  // What is still yours to send. The point of a milestone check is knowing both
194
185
  // directions, not just that you took what was waiting.
195
- if (!skipBackend && siteContentUuid(siteDir)) {
186
+ if (!skipBackend && siteContentUuid(siteDir, backend)) {
196
187
  try {
197
- const probe = await probeUnpushed(siteDir)
188
+ // Whose asset ids the comparison reads — see status.js. Offline, so the
189
+ // origin is resolved rather than taken from a client.
190
+ const probe = await probeUnpushed(siteDir, { backend })
198
191
  if (probe.changed)
199
192
  say.dim(
200
193
  `Unpushed: ${probe.changed} entit${probe.changed === 1 ? 'y' : 'ies'} changed locally`
@@ -30,12 +30,11 @@
30
30
  * uniweb register --json Porcelain: ONE compact JSON line on stdout
31
31
  * ({ok,scope,origin,entities:[{name,uuid,version,unchanged}]}),
32
32
  * all human output to stderr — for scripted callers
33
- * uniweb register --backend <url> Override the backend origin
34
- * uniweb register --token <bearer> Submit with this bearer; skips `uniweb login`
35
33
  *
36
- * Endpoint resolution: --backend <url> > UNIWEB_REGISTER_URL >
37
- * the logged-in session origin > ~/.uniweb/config.json > the default (uniweb.app).
38
- * Auth (submit only): --token <bearer> > UNIWEB_TOKEN > `uniweb login` session.
34
+ * Endpoint: UNIWEB_REGISTER_URL > the backend you are logged in to > the default
35
+ * (~/.uniweb/config.json, else uniweb.app) — resolveBackendOrigin. No `--backend`:
36
+ * switching is `uniweb login --backend <url>`.
37
+ * Auth (submit only): UNIWEB_TOKEN > the stored session > `uniweb login`.
39
38
  */
40
39
 
41
40
  // DEFERRED foundation-registration capabilities (the legacy `uniweb publish` had
@@ -388,12 +387,9 @@ async function runRegister(args = []) {
388
387
  const dryRun = args.includes('--dry-run')
389
388
  const output = flagValue(args, '-o') || flagValue(args, '--output')
390
389
  const scopeFlag = flagValue(args, '--scope')
391
- const tokenFlag = flagValue(args, '--token')
392
- // Origin: --backend (matches deploy/publish + the
393
- // origin-selection convention); either overrides UNIWEB_REGISTER_URL / default.
390
+ // The backend you are logged in to (UNIWEB_REGISTER_URL overrides it for a script) —
391
+ // resolveBackendOrigin, like every backend command.
394
392
  const client = new BackendClient({
395
- originFlag: flagValue(args, '--backend'),
396
- token: tokenFlag,
397
393
  args,
398
394
  command: 'Registering'
399
395
  })
@@ -489,7 +485,7 @@ async function runRegister(args = []) {
489
485
  // 0 create / N pick), persist to package.json, and reuse the session token.
490
486
  if (!scope && !isPreview) {
491
487
  const token = await client.token()
492
- const sess = await readRegistryAuth()
488
+ const sess = await readRegistryAuth(client.origin)
493
489
  const derived = await deriveScope({
494
490
  apiBase: client.origin,
495
491
  token,
@@ -606,7 +602,7 @@ async function runRegister(args = []) {
606
602
  )
607
603
  return { exitCode: 2 }
608
604
  }
609
- // Submit — the client carries the bearer (--token › UNIWEB_TOKEN › stored
605
+ // Submit — the client carries the bearer (UNIWEB_TOKEN › stored
610
606
  // session › login), resolved lazily on this first authed call.
611
607
  info(`Submitting to ${colors.dim}${client.origin}${colors.reset} …`)
612
608
  let res
@@ -615,7 +611,7 @@ async function runRegister(args = []) {
615
611
  } catch (err) {
616
612
  error(`Could not reach the registry at ${client.origin}: ${err.message}`)
617
613
  log(
618
- ` ${colors.dim}Set the endpoint with --backend <url> or UNIWEB_REGISTER_URL.${colors.reset}`
614
+ ` ${colors.dim}Is that the backend you meant? Switch with: uniweb login --backend <url>${colors.reset}`
619
615
  )
620
616
  return { exitCode: 2 }
621
617
  }
@@ -650,7 +646,7 @@ async function runRegister(args = []) {
650
646
  ` ${colors.dim}The registry didn't accept your credentials — it may use different ones than \`uniweb login\`.${colors.reset}`
651
647
  )
652
648
  log(
653
- ` ${colors.dim}Supply a registry bearer with --token <bearer> (or UNIWEB_TOKEN); an existing one may be wrong or expired.${colors.reset}`
649
+ ` ${colors.dim}Log in again (uniweb login --backend <url>), or check UNIWEB_TOKEN — an existing one may be wrong or expired.${colors.reset}`
654
650
  )
655
651
  }
656
652
  // ⭐ **Surface `detail` as a sentence, not as a JSON dump.** The registry
@@ -24,14 +24,13 @@ import { existsSync, readFileSync } from 'node:fs'
24
24
  import { join } from 'node:path'
25
25
  import yaml from 'js-yaml'
26
26
 
27
- import { resolveSiteDir, resolveSiteBackend } from './deploy.js'
27
+ import { resolveSiteDir } from './deploy.js'
28
28
  import { probeUnpushed } from '../backend/site-sync.js'
29
- import { BackendClient } from '../backend/client.js'
30
- import { readFlagValue } from '../utils/args.js'
29
+ import { BackendClient, resolveBackendOrigin } from '../backend/client.js'
30
+ import { readBackendState } from '@uniweb/build/uwx'
31
31
  import { resolveLocalFoundation } from '../backend/foundation-bring-along.js'
32
32
  import { computeFoundationDigest } from '../utils/code-upload.js'
33
33
  import { checkFlags } from '../utils/flag-guard.js'
34
- import { readSiteIdentity } from '../utils/site-identity.js'
35
34
 
36
35
  const c = {
37
36
  reset: '\x1b[0m',
@@ -87,15 +86,23 @@ export async function status(args = []) {
87
86
  const remote = args.includes('--remote')
88
87
  const siteDir = await resolveSiteDir(args, 'status')
89
88
  const siteYml = readSiteYml(siteDir)
90
- const uuid = siteYml.$uuid || null
91
89
  const fnd = foundationRef(siteYml)
92
90
  const { scope: fndScope, version: fndVersion } = splitFoundationRef(fnd)
93
91
 
94
92
  // Local content diff — builds the sync packages, never authenticates.
93
+ //
94
+ // ⭐ The ORIGIN is resolved here rather than taken from the client below, because
95
+ // this probe is deliberately offline and the client may never be built. It decides
96
+ // whose asset ids the comparison reads: against the wrong backend every media ref
97
+ // reads as changed.
98
+ const probeBackend = resolveBackendOrigin()
99
+ // ⭐ Identity is this backend's, from sync.json. It read `site.yml::$uuid`, so after
100
+ // step 4 moved the key every project reported itself as never synced.
101
+ const uuid = readBackendState(siteDir, probeBackend).site?.uuid || null
95
102
  let probe = null
96
103
  let probeErr = null
97
104
  try {
98
- probe = await probeUnpushed(siteDir)
105
+ probe = await probeUnpushed(siteDir, { backend: probeBackend })
99
106
  } catch (err) {
100
107
  probeErr = err.message
101
108
  }
@@ -108,11 +115,6 @@ export async function status(args = []) {
108
115
  if (remote) {
109
116
  try {
110
117
  const client = new BackendClient({
111
- originFlag:
112
- readFlagValue(args, '--backend'),
113
- siteScope: readSiteIdentity(siteDir).backend,
114
- siteBackend: await resolveSiteBackend(siteDir),
115
- token: readFlagValue(args, '--token') || undefined,
116
118
  args,
117
119
  command: 'Status'
118
120
  })
@@ -35,7 +35,6 @@
35
35
  * uniweb sync refresh, then push
36
36
  * uniweb sync --no-git skip the git remote half of the refresh
37
37
  * uniweb sync --force forwarded to push (overwrite upstream changes)
38
- * uniweb sync --backend <url> override the backend origin
39
38
  */
40
39
 
41
40
  import { checkFlags } from '../utils/flag-guard.js'
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-09-20T19:01:50.411Z",
3
+ "generatedAt": "2026-09-21T13:20:05.572Z",
4
4
  "packages": {
5
5
  "@uniweb/api": {
6
6
  "version": "0.4.1",
@@ -10,7 +10,7 @@
10
10
  ]
11
11
  },
12
12
  "@uniweb/build": {
13
- "version": "0.55.1",
13
+ "version": "0.56.0",
14
14
  "path": "framework/build",
15
15
  "deps": [
16
16
  "@uniweb/content-reader",
@@ -126,7 +126,7 @@
126
126
  "deps": []
127
127
  },
128
128
  "@uniweb/unipress": {
129
- "version": "0.9.26",
129
+ "version": "0.9.27",
130
130
  "path": "framework/unipress",
131
131
  "deps": [
132
132
  "@uniweb/build",