uniweb 0.81.2 → 0.82.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 +6 -6
- package/partials/agents.md +4 -4
- package/src/backend/client.js +8 -2
- package/src/backend/payment-handoff.js +151 -13
- package/src/commands/build.js +1 -7
- package/src/commands/publish.js +25 -8
- package/src/framework-index.json +11 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.82.0",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -42,15 +42,15 @@
|
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
44
|
"@uniweb/content-writer": "^0.3.4",
|
|
45
|
-
"@uniweb/
|
|
46
|
-
"@uniweb/
|
|
47
|
-
"@uniweb/runtime": "^0.28.10",
|
|
45
|
+
"@uniweb/kit": "^0.19.16",
|
|
46
|
+
"@uniweb/core": "^0.37.0",
|
|
48
47
|
"@uniweb/semantic-parser": "^1.5.1",
|
|
49
|
-
"@uniweb/schemas": "^0.
|
|
48
|
+
"@uniweb/schemas": "^0.11.0",
|
|
49
|
+
"@uniweb/runtime": "^0.29.0"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
|
-
"@uniweb/build": "^0.75.1",
|
|
53
52
|
"@uniweb/content-reader": "^1.2.8",
|
|
53
|
+
"@uniweb/build": "^0.76.0",
|
|
54
54
|
"@uniweb/semantic-parser": "^1.5.1"
|
|
55
55
|
},
|
|
56
56
|
"peerDependenciesMeta": {
|
package/partials/agents.md
CHANGED
|
@@ -1762,7 +1762,7 @@ export default function DocsLayout({ header, body, footer, left, right, params }
|
|
|
1762
1762
|
|
|
1763
1763
|
Layouts are full components with their own `params` in `meta.js`, not just structural wrappers — a header height or sidebar width is a layout param.
|
|
1764
1764
|
|
|
1765
|
-
**Layout `meta.js`** declares areas and optional scroll behavior: `{ areas: ['header', 'footer', 'left'], scroll: 'self' }`. Area names are arbitrary. `scroll` controls scroll restoration: unset = runtime manages `window` (default), `'self'` = the layout scrolls itself, or a CSS selector (`'main'`) = runtime manages that element.
|
|
1765
|
+
**Layout `meta.js`** declares areas and optional scroll behavior: `{ areas: ['header', 'footer', 'left'], scroll: 'self' }`. Area names are arbitrary. `areas` tells an editor what to offer; a page renders the areas its site's `layout/` folder fills. `scroll` controls scroll restoration: unset = runtime manages `window` (default), `'self'` = the layout scrolls itself, or a CSS selector (`'main'`) = runtime manages that element.
|
|
1766
1766
|
|
|
1767
1767
|
Two optional keys tune how areas behave across a navigation. `transitions` renames or opts regions out of per-area view transitions (`{ left: null }`, or `false` for the whole layout). `layers` sets which area paints on top — every area is stacked above the body by default, so a fixed header works without declaring anything, but areas are equal to each other, so a layout whose chrome overlaps says which wins: `layers: { header: 2, left: 1 }`. Both take an object to override per region, or `false` to opt out. **A `z-index` inside an area cannot lift it past another area** — each area is its own stacking context — so reach for `layers` rather than a bigger number, and for `<Overlay>` when a modal needs to escape the area entirely.
|
|
1768
1768
|
|
|
@@ -1890,7 +1890,7 @@ function Article({ content, block }) {
|
|
|
1890
1890
|
|
|
1891
1891
|
When a record genuinely needs to be a single object, that's the foundation's job — read `[0]`, or reshape once with a `handlers.data` hook.
|
|
1892
1892
|
|
|
1893
|
-
**Declaring keys and schemas.** `meta.js` declares each `content.data` key the component receives, with its schema, in a single `data:` field — there is no separate `schemas:` key. Each value is a **named ref**, an **inline field map**, an **inline rich-form** (`{ fields: [...] }`, an editor form), or `{}` for a key with no schema (an external API's records). Refs resolve on disk at build time, never fetched: `@/name` (this foundation's `schemas/`), `@std/name` (shared standards, from `@uniweb/schemas`), `@org/name` (an org's own `@org/schemas` package). A schema
|
|
1893
|
+
**Declaring keys and schemas.** `meta.js` declares each `content.data` key the component receives, with its schema, in a single `data:` field — there is no separate `schemas:` key. Each value is a **named ref**, an **inline field map**, an **inline rich-form** (`{ fields: [...] }`, an editor form), or `{}` for a key with no schema (an external API's records). Refs resolve on disk at build time, never fetched: `@/name` (this foundation's `schemas/`), `@std/name` (shared standards, from `@uniweb/schemas`), `@org/name` (an org's own `@org/schemas` package). A schema drives the editor, checks your data (`uniweb validate`), and lets a fetch of another name fill the key — it fills no field: **a record reaches the component as it is**, so a field it lacks is absent, and what that renders as is your component's choice (`record.status ?? 'published'`). **A named schema declares no `default:`** — the build refuses one; a `default:` belongs only in an inline field map or form, where an editor pre-fills from it. ⛔ **Delivery was default-on until this change** — every fetched key reached every component, and `data:` was a hint; a component that reads a key it does not declare now receives nothing under it. `data: false` declares nothing, like no `data:`. Keys a foundation's handlers (or a shared hook) read go in `main.js` `data:`, in the same form, and every section receives them.
|
|
1894
1894
|
|
|
1895
1895
|
```js
|
|
1896
1896
|
// meta.js
|
|
@@ -1898,7 +1898,7 @@ export default {
|
|
|
1898
1898
|
data: {
|
|
1899
1899
|
articles: '@/article', // named ref (this foundation)
|
|
1900
1900
|
authors: '@std/person', // named ref (shared standard)
|
|
1901
|
-
pricing: { tier: { type: 'string'
|
|
1901
|
+
pricing: { tier: { type: 'string' } }, // inline field map
|
|
1902
1902
|
},
|
|
1903
1903
|
}
|
|
1904
1904
|
```
|
|
@@ -2415,7 +2415,7 @@ Content handlers are a transform layer between data assembly and the component,
|
|
|
2415
2415
|
| `content` | After the data handler | `(data, block)` | ProseMirror document, or null | Transform raw content (Loom instantiation, template expansion) |
|
|
2416
2416
|
| `props` | After parsing, defaults, and guarantees | `(content, params, block)` | `{ content, params }`, or null | Post-process the final shape before the component sees it |
|
|
2417
2417
|
|
|
2418
|
-
The `content` handler receives `block.parsedContent.data` — the section's declared keys, its foundation's `main.js` `data:` included, so **declare there every key a handler reads** — and reads raw ProseMirror from `block.rawContent`, returning a new ProseMirror document that the framework re-parses through the semantic parser. Returning `null` — or the same reference as `block.rawContent` — signals no change.
|
|
2418
|
+
The `content` handler receives `block.parsedContent.data` — the section's declared keys, its foundation's `main.js` `data:` included, so **declare there every key a handler reads** — and reads raw ProseMirror from `block.rawContent`, returning a new ProseMirror document that the framework re-parses through the semantic parser. Returning `null` — or the same reference as `block.rawContent` — signals no change. Anything that is not a document is ignored, with a warning once per handler: the first argument is the section's *data*, so a pass-through `(x) => x` returns the data, not the content.
|
|
2419
2419
|
|
|
2420
2420
|
> **`block.rawContent` may or may not be wrapped.** Unwrap it defensively — `const doc = block.rawContent?.doc ?? block.rawContent` — before passing it to `instantiateContent` / `instantiateRepeated`. This is the first thing a hand-written handler gets wrong.
|
|
2421
2421
|
|
package/src/backend/client.js
CHANGED
|
@@ -748,8 +748,14 @@ export class BackendClient {
|
|
|
748
748
|
* @param {string[]} [opts.languages]
|
|
749
749
|
* @returns {Promise<Response>}
|
|
750
750
|
*/
|
|
751
|
-
async publishSite(uuid, { languages } = {}) {
|
|
752
|
-
|
|
751
|
+
async publishSite(uuid, { languages, confirm, unattended = false } = {}) {
|
|
752
|
+
// `confirm`: the token of a confirm the backend offered on a refused publish, sent back as it
|
|
753
|
+
// came, to settle a change to the site's plan and publish in one call. `unattended`: no one
|
|
754
|
+
// answered it — `--yes` did, which the backend settles for a reduction and nothing else.
|
|
755
|
+
const query = confirm
|
|
756
|
+
? `?confirm=${encodeURIComponent(confirm)}${unattended ? '&unattended=true' : ''}`
|
|
757
|
+
: ''
|
|
758
|
+
return this.request(`/dev/site/publish/${encodeURIComponent(uuid)}${query}`, {
|
|
753
759
|
method: 'POST',
|
|
754
760
|
...(languages ? { body: JSON.stringify({ languages }) } : {})
|
|
755
761
|
})
|
|
@@ -91,8 +91,8 @@ const OPENABLE = /^https?:\/\//i
|
|
|
91
91
|
* @param {string} [o.contentType] - the response's content-type header
|
|
92
92
|
* @param {string} [o.body] - the raw response body
|
|
93
93
|
* @returns {{ kind: 'not-payment' }
|
|
94
|
-
* | { kind: 'settle', url: string, handle: string|null, reason: string, message: string|null }
|
|
95
|
-
* | { kind: 'stop', reason: string|null, message: string|null }}
|
|
94
|
+
* | { kind: 'settle', url: string, handle: string|null, reason: string, message: string|null, confirm: { prompt: string, token: string, reduction: boolean }|null }
|
|
95
|
+
* | { kind: 'stop', reason: string|null, message: string|null, confirm: { prompt: string, token: string, reduction: boolean }|null }}
|
|
96
96
|
*/
|
|
97
97
|
export function readPaymentRefusal({ status, contentType = '', body = '' } = {}) {
|
|
98
98
|
if (status !== 402) return { kind: 'not-payment' }
|
|
@@ -134,7 +134,17 @@ export function readPaymentRefusal({ status, contentType = '', body = '' } = {})
|
|
|
134
134
|
// problem body carries the number, so the two are not distinguishable by their
|
|
135
135
|
// fields. Reading a URL out of a body that never promised this envelope is how
|
|
136
136
|
// a `settle` gets synthesised from something that is not a refusal at all.
|
|
137
|
-
if (!isProblem) return { kind: 'stop', reason, message }
|
|
137
|
+
if (!isProblem) return { kind: 'stop', reason, message, confirm: null }
|
|
138
|
+
|
|
139
|
+
// ⭐ A CHANGE THE TERMINAL MAY CONFIRM (since 2026-10-03): `{ prompt, token, reduction }`
|
|
140
|
+
// beside the door, when everything the publish owes is a change to a plan the site already has.
|
|
141
|
+
// `prompt` is the backend's sentence, shown verbatim; `token` goes back as it came. Read only
|
|
142
|
+
// whole — a confirm missing either half is not one.
|
|
143
|
+
const confirm =
|
|
144
|
+
problem.confirm && typeof problem.confirm.prompt === 'string' && problem.confirm.prompt &&
|
|
145
|
+
typeof problem.confirm.token === 'string' && problem.confirm.token
|
|
146
|
+
? { prompt: problem.confirm.prompt, token: problem.confirm.token, reduction: problem.confirm.reduction === true }
|
|
147
|
+
: null
|
|
138
148
|
|
|
139
149
|
// The door, if one was handed over. `remedy_url` is the current spelling — ONE
|
|
140
150
|
// key for every reason, which is what lets a reason we have never heard of still
|
|
@@ -160,7 +170,7 @@ export function readPaymentRefusal({ status, contentType = '', body = '' } = {})
|
|
|
160
170
|
// the platform's URL opener; a `file:` or a `javascript:` URL is not a place a
|
|
161
171
|
// person goes. Refusing them costs a legitimate backend nothing.
|
|
162
172
|
const url = candidate && OPENABLE.test(candidate) ? candidate : null
|
|
163
|
-
if (!url) return { kind: 'stop', reason, message }
|
|
173
|
+
if (!url) return { kind: 'stop', reason, message, confirm }
|
|
164
174
|
|
|
165
175
|
return {
|
|
166
176
|
kind: 'settle',
|
|
@@ -170,7 +180,8 @@ export function readPaymentRefusal({ status, contentType = '', body = '' } = {})
|
|
|
170
180
|
? legacy.handle
|
|
171
181
|
: null,
|
|
172
182
|
reason,
|
|
173
|
-
message
|
|
183
|
+
message,
|
|
184
|
+
confirm
|
|
174
185
|
}
|
|
175
186
|
}
|
|
176
187
|
|
|
@@ -187,9 +198,11 @@ export function readPaymentRefusal({ status, contentType = '', body = '' } = {})
|
|
|
187
198
|
* @param {string[]} o.args - argv slice (for --non-interactive detection)
|
|
188
199
|
* @param {object} o.say - { ok, info, warn, err, dim } reporters
|
|
189
200
|
* @param {(url: string) => Promise<boolean>} [o.open] - injected for tests
|
|
201
|
+
* @param {boolean} [o.interactive] - whether someone is at the terminal, when the caller has decided
|
|
202
|
+
* it already (`settleRefusal`); else read from `args` and stdin
|
|
190
203
|
* @returns {Promise<{ opened: boolean }>}
|
|
191
204
|
*/
|
|
192
|
-
export async function reportPaymentRefusal({ verdict, args = [], say, open }) {
|
|
205
|
+
export async function reportPaymentRefusal({ verdict, args = [], say, open, interactive }) {
|
|
193
206
|
// The backend's own sentence is the HEADLINE when there is one. A generic
|
|
194
207
|
// lead would be wrong as often as right — "payment is required" does not
|
|
195
208
|
// describe a declined card — and `detail` is written for this reader.
|
|
@@ -201,24 +214,149 @@ export async function reportPaymentRefusal({ verdict, args = [], say, open }) {
|
|
|
201
214
|
return { opened: false }
|
|
202
215
|
}
|
|
203
216
|
|
|
204
|
-
|
|
205
|
-
|
|
217
|
+
if (interactive === undefined) {
|
|
218
|
+
const { isNonInteractive } = await import('../utils/interactive.js')
|
|
219
|
+
interactive = !isNonInteractive(args)
|
|
220
|
+
}
|
|
221
|
+
if (!interactive) {
|
|
206
222
|
say.dim(`Finish this in a browser, then re-run \`uniweb publish\`:`)
|
|
207
223
|
say.dim(` ${verdict.url}`)
|
|
208
224
|
return { opened: false }
|
|
209
225
|
}
|
|
210
226
|
|
|
227
|
+
return openDoor({ url: verdict.url, say, open })
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Open the backend's door, verbatim, and say how to come back. */
|
|
231
|
+
async function openDoor({ url, say, open }) {
|
|
211
232
|
const openBrowser = open || (await import('../utils/registry-auth.js')).openBrowser
|
|
212
|
-
// ⛔ Reason-agnostic wording. The backend's own `detail`
|
|
213
|
-
//
|
|
214
|
-
//
|
|
233
|
+
// ⛔ Reason-agnostic wording. The backend's own `detail` carries the specifics;
|
|
234
|
+
// a lead sentence naming payment would be wrong the moment a refusal is a quota
|
|
235
|
+
// or an unverified domain — and the reason set is open by design.
|
|
215
236
|
say.info('Opening your browser to finish this…')
|
|
216
|
-
say.dim(` ${
|
|
237
|
+
say.dim(` ${url}`)
|
|
217
238
|
// VERBATIM. Nothing is appended — see the header.
|
|
218
|
-
const opened = await openBrowser(
|
|
239
|
+
const opened = await openBrowser(url)
|
|
219
240
|
if (!opened) {
|
|
220
241
|
say.warn('Could not open a browser automatically — open the URL above.')
|
|
221
242
|
}
|
|
222
243
|
say.dim('Once that is done, re-run `uniweb publish`.')
|
|
223
244
|
return { opened }
|
|
224
245
|
}
|
|
246
|
+
|
|
247
|
+
const CONFIRM = 'Confirm and publish'
|
|
248
|
+
const CONTINUE = 'Continue and publish'
|
|
249
|
+
const OPEN = 'Open it in the app'
|
|
250
|
+
const CANCEL = 'Cancel'
|
|
251
|
+
|
|
252
|
+
// A reduction offered again and again, with no one to ask, is answered this many times and no more.
|
|
253
|
+
const MAX_UNATTENDED = 3
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* ⭐ A REFUSAL THE TERMINAL MAY SETTLE (since 2026-10-03). When everything a publish owes is a change
|
|
257
|
+
* to a plan the site already has, the `402` carries `confirm: { prompt, token, reduction }` beside its
|
|
258
|
+
* door, and the backend's sentence is asked at an interactive terminal: confirm, open the app, or
|
|
259
|
+
* cancel. Confirming publishes again with the token (`?confirm=`):
|
|
260
|
+
*
|
|
261
|
+
* - `200` — settled and published: the response is returned, for the caller to finish with.
|
|
262
|
+
* - `402` with a fresh `confirm` — the change or its price moved and nothing was applied: asked again,
|
|
263
|
+
* with the new sentence.
|
|
264
|
+
* - `402` without one — what is owed can no longer be confirmed here (it now asks for a first plan,
|
|
265
|
+
* or the plan's card can no longer be named): its sentence, and its door. ⛔ Not a declined card —
|
|
266
|
+
* a confirmed change is invoiced as it applies, and a decline later shows in the app's billing.
|
|
267
|
+
* *(This said "the card could not settle it" until the day it was written, 2026-10-03.)*
|
|
268
|
+
*
|
|
269
|
+
* ⭐ **A CHARGE AND A REDUCTION ARE ASKED DIFFERENTLY** (`reduction`, a field the backend keeps
|
|
270
|
+
* stable). A charge is confirmed only by someone at the terminal: Enter cancels, no flag answers it,
|
|
271
|
+
* and with `--yes` — which promises never to block on a prompt — it is not asked either, but reported
|
|
272
|
+
* with the way to confirm it. A reduction — the offer only lowers what the site pays — asks lightly: Enter continues, and
|
|
273
|
+
* `--yes` answers it without asking, with or without a terminal, telling the backend it was answered
|
|
274
|
+
* unattended (`&unattended=true`), which settles a reduction and nothing else. *[Diego, 2026-10-03:
|
|
275
|
+
* "allow it for reductions only, keep charges interactive".]*
|
|
276
|
+
*
|
|
277
|
+
* With no one at the terminal and nothing to answer it, there is no prompt: the refusal is reported as
|
|
278
|
+
* before, its door printed, and the publish fails. A refusal with no `confirm` behaves as it always has.
|
|
279
|
+
*
|
|
280
|
+
* @param {object} o
|
|
281
|
+
* @param {ReturnType<typeof readPaymentRefusal>} o.verdict - the first refusal
|
|
282
|
+
* @param {string[]} o.args - argv slice (for `--yes` and --non-interactive detection)
|
|
283
|
+
* @param {object} o.say - { ok, info, warn, err, dim } reporters
|
|
284
|
+
* @param {(token: string, options?: { unattended?: boolean }) => Promise<Response>} o.republish - the
|
|
285
|
+
* same publish, with `?confirm=<token>` (and `&unattended=true` when no one answered)
|
|
286
|
+
* @param {boolean} [o.interactive] - whether someone is at the terminal; read from `args` and stdin
|
|
287
|
+
* @param {boolean} [o.yes] - whether `--yes` answers a reduction; read from `args`
|
|
288
|
+
* @param {(question: string, options: string[], fallback: string) => Promise<string>} [o.ask]
|
|
289
|
+
* @param {(url: string) => Promise<boolean>} [o.open]
|
|
290
|
+
* @returns {Promise<{ exitCode: number } | { response: Response, body?: string }>} `response` when
|
|
291
|
+
* the publish went through, or failed for a reason that is not payment (with its body, read)
|
|
292
|
+
*/
|
|
293
|
+
export async function settleRefusal({ verdict, args = [], say, republish, interactive, yes, ask, open }) {
|
|
294
|
+
if (interactive === undefined) {
|
|
295
|
+
const { isNonInteractive } = await import('../utils/interactive.js')
|
|
296
|
+
interactive = !isNonInteractive(args)
|
|
297
|
+
}
|
|
298
|
+
if (yes === undefined) yes = args.includes('--yes')
|
|
299
|
+
let current = verdict
|
|
300
|
+
let unattended = 0
|
|
301
|
+
for (;;) {
|
|
302
|
+
const offer = current.confirm
|
|
303
|
+
const reduction = offer?.reduction === true
|
|
304
|
+
let answered
|
|
305
|
+
|
|
306
|
+
if (reduction && yes && unattended < MAX_UNATTENDED) {
|
|
307
|
+
// `--yes` answers a change that only lowers the bill — said, so the log shows what was accepted.
|
|
308
|
+
unattended++
|
|
309
|
+
say.info(offer.prompt)
|
|
310
|
+
say.dim('Continuing: `--yes` answers a change that only lowers what the site pays.')
|
|
311
|
+
answered = { unattended: true }
|
|
312
|
+
} else if (!offer || !interactive || (yes && !reduction)) {
|
|
313
|
+
await reportPaymentRefusal({ verdict: current, args, say, open, interactive: interactive && !yes })
|
|
314
|
+
if (offer && !reduction) {
|
|
315
|
+
say.dim('This change charges the site\'s card, so only you can confirm it: run `uniweb publish` at a terminal, without `--yes`.')
|
|
316
|
+
} else if (reduction) {
|
|
317
|
+
say.dim('This change only lowers what the site pays: `uniweb publish --yes` continues it without a prompt.')
|
|
318
|
+
}
|
|
319
|
+
return { exitCode: 1 }
|
|
320
|
+
} else {
|
|
321
|
+
const go = reduction ? CONTINUE : CONFIRM
|
|
322
|
+
const choices = [go, ...(current.kind === 'settle' ? [OPEN] : []), CANCEL]
|
|
323
|
+
// Enter continues a reduction and cancels a charge.
|
|
324
|
+
const fallback = reduction ? CONTINUE : CANCEL
|
|
325
|
+
const pick = ask
|
|
326
|
+
? await ask(offer.prompt, choices, fallback)
|
|
327
|
+
: await (await import('../utils/interactive.js')).pickOne(offer.prompt, choices, fallback)
|
|
328
|
+
|
|
329
|
+
if (pick === OPEN) {
|
|
330
|
+
await openDoor({ url: current.url, say, open })
|
|
331
|
+
return { exitCode: 1 }
|
|
332
|
+
}
|
|
333
|
+
if (pick !== go) {
|
|
334
|
+
say.dim('Cancelled — nothing changed, and nothing was made live. The site is synced as a draft.')
|
|
335
|
+
return { exitCode: 1 }
|
|
336
|
+
}
|
|
337
|
+
answered = {}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
let response
|
|
341
|
+
try {
|
|
342
|
+
response = await republish(offer.token, answered)
|
|
343
|
+
} catch (err) {
|
|
344
|
+
say.err(String(err?.message || err))
|
|
345
|
+
return { exitCode: 1 }
|
|
346
|
+
}
|
|
347
|
+
if (response.ok) return { response }
|
|
348
|
+
|
|
349
|
+
const body = await response.text().catch(() => '')
|
|
350
|
+
const next = readPaymentRefusal({
|
|
351
|
+
status: response.status,
|
|
352
|
+
contentType: response.headers?.get?.('content-type') || '',
|
|
353
|
+
body
|
|
354
|
+
})
|
|
355
|
+
if (next.kind === 'not-payment') return { response, body }
|
|
356
|
+
if (next.confirm) {
|
|
357
|
+
// Nothing was applied: the change or its price moved since the offer. Ask again.
|
|
358
|
+
say.warn('The change or its price moved since that offer, so nothing was applied.')
|
|
359
|
+
}
|
|
360
|
+
current = next
|
|
361
|
+
}
|
|
362
|
+
}
|
package/src/commands/build.js
CHANGED
|
@@ -249,10 +249,7 @@ async function buildFoundation(projectDir, options = {}) {
|
|
|
249
249
|
log('')
|
|
250
250
|
log(`${colors.bright}Share with clients:${colors.reset}`)
|
|
251
251
|
log(
|
|
252
|
-
` ${colors.bright}uniweb
|
|
253
|
-
)
|
|
254
|
-
log(
|
|
255
|
-
` ${colors.bright}uniweb handoff <email>${colors.reset} Hand off a site to a client`
|
|
252
|
+
` ${colors.bright}uniweb register${colors.reset} Release your foundation to the catalog (alias: uniweb release)`
|
|
256
253
|
)
|
|
257
254
|
}
|
|
258
255
|
|
|
@@ -1026,9 +1023,6 @@ function showNextSteps(hasFoundations, hasSites) {
|
|
|
1026
1023
|
log(
|
|
1027
1024
|
` ${colors.bright}uniweb register${colors.reset} Release your foundation to the catalog (alias: uniweb release)`
|
|
1028
1025
|
)
|
|
1029
|
-
log(
|
|
1030
|
-
` ${colors.bright}uniweb handoff <email>${colors.reset} Hand off a site to a client`
|
|
1031
|
-
)
|
|
1032
1026
|
}
|
|
1033
1027
|
if (hasSites) {
|
|
1034
1028
|
log('')
|
package/src/commands/publish.js
CHANGED
|
@@ -112,7 +112,7 @@ import {
|
|
|
112
112
|
} from '../backend/foundation-bring-along.js'
|
|
113
113
|
import {
|
|
114
114
|
readPaymentRefusal,
|
|
115
|
-
|
|
115
|
+
settleRefusal
|
|
116
116
|
} from '../backend/payment-handoff.js'
|
|
117
117
|
import { reportSchemalessQueries } from '../utils/schemaless-report.js'
|
|
118
118
|
import {
|
|
@@ -1050,31 +1050,43 @@ export async function publish(args = []) {
|
|
|
1050
1050
|
say.dim('Is that the backend you meant? Switch with: uniweb login --backend <url>')
|
|
1051
1051
|
return { exitCode: 1 }
|
|
1052
1052
|
}
|
|
1053
|
+
// The body of a publish that did not go through — read once, and kept for the report below.
|
|
1054
|
+
let failedBody = ''
|
|
1053
1055
|
if (!pubRes.ok) {
|
|
1054
|
-
|
|
1056
|
+
failedBody = await pubRes.text().catch(() => '')
|
|
1055
1057
|
|
|
1056
1058
|
// A 402 is the backend's payment gate — the ONLY gate, evaluated here on
|
|
1057
1059
|
// every publish against whatever posture that deployment runs. It is a
|
|
1058
1060
|
// refusal, not a fault: the content is already synced as a draft, so the
|
|
1059
1061
|
// recovery is to settle and re-run. Give it the backend's own sentence
|
|
1060
|
-
// rather than the raw envelope
|
|
1062
|
+
// rather than the raw envelope — and when it offers a change the terminal
|
|
1063
|
+
// may confirm, ask, and publish again with the answer.
|
|
1061
1064
|
const refusal = readPaymentRefusal({
|
|
1062
1065
|
status: pubRes.status,
|
|
1063
1066
|
contentType: pubRes.headers?.get?.('content-type') || '',
|
|
1064
|
-
body
|
|
1067
|
+
body: failedBody
|
|
1065
1068
|
})
|
|
1066
1069
|
if (refusal.kind !== 'not-payment') {
|
|
1067
|
-
await
|
|
1068
|
-
|
|
1070
|
+
const settled = await settleRefusal({
|
|
1071
|
+
verdict: refusal,
|
|
1072
|
+
args,
|
|
1073
|
+
say,
|
|
1074
|
+
republish: (token, { unattended } = {}) =>
|
|
1075
|
+
client.publishSite(siteUuid, { ...(languages ? { languages } : {}), confirm: token, unattended })
|
|
1076
|
+
})
|
|
1077
|
+
if ('exitCode' in settled) return { exitCode: settled.exitCode }
|
|
1078
|
+
pubRes = settled.response
|
|
1079
|
+
failedBody = settled.body ?? ''
|
|
1069
1080
|
}
|
|
1070
|
-
|
|
1081
|
+
}
|
|
1082
|
+
if (!pubRes.ok) {
|
|
1071
1083
|
say.err(`Publish rejected: HTTP ${pubRes.status} ${pubRes.statusText}`)
|
|
1072
1084
|
if (pubRes.status === 401 || pubRes.status === 403) {
|
|
1073
1085
|
say.dim(
|
|
1074
1086
|
"Credentials weren't accepted — run `uniweb login` again."
|
|
1075
1087
|
)
|
|
1076
1088
|
}
|
|
1077
|
-
if (
|
|
1089
|
+
if (failedBody) say.dim(failedBody.slice(0, 800))
|
|
1078
1090
|
return { exitCode: 1 }
|
|
1079
1091
|
}
|
|
1080
1092
|
let result
|
|
@@ -1163,6 +1175,11 @@ export async function publish(args = []) {
|
|
|
1163
1175
|
say.ok(
|
|
1164
1176
|
`Published ${c.bold}${siteUuid}${c.reset}${result.status ? ` (${result.status})` : ''}`
|
|
1165
1177
|
)
|
|
1178
|
+
// What the request changed, in the backend's own sentences — a confirmed change, or a reduction
|
|
1179
|
+
// (`applied`, its §4A). Absent when nothing changed.
|
|
1180
|
+
for (const line of Array.isArray(result.applied) ? result.applied : []) {
|
|
1181
|
+
if (typeof line === 'string' && line) say.info(line)
|
|
1182
|
+
}
|
|
1166
1183
|
|
|
1167
1184
|
// ⛔ SAY IT WHEN A LANGUAGE ASKED FOR DID NOT GO OUT.
|
|
1168
1185
|
//
|
package/src/framework-index.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-10-
|
|
3
|
+
"generatedAt": "2026-10-05T18:23:45.673Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/api": {
|
|
6
|
-
"version": "0.6.
|
|
6
|
+
"version": "0.6.10",
|
|
7
7
|
"path": "framework/api",
|
|
8
8
|
"deps": [
|
|
9
9
|
"@uniweb/core"
|
|
10
10
|
]
|
|
11
11
|
},
|
|
12
12
|
"@uniweb/build": {
|
|
13
|
-
"version": "0.
|
|
13
|
+
"version": "0.76.0",
|
|
14
14
|
"path": "framework/build",
|
|
15
15
|
"deps": [
|
|
16
16
|
"@uniweb/content-reader",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"deps": []
|
|
36
36
|
},
|
|
37
37
|
"@uniweb/core": {
|
|
38
|
-
"version": "0.
|
|
38
|
+
"version": "0.37.0",
|
|
39
39
|
"path": "framework/core",
|
|
40
40
|
"deps": [
|
|
41
41
|
"@uniweb/semantic-parser",
|
|
@@ -48,14 +48,14 @@
|
|
|
48
48
|
"deps": []
|
|
49
49
|
},
|
|
50
50
|
"@uniweb/icons": {
|
|
51
|
-
"version": "0.4.
|
|
51
|
+
"version": "0.4.28",
|
|
52
52
|
"path": "framework/icons",
|
|
53
53
|
"deps": [
|
|
54
54
|
"@uniweb/core"
|
|
55
55
|
]
|
|
56
56
|
},
|
|
57
57
|
"@uniweb/kit": {
|
|
58
|
-
"version": "0.19.
|
|
58
|
+
"version": "0.19.16",
|
|
59
59
|
"path": "framework/kit",
|
|
60
60
|
"deps": [
|
|
61
61
|
"@uniweb/core",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"deps": []
|
|
77
77
|
},
|
|
78
78
|
"@uniweb/projections": {
|
|
79
|
-
"version": "0.7.
|
|
79
|
+
"version": "0.7.14",
|
|
80
80
|
"path": "framework/projections",
|
|
81
81
|
"deps": [
|
|
82
82
|
"@uniweb/content-writer",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
]
|
|
85
85
|
},
|
|
86
86
|
"@uniweb/runtime": {
|
|
87
|
-
"version": "0.
|
|
87
|
+
"version": "0.29.0",
|
|
88
88
|
"path": "framework/runtime",
|
|
89
89
|
"deps": [
|
|
90
90
|
"@uniweb/core",
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
"deps": []
|
|
98
98
|
},
|
|
99
99
|
"@uniweb/schemas": {
|
|
100
|
-
"version": "0.
|
|
100
|
+
"version": "0.11.0",
|
|
101
101
|
"path": "framework/schemas",
|
|
102
102
|
"deps": []
|
|
103
103
|
},
|
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
"deps": []
|
|
118
118
|
},
|
|
119
119
|
"@uniweb/templates": {
|
|
120
|
-
"version": "0.17.
|
|
120
|
+
"version": "0.17.13",
|
|
121
121
|
"path": "framework/templates",
|
|
122
122
|
"deps": []
|
|
123
123
|
},
|
|
@@ -127,7 +127,7 @@
|
|
|
127
127
|
"deps": []
|
|
128
128
|
},
|
|
129
129
|
"@uniweb/unipress": {
|
|
130
|
-
"version": "0.10.
|
|
130
|
+
"version": "0.10.20",
|
|
131
131
|
"path": "framework/unipress",
|
|
132
132
|
"deps": [
|
|
133
133
|
"@uniweb/build",
|