uniweb 0.31.0 → 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 +5 -5
- package/src/backend/client.js +0 -22
- package/src/backend/payment-handoff.js +136 -100
- package/src/commands/publish.js +29 -21
- package/src/commands/push.js +4 -0
- package/src/framework-index.json +3 -3
- 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.31.
|
|
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,14 +41,14 @@
|
|
|
41
41
|
"js-yaml": "^4.1.0",
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
|
-
"@uniweb/kit": "^0.13.7",
|
|
45
44
|
"@uniweb/core": "^0.13.0",
|
|
46
|
-
"@uniweb/
|
|
47
|
-
"@uniweb/runtime": "^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.27.1",
|
|
50
51
|
"@uniweb/semantic-parser": "^1.3.1",
|
|
51
|
-
"@uniweb/build": "^0.27.0",
|
|
52
52
|
"@uniweb/content-reader": "^1.2.4"
|
|
53
53
|
},
|
|
54
54
|
"peerDependenciesMeta": {
|
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
|
|
|
@@ -440,6 +440,8 @@ export async function publish(args = []) {
|
|
|
440
440
|
let probe
|
|
441
441
|
try {
|
|
442
442
|
probe = await emitSyncPackages(siteDir, {
|
|
443
|
+
// Resolves a foundation-relative `@/x` model ref into `@org/x`.
|
|
444
|
+
...(asOrg ? { org: asOrg } : {}),
|
|
443
445
|
...(foundationDir ? { foundationDir } : {}),
|
|
444
446
|
resolveModel
|
|
445
447
|
})
|
|
@@ -639,6 +641,8 @@ export async function publish(args = []) {
|
|
|
639
641
|
let pkg
|
|
640
642
|
try {
|
|
641
643
|
pkg = await emitSyncPackages(siteDir, {
|
|
644
|
+
// Resolves a foundation-relative `@/x` model ref into `@org/x`.
|
|
645
|
+
...(asOrg ? { org: asOrg } : {}),
|
|
642
646
|
...(foundationDir ? { foundationDir } : {}),
|
|
643
647
|
resolveModel,
|
|
644
648
|
priorHashes,
|
|
@@ -678,18 +682,6 @@ export async function publish(args = []) {
|
|
|
678
682
|
return { exitCode: 1 }
|
|
679
683
|
}
|
|
680
684
|
|
|
681
|
-
// 6. Payment gate — the backend says whether go-live needs payment. Settles
|
|
682
|
-
// via a browser handoff to uniweb.app; degrades to "proceed" when the
|
|
683
|
-
// backend exposes no payment route. The draft is already synced, so a
|
|
684
|
-
// decline leaves a recoverable state (re-run after paying).
|
|
685
|
-
const pay = await settlePaymentIfNeeded({ client, uuid: siteUuid, args, say })
|
|
686
|
-
if (!pay.proceed) {
|
|
687
|
-
say.info(
|
|
688
|
-
'Site synced as a draft but not made live. Re-run `uniweb publish` once payment is complete.'
|
|
689
|
-
)
|
|
690
|
-
return { exitCode: 0 }
|
|
691
|
-
}
|
|
692
|
-
|
|
693
685
|
// 7. Go live — make the just-pushed composite live (its current backend state).
|
|
694
686
|
const siteContent = JSON.parse(await readFile(contentPath, 'utf8'))
|
|
695
687
|
const languages = languagesFromContent(siteContent)
|
|
@@ -705,13 +697,29 @@ export async function publish(args = []) {
|
|
|
705
697
|
return { exitCode: 1 }
|
|
706
698
|
}
|
|
707
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
|
+
|
|
708
717
|
say.err(`Publish rejected: HTTP ${pubRes.status} ${pubRes.statusText}`)
|
|
709
718
|
if (pubRes.status === 401 || pubRes.status === 403) {
|
|
710
719
|
say.dim(
|
|
711
720
|
"Credentials weren't accepted — run `uniweb login` (or pass --token <bearer>)."
|
|
712
721
|
)
|
|
713
722
|
}
|
|
714
|
-
const body = await pubRes.text().catch(() => '')
|
|
715
723
|
if (body) say.dim(body.slice(0, 800))
|
|
716
724
|
return { exitCode: 1 }
|
|
717
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.27.
|
|
6
|
+
"version": "0.27.1",
|
|
7
7
|
"path": "framework/build",
|
|
8
8
|
"deps": [
|
|
9
9
|
"@uniweb/content-reader",
|
|
@@ -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 =
|