uniweb 0.12.35 → 0.12.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/partials/agents.md +11 -11
- package/src/backend/client.js +78 -24
- package/src/backend/foundation-bring-along.js +229 -0
- package/src/backend/payment-handoff.js +105 -0
- package/src/backend/site-sync.js +2 -2
- package/src/commands/build.js +39 -35
- package/src/commands/clone.js +3 -3
- package/src/commands/deploy.js +95 -424
- package/src/commands/export.js +5 -3
- package/src/commands/publish.js +285 -95
- package/src/commands/pull.js +7 -5
- package/src/commands/push.js +8 -6
- package/src/commands/register.js +13 -5
- package/src/commands/rename.js +3 -2
- package/src/commands/runtime.js +1 -1
- package/src/commands/status.js +24 -5
- package/src/framework-index.json +4 -4
- package/src/index.js +63 -48
- package/src/utils/asset-upload.js +3 -3
- package/src/utils/code-upload.js +43 -3
- package/src/utils/config.js +30 -5
- package/src/utils/registry-auth.js +84 -33
package/src/commands/runtime.js
CHANGED
|
@@ -76,7 +76,7 @@ export async function runtime(args = []) {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
// Version from the SPA build's manifest (the backend keys the version on it);
|
|
79
|
-
// --version overrides, parity with
|
|
79
|
+
// --version overrides, parity with the backend's runtime install --version.
|
|
80
80
|
let version = readFlagValue(rest, '--version')
|
|
81
81
|
if (!version) {
|
|
82
82
|
try {
|
package/src/commands/status.js
CHANGED
|
@@ -24,10 +24,12 @@ import { existsSync, readFileSync } from 'node:fs'
|
|
|
24
24
|
import { join } from 'node:path'
|
|
25
25
|
import yaml from 'js-yaml'
|
|
26
26
|
|
|
27
|
-
import { resolveSiteDir } from './deploy.js'
|
|
27
|
+
import { resolveSiteDir, resolveSiteBackend } from './deploy.js'
|
|
28
28
|
import { probeUnpushed } from '../backend/site-sync.js'
|
|
29
29
|
import { BackendClient } from '../backend/client.js'
|
|
30
30
|
import { readFlagValue } from '../utils/args.js'
|
|
31
|
+
import { resolveLocalFoundation } from '../backend/foundation-bring-along.js'
|
|
32
|
+
import { computeFoundationDigest } from '../utils/code-upload.js'
|
|
31
33
|
|
|
32
34
|
const c = {
|
|
33
35
|
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
@@ -86,16 +88,28 @@ export async function status(args = []) {
|
|
|
86
88
|
// on 404 / any failure, so a backend without the endpoints just shows local.
|
|
87
89
|
let site = null
|
|
88
90
|
let fdnLatest = null
|
|
91
|
+
let foundationFresh = null // true/false when both digests are known; else null
|
|
89
92
|
if (remote) {
|
|
90
93
|
try {
|
|
91
94
|
const client = new BackendClient({
|
|
92
95
|
originFlag: readFlagValue(args, '--backend') || readFlagValue(args, '--registry'),
|
|
96
|
+
siteBackend: await resolveSiteBackend(siteDir),
|
|
93
97
|
token: readFlagValue(args, '--token') || undefined,
|
|
94
98
|
args,
|
|
95
99
|
command: 'Status',
|
|
96
100
|
})
|
|
97
101
|
if (uuid) site = await client.siteStatus(uuid)
|
|
98
|
-
|
|
102
|
+
// Foundation freshness: prefer the LOCAL foundation's scoped name (so a
|
|
103
|
+
// local-foundation site can be checked too); fall back to a scoped
|
|
104
|
+
// site.yml ref. The digest compare is read-only — it never builds, so it
|
|
105
|
+
// only fires when the local foundation is already built (dist present).
|
|
106
|
+
const local = resolveLocalFoundation(siteDir, siteYml)
|
|
107
|
+
const lookupName = local?.scopedName || fndScope
|
|
108
|
+
if (lookupName) fdnLatest = await client.readFoundationLatest(lookupName)
|
|
109
|
+
if (local?.dir && fdnLatest?.digest) {
|
|
110
|
+
const localDigest = computeFoundationDigest(join(local.dir, 'dist'))
|
|
111
|
+
if (localDigest) foundationFresh = localDigest === fdnLatest.digest
|
|
112
|
+
}
|
|
99
113
|
} catch {
|
|
100
114
|
// degrade silently
|
|
101
115
|
}
|
|
@@ -110,7 +124,7 @@ export async function status(args = []) {
|
|
|
110
124
|
changed: probe ? probe.changed : null,
|
|
111
125
|
unchanged: probe ? probe.unchanged : null,
|
|
112
126
|
...(probeErr ? { error: probeErr } : {}),
|
|
113
|
-
...(remote ? { remote: { site, foundation_latest: fdnLatest?.latest_version ?? null } } : {}),
|
|
127
|
+
...(remote ? { remote: { site, foundation_latest: fdnLatest?.latest_version ?? null, foundation_fresh: foundationFresh } } : {}),
|
|
114
128
|
})
|
|
115
129
|
)
|
|
116
130
|
return { exitCode: 0 }
|
|
@@ -123,7 +137,7 @@ export async function status(args = []) {
|
|
|
123
137
|
say.ok(`Synced — site-content ${c.bold}${uuid}${c.reset}`)
|
|
124
138
|
} else {
|
|
125
139
|
say.warn('Not synced — this site has never been pushed to a backend.')
|
|
126
|
-
say.dim('Run `uniweb push` to create it, or `uniweb
|
|
140
|
+
say.dim('Run `uniweb push` to create it, or `uniweb publish` to sync and go live in one step.')
|
|
127
141
|
}
|
|
128
142
|
|
|
129
143
|
// Content
|
|
@@ -142,7 +156,7 @@ export async function status(args = []) {
|
|
|
142
156
|
(probe.unchanged ? ` (${probe.unchanged} unchanged)` : '') +
|
|
143
157
|
'.'
|
|
144
158
|
)
|
|
145
|
-
say.dim('Run `uniweb
|
|
159
|
+
say.dim('Run `uniweb publish` to sync and go live (or `uniweb push` to sync only).')
|
|
146
160
|
}
|
|
147
161
|
|
|
148
162
|
// Foundation
|
|
@@ -162,6 +176,11 @@ export async function status(args = []) {
|
|
|
162
176
|
if (fdnLatest?.latest_version && fndVersion && fdnLatest.latest_version !== fndVersion) {
|
|
163
177
|
say.info(`A newer foundation version (${fdnLatest.latest_version}) is registered than the site pins (${fndVersion}).`)
|
|
164
178
|
}
|
|
179
|
+
if (foundationFresh === false) {
|
|
180
|
+
say.info('Local foundation differs from the registered version — `uniweb register` (or `uniweb publish`) to release the change.')
|
|
181
|
+
} else if (foundationFresh === true) {
|
|
182
|
+
say.ok('Local foundation matches the registered version.')
|
|
183
|
+
}
|
|
165
184
|
if (!site && !fdnLatest) {
|
|
166
185
|
say.dim('(No remote signals — the backend may not expose them yet.)')
|
|
167
186
|
}
|
package/src/framework-index.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-06-
|
|
3
|
+
"generatedAt": "2026-06-25T01:51:21.593Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/build": {
|
|
6
|
-
"version": "0.14.
|
|
6
|
+
"version": "0.14.19",
|
|
7
7
|
"path": "framework/build",
|
|
8
8
|
"deps": [
|
|
9
9
|
"@uniweb/content-reader",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"deps": []
|
|
21
21
|
},
|
|
22
22
|
"@uniweb/content-writer": {
|
|
23
|
-
"version": "0.2.
|
|
23
|
+
"version": "0.2.6",
|
|
24
24
|
"path": "framework/content-writer",
|
|
25
25
|
"deps": []
|
|
26
26
|
},
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
"deps": []
|
|
100
100
|
},
|
|
101
101
|
"@uniweb/unipress": {
|
|
102
|
-
"version": "0.4.
|
|
102
|
+
"version": "0.4.25",
|
|
103
103
|
"path": "framework/unipress",
|
|
104
104
|
"deps": [
|
|
105
105
|
"@uniweb/build",
|
package/src/index.js
CHANGED
|
@@ -606,8 +606,10 @@ async function main() {
|
|
|
606
606
|
process.exit(result?.exitCode ?? 0)
|
|
607
607
|
}
|
|
608
608
|
|
|
609
|
-
// Handle register command (dynamic import — depends on @uniweb/build)
|
|
610
|
-
|
|
609
|
+
// Handle register command (dynamic import — depends on @uniweb/build).
|
|
610
|
+
// `release` is a code-only synonym: `register` reads naturally the first
|
|
611
|
+
// time, `release` for updates (shipping-model.md §6.1). Same act, same code.
|
|
612
|
+
if (command === 'register' || command === 'release') {
|
|
611
613
|
const { register } = await importProjectCommand('./commands/register.js')
|
|
612
614
|
const result = await register(args.slice(1))
|
|
613
615
|
process.exit(result?.exitCode ?? 0)
|
|
@@ -670,15 +672,16 @@ async function main() {
|
|
|
670
672
|
return
|
|
671
673
|
}
|
|
672
674
|
|
|
673
|
-
// Handle publish command —
|
|
674
|
-
//
|
|
675
|
+
// Handle publish command — the smart Uniweb-hosting path: bring the
|
|
676
|
+
// foundation along, sync, settle payment, go live. Distinct from `deploy`
|
|
677
|
+
// (third-party hosts) and `register`/`release` (foundation code → catalog).
|
|
675
678
|
if (command === 'publish') {
|
|
676
679
|
const { publish } = await importProjectCommand('./commands/publish.js')
|
|
677
680
|
const result = await publish(args.slice(1))
|
|
678
681
|
process.exit(result?.exitCode ?? 0)
|
|
679
682
|
}
|
|
680
683
|
|
|
681
|
-
// Handle deploy command (dynamic import —
|
|
684
|
+
// Handle deploy command — third-party hosts only (dynamic import — @uniweb/build)
|
|
682
685
|
if (command === 'deploy') {
|
|
683
686
|
const { deploy } = await importProjectCommand('./commands/deploy.js')
|
|
684
687
|
await deploy(args.slice(1))
|
|
@@ -1086,18 +1089,26 @@ async function main() {
|
|
|
1086
1089
|
* without loading @uniweb/build or any project context.
|
|
1087
1090
|
*/
|
|
1088
1091
|
function printCommandHelp(command) {
|
|
1092
|
+
// `release` is a synonym of `register` (shipping-model.md §6.1) — show the
|
|
1093
|
+
// same help block.
|
|
1094
|
+
if (command === 'release') command = 'register'
|
|
1089
1095
|
const blocks = {
|
|
1090
1096
|
deploy: `
|
|
1091
|
-
${colors.cyan}${colors.bright}uniweb deploy${colors.reset} ${colors.dim}—
|
|
1097
|
+
${colors.cyan}${colors.bright}uniweb deploy${colors.reset} ${colors.dim}— Ship a site to its resolved target${colors.reset}
|
|
1092
1098
|
|
|
1093
1099
|
${colors.bright}Usage:${colors.reset}
|
|
1094
|
-
uniweb deploy [options]
|
|
1100
|
+
uniweb deploy --host <name> [options]
|
|
1095
1101
|
|
|
1096
|
-
|
|
1097
|
-
|
|
1102
|
+
Ships a site to its resolved target. A THIRD-PARTY host builds dist/ (bundle
|
|
1103
|
+
mode) and hands it to a host adapter for upload + invalidation. A UNIWEB target
|
|
1104
|
+
(\`--host=uniweb\`, or a \`uniweb\` target in deploy.yml) delegates to
|
|
1105
|
+
${colors.cyan}uniweb publish${colors.reset} (sync + dynamic serving; brings the foundation along) — the
|
|
1106
|
+
canonical direct verb for Uniweb hosting. With no host chosen, deploy prompts for
|
|
1107
|
+
a third-party adapter. For a self-contained dist/ you upload yourself, use
|
|
1108
|
+
${colors.cyan}uniweb export${colors.reset}.
|
|
1098
1109
|
|
|
1099
1110
|
${colors.bright}Hosts:${colors.reset}
|
|
1100
|
-
uniweb Uniweb hosting (
|
|
1111
|
+
uniweb Uniweb hosting (delegates to \`uniweb publish\`)
|
|
1101
1112
|
cloudflare-pages Cloudflare Pages (build artifact + adapter postBuild)
|
|
1102
1113
|
netlify Netlify (alias of cloudflare-pages adapter)
|
|
1103
1114
|
vercel Vercel (build-only — deploy via \`npx vercel\`)
|
|
@@ -1106,43 +1117,40 @@ ${colors.bright}Hosts:${colors.reset}
|
|
|
1106
1117
|
generic-static Plain static-host build, no host-specific helpers
|
|
1107
1118
|
|
|
1108
1119
|
${colors.bright}Options:${colors.reset}
|
|
1120
|
+
--host <name> The host to ship to (no value → interactive third-party picker, TTY only)
|
|
1109
1121
|
--target <name> Pick a target from deploy.yml (default: deploy.yml's \`default:\`)
|
|
1110
|
-
--
|
|
1111
|
-
--host No value → interactive picker (TTY only)
|
|
1112
|
-
--dry-run Resolve site.yml + foundation/runtime; print summary; no writes
|
|
1113
|
-
--no-auto-publish Don't auto-publish workspace-local foundation as part of deploy
|
|
1122
|
+
--dry-run Resolve the target + adapter; print summary; upload nothing
|
|
1114
1123
|
--no-save Skip the auto-save of lastDeploy in deploy.yml
|
|
1115
|
-
--backend <url> Override the default backend origin (\$UNIWEB_REGISTER_URL or built-in)
|
|
1116
1124
|
--non-interactive Fail with usage info instead of prompting
|
|
1117
1125
|
|
|
1118
|
-
${colors.bright}Auth:${colors.reset}
|
|
1119
|
-
\`host: uniweb\` requires authentication. Run \`uniweb login\` first, set
|
|
1120
|
-
\`UNIWEB_TOKEN=<bearer>\` env var, or use a static-host adapter that
|
|
1121
|
-
doesn't need a Uniweb account. CI / agents / piped stdin auto-detect
|
|
1122
|
-
non-interactive mode and bail with an actionable error instead of
|
|
1123
|
-
hanging on a browser callback.
|
|
1124
|
-
|
|
1125
1126
|
${colors.bright}Examples:${colors.reset}
|
|
1126
|
-
uniweb deploy
|
|
1127
|
-
uniweb deploy --
|
|
1128
|
-
uniweb deploy --host=
|
|
1129
|
-
uniweb deploy --target=preview #
|
|
1127
|
+
uniweb deploy --host=cloudflare-pages # Build + upload to Cloudflare Pages
|
|
1128
|
+
uniweb deploy --host=s3-cloudfront # Build + upload + invalidate
|
|
1129
|
+
uniweb deploy --host=uniweb # → delegates to \`uniweb publish\`
|
|
1130
|
+
uniweb deploy --target=preview # Named target from deploy.yml
|
|
1130
1131
|
`,
|
|
1131
1132
|
publish: `
|
|
1132
|
-
${colors.cyan}${colors.bright}uniweb publish${colors.reset} ${colors.dim}— Publish a
|
|
1133
|
+
${colors.cyan}${colors.bright}uniweb publish${colors.reset} ${colors.dim}— Publish a site to Uniweb hosting (the smart path)${colors.reset}
|
|
1133
1134
|
|
|
1134
1135
|
${colors.bright}Usage:${colors.reset}
|
|
1135
1136
|
uniweb publish [options]
|
|
1136
1137
|
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1138
|
+
The most ergonomic command in the tool: \`uniweb login && uniweb publish\` reads
|
|
1139
|
+
your project and makes the site live on Uniweb hosting. It resolves which site,
|
|
1140
|
+
BRINGS THE FOUNDATION ALONG (releases the local foundation when its code
|
|
1141
|
+
changed), syncs content, settles payment when go-live needs it (a browser
|
|
1142
|
+
handoff), and goes live. A published-registry foundation needs no release; an
|
|
1143
|
+
already-paid site opens no browser.
|
|
1144
|
+
|
|
1145
|
+
For a third-party host use \`uniweb deploy --host=<name>\`; to register a
|
|
1146
|
+
FOUNDATION on its own use \`uniweb register\` (alias \`uniweb release\`).
|
|
1141
1147
|
|
|
1142
1148
|
${colors.bright}Options:${colors.reset}
|
|
1149
|
+
--dry-run Resolve everything; release/sync/POST nothing
|
|
1150
|
+
--yes Skip confirmations (CI); never block on a prompt
|
|
1151
|
+
--no-save Skip the deploy.yml lastDeploy auto-save
|
|
1143
1152
|
--backend <url> Backend origin (default: \$UNIWEB_REGISTER_URL or built-in)
|
|
1144
1153
|
--token <bearer> Auth bearer (skips \`uniweb login\`)
|
|
1145
|
-
--dry-run Resolve everything; POST nothing
|
|
1146
1154
|
`,
|
|
1147
1155
|
create: `
|
|
1148
1156
|
${colors.cyan}${colors.bright}uniweb create${colors.reset} ${colors.dim}— Create a new project${colors.reset}
|
|
@@ -1276,15 +1284,18 @@ ${colors.bright}Options:${colors.reset}
|
|
|
1276
1284
|
Exit codes: 0 clean (or warn-only), 1 violations under --strict, 2 setup error.
|
|
1277
1285
|
`,
|
|
1278
1286
|
register: `
|
|
1279
|
-
${colors.cyan}${colors.bright}uniweb register${colors.reset} ${colors.dim}— Register a foundation + its data schemas with the backend registry${colors.reset}
|
|
1287
|
+
${colors.cyan}${colors.bright}uniweb register${colors.reset} ${colors.dim}— Register (release) a foundation + its data schemas with the backend registry${colors.reset}
|
|
1280
1288
|
|
|
1281
1289
|
${colors.bright}Usage:${colors.reset}
|
|
1282
1290
|
uniweb register [options]
|
|
1291
|
+
uniweb release [options] ${colors.dim}(synonym — reads naturally for updates)${colors.reset}
|
|
1283
1292
|
|
|
1284
1293
|
Builds one \`.uwx\` document and submits it to the registry over HTTP. Run
|
|
1285
|
-
\`uniweb login\` first (or pass \`--token\`). \`register\`
|
|
1286
|
-
schemas); \`uniweb publish\` makes a synced SITE
|
|
1287
|
-
|
|
1294
|
+
\`uniweb login\` first (or pass \`--token\`). \`register\`/\`release\` are for
|
|
1295
|
+
FOUNDATIONS (and schemas) — code only; \`uniweb publish\` makes a synced SITE
|
|
1296
|
+
live (and brings its foundation along); \`uniweb deploy\` hosts on a third-party
|
|
1297
|
+
host. \`register\` and \`release\` are the same act: \`register\` reads naturally
|
|
1298
|
+
the first time, \`release\` for updates.
|
|
1288
1299
|
|
|
1289
1300
|
Auto-detects what you run it in:
|
|
1290
1301
|
• a foundation the foundation + the data schemas it defines/renders
|
|
@@ -1300,7 +1311,7 @@ ${colors.bright}Options:${colors.reset}
|
|
|
1300
1311
|
--scope @org Publish under @org (resolves @/x -> @org/x); default: package.json uniweb.scope
|
|
1301
1312
|
--dry-run Print the .uwx; submit nothing
|
|
1302
1313
|
-o, --output <f> Write the .uwx to a file; submit nothing
|
|
1303
|
-
--
|
|
1314
|
+
--backend <url> Backend origin (alias: --registry; default: \$UNIWEB_REGISTER_URL or a local URL)
|
|
1304
1315
|
--token <bearer> Submit with this bearer; skips \`uniweb login\` (or set UNIWEB_TOKEN)
|
|
1305
1316
|
--non-interactive Fail with usage info instead of prompting
|
|
1306
1317
|
|
|
@@ -1470,10 +1481,11 @@ ${colors.bright}Commands:${colors.reset}
|
|
|
1470
1481
|
rename <type> Rename a foundation, site, or extension across the workspace
|
|
1471
1482
|
dev Start a dev server for a site
|
|
1472
1483
|
build Build the current project
|
|
1473
|
-
|
|
1484
|
+
publish Publish a site to Uniweb hosting (smart: foundation + sync + go live)
|
|
1485
|
+
deploy Deploy a site to a third-party host (--host=<adapter>)
|
|
1474
1486
|
export Export a self-contained site for third-party hosting
|
|
1475
|
-
publish Publish a synced site (make its backend state live)
|
|
1476
1487
|
register Register a foundation + its data schemas with the backend registry
|
|
1488
|
+
release Release a foundation version (synonym of register)
|
|
1477
1489
|
runtime register Register an @uniweb/runtime version to the backend (@std only)
|
|
1478
1490
|
push Push a site's content to the backend
|
|
1479
1491
|
pull Pull a site's content from the backend
|
|
@@ -1506,21 +1518,24 @@ ${colors.bright}Global Options:${colors.reset}
|
|
|
1506
1518
|
Auto-detected when CI=true or no TTY (pipes, agents)
|
|
1507
1519
|
|
|
1508
1520
|
${colors.bright}Publish Options:${colors.reset}
|
|
1521
|
+
--dry-run Resolve everything; release/sync/POST nothing
|
|
1522
|
+
--yes Skip confirmations (CI); never block on a prompt
|
|
1523
|
+
--no-save Skip the deploy.yml lastDeploy auto-save
|
|
1509
1524
|
--backend <url> Backend origin (default: \$UNIWEB_REGISTER_URL or built-in)
|
|
1510
1525
|
--token <bearer> Auth bearer (skips \`uniweb login\`)
|
|
1511
|
-
--dry-run Resolve everything; POST nothing
|
|
1512
1526
|
|
|
1513
|
-
uniweb publish
|
|
1514
|
-
|
|
1527
|
+
uniweb publish is the smart Uniweb-hosting path: it brings the site's
|
|
1528
|
+
foundation along, syncs, and goes live. To register a foundation on its own
|
|
1529
|
+
use \`uniweb register\` (alias \`uniweb release\`); for a third-party host use
|
|
1530
|
+
\`uniweb deploy --host=<name>\`.
|
|
1515
1531
|
|
|
1516
1532
|
${colors.bright}Deploy Options:${colors.reset}
|
|
1533
|
+
--host <name> The host to ship to (no value → interactive third-party
|
|
1534
|
+
picker, TTY only). Third-party: cloudflare-pages, netlify,
|
|
1535
|
+
vercel, github-pages, s3-cloudfront, generic-static.
|
|
1536
|
+
\`--host=uniweb\` delegates to \`uniweb publish\`.
|
|
1517
1537
|
--target <name> Pick a target from deploy.yml (default: deploy.yml's \`default:\`)
|
|
1518
|
-
--
|
|
1519
|
-
Without a value, opens an interactive picker (TTY only).
|
|
1520
|
-
Hosts: uniweb, cloudflare-pages, netlify, vercel,
|
|
1521
|
-
github-pages, s3-cloudfront, generic-static.
|
|
1522
|
-
--dry-run Resolve site.yml + foundation/runtime; print summary; no writes
|
|
1523
|
-
--no-auto-publish Don't auto-publish workspace-local foundation as part of deploy
|
|
1538
|
+
--dry-run Resolve the target + adapter; print summary; upload nothing
|
|
1524
1539
|
--no-save Skip the auto-save of lastDeploy in deploy.yml
|
|
1525
1540
|
|
|
1526
1541
|
${colors.bright}Dev Options:${colors.reset}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Site asset delivery — the asset lane for `uniweb
|
|
2
|
+
* Site asset delivery — the asset lane for `uniweb publish` (channel
|
|
3
3
|
* framework-backend-f90d). After the link build processes a site's media into
|
|
4
4
|
* `dist/assets/`, those bytes are delivered to the backend's content-addressed
|
|
5
|
-
* asset store, and the
|
|
5
|
+
* asset store, and the publish step rewrites the content's local refs to durable
|
|
6
6
|
* serve URLs:
|
|
7
7
|
*
|
|
8
8
|
* 1. PLAN — POST {apiBase}/dev/assets with the file list ({ path,
|
|
@@ -147,7 +147,7 @@ export async function uploadSiteAssets({ apiBase, token, distDir, files, onProgr
|
|
|
147
147
|
onProgress(`↑ ${src.path}`)
|
|
148
148
|
let putRes
|
|
149
149
|
try {
|
|
150
|
-
// The plan's url may be origin-relative (direct mode →
|
|
150
|
+
// The plan's url may be origin-relative (direct mode → the backend) or
|
|
151
151
|
// absolute (presigned → storage); new URL() resolves both.
|
|
152
152
|
putRes = await fetch(new URL(up.url, origin), { method: up.method || 'PUT', headers, body: src.bytes ?? readFileSync(src.diskPath) })
|
|
153
153
|
} catch (err) {
|
package/src/utils/code-upload.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* 1. PLAN — POST {apiBase}/dev/registry/code-uploads with the file list
|
|
9
9
|
* ({ path, content_type, size, sha256? }). The response carries
|
|
10
10
|
* one upload target per file ({ path, method, url, headers })
|
|
11
|
-
* plus mode: 'direct' (dev — URLs point back at
|
|
11
|
+
* plus mode: 'direct' (dev — URLs point back at the backend) or
|
|
12
12
|
* 'presigned' (prod — storage PUTs; bytes never transit the
|
|
13
13
|
* backend). The CLI never branches on the mode.
|
|
14
14
|
* 2. UPLOAD — PUT each file's raw bytes to its URL with the given headers.
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
37
|
import { createHash } from 'node:crypto'
|
|
38
|
-
import { readdirSync, readFileSync, statSync } from 'node:fs'
|
|
38
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
39
39
|
import { join } from 'node:path'
|
|
40
40
|
|
|
41
41
|
// Extension → declared content type. Extension-honest by construction (Vite
|
|
@@ -111,6 +111,46 @@ export function uploadOrder(files) {
|
|
|
111
111
|
return [...rest, ...entry]
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/**
|
|
115
|
+
* The deterministic content digest of a built foundation — the freshness
|
|
116
|
+
* fingerprint `register` records and `publish`/`status` later compare against
|
|
117
|
+
* (shipping-model.md §4.1). It answers one question — "did the foundation change
|
|
118
|
+
* since it was released?" — with NO local state, so it has to be reproducible:
|
|
119
|
+
* the same source, built on any machine, must yield the same digest.
|
|
120
|
+
*
|
|
121
|
+
* Algorithm (§4.1): `sha256` over the newline-joined, SORTED list of the
|
|
122
|
+
* per-file `sha256` (hex) of exactly what `register` ships for a foundation —
|
|
123
|
+
* - the code-upload set (`collectDistFiles`: entry + chunks + assets, with
|
|
124
|
+
* `meta/**` and `*.map` already excluded), and
|
|
125
|
+
* - the schema source (`meta/schema.json`) — NOT uploaded as a file, and not
|
|
126
|
+
* sent verbatim: register submits its *content* as the foundation-schema
|
|
127
|
+
* ENTITY in the `.uwx`. It's read here only to fold the schema into the
|
|
128
|
+
* local freshness fingerprint (so a schema edit shows as "changed").
|
|
129
|
+
* Hashes are SORTED (so the digest is order-independent) and taken over file
|
|
130
|
+
* CONTENTS, not names, so content-hashed chunk filenames don't perturb it.
|
|
131
|
+
* Returns null when there's nothing to hash (no usable dist), so callers skip
|
|
132
|
+
* cleanly rather than fingerprinting emptiness.
|
|
133
|
+
*
|
|
134
|
+
* The result is OPAQUE to the backend — it stores the string and returns it
|
|
135
|
+
* verbatim; the framework owns the algorithm end-to-end and can change it
|
|
136
|
+
* freely (a digest only ever compares against one the same CLI computed).
|
|
137
|
+
*
|
|
138
|
+
* @param {string} distDir - the built `dist/` directory
|
|
139
|
+
* @returns {string|null} `sha256:<hex>` or null when there's nothing to hash
|
|
140
|
+
*/
|
|
141
|
+
export function computeFoundationDigest(distDir) {
|
|
142
|
+
const hashes = collectDistFiles(distDir).map((f) => f.sha256)
|
|
143
|
+
// The schema rides in the register `.uwx`, not the code-upload set, so fold
|
|
144
|
+
// it in explicitly — a schema-only change is still a foundation change.
|
|
145
|
+
const schemaPath = join(distDir, 'meta', 'schema.json')
|
|
146
|
+
if (existsSync(schemaPath)) {
|
|
147
|
+
hashes.push(createHash('sha256').update(readFileSync(schemaPath)).digest('hex'))
|
|
148
|
+
}
|
|
149
|
+
if (!hashes.length) return null
|
|
150
|
+
hashes.sort()
|
|
151
|
+
return 'sha256:' + createHash('sha256').update(hashes.join('\n')).digest('hex')
|
|
152
|
+
}
|
|
153
|
+
|
|
114
154
|
/**
|
|
115
155
|
* The gateway serve URL for a file of a registered foundation version.
|
|
116
156
|
* Mirrors the backend storage convention: scope WITHOUT the '@'.
|
|
@@ -180,7 +220,7 @@ export async function uploadFoundationCode({
|
|
|
180
220
|
const plan = await planRes.json()
|
|
181
221
|
const targets = new Map((plan.uploads || []).map((u) => [u.path, u]))
|
|
182
222
|
const serveBase = plan.serve_base || null
|
|
183
|
-
// The ONE mode-aware bit: direct-mode PUTs are bearer-authed
|
|
223
|
+
// The ONE mode-aware bit: direct-mode PUTs are bearer-authed backend
|
|
184
224
|
// routes; presigned URLs are self-authorizing and must NOT carry a
|
|
185
225
|
// bearer (foreign auth headers can break signed-request validation).
|
|
186
226
|
const authHeaders = plan.mode === 'direct' ? { Authorization: `Bearer ${token}` } : {}
|
package/src/utils/config.js
CHANGED
|
@@ -49,14 +49,35 @@ function readCliConfig() {
|
|
|
49
49
|
return _cliConfig
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* The origin the LAST `uniweb login` authenticated against — persisted on the
|
|
54
|
+
* session record so subsequent verbs default to the backend you logged into
|
|
55
|
+
* (no `--backend` per command). Sync read; null when there's no session or it
|
|
56
|
+
* carries no origin (older sessions). Read directly (not via registry-auth.js)
|
|
57
|
+
* to keep this module off the optional-peer / import-cycle path.
|
|
58
|
+
* @returns {string|null}
|
|
59
|
+
*/
|
|
60
|
+
function readSessionOrigin() {
|
|
61
|
+
try {
|
|
62
|
+
const p = join(homedir(), '.uniweb', 'registry-auth.json')
|
|
63
|
+
if (!existsSync(p)) return null
|
|
64
|
+
const o = JSON.parse(readFileSync(p, 'utf8'))?.origin
|
|
65
|
+
return o || null
|
|
66
|
+
} catch {
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
52
71
|
/**
|
|
53
72
|
* Get the backend's API base origin. `register` POSTs to
|
|
54
73
|
* {origin}/dev/registry/register, `login` to {origin}/dev/auth/login, etc.
|
|
55
|
-
* (BackendClient.resolveBackendOrigin layers the --backend
|
|
56
|
-
* top of this.)
|
|
74
|
+
* (BackendClient.resolveBackendOrigin layers the --backend flag and a site's
|
|
75
|
+
* deploy.yml backend on top of this.)
|
|
57
76
|
*
|
|
58
|
-
* Priority: UNIWEB_REGISTER_URL's origin >
|
|
59
|
-
* >
|
|
77
|
+
* Priority: UNIWEB_REGISTER_URL's origin > the logged-in session origin >
|
|
78
|
+
* ~/.uniweb/config.json registryApiUrl > the default (uniweb.app). Local dev
|
|
79
|
+
* points at a local backend EXPLICITLY (--backend / UNIWEB_REGISTER_URL /
|
|
80
|
+
* `uniweb login --backend …`), rather than the default being localhost.
|
|
60
81
|
* @returns {string}
|
|
61
82
|
*/
|
|
62
83
|
export function getRegistryApiBaseUrl() {
|
|
@@ -64,11 +85,15 @@ export function getRegistryApiBaseUrl() {
|
|
|
64
85
|
if (fromEnv) {
|
|
65
86
|
try { return new URL(fromEnv).origin } catch { /* fall through */ }
|
|
66
87
|
}
|
|
88
|
+
const fromSession = readSessionOrigin()
|
|
89
|
+
if (fromSession) {
|
|
90
|
+
try { return new URL(fromSession).origin } catch { return fromSession }
|
|
91
|
+
}
|
|
67
92
|
const fromCfg = readCliConfig().registryApiUrl
|
|
68
93
|
if (fromCfg) {
|
|
69
94
|
try { return new URL(fromCfg).origin } catch { return fromCfg }
|
|
70
95
|
}
|
|
71
|
-
return '
|
|
96
|
+
return 'https://uniweb.app'
|
|
72
97
|
}
|
|
73
98
|
|
|
74
99
|
/**
|