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.
- package/package.json +5 -5
- 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 +3 -3
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.60.0",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -43,13 +43,13 @@
|
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
44
|
"@uniweb/content-writer": "^0.3.4",
|
|
45
45
|
"@uniweb/core": "^0.29.6",
|
|
46
|
-
"@uniweb/runtime": "^0.27.2",
|
|
47
|
-
"@uniweb/semantic-parser": "^1.4.1",
|
|
48
46
|
"@uniweb/kit": "^0.19.3",
|
|
49
|
-
"@uniweb/
|
|
47
|
+
"@uniweb/runtime": "^0.27.2",
|
|
48
|
+
"@uniweb/schemas": "^0.3.3",
|
|
49
|
+
"@uniweb/semantic-parser": "^1.4.1"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
|
-
"@uniweb/build": "^0.
|
|
52
|
+
"@uniweb/build": "^0.56.0",
|
|
53
53
|
"@uniweb/content-reader": "^1.2.5",
|
|
54
54
|
"@uniweb/semantic-parser": "^1.4.1"
|
|
55
55
|
},
|
package/partials/agents.md
CHANGED
|
@@ -2526,7 +2526,7 @@ uniweb push / pull / clone / status # Git-style content sync with the Uniweb b
|
|
|
2526
2526
|
uniweb refresh / sync # Catch up (git + backend, never pushes) / catch up, then push
|
|
2527
2527
|
uniweb push --org @acme # First push/publish of a site: who owns it (see below)
|
|
2528
2528
|
uniweb register [--scope @org] # Register a foundation + its data schemas to the registry
|
|
2529
|
-
uniweb login / logout #
|
|
2529
|
+
uniweb login / logout # One backend at a time: log in (--backend <url>) or out
|
|
2530
2530
|
uniweb org list / create <handle> # Publish orgs you belong to — the @org in a scoped ref
|
|
2531
2531
|
uniweb content export [dir] # Package a site (or a built foundation's schema) as .uwx
|
|
2532
2532
|
|
|
@@ -2560,7 +2560,7 @@ uniweb <command> --help # Per-command flags — no side effects. Prefe
|
|
|
2560
2560
|
| Any static host at all | `uniweb export` | full control; no Uniweb account needed |
|
|
2561
2561
|
| Uniweb Cloud | `uniweb publish` | teams with non-technical content authors, or client work |
|
|
2562
2562
|
|
|
2563
|
-
`uniweb deploy` never assumes a host: with nothing configured it opens a picker listing only destinations it can act on, and records the choice in `deploy.yml` so later runs go straight there. On Cloudflare Pages / Netlify / Vercel, `add ci` also adds per-PR previews that comment the URL. Adapters: `github-pages`, `cloudflare-pages`, `netlify`, `vercel`, plus `s3-cloudfront` for `deploy`. That record is `deploy.yml`, beside `site.yml` — **the CLI writes it, you don't create it**: the first successful deploy scaffolds the whole file, and later deploys rewrite only its `
|
|
2563
|
+
`uniweb deploy` never assumes a host: with nothing configured it opens a picker listing only destinations it can act on, and records the choice in `deploy.yml` so later runs go straight there. On Cloudflare Pages / Netlify / Vercel, `add ci` also adds per-PR previews that comment the URL. Adapters: `github-pages`, `cloudflare-pages`, `netlify`, `vercel`, plus `s3-cloudfront` for `deploy`. That record is `deploy.yml`, beside `site.yml` — **the CLI writes it, you don't create it**: the first successful deploy scaffolds the whole file, and later deploys rewrite only its `deploys:` block. Edit `targets:` when you want to change where a site ships. Host credentials come from the environment, never from that committed file.
|
|
2564
2564
|
|
|
2565
2565
|
Foundations have their own free path too: `uniweb add ci --target foundation` publishes to permanent versioned URLs on GitHub Pages.
|
|
2566
2566
|
|
|
@@ -2582,7 +2582,7 @@ Foundations have their own free path too: `uniweb add ci --target foundation` pu
|
|
|
2582
2582
|
> uniweb publish --personal # your personal account, deliberately
|
|
2583
2583
|
> ```
|
|
2584
2584
|
>
|
|
2585
|
-
> The answer is recorded in `
|
|
2585
|
+
> The answer is recorded in `sync.json` and committed, so it is a one-time choice per site,
|
|
2586
2586
|
> not per machine. **Ask the human which one to use** rather than picking for them — a site in
|
|
2587
2587
|
> the wrong org cannot be moved from here. Sites that already exist are unaffected: their
|
|
2588
2588
|
> ownership is settled, so nothing is asked.
|
|
@@ -2598,11 +2598,13 @@ Foundations have their own free path too: `uniweb add ci --target foundation` pu
|
|
|
2598
2598
|
|
|
2599
2599
|
**Nobody's work is overwritten without asking.** `uniweb push` is refused if the site changed on the backend since your last pull — usually an author editing in the app — and reports what changed; nothing is written. Edits to different sections never collide. `uniweb pull` overwrites rather than merges, so it refuses while you have uncommitted changes. The recovery for both is `uniweb pull --merge` (or `uniweb refresh`, which runs `git pull` first): changes to different parts of a file combine silently, and a genuine overlap leaves conflict markers and a non-zero exit — so `uniweb pull --merge && uniweb push` never ships markers. `--force` is the deliberate overwrite (on `push` it replaces the backend's changes, on `pull` it discards yours); don't use it to get past a refusal. `uniweb sync` is `refresh` then `push`; neither publishes.
|
|
2600
2600
|
|
|
2601
|
+
**`sync.json` says which site this is, on each backend.** The first push to a backend records what that backend assigned — the site's id and owner, the ids of its records and uploaded files — in `sync.json` beside `site.yml`. Commit it; never edit it. **To make a new site from a copy of a project, run `uniweb forget --all` in the copy before its first push** — the copy carries the original's `sync.json`, so otherwise its push updates the original's site. When two projects in one workspace hold the same site, a push or publish from either is refused until that is done. `uniweb forget --backend <url>` removes just one backend's records, such as a scratch server's. A record file's `$uuid` is its own id and stays in both cases. **`push`, `pull` and `publish` go to the backend you are logged in to** — the last `uniweb login --backend <url>` — and never to one you are not logged in to. A bare `uniweb login` logs in to https://uniweb.app; for any other backend, name it — logging in is how you switch, and the backend commands take no `--backend` of their own. When the project has no site on that backend but has one elsewhere, a push says so before creating a new one.
|
|
2602
|
+
|
|
2601
2603
|
**The Cloud also provides a real backend for structured data:** a database for every registered data schema, and a CMS that edits both static page content and dynamic data entities typed by those schemas. That's the piece that makes it viable for teams and client work — the client manages records, not markdown files.
|
|
2602
2604
|
|
|
2603
2605
|
Either side can publish. Nothing about this changes how you build: the same foundation and the same site run under `uniweb dev`, `uniweb export`, or a CI deploy with no account at all.
|
|
2604
2606
|
|
|
2605
|
-
**Publishing vs registering.** Foundations on Uniweb Cloud live in the catalog as `@org/name@version`. When a foundation powers a single site, **don't run `uniweb register` yourself** — `uniweb publish` from the site directory releases the local foundation to the catalog (when its code changed) and goes live in one step. Register deliberately only when the foundation is a product meant for multiple sites; consuming sites then pin `foundation: '@org/name@1.2.3'`. **The catalog is private and access-segregated, not a public package registry** — people see only the foundations licensed to sites they own or edit. The *site* carries the license, and it rides along with site ownership when a developer hands a site to a client. Don't describe publishing as making a foundation publicly discoverable. Schemas can also be registered on their own from a schemas-only package (`@uniweb/schemas`, any `@org/schemas`, or a bare folder of `schemas/*.{yml,json,js}`) — that's how `@std` schemas are published. Auth via `uniweb login
|
|
2607
|
+
**Publishing vs registering.** Foundations on Uniweb Cloud live in the catalog as `@org/name@version`. When a foundation powers a single site, **don't run `uniweb register` yourself** — `uniweb publish` from the site directory releases the local foundation to the catalog (when its code changed) and goes live in one step. Register deliberately only when the foundation is a product meant for multiple sites; consuming sites then pin `foundation: '@org/name@1.2.3'`. **The catalog is private and access-segregated, not a public package registry** — people see only the foundations licensed to sites they own or edit. The *site* carries the license, and it rides along with site ownership when a developer hands a site to a client. Don't describe publishing as making a foundation publicly discoverable. Schemas can also be registered on their own from a schemas-only package (`@uniweb/schemas`, any `@org/schemas`, or a bare folder of `schemas/*.{yml,json,js}`) — that's how `@std` schemas are published. Auth via `uniweb login` (`uniweb login --backend <url> --token <bearer>` without a terminal) or `UNIWEB_TOKEN`; preview with `--dry-run`.
|
|
2606
2608
|
|
|
2607
2609
|
### Staying current
|
|
2608
2610
|
|
|
@@ -35,8 +35,9 @@ import { mkdirSync, existsSync, writeFileSync } from 'node:fs'
|
|
|
35
35
|
import { dirname, join } from 'node:path'
|
|
36
36
|
import { ASSET_SLOTS } from '@uniweb/semantic-parser'
|
|
37
37
|
import {
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
readBackendState,
|
|
39
|
+
updateBackendMap,
|
|
40
|
+
carryServed,
|
|
40
41
|
servedFingerprint
|
|
41
42
|
} from '@uniweb/build/uwx'
|
|
42
43
|
|
|
@@ -116,7 +117,7 @@ export async function downloadMissingAssets({
|
|
|
116
117
|
}) {
|
|
117
118
|
const doFetch = fetchImpl || ((u) => globalThis.fetch(u))
|
|
118
119
|
// Read first: a bare-string reference is only recognizable through the map.
|
|
119
|
-
const map =
|
|
120
|
+
const map = readBackendState(siteDir, origin).assets || {}
|
|
120
121
|
const refs = collectAssetRefs(document, map)
|
|
121
122
|
const out = { downloaded: [], present: [], failed: [], skipped: [] }
|
|
122
123
|
if (!refs.length) return out
|
|
@@ -166,6 +167,7 @@ export async function downloadMissingAssets({
|
|
|
166
167
|
|
|
167
168
|
// Newly-landed assets become known ones, so the next pull restores them to
|
|
168
169
|
// this path rather than fetching them again to a second location.
|
|
169
|
-
if (Object.keys(learned).length)
|
|
170
|
+
if (Object.keys(learned).length)
|
|
171
|
+
updateBackendMap(siteDir, origin, 'assets', learned, carryServed)
|
|
170
172
|
return out
|
|
171
173
|
}
|
package/src/backend/client.js
CHANGED
|
@@ -42,65 +42,31 @@ import { uploadFoundationCode } from '../utils/code-upload.js'
|
|
|
42
42
|
import { uploadSiteAssets } from '../utils/asset-upload.js'
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
|
-
* Resolve the backend
|
|
46
|
-
* first). A full URL is reduced to its origin, so callers may pass a whole
|
|
47
|
-
* endpoint URL; an unparseable value falls through to the next tier.
|
|
45
|
+
* Resolve the backend a command talks to:
|
|
48
46
|
*
|
|
49
|
-
* 1.
|
|
50
|
-
* 2.
|
|
51
|
-
* 3.
|
|
52
|
-
*
|
|
53
|
-
* 5–7. the logged-in session origin > ~/.uniweb/config.json > the default
|
|
54
|
-
* (uniweb.app) — all via getRegistryApiBaseUrl()
|
|
47
|
+
* 1. UNIWEB_REGISTER_URL env — the override for automation (CI, scripts), one process
|
|
48
|
+
* 2. ⭐ the backend the user is LOGGED IN TO — the one session there is
|
|
49
|
+
* 3. the default backend — ~/.uniweb/config.json `registryApiUrl`, else uniweb.app —
|
|
50
|
+
* where the command's first request then asks the user to log in
|
|
55
51
|
*
|
|
56
|
-
* ⭐
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* project already answered.
|
|
52
|
+
* ⭐ **That is the whole ladder, for every command** *[Diego, 2026-09-21: "push and pull
|
|
53
|
+
* are also meant to go to the backend you are logged into" · "We do not allow any
|
|
54
|
+
* communication with backend if the user is not logged into a backend. The default
|
|
55
|
+
* backend for login, if not specified, is uniweb.app" · switching "via login to another
|
|
56
|
+
* backend is good, and the only way to switch"]*. Logging in is how a backend is chosen.
|
|
62
57
|
*
|
|
63
|
-
*
|
|
64
|
-
* `
|
|
65
|
-
*
|
|
66
|
-
*
|
|
58
|
+
* ⛔ **No `--backend` tier, and no project tier.** The backend verbs had a per-command
|
|
59
|
+
* `--backend` until 2026-09-21 — it predates per-backend sessions, when one session
|
|
60
|
+
* slot made "aim this one command elsewhere" a flag's job; switching is a login now, and
|
|
61
|
+
* a script aims with UNIWEB_REGISTER_URL without touching the machine's login. A
|
|
62
|
+
* project's sync.json and deploy.yml routed commands too, until the same day.
|
|
63
|
+
* `--backend` survives only where it SELECTS rather than routes: `login` (where to log
|
|
64
|
+
* in) and `forget` (which backend's records to remove). `logout` needs none: there is
|
|
65
|
+
* one session.
|
|
67
66
|
*
|
|
68
|
-
* ⚠️ The explicit overrides stay ON TOP deliberately. `--backend` and the env var are how
|
|
69
|
-
* you deliberately aim elsewhere — at a staging mirror, say — and a project file must not
|
|
70
|
-
* be able to veto a flag the user just typed. A wrong aim is then caught by the guard
|
|
71
|
-
* rather than silently redirected.
|
|
72
|
-
*
|
|
73
|
-
* @param {string} [flag] - the raw value of --backend, if supplied
|
|
74
|
-
* @param {object} [opts]
|
|
75
|
-
* @param {string} [opts.siteScope] - the project's `site.yml::$backend`
|
|
76
|
-
* @param {string} [opts.siteBackend] - a site's deploy.yml-bound backend origin
|
|
77
67
|
* @returns {string} a bare origin with no trailing slash
|
|
78
68
|
*/
|
|
79
|
-
export function resolveBackendOrigin(
|
|
80
|
-
const norm = (v) => {
|
|
81
|
-
try {
|
|
82
|
-
return new URL(v).origin
|
|
83
|
-
} catch {
|
|
84
|
-
return null
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
if (flag) {
|
|
88
|
-
const o = norm(flag)
|
|
89
|
-
if (o) return o
|
|
90
|
-
}
|
|
91
|
-
const env = process.env.UNIWEB_REGISTER_URL
|
|
92
|
-
if (env) {
|
|
93
|
-
const o = norm(env)
|
|
94
|
-
if (o) return o
|
|
95
|
-
}
|
|
96
|
-
if (siteScope) {
|
|
97
|
-
const o = norm(siteScope)
|
|
98
|
-
if (o) return o
|
|
99
|
-
}
|
|
100
|
-
if (siteBackend) {
|
|
101
|
-
const o = norm(siteBackend)
|
|
102
|
-
if (o) return o
|
|
103
|
-
}
|
|
69
|
+
export function resolveBackendOrigin() {
|
|
104
70
|
return getRegistryApiBaseUrl()
|
|
105
71
|
}
|
|
106
72
|
|
|
@@ -142,10 +108,8 @@ export const DISCOVERY_DEFAULTS = {}
|
|
|
142
108
|
export class BackendClient {
|
|
143
109
|
/**
|
|
144
110
|
* @param {object} [opts]
|
|
145
|
-
* @param {string} [opts.origin] - explicit origin (wins over
|
|
146
|
-
*
|
|
147
|
-
* @param {string} [opts.siteScope] - the project's `site.yml::$backend` (site verbs)
|
|
148
|
-
* @param {string} [opts.siteBackend] - a site's deploy.yml-bound backend (site verbs)
|
|
111
|
+
* @param {string} [opts.origin] - explicit origin (wins over the ladder; tests and
|
|
112
|
+
* internal callers — no command takes one from the user)
|
|
149
113
|
* @param {string} [opts.token] - explicit bearer (wins over env + stored session)
|
|
150
114
|
* @param {() => Promise<string>} [opts.getToken] - injected bearer resolver (tests, or
|
|
151
115
|
* callers with their own auth); used when no explicit token/env is present
|
|
@@ -155,9 +119,6 @@ export class BackendClient {
|
|
|
155
119
|
*/
|
|
156
120
|
constructor({
|
|
157
121
|
origin,
|
|
158
|
-
originFlag,
|
|
159
|
-
siteScope,
|
|
160
|
-
siteBackend,
|
|
161
122
|
token,
|
|
162
123
|
getToken,
|
|
163
124
|
args = [],
|
|
@@ -165,7 +126,7 @@ export class BackendClient {
|
|
|
165
126
|
fetchImpl
|
|
166
127
|
} = {}) {
|
|
167
128
|
this.origin = (
|
|
168
|
-
origin || resolveBackendOrigin(
|
|
129
|
+
origin || resolveBackendOrigin()
|
|
169
130
|
).replace(/\/+$/, '')
|
|
170
131
|
this._token = token || process.env.UNIWEB_TOKEN || null
|
|
171
132
|
this._getToken = getToken || null
|
|
@@ -176,9 +137,9 @@ export class BackendClient {
|
|
|
176
137
|
}
|
|
177
138
|
|
|
178
139
|
/**
|
|
179
|
-
* The session bearer, resolved lazily and memoized. Order
|
|
180
|
-
*
|
|
181
|
-
* session > interactive login (ensureRegistryAuth).
|
|
140
|
+
* The session bearer, resolved lazily and memoized. Order: an explicit `token`
|
|
141
|
+
* (constructor — tests and internal callers; no command takes one from the user) /
|
|
142
|
+
* UNIWEB_TOKEN > the stored session > interactive login (ensureRegistryAuth).
|
|
182
143
|
* @returns {Promise<string>}
|
|
183
144
|
*/
|
|
184
145
|
async token() {
|
|
@@ -187,27 +148,13 @@ export class BackendClient {
|
|
|
187
148
|
this._token = await this._getToken()
|
|
188
149
|
return this._token
|
|
189
150
|
}
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if (
|
|
198
|
-
stored?.token &&
|
|
199
|
-
!isExpired(stored) &&
|
|
200
|
-
stored.origin &&
|
|
201
|
-
stored.origin.replace(/\/+$/, '') !== this.origin
|
|
202
|
-
) {
|
|
203
|
-
console.error(
|
|
204
|
-
`\x1b[33m⚠\x1b[0m Logged in to ${stored.origin}, but this command targets ${this.origin} — the session may be rejected. Run \`uniweb login --backend ${this.origin}\`, or pass --token.`
|
|
205
|
-
)
|
|
206
|
-
}
|
|
207
|
-
} catch {
|
|
208
|
-
/* advisory only */
|
|
209
|
-
}
|
|
210
|
-
}
|
|
151
|
+
// ⛔ NO ORIGIN-MISMATCH GUARD HERE, AND DO NOT RE-ADD ONE (removed 2026-09-20).
|
|
152
|
+
// It existed because the session store held ONE record: a session for another
|
|
153
|
+
// backend was the only session, so it was about to be sent here and rejected, and a
|
|
154
|
+
// warning was the best available move. Sessions are keyed by origin now
|
|
155
|
+
// (utils/registry-auth.js), so being logged into another backend is just a fact
|
|
156
|
+
// about another backend — `ensureRegistryAuth` below finds this origin's session or
|
|
157
|
+
// asks for one. Warning about the other would be noise about nothing.
|
|
211
158
|
this._token = await ensureRegistryAuth({
|
|
212
159
|
apiBase: this.origin,
|
|
213
160
|
command: this._command,
|
|
@@ -255,8 +202,8 @@ export class BackendClient {
|
|
|
255
202
|
// ── Discovery ─────────────────────────────────────────────────────────────────
|
|
256
203
|
|
|
257
204
|
/**
|
|
258
|
-
* The session bearer IF one can be had without asking — an explicit
|
|
259
|
-
* env var, or a stored unexpired session. Never prompts, never logs in, returns null
|
|
205
|
+
* The session bearer IF one can be had without asking — an explicit `token`, the
|
|
206
|
+
* UNIWEB_TOKEN env var, or a stored unexpired session. Never prompts, never logs in, returns null
|
|
260
207
|
* instead. `token()` is the one that may block; this is for calls that want to be
|
|
261
208
|
* authenticated when possible but must not *cause* an authentication.
|
|
262
209
|
* @returns {Promise<string|null>}
|
|
@@ -276,7 +223,7 @@ export class BackendClient {
|
|
|
276
223
|
}
|
|
277
224
|
}
|
|
278
225
|
try {
|
|
279
|
-
const stored = await readRegistryAuth()
|
|
226
|
+
const stored = await readRegistryAuth(this.origin)
|
|
280
227
|
if (stored?.token && !isExpired(stored)) return stored.token
|
|
281
228
|
} catch {
|
|
282
229
|
/* advisory — a missing or unreadable session is simply "no token" */
|
|
@@ -455,8 +402,8 @@ export class BackendClient {
|
|
|
455
402
|
* the field itself is required and cannot be blank.
|
|
456
403
|
*
|
|
457
404
|
* NOT idempotent: two calls mint two sites (a name is not a unique key, by
|
|
458
|
-
* design). Callers must guard on
|
|
459
|
-
* immediately — see `ensureSiteExists`.
|
|
405
|
+
* design). Callers must guard on the site's uuid for this backend (sync.json) and
|
|
406
|
+
* write the result back immediately — see `ensureSiteExists`.
|
|
460
407
|
*
|
|
461
408
|
* @param {{ name: string, foundation: string, asOrg?: string|null }} opts
|
|
462
409
|
* @returns {Promise<Response>} `{ site_content_uuid }`
|
|
@@ -545,7 +492,7 @@ export class BackendClient {
|
|
|
545
492
|
/**
|
|
546
493
|
* POST /dev/site/publish/{uuid} — CMS-publish a synced site (make its CURRENT
|
|
547
494
|
* backend state live; it does NOT push local files). `{uuid}` is the site-content
|
|
548
|
-
* uuid (
|
|
495
|
+
* uuid on this backend (sync.json); a never-synced site 404s (sync first, or use deploy).
|
|
549
496
|
* Languages, when present, go in the body; absent → no body. Returns the raw
|
|
550
497
|
* Response ({ deploy_uuid, url, published_folder_uuid, status } on 200).
|
|
551
498
|
*
|
|
@@ -38,7 +38,6 @@ import { execFileSync } from 'node:child_process'
|
|
|
38
38
|
|
|
39
39
|
import { detectFoundationType, isExtensionUrl } from '@uniweb/build'
|
|
40
40
|
import { computeFoundationDigest } from '../utils/code-upload.js'
|
|
41
|
-
import { readFlagValue } from '../utils/args.js'
|
|
42
41
|
import { isNonInteractive } from '../utils/interactive.js'
|
|
43
42
|
import { compareSemverPrecedence } from '../utils/semver-precedence.js'
|
|
44
43
|
|
|
@@ -151,14 +150,12 @@ function readPkgField(dir, field) {
|
|
|
151
150
|
}
|
|
152
151
|
}
|
|
153
152
|
|
|
154
|
-
//
|
|
155
|
-
//
|
|
153
|
+
// What travels to the spawned `uniweb register` / `build` on the command line: only
|
|
154
|
+
// --non-interactive. The BACKEND travels in the child's environment
|
|
155
|
+
// (releaseFoundation), and the SESSION is the shared session file — or UNIWEB_TOKEN,
|
|
156
|
+
// which the child inherits. The backend commands take no `--backend` or `--token`.
|
|
156
157
|
function forwardedFlags(args) {
|
|
157
158
|
const out = []
|
|
158
|
-
for (const name of ['--backend', '--token']) {
|
|
159
|
-
const v = readFlagValue(args, name)
|
|
160
|
-
if (v) out.push(name, v)
|
|
161
|
-
}
|
|
162
159
|
if (isNonInteractive(args)) out.push('--non-interactive')
|
|
163
160
|
return out
|
|
164
161
|
}
|
|
@@ -322,7 +319,7 @@ async function bringLocalCodeAlong({
|
|
|
322
319
|
}
|
|
323
320
|
say.info(`Releasing the ${kind} ${label} (not yet registered)…`)
|
|
324
321
|
return {
|
|
325
|
-
released: releaseFoundation(local, args, cliBin, say),
|
|
322
|
+
released: releaseFoundation(local, args, cliBin, say, client?.origin),
|
|
326
323
|
proceed: true,
|
|
327
324
|
ref: pinnedRef()
|
|
328
325
|
}
|
|
@@ -365,7 +362,7 @@ async function bringLocalCodeAlong({
|
|
|
365
362
|
: `Releasing the ${kind} ${label} — not newer than the registered latest ${reg.latest_version}, so the registry takes it only if ${local.version} is already registered with this code…`
|
|
366
363
|
)
|
|
367
364
|
return {
|
|
368
|
-
released: releaseFoundation(local, args, cliBin, say),
|
|
365
|
+
released: releaseFoundation(local, args, cliBin, say, client?.origin),
|
|
369
366
|
proceed: true,
|
|
370
367
|
ref: pinnedRef()
|
|
371
368
|
}
|
|
@@ -396,7 +393,7 @@ async function bringLocalCodeAlong({
|
|
|
396
393
|
)
|
|
397
394
|
if (reRelease)
|
|
398
395
|
return {
|
|
399
|
-
released: releaseFoundation(local, args, cliBin, say),
|
|
396
|
+
released: releaseFoundation(local, args, cliBin, say, client?.origin),
|
|
400
397
|
proceed: true,
|
|
401
398
|
ref: pinnedRef()
|
|
402
399
|
}
|
|
@@ -490,12 +487,15 @@ function buildFoundation(local, cliBin) {
|
|
|
490
487
|
// Release = `uniweb register` in the foundation directory (the one foundation
|
|
491
488
|
// release path). Returns true on success; throws to the caller on failure so
|
|
492
489
|
// publish stops before going live with missing code.
|
|
493
|
-
function releaseFoundation(local, args, cliBin, say) {
|
|
490
|
+
function releaseFoundation(local, args, cliBin, say, origin) {
|
|
494
491
|
console.log('')
|
|
492
|
+
// ⭐ Pinned to the parent's backend through the environment, so the release lands
|
|
493
|
+
// where the publish goes by construction — not because two processes happen to read
|
|
494
|
+
// the same session file the same way.
|
|
495
495
|
execFileSync('node', [cliBin, 'register', ...forwardedFlags(args)], {
|
|
496
496
|
cwd: local.dir,
|
|
497
497
|
stdio: 'inherit',
|
|
498
|
-
env: process.env
|
|
498
|
+
env: origin ? { ...process.env, UNIWEB_REGISTER_URL: origin } : process.env
|
|
499
499
|
})
|
|
500
500
|
console.log('')
|
|
501
501
|
return true
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
*
|
|
39
39
|
* ## ⛔ A HASH, never the block
|
|
40
40
|
*
|
|
41
|
-
* `$secrets`
|
|
41
|
+
* `$secrets` names every secret the site has (values are the `#ref` marker, never
|
|
42
|
+
* secret material — the old wording here said otherwise), and `$services[].config` is opaque and
|
|
42
43
|
* per-service — anything may be in it. `deploy.yml` is committed, so recording
|
|
43
44
|
* either verbatim would write them into git. Equality is all this gate needs;
|
|
44
45
|
* *what* differs is a question for the backend's own copy of the request, not for
|
|
@@ -91,11 +92,14 @@ function stableString(value) {
|
|
|
91
92
|
* @param {object} siteYml
|
|
92
93
|
* @returns {{servicesRequest?: string, secretsRequest?: string}}
|
|
93
94
|
*/
|
|
94
|
-
export function fingerprintRequest(siteYml) {
|
|
95
|
+
export function fingerprintRequest(siteYml, provisioned = {}) {
|
|
95
96
|
const out = {}
|
|
96
|
-
|
|
97
|
+
// ⭐ Two sources, on purpose. The PROVISIONED rows come from `sync.json` for the
|
|
98
|
+
// backend being published to (they were `site.yml::$services` / `$secrets` until
|
|
99
|
+
// 2026-09-20). The language selection below is still authored, so still site.yml.
|
|
100
|
+
const services = fingerprintDeclaration(provisioned?.services)
|
|
97
101
|
if (services) out.servicesRequest = services
|
|
98
|
-
const secrets = fingerprintDeclaration(
|
|
102
|
+
const secrets = fingerprintDeclaration(provisioned?.secrets)
|
|
99
103
|
if (secrets) out.secretsRequest = secrets
|
|
100
104
|
// ⭐ The language selection is a request too, and it is the one that moves a
|
|
101
105
|
// PRICE — the line is billed on how many languages go out. Banking it is what
|
|
@@ -124,8 +128,8 @@ export function fingerprintRequest(siteYml) {
|
|
|
124
128
|
* @param {object|null} lastDeploy - `deploy.yml::lastDeploy.<target>`, or null
|
|
125
129
|
* @returns {{declare: boolean, reason: 'no-record'|'changed'|'unchanged'|'undeclared'}}
|
|
126
130
|
*/
|
|
127
|
-
export function decideDeclaration(siteYml, lastDeploy) {
|
|
128
|
-
const now = fingerprintRequest(siteYml)
|
|
131
|
+
export function decideDeclaration(siteYml, lastDeploy, provisioned = {}) {
|
|
132
|
+
const now = fingerprintRequest(siteYml, provisioned)
|
|
129
133
|
if (!now.servicesRequest && !now.secretsRequest) {
|
|
130
134
|
// Nothing in the file to send. The gate is moot; say so rather than
|
|
131
135
|
// reporting "unchanged", which would imply a comparison happened.
|
|
@@ -178,8 +182,8 @@ export function decideDeclaration(siteYml, lastDeploy) {
|
|
|
178
182
|
* @param {object|null} lastDeploy - the banked base
|
|
179
183
|
* @returns {{action:'none'|'adopt'|'send'|'conflict', local:string|null, remote:string|null}}
|
|
180
184
|
*/
|
|
181
|
-
export function reconcileRequest(siteYml, remoteServices, lastDeploy) {
|
|
182
|
-
return reconcile(
|
|
185
|
+
export function reconcileRequest(siteYml, remoteServices, lastDeploy, provisioned = {}) {
|
|
186
|
+
return reconcile(provisioned?.services, remoteServices, lastDeploy?.servicesRequest)
|
|
183
187
|
}
|
|
184
188
|
|
|
185
189
|
/**
|