uniweb 0.59.1 → 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.
- package/package.json +3 -3
- package/partials/agents.md +6 -4
- package/src/backend/asset-download.js +6 -4
- package/src/backend/client.js +38 -91
- package/src/backend/foundation-bring-along.js +12 -12
- package/src/backend/service-request.js +12 -8
- package/src/backend/site-sync.js +243 -164
- package/src/commands/clone.js +42 -39
- package/src/commands/deploy.js +45 -27
- package/src/commands/forget.js +197 -0
- package/src/commands/publish.js +102 -73
- package/src/commands/pull.js +58 -46
- package/src/commands/push.js +69 -53
- package/src/commands/refresh.js +22 -29
- package/src/commands/register.js +10 -14
- package/src/commands/status.js +13 -11
- package/src/commands/sync.js +0 -1
- package/src/framework-index.json +4 -4
- package/src/index.js +100 -75
- package/src/utils/args.js +3 -4
- package/src/utils/asset-upload.js +18 -5
- package/src/utils/config.js +79 -42
- package/src/utils/flag-guard.js +70 -27
- package/src/utils/interactive.js +38 -0
- package/src/utils/records-guard.js +3 -2
- package/src/utils/registry-auth.js +150 -28
- package/src/utils/scaffold.js +11 -0
- package/src/utils/session-file.js +95 -0
- package/src/utils/site-copies.js +123 -0
- package/src/utils/site-data-upload.js +4 -3
- package/src/utils/site-identity.js +89 -299
package/src/commands/push.js
CHANGED
|
@@ -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
|
-
*
|
|
10
|
-
* push has none — it CREATEs the site (uuid-less),
|
|
11
|
-
* and returns the new uuid, which `push` records
|
|
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:
|
|
53
|
-
*
|
|
54
|
-
*
|
|
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 {
|
|
68
|
+
import { updateBackendMap, carryServed, SYNC_STORE_FILE } from '@uniweb/build/uwx'
|
|
68
69
|
import { BackendClient } from '../backend/client.js'
|
|
69
|
-
import { resolveSiteDir
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
// ⛔
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
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
|
|
185
|
-
//
|
|
186
|
-
//
|
|
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.
|
|
193
|
-
//
|
|
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
|
|
196
|
-
if (
|
|
197
|
-
|
|
198
|
-
|
|
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
|
|
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 =
|
|
476
|
+
const rec = updateBackendMap(siteDir, client.origin, 'assets', ids, carryServed)
|
|
462
477
|
if (rec.written) {
|
|
463
478
|
note(
|
|
464
|
-
`${
|
|
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 } : {})
|
package/src/commands/refresh.js
CHANGED
|
@@ -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
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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`
|
package/src/commands/register.js
CHANGED
|
@@ -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
|
|
37
|
-
*
|
|
38
|
-
*
|
|
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
|
-
|
|
392
|
-
//
|
|
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 (
|
|
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}
|
|
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}
|
|
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
|
package/src/commands/status.js
CHANGED
|
@@ -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
|
|
27
|
+
import { resolveSiteDir } from './deploy.js'
|
|
28
28
|
import { probeUnpushed } from '../backend/site-sync.js'
|
|
29
|
-
import { BackendClient } from '../backend/client.js'
|
|
30
|
-
import {
|
|
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
|
})
|
package/src/commands/sync.js
CHANGED
|
@@ -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'
|
package/src/framework-index.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-09-
|
|
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.
|
|
13
|
+
"version": "0.56.0",
|
|
14
14
|
"path": "framework/build",
|
|
15
15
|
"deps": [
|
|
16
16
|
"@uniweb/content-reader",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
]
|
|
84
84
|
},
|
|
85
85
|
"@uniweb/runtime": {
|
|
86
|
-
"version": "0.27.
|
|
86
|
+
"version": "0.27.2",
|
|
87
87
|
"path": "framework/runtime",
|
|
88
88
|
"deps": [
|
|
89
89
|
"@uniweb/core",
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
"deps": []
|
|
127
127
|
},
|
|
128
128
|
"@uniweb/unipress": {
|
|
129
|
-
"version": "0.9.
|
|
129
|
+
"version": "0.9.27",
|
|
130
130
|
"path": "framework/unipress",
|
|
131
131
|
"deps": [
|
|
132
132
|
"@uniweb/build",
|