uniweb 0.30.1 → 0.31.1
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 +37 -13
- package/src/backend/client.js +0 -22
- package/src/backend/payment-handoff.js +136 -100
- package/src/commands/publish.js +36 -23
- package/src/commands/push.js +4 -0
- package/src/framework-index.json +9 -9
- package/src/utils/registry-auth.js +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.1",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,13 +41,13 @@
|
|
|
41
41
|
"js-yaml": "^4.1.0",
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
|
-
"@uniweb/core": "^0.
|
|
45
|
-
"@uniweb/kit": "^0.13.
|
|
46
|
-
"@uniweb/
|
|
47
|
-
"@uniweb/
|
|
44
|
+
"@uniweb/core": "^0.13.0",
|
|
45
|
+
"@uniweb/kit": "^0.13.7",
|
|
46
|
+
"@uniweb/runtime": "^0.13.0",
|
|
47
|
+
"@uniweb/semantic-parser": "^1.3.1"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@uniweb/build": "^0.
|
|
50
|
+
"@uniweb/build": "^0.27.1",
|
|
51
51
|
"@uniweb/semantic-parser": "^1.3.1",
|
|
52
52
|
"@uniweb/content-reader": "^1.2.4"
|
|
53
53
|
},
|
package/partials/agents.md
CHANGED
|
@@ -287,7 +287,7 @@ Paths are relative to the site package you identified in step 1 — `site/` in t
|
|
|
287
287
|
|
|
288
288
|
**Change a nav item.** First check how nav is produced. If `<site>/layout/header.md` lists the links (a markdown list, or a `yaml:nav` block), edit it there. If it doesn't, the Header is generating nav from the page hierarchy — change page titles and order in `site.yml` / `page.yml` instead.
|
|
289
289
|
|
|
290
|
-
**Update the project's Uniweb dependencies — and this file.** `uniweb update`. One command aligns every `@uniweb/*` dependency *and* refreshes this AGENTS.md together
|
|
290
|
+
**Update the project's Uniweb dependencies — and this file.** `npx uniweb@latest update`. One command aligns every `@uniweb/*` dependency *and* refreshes this AGENTS.md together. Preview with `--dry-run`. ⛔ **The `@latest` is doing the work** — a bare `uniweb update` runs the CLI this project already pins, which aligns you to the matrix you already have and correctly reports nothing to do. **Don't reach for `npm update` / `pnpm update`** — see *Staying current* in Part 5 for why that breaks things quietly.
|
|
291
291
|
|
|
292
292
|
**Change one section's columns / spacing / variant.** Check that type's `meta.js` `params:` first. If the knob exists, set it in that section's frontmatter and you're done, in the content lane. If it doesn't, it's a foundation change — see the warning in step 3.
|
|
293
293
|
|
|
@@ -1751,7 +1751,7 @@ fetch:
|
|
|
1751
1751
|
limit: 3
|
|
1752
1752
|
```
|
|
1753
1753
|
|
|
1754
|
-
**Lean lists with `deferred:`.** Collections with heavy fields (article bodies, large nested arrays) can declare `deferred: [body]` in `site.yml`. The cascade payload omits those fields; per-record full files are emitted at `/data/<name>/<slug>.json` (file-based collections) or fetched from an author-declared `detailUrl:` (API-backed). On dynamic-route pages the focused record's full data is delivered automatically; elsewhere components fetch on demand via `useEntityDetail`.
|
|
1754
|
+
**Lean lists with `deferred:`.** Collections with heavy fields (article bodies, large nested arrays) can declare `deferred: [body]` in `site.yml`. The cascade payload omits those fields; per-record full files are emitted at `/data/<name>/<slug>.json` (file-based collections) or fetched from an author-declared `detailUrl:` (API-backed). On dynamic-route pages the focused record's full data is delivered automatically; elsewhere components fetch on demand via `useEntityDetail`. The hook is safe to call on any collection: when there is no separate detail source it returns the record you passed in, because nothing was stripped from it.
|
|
1755
1755
|
|
|
1756
1756
|
**Component-side fetching.** When a component genuinely needs to fetch on its own (a search box, "load more", a lazy popover), use the kit hooks — `useFetched`, `useCacheEntry`, `useEntityDetail`. They share the framework's cache and dispatcher with declarative fetches; same-key requests dedupe automatically.
|
|
1757
1757
|
|
|
@@ -2024,8 +2024,9 @@ collector's event dimension into a cardinality problem.
|
|
|
2024
2024
|
|
|
2025
2025
|
### Choosing what a site sends
|
|
2026
2026
|
|
|
2027
|
-
By default a site sends `page_view`, `outbound_click` and `section_view
|
|
2028
|
-
|
|
2027
|
+
By default a site sends `page_view`, `outbound_click` and `section_view` — or,
|
|
2028
|
+
where a host supplies the collector, whatever that host declares it collects.
|
|
2029
|
+
Narrow or widen that with `emit`:
|
|
2029
2030
|
|
|
2030
2031
|
```yaml
|
|
2031
2032
|
# site.yml — your own collector
|
|
@@ -2044,10 +2045,19 @@ are read key by key, so naming `emit` alone overrides nothing else the host
|
|
|
2044
2045
|
declared. And declaring your own `endpoint:` always wins, so a site pointing at
|
|
2045
2046
|
its own collector keeps working on any host, including none.
|
|
2046
2047
|
|
|
2047
|
-
`minimal` is `page_view` alone. `standard` is the default
|
|
2048
|
-
yes, so an event added in a later framework
|
|
2049
|
-
changing anything — which is exactly why
|
|
2050
|
-
curated set that a release cannot grow behind
|
|
2048
|
+
`minimal` is `page_view` alone. `standard` is the default when you supply your
|
|
2049
|
+
own `endpoint:`. `all` is a standing yes, so an event added in a later framework
|
|
2050
|
+
release is included without you changing anything — which is exactly why
|
|
2051
|
+
`standard` exists as well: it is a curated set that a release cannot grow behind
|
|
2052
|
+
your back.
|
|
2053
|
+
|
|
2054
|
+
⭐ **Saying nothing means two different things, and which one depends on who
|
|
2055
|
+
supplies the address.** A site with its own `endpoint:` gets `standard`. A site
|
|
2056
|
+
on a **host-supplied** collector gets **whatever that host declares it
|
|
2057
|
+
collects** — it has no address of its own, so the arrangement is that the host
|
|
2058
|
+
does analytics for it, and the set grows when the host starts collecting
|
|
2059
|
+
something new. **Naming `emit` always wins**, so pin it if you would rather not
|
|
2060
|
+
follow your host.
|
|
2051
2061
|
|
|
2052
2062
|
A site can also set **`flushIntervalMs`** to widen the batching window (default
|
|
2053
2063
|
5000, milliseconds — `30` is thirty *milliseconds*). A host that supplies your
|
|
@@ -2216,7 +2226,8 @@ uniweb i18n init-freeform / update-hash / move / rename / prune --freeform
|
|
|
2216
2226
|
uniweb -v # Installed CLI version — and whether a newer one exists
|
|
2217
2227
|
uniweb doctor # Diagnose project configuration (--fix to auto-repair)
|
|
2218
2228
|
uniweb validate # Check file-based data against declared schemas (--strict for CI)
|
|
2219
|
-
uniweb update
|
|
2229
|
+
npx uniweb@latest update # Align @uniweb/* deps + AGENTS.md (--dry-run, --yes)
|
|
2230
|
+
# bare `uniweb update` aligns to the CLI you ALREADY have
|
|
2220
2231
|
uniweb inspect <path> # Show parsed content for a section or page (--raw for the AST)
|
|
2221
2232
|
|
|
2222
2233
|
uniweb <command> --help # Per-command flags — no side effects. Prefer this over guessing.
|
|
@@ -2284,15 +2295,28 @@ Either side can publish. Nothing about this changes how you build: the same foun
|
|
|
2284
2295
|
```bash
|
|
2285
2296
|
uniweb -v # installed CLI version, and whether a newer one is available
|
|
2286
2297
|
uniweb doctor # report drift in this project, changing nothing
|
|
2287
|
-
uniweb update --dry-run
|
|
2288
|
-
uniweb update
|
|
2298
|
+
npx uniweb@latest update --dry-run # preview exactly what update would change
|
|
2299
|
+
npx uniweb@latest update # apply: align @uniweb/* deps AND refresh AGENTS.md
|
|
2289
2300
|
```
|
|
2290
2301
|
|
|
2291
|
-
**`uniweb update` is the command for bringing a project up to date.** It aligns the project's `@uniweb/*` dependencies *and* this AGENTS.md to the version matrix of the CLI that runs it. Deps and documentation move together — that's the whole point of the verb.
|
|
2302
|
+
**`npx uniweb@latest update` is the command for bringing a project up to date.** It aligns the project's `@uniweb/*` dependencies *and* this AGENTS.md to the version matrix of the CLI that runs it. Deps and documentation move together — that's the whole point of the verb.
|
|
2303
|
+
|
|
2304
|
+
> ### ⛔ WRITE `@latest`. A BARE `uniweb update` IS USUALLY A NO-OP, AND IT SAYS SO CONVINCINGLY
|
|
2305
|
+
>
|
|
2306
|
+
> `update` reconciles this project against **the matrix of the CLI that runs it** — and in a project,
|
|
2307
|
+
> `uniweb` resolves to the copy in your own `node_modules`, pinned by your own `package.json`. That
|
|
2308
|
+
> copy has no way to know a newer release exists.
|
|
2309
|
+
>
|
|
2310
|
+
> ⇒ **It reports everything aligned and changes nothing — correctly.** It answered the question it
|
|
2311
|
+
> was asked, which is why the output looks like success rather than a mistake. The same applies to
|
|
2312
|
+
> `pnpm uniweb update` and to a stale global install.
|
|
2313
|
+
>
|
|
2314
|
+
> ⭐ **`@latest` is what makes it a question about the newest release** rather than about the one you
|
|
2315
|
+
> already have. It also bumps the pin, so the next bare invocation is no longer stale.
|
|
2292
2316
|
|
|
2293
2317
|
> **Don't run `npm update` or `pnpm update` on the `@uniweb/*` packages.** They're a matched set resolved by the CLI's version matrix, not independently versioned libraries you upgrade one at a time. Updating them directly gets you a combination nobody tested, and it won't refresh AGENTS.md — so this guide silently drifts out of sync with the code it describes, which is worse than being out of date, because nothing looks wrong.
|
|
2294
2318
|
|
|
2295
|
-
Two ordering rules: `update` won't refresh AGENTS.md while declared deps still lag the CLI, or while edited deps haven't been installed — either would put the doc ahead of the code. And updating the CLI itself is your package manager's job (`npm i -g uniweb@latest`, `pnpm add -g uniweb@latest`); `uniweb update` does not do that.
|
|
2319
|
+
Two ordering rules: `update` won't refresh AGENTS.md while declared deps still lag the CLI, or while edited deps haven't been installed — either would put the doc ahead of the code. And updating the CLI itself is your package manager's job (`npm i -g uniweb@latest`, `pnpm add -g uniweb@latest`); `uniweb update` does not do that.
|
|
2296
2320
|
|
|
2297
2321
|
### `package.json` `uniweb` block
|
|
2298
2322
|
|
package/src/backend/client.js
CHANGED
|
@@ -537,28 +537,6 @@ export class BackendClient {
|
|
|
537
537
|
}
|
|
538
538
|
}
|
|
539
539
|
|
|
540
|
-
/**
|
|
541
|
-
* ASSUMED endpoint (not built yet).
|
|
542
|
-
* GET /dev/site/{uuid}/can-go-live — the pre-go-live payment gate:
|
|
543
|
-
* { ok: true } // already paid → proceed
|
|
544
|
-
* { payment_required: true, checkout_url, wait_token? } // open the URL, settle, retry
|
|
545
|
-
* The framework is provider-agnostic: it only opens `checkout_url` and waits.
|
|
546
|
-
* null on 404 / any failure → the caller PROCEEDS (degrade: publish ships
|
|
547
|
-
* before payment lands; same posture as siteStatus on a missing route).
|
|
548
|
-
* @param {string} uuid - the site-content uuid
|
|
549
|
-
* @returns {Promise<object|null>}
|
|
550
|
-
*/
|
|
551
|
-
async canGoLive(uuid) {
|
|
552
|
-
try {
|
|
553
|
-
const res = await this.request(
|
|
554
|
-
`/dev/site/${encodeURIComponent(uuid)}/can-go-live`
|
|
555
|
-
)
|
|
556
|
-
return res.ok ? await res.json().catch(() => null) : null
|
|
557
|
-
} catch {
|
|
558
|
-
return null
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
|
|
562
540
|
/**
|
|
563
541
|
* Deliver a site's processed assets (plan → PUT-per-file) to the backend's
|
|
564
542
|
* content-addressed store. Thin pass-through to utils/asset-upload.js with this
|
|
@@ -1,124 +1,160 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Payment
|
|
3
|
-
*
|
|
2
|
+
* Payment refusal — what `uniweb publish` does when the backend says a site
|
|
3
|
+
* cannot go live until it is paid for.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* THE BACKEND IS THE ONLY GATE. It evaluates on every publish, on every
|
|
6
|
+
* backend, whatever that deployment is configured to require — a backend with
|
|
7
|
+
* subscriptions switched off simply never refuses. The CLI holds no opinion about whether a
|
|
8
|
+
* backend charges and must never form one: it attempts the publish and reads
|
|
9
|
+
* the answer.
|
|
8
10
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* ⛔ THERE IS NO PRE-FLIGHT, AND ADDING ONE BACK IS A REGRESSION. A
|
|
12
|
+
* `GET …/can-go-live` probe used to run before go-live; it called a route no
|
|
13
|
+
* backend serves, and it folded every failure — 404 included — into
|
|
14
|
+
* "proceed". A check that answers "fine" when it cannot reach the server is not
|
|
15
|
+
* a check, and it made the CLI assume a posture it has no business assuming. A
|
|
16
|
+
* pre-flight also cannot be authoritative: the backend re-evaluates at publish
|
|
17
|
+
* time regardless, so a second asker is a second producer of one decision.
|
|
14
18
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
+
* WHAT THE CLI KNOWS ABOUT PAYMENT: nothing. It opens whatever settlement URL
|
|
20
|
+
* the backend hands it, VERBATIM — provider-agnostic, and route-agnostic. The
|
|
21
|
+
* app drives the provider and settles with the backend.
|
|
22
|
+
*
|
|
23
|
+
* ⛔ AND IT APPENDS NOTHING TO THAT URL. The old handoff added
|
|
24
|
+
* `redirect_uri=http://127.0.0.1:<port>/callback`, `state` and `wait_token`,
|
|
25
|
+
* then waited on a loopback for the app to redirect back. Two reasons it is
|
|
26
|
+
* gone: it obliged the web app to know a CLI exists and honour a callback, and
|
|
27
|
+
* a loopback callback requires the browser and the CLI
|
|
28
|
+
* on the SAME MACHINE, so over SSH or in a container it hung for its full
|
|
29
|
+
* timeout and then reported "payment was not completed", which was false.
|
|
19
30
|
*/
|
|
20
31
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
import { awaitBrowserCallback } from '../utils/registry-auth.js'
|
|
24
|
-
import { isNonInteractive } from '../utils/interactive.js'
|
|
25
|
-
|
|
26
|
-
// Append query params to a backend-supplied URL without disturbing its own.
|
|
27
|
-
function withParams(url, params) {
|
|
28
|
-
const u = new URL(url)
|
|
29
|
-
for (const [k, v] of Object.entries(params)) {
|
|
30
|
-
if (v != null) u.searchParams.set(k, String(v))
|
|
31
|
-
}
|
|
32
|
-
return u.toString()
|
|
33
|
-
}
|
|
32
|
+
/** Reasons the CLI knows how to act on. An ALLOWLIST, never an inventory. */
|
|
33
|
+
const ACTIONABLE_REASONS = new Set(['no_subscription'])
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
*
|
|
36
|
+
* Read a `402` from the publish call and decide what the CLI does. Pure — no
|
|
37
|
+
* network, no browser, no process exit — so the decision is testable on its own.
|
|
38
|
+
*
|
|
39
|
+
* ⛔ THE RULE, and it is a property of the wire rather than a CLI preference:
|
|
40
|
+
* NEVER route to a purchase surface from the ABSENCE of a recognised token. A
|
|
41
|
+
* purchase surface is opened by a `reason` that NAMES one; everything else
|
|
42
|
+
* surfaces the backend's own sentence and stops. Degrading that way means an
|
|
43
|
+
* older CLI shows you the message — annoying, honest, recoverable. Degrading
|
|
44
|
+
* the other way sends someone to a checkout for something they cannot buy.
|
|
45
|
+
*
|
|
46
|
+
* ⛔ Do NOT turn `ACTIONABLE_REASONS` into "every reason the backend has". The
|
|
47
|
+
* set is open by design and a stale copy here fails in the worst direction; an
|
|
48
|
+
* allowlist is safe precisely because what it misses lands on `stop`.
|
|
49
|
+
* ⛔ Do NOT parse `detail` — it is deliberately not asserted word-for-word, and
|
|
50
|
+
* `reason` exists to do the job parsing it would.
|
|
51
|
+
*
|
|
52
|
+
* NOTE ON `reason`'s PRESENCE: it is NOT guaranteed by the content type. A 402
|
|
53
|
+
* naming a condition the caller can act on carries one; nothing guarantees it
|
|
54
|
+
* in general — a declined card is `problem+json` and deliberately carries none,
|
|
55
|
+
* because there is no machine decision for a client to make about it. This reads `reason` when it is there and needs
|
|
56
|
+
* no invariant, which is why that correction cost this branch nothing.
|
|
37
57
|
*
|
|
38
58
|
* @param {object} o
|
|
39
|
-
* @param {
|
|
40
|
-
* @param {string
|
|
41
|
-
*
|
|
42
|
-
* @
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* @returns {Promise<{ proceed: boolean }>} proceed:false → the caller aborts go-live.
|
|
59
|
+
* @param {number} o.status - the HTTP status
|
|
60
|
+
* @param {string} [o.contentType] - the response's content-type header
|
|
61
|
+
* @param {string} [o.body] - the raw response body
|
|
62
|
+
* @returns {{ kind: 'not-payment' }
|
|
63
|
+
* | { kind: 'settle', url: string, handle: string|null, reason: string, message: string|null }
|
|
64
|
+
* | { kind: 'stop', reason: string|null, message: string|null }}
|
|
46
65
|
*/
|
|
47
|
-
export
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (!
|
|
66
|
+
export function readPaymentRefusal({ status, contentType = '', body = '' } = {}) {
|
|
67
|
+
if (status !== 402) return { kind: 'not-payment' }
|
|
68
|
+
|
|
69
|
+
let problem = null
|
|
70
|
+
try {
|
|
71
|
+
problem = JSON.parse(body)
|
|
72
|
+
} catch {
|
|
73
|
+
/* a non-JSON 402 is simply unrecognised — it falls to `stop` below */
|
|
74
|
+
}
|
|
75
|
+
if (!problem || typeof problem !== 'object') {
|
|
76
|
+
return { kind: 'stop', reason: null, message: null }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// The human sentence, in the backend's own words. `detail` is the 7807
|
|
80
|
+
// member; `title` is the fallback when a body carries no detail.
|
|
81
|
+
const message =
|
|
82
|
+
(typeof problem.detail === 'string' && problem.detail) ||
|
|
83
|
+
(typeof problem.title === 'string' && problem.title) ||
|
|
84
|
+
null
|
|
85
|
+
|
|
86
|
+
// `status` is NOT a discriminator: the backend's problem bodies carry it as
|
|
87
|
+
// the NUMBER 402 while at least one hand-built 402 elsewhere on their wire
|
|
88
|
+
// carries a STRING. Same key, two types, neither failing loudly — so this
|
|
89
|
+
// reads `reason` and the content type instead, and never `body.status`.
|
|
90
|
+
const isProblem = String(contentType).includes('application/problem+json')
|
|
91
|
+
const reason =
|
|
92
|
+
isProblem && typeof problem.reason === 'string' && problem.reason
|
|
93
|
+
? problem.reason
|
|
94
|
+
: null
|
|
57
95
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (dryRun) {
|
|
61
|
-
say.dim(
|
|
62
|
-
`Payment : would check whether go-live needs payment for ${uuid}`
|
|
63
|
-
)
|
|
64
|
-
return { proceed: true }
|
|
96
|
+
if (!reason || !ACTIONABLE_REASONS.has(reason)) {
|
|
97
|
+
return { kind: 'stop', reason, message }
|
|
65
98
|
}
|
|
66
99
|
|
|
67
|
-
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
100
|
+
// Actionable — but only if the backend actually handed over somewhere to go.
|
|
101
|
+
// A recognised reason with no settlement block is a backend that has not
|
|
102
|
+
// built that half yet: still a stop, and still with its own sentence.
|
|
103
|
+
const s = problem.settlement
|
|
104
|
+
const url = s && typeof s.url === 'string' && s.url ? s.url : null
|
|
105
|
+
if (!url) return { kind: 'stop', reason, message }
|
|
71
106
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
107
|
+
return {
|
|
108
|
+
kind: 'settle',
|
|
109
|
+
url,
|
|
110
|
+
handle: s && typeof s.handle === 'string' && s.handle ? s.handle : null,
|
|
111
|
+
reason,
|
|
112
|
+
message
|
|
78
113
|
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Report a payment refusal to the user, and open the settlement page when the
|
|
118
|
+
* backend supplied one.
|
|
119
|
+
*
|
|
120
|
+
* ⛔ Returns rather than exits — the caller owns the exit code, and a refusal
|
|
121
|
+
* is not a crash: the content is already synced as a draft, so re-running
|
|
122
|
+
* after paying is the recovery.
|
|
123
|
+
*
|
|
124
|
+
* @param {object} o
|
|
125
|
+
* @param {ReturnType<typeof readPaymentRefusal>} o.verdict
|
|
126
|
+
* @param {string[]} o.args - argv slice (for --non-interactive detection)
|
|
127
|
+
* @param {object} o.say - { ok, info, warn, err, dim } reporters
|
|
128
|
+
* @param {(url: string) => Promise<boolean>} [o.open] - injected for tests
|
|
129
|
+
* @returns {Promise<{ opened: boolean }>}
|
|
130
|
+
*/
|
|
131
|
+
export async function reportPaymentRefusal({ verdict, args = [], say, open }) {
|
|
132
|
+
// The backend's own sentence is the HEADLINE when there is one. A generic
|
|
133
|
+
// lead would be wrong as often as right — "payment is required" does not
|
|
134
|
+
// describe a declined card — and `detail` is written for this reader.
|
|
135
|
+
say.err(verdict.message || 'This site cannot go live until it is paid for.')
|
|
79
136
|
|
|
80
|
-
if (
|
|
81
|
-
|
|
82
|
-
|
|
137
|
+
if (verdict.kind !== 'settle') {
|
|
138
|
+
// The push completed before go-live, so the content is safely stored.
|
|
139
|
+
say.dim('The site is synced as a draft; nothing was made live.')
|
|
140
|
+
return { opened: false }
|
|
83
141
|
}
|
|
84
142
|
|
|
143
|
+
const { isNonInteractive } = await import('../utils/interactive.js')
|
|
85
144
|
if (isNonInteractive(args)) {
|
|
86
|
-
say.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
say.dim(`Complete it in a browser, then re-run: ${checkoutUrl}`)
|
|
90
|
-
return { proceed: false }
|
|
145
|
+
say.dim(`Complete it in a browser, then re-run \`uniweb publish\`:`)
|
|
146
|
+
say.dim(` ${verdict.url}`)
|
|
147
|
+
return { opened: false }
|
|
91
148
|
}
|
|
92
149
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
withParams(checkoutUrl, {
|
|
101
|
-
redirect_uri: redirectUri,
|
|
102
|
-
state,
|
|
103
|
-
wait_token: verdict.wait_token
|
|
104
|
-
}),
|
|
105
|
-
validate: (params) => {
|
|
106
|
-
if (params.get('error')) return { error: params.get('error') }
|
|
107
|
-
if (params.get('state') !== state)
|
|
108
|
-
return { error: 'state mismatch — please retry.' }
|
|
109
|
-
return { value: true } // ok=1 / any non-error return = the app settled with the backend
|
|
110
|
-
},
|
|
111
|
-
openingLabel: 'Opening uniweb.app to complete payment…',
|
|
112
|
-
waitingLabel: 'Waiting for payment to complete (5 min)…',
|
|
113
|
-
timeoutMs: 5 * 60 * 1000,
|
|
114
|
-
okTitle: 'Payment complete',
|
|
115
|
-
errTitle: 'Payment failed'
|
|
116
|
-
})
|
|
117
|
-
} catch (err) {
|
|
118
|
-
say.err(`Payment was not completed: ${err.message}`)
|
|
119
|
-
say.dim('Re-run `uniweb publish` once payment is done.')
|
|
120
|
-
return { proceed: false }
|
|
150
|
+
const openBrowser = open || (await import('../utils/registry-auth.js')).openBrowser
|
|
151
|
+
say.info('Opening your browser to complete it…')
|
|
152
|
+
say.dim(` ${verdict.url}`)
|
|
153
|
+
// VERBATIM. Nothing is appended — see the header.
|
|
154
|
+
const opened = await openBrowser(verdict.url)
|
|
155
|
+
if (!opened) {
|
|
156
|
+
say.warn('Could not open a browser automatically — open the URL above.')
|
|
121
157
|
}
|
|
122
|
-
say.
|
|
123
|
-
return {
|
|
158
|
+
say.dim('Once payment is complete, re-run `uniweb publish`.')
|
|
159
|
+
return { opened }
|
|
124
160
|
}
|
package/src/commands/publish.js
CHANGED
|
@@ -88,7 +88,10 @@ import {
|
|
|
88
88
|
bringFoundationAlong,
|
|
89
89
|
bringExtensionsAlong
|
|
90
90
|
} from '../backend/foundation-bring-along.js'
|
|
91
|
-
import {
|
|
91
|
+
import {
|
|
92
|
+
readPaymentRefusal,
|
|
93
|
+
reportPaymentRefusal
|
|
94
|
+
} from '../backend/payment-handoff.js'
|
|
92
95
|
import { reportSchemalessCollections } from '../utils/schemaless-report.js'
|
|
93
96
|
import { uploadSiteData } from '../utils/site-data-upload.js'
|
|
94
97
|
|
|
@@ -353,13 +356,10 @@ export async function publish(args = []) {
|
|
|
353
356
|
cliBin: process.argv[1],
|
|
354
357
|
dryRun: true
|
|
355
358
|
})
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
say,
|
|
361
|
-
dryRun: true
|
|
362
|
-
})
|
|
359
|
+
// No payment line on a dry run. The backend is the only gate and it
|
|
360
|
+
// answers at go-live, so the honest dry-run answer is silence rather than
|
|
361
|
+
// a guess. (The old "would check whether go-live needs payment" described
|
|
362
|
+
// a pre-flight probe that has been removed — payment-handoff.js.)
|
|
363
363
|
return { exitCode: 0 }
|
|
364
364
|
}
|
|
365
365
|
|
|
@@ -409,8 +409,13 @@ export async function publish(args = []) {
|
|
|
409
409
|
if (!fnd.proceed) return { exitCode: fnd.refused ? 1 : 0 }
|
|
410
410
|
|
|
411
411
|
// 2. Build the site data (link mode): dist/site-content.json (+ per-locale),
|
|
412
|
-
// dist/data/*, dist/
|
|
413
|
-
//
|
|
412
|
+
// dist/data/*, dist/assets/*. Spawn the SAME CLI binary so the inner
|
|
413
|
+
// build can't resolve to a different installed version.
|
|
414
|
+
//
|
|
415
|
+
// ⛔ NO SEARCH INDEX. This listed `dist/_search/*` until 2026-08-26; the
|
|
416
|
+
// link lane stopped emitting one on 2026-08-01 (`@uniweb/build`
|
|
417
|
+
// `site/build-site-data.js` step 5) because only one of the two
|
|
418
|
+
// publishers produced it.
|
|
414
419
|
say.info('Building site…')
|
|
415
420
|
console.log('')
|
|
416
421
|
execSync(`node ${JSON.stringify(process.argv[1])} build --link`, {
|
|
@@ -435,6 +440,8 @@ export async function publish(args = []) {
|
|
|
435
440
|
let probe
|
|
436
441
|
try {
|
|
437
442
|
probe = await emitSyncPackages(siteDir, {
|
|
443
|
+
// Resolves a foundation-relative `@/x` model ref into `@org/x`.
|
|
444
|
+
...(asOrg ? { org: asOrg } : {}),
|
|
438
445
|
...(foundationDir ? { foundationDir } : {}),
|
|
439
446
|
resolveModel
|
|
440
447
|
})
|
|
@@ -634,6 +641,8 @@ export async function publish(args = []) {
|
|
|
634
641
|
let pkg
|
|
635
642
|
try {
|
|
636
643
|
pkg = await emitSyncPackages(siteDir, {
|
|
644
|
+
// Resolves a foundation-relative `@/x` model ref into `@org/x`.
|
|
645
|
+
...(asOrg ? { org: asOrg } : {}),
|
|
637
646
|
...(foundationDir ? { foundationDir } : {}),
|
|
638
647
|
resolveModel,
|
|
639
648
|
priorHashes,
|
|
@@ -673,18 +682,6 @@ export async function publish(args = []) {
|
|
|
673
682
|
return { exitCode: 1 }
|
|
674
683
|
}
|
|
675
684
|
|
|
676
|
-
// 6. Payment gate — the backend says whether go-live needs payment. Settles
|
|
677
|
-
// via a browser handoff to uniweb.app; degrades to "proceed" when the
|
|
678
|
-
// backend exposes no payment route. The draft is already synced, so a
|
|
679
|
-
// decline leaves a recoverable state (re-run after paying).
|
|
680
|
-
const pay = await settlePaymentIfNeeded({ client, uuid: siteUuid, args, say })
|
|
681
|
-
if (!pay.proceed) {
|
|
682
|
-
say.info(
|
|
683
|
-
'Site synced as a draft but not made live. Re-run `uniweb publish` once payment is complete.'
|
|
684
|
-
)
|
|
685
|
-
return { exitCode: 0 }
|
|
686
|
-
}
|
|
687
|
-
|
|
688
685
|
// 7. Go live — make the just-pushed composite live (its current backend state).
|
|
689
686
|
const siteContent = JSON.parse(await readFile(contentPath, 'utf8'))
|
|
690
687
|
const languages = languagesFromContent(siteContent)
|
|
@@ -700,13 +697,29 @@ export async function publish(args = []) {
|
|
|
700
697
|
return { exitCode: 1 }
|
|
701
698
|
}
|
|
702
699
|
if (!pubRes.ok) {
|
|
700
|
+
const body = await pubRes.text().catch(() => '')
|
|
701
|
+
|
|
702
|
+
// A 402 is the backend's payment gate — the ONLY gate, evaluated here on
|
|
703
|
+
// every publish against whatever posture that deployment runs. It is a
|
|
704
|
+
// refusal, not a fault: the content is already synced as a draft, so the
|
|
705
|
+
// recovery is to settle and re-run. Give it the backend's own sentence
|
|
706
|
+
// rather than the raw envelope.
|
|
707
|
+
const refusal = readPaymentRefusal({
|
|
708
|
+
status: pubRes.status,
|
|
709
|
+
contentType: pubRes.headers?.get?.('content-type') || '',
|
|
710
|
+
body
|
|
711
|
+
})
|
|
712
|
+
if (refusal.kind !== 'not-payment') {
|
|
713
|
+
await reportPaymentRefusal({ verdict: refusal, args, say })
|
|
714
|
+
return { exitCode: 1 }
|
|
715
|
+
}
|
|
716
|
+
|
|
703
717
|
say.err(`Publish rejected: HTTP ${pubRes.status} ${pubRes.statusText}`)
|
|
704
718
|
if (pubRes.status === 401 || pubRes.status === 403) {
|
|
705
719
|
say.dim(
|
|
706
720
|
"Credentials weren't accepted — run `uniweb login` (or pass --token <bearer>)."
|
|
707
721
|
)
|
|
708
722
|
}
|
|
709
|
-
const body = await pubRes.text().catch(() => '')
|
|
710
723
|
if (body) say.dim(body.slice(0, 800))
|
|
711
724
|
return { exitCode: 1 }
|
|
712
725
|
}
|
package/src/commands/push.js
CHANGED
|
@@ -373,6 +373,8 @@ export async function push(args = [], deps = {}) {
|
|
|
373
373
|
let mediaRefs = []
|
|
374
374
|
try {
|
|
375
375
|
const probe = await emitSyncPackages(siteDir, {
|
|
376
|
+
// Resolves a foundation-relative `@/x` model ref into `@org/x`.
|
|
377
|
+
...(asOrg ? { org: asOrg } : {}),
|
|
376
378
|
...(foundationDir ? { foundationDir } : {}),
|
|
377
379
|
resolveModel: makeModelResolver({ client, offline: false })
|
|
378
380
|
})
|
|
@@ -467,6 +469,8 @@ export async function push(args = [], deps = {}) {
|
|
|
467
469
|
let pkg
|
|
468
470
|
try {
|
|
469
471
|
pkg = await emitSyncPackages(siteDir, {
|
|
472
|
+
// Resolves a foundation-relative `@/x` model ref into `@org/x`.
|
|
473
|
+
...(asOrg ? { org: asOrg } : {}),
|
|
470
474
|
...(foundationDir ? { foundationDir } : {}),
|
|
471
475
|
resolveModel: makeModelResolver({
|
|
472
476
|
client,
|
package/src/framework-index.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-08-
|
|
3
|
+
"generatedAt": "2026-08-27T16:19:55.310Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/build": {
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.27.1",
|
|
7
7
|
"path": "framework/build",
|
|
8
8
|
"deps": [
|
|
9
9
|
"@uniweb/content-reader",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"deps": []
|
|
30
30
|
},
|
|
31
31
|
"@uniweb/core": {
|
|
32
|
-
"version": "0.
|
|
32
|
+
"version": "0.13.0",
|
|
33
33
|
"path": "framework/core",
|
|
34
34
|
"deps": [
|
|
35
35
|
"@uniweb/semantic-parser",
|
|
@@ -42,14 +42,14 @@
|
|
|
42
42
|
"deps": []
|
|
43
43
|
},
|
|
44
44
|
"@uniweb/icons": {
|
|
45
|
-
"version": "0.4.
|
|
45
|
+
"version": "0.4.4",
|
|
46
46
|
"path": "framework/icons",
|
|
47
47
|
"deps": [
|
|
48
48
|
"@uniweb/core"
|
|
49
49
|
]
|
|
50
50
|
},
|
|
51
51
|
"@uniweb/kit": {
|
|
52
|
-
"version": "0.13.
|
|
52
|
+
"version": "0.13.7",
|
|
53
53
|
"path": "framework/kit",
|
|
54
54
|
"deps": [
|
|
55
55
|
"@uniweb/core",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"deps": []
|
|
69
69
|
},
|
|
70
70
|
"@uniweb/projections": {
|
|
71
|
-
"version": "0.3.
|
|
71
|
+
"version": "0.3.7",
|
|
72
72
|
"path": "framework/projections",
|
|
73
73
|
"deps": [
|
|
74
74
|
"@uniweb/content-writer",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
]
|
|
77
77
|
},
|
|
78
78
|
"@uniweb/runtime": {
|
|
79
|
-
"version": "0.
|
|
79
|
+
"version": "0.13.0",
|
|
80
80
|
"path": "framework/runtime",
|
|
81
81
|
"deps": [
|
|
82
82
|
"@uniweb/core",
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
"deps": []
|
|
90
90
|
},
|
|
91
91
|
"@uniweb/schemas": {
|
|
92
|
-
"version": "0.2.
|
|
92
|
+
"version": "0.2.11",
|
|
93
93
|
"path": "framework/schemas",
|
|
94
94
|
"deps": []
|
|
95
95
|
},
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
"deps": []
|
|
115
115
|
},
|
|
116
116
|
"@uniweb/unipress": {
|
|
117
|
-
"version": "0.8.
|
|
117
|
+
"version": "0.8.14",
|
|
118
118
|
"path": "framework/unipress",
|
|
119
119
|
"deps": [
|
|
120
120
|
"@uniweb/build",
|
|
@@ -321,7 +321,9 @@ async function loginViaTokenPaste({ apiBase, nonInteractive }) {
|
|
|
321
321
|
}
|
|
322
322
|
|
|
323
323
|
// Open a URL in the default browser. Returns whether it launched.
|
|
324
|
-
|
|
324
|
+
// Exported for the publish payment refusal, which opens the backend's
|
|
325
|
+
// settlement URL VERBATIM and needs no loopback (backend/payment-handoff.js).
|
|
326
|
+
export async function openBrowser(url) {
|
|
325
327
|
try {
|
|
326
328
|
const { exec } = await import('node:child_process')
|
|
327
329
|
const cmd =
|