uniweb 0.12.26 → 0.12.28
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 +3 -0
- package/src/backend/client.js +339 -0
- package/src/commands/clone.js +18 -32
- package/src/commands/deploy.js +218 -1783
- package/src/commands/handoff.js +9 -246
- package/src/commands/invite.js +10 -318
- package/src/commands/org.js +6 -8
- package/src/commands/publish.js +128 -1153
- package/src/commands/pull.js +22 -36
- package/src/commands/push.js +43 -101
- package/src/commands/register.js +184 -39
- package/src/commands/runtime.js +141 -0
- package/src/commands/template.js +13 -221
- package/src/framework-index.json +18 -7
- package/src/index.js +74 -100
- package/src/utils/asset-upload.js +162 -0
- package/src/utils/code-upload.js +245 -0
- package/src/utils/config.js +11 -44
- package/src/utils/registry-auth.js +35 -1
- package/src/utils/registry-orgs.js +141 -73
- package/src/utils/runtime-upload.js +163 -0
- package/src/commands/login.js +0 -230
- package/src/utils/auth.js +0 -212
- package/src/utils/registry.js +0 -466
package/src/commands/pull.js
CHANGED
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
* uniweb pull --registry <url> Override the backend origin
|
|
27
27
|
* uniweb pull --token <bearer> Read with this bearer; skips `uniweb login`
|
|
28
28
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
29
|
+
* Backend: via BackendClient (the content + folder pull lanes), both keyed by
|
|
30
|
+
* `site.yml::$uuid`. Origin from
|
|
31
31
|
* --registry > UNIWEB_REGISTER_URL > the local default.
|
|
32
32
|
* Auth: --token > UNIWEB_TOKEN > `uniweb login` session.
|
|
33
33
|
*
|
|
@@ -46,10 +46,9 @@ import {
|
|
|
46
46
|
resolveCollectionsConfig,
|
|
47
47
|
} from '@uniweb/build/uwx'
|
|
48
48
|
import { makeModelResolver } from './push.js'
|
|
49
|
-
import {
|
|
49
|
+
import { BackendClient } from '../backend/client.js'
|
|
50
50
|
import { resolveSiteDir as defaultResolveSiteDir } from './deploy.js'
|
|
51
51
|
|
|
52
|
-
const DEFAULT_BACKEND_ORIGIN = 'http://localhost:8080'
|
|
53
52
|
const FOLDER_MODEL = '@uniweb/folder'
|
|
54
53
|
|
|
55
54
|
const colors = { reset: '\x1b[0m', bright: '\x1b[1m', dim: '\x1b[2m', red: '\x1b[31m', green: '\x1b[32m', blue: '\x1b[36m' }
|
|
@@ -111,22 +110,20 @@ export function splitCollectionsPull(payload) {
|
|
|
111
110
|
* fetch), `resolveSiteDir`, `getToken` (skip auth).
|
|
112
111
|
*/
|
|
113
112
|
export async function pull(args = [], deps = {}) {
|
|
114
|
-
const fetchImpl = deps.fetch || globalThis.fetch
|
|
115
113
|
const resolveSiteDir = deps.resolveSiteDir || defaultResolveSiteDir
|
|
116
114
|
|
|
117
115
|
const dryRun = args.includes('--dry-run')
|
|
118
116
|
const tokenFlag = flagValue(args, '--token')
|
|
119
117
|
const prune = !(args.includes('--no-delete') || args.includes('--no-prune')) // git-like by default
|
|
120
118
|
const noCollections = args.includes('--no-collections') || args.includes('--content-only')
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
119
|
+
const client = new BackendClient({
|
|
120
|
+
originFlag: flagValue(args, '--registry'),
|
|
121
|
+
token: tokenFlag,
|
|
122
|
+
getToken: deps.getToken,
|
|
123
|
+
fetchImpl: deps.fetch,
|
|
124
|
+
args,
|
|
125
|
+
command: 'Pulling',
|
|
126
|
+
})
|
|
130
127
|
|
|
131
128
|
const siteDir = await resolveSiteDir(args, 'pull')
|
|
132
129
|
// One identity per site: `site.yml::$uuid`. Both lanes (content + folder) are keyed
|
|
@@ -138,33 +135,22 @@ export async function pull(args = [], deps = {}) {
|
|
|
138
135
|
return { exitCode: 0 }
|
|
139
136
|
}
|
|
140
137
|
|
|
141
|
-
// Lazy bearer — acquired on first GET (a dry run stays offline). Tests inject
|
|
142
|
-
// deps.getToken to skip auth entirely.
|
|
143
|
-
let cachedToken = null
|
|
144
|
-
const getToken =
|
|
145
|
-
deps.getToken ||
|
|
146
|
-
(async () => {
|
|
147
|
-
if (cachedToken) return cachedToken
|
|
148
|
-
cachedToken = tokenFlag || process.env.UNIWEB_TOKEN || (await ensureRegistryAuth({ apiBase, command: 'Pulling', args }))
|
|
149
|
-
return cachedToken
|
|
150
|
-
})
|
|
151
|
-
|
|
152
138
|
if (dryRun) {
|
|
153
|
-
info(`Dry run — would
|
|
154
|
-
if (!noCollections) info(`Dry run — would
|
|
139
|
+
info(`Dry run — would pull content from ${colors.dim}${client.origin}${colors.reset}`)
|
|
140
|
+
if (!noCollections) info(`Dry run — would also pull collections`)
|
|
155
141
|
return { exitCode: 0 }
|
|
156
142
|
}
|
|
157
143
|
|
|
158
|
-
// GET a pull lane and parse it as JSON.
|
|
144
|
+
// GET a pull lane via the client and parse it as JSON. `doRequest` is a thunk
|
|
145
|
+
// returning the client's Response promise. 404 → null (deleted / no access); any
|
|
159
146
|
// failure is reported and returns null (the lane is skipped, not fatal).
|
|
160
|
-
const getJson = async (
|
|
161
|
-
|
|
162
|
-
info(`Pulling ${colors.bright}${label}${colors.reset} from ${colors.dim}${url}${colors.reset} …`)
|
|
147
|
+
const getJson = async (label, doRequest) => {
|
|
148
|
+
info(`Pulling ${colors.bright}${label}${colors.reset} from ${colors.dim}${client.origin}${colors.reset} …`)
|
|
163
149
|
let res
|
|
164
150
|
try {
|
|
165
|
-
res = await
|
|
151
|
+
res = await doRequest()
|
|
166
152
|
} catch (err) {
|
|
167
|
-
error(`Could not reach the backend at ${
|
|
153
|
+
error(`Could not reach the backend at ${client.origin}: ${err.message}`)
|
|
168
154
|
note('Set the origin with --registry <url> or UNIWEB_REGISTER_URL.')
|
|
169
155
|
return null
|
|
170
156
|
}
|
|
@@ -192,7 +178,7 @@ export async function pull(args = [], deps = {}) {
|
|
|
192
178
|
|
|
193
179
|
// Lane 1 — content → config + pages/** + layout/**.
|
|
194
180
|
const siteDoc = extractDocument(
|
|
195
|
-
await getJson(
|
|
181
|
+
await getJson('content', () => client.pullSiteContent(siteContentUuid))
|
|
196
182
|
)
|
|
197
183
|
if (siteDoc) {
|
|
198
184
|
const report = siteContentDocumentToProject({ document: siteDoc, siteRoot: siteDir, prune })
|
|
@@ -206,10 +192,10 @@ export async function pull(args = [], deps = {}) {
|
|
|
206
192
|
// holds a folder uuid). Models are resolved by name (async) up front, so
|
|
207
193
|
// collectionsToProject keeps its synchronous contract.
|
|
208
194
|
if (!noCollections) {
|
|
209
|
-
const payload = await getJson(
|
|
195
|
+
const payload = await getJson('collections', () => client.pullFolder(siteContentUuid))
|
|
210
196
|
if (payload) {
|
|
211
197
|
const { folderDoc, recordDocs } = splitCollectionsPull(payload)
|
|
212
|
-
const resolveModel = makeModelResolver({
|
|
198
|
+
const resolveModel = makeModelResolver({ client })
|
|
213
199
|
const declByModel = new Map()
|
|
214
200
|
for (const model of [...new Set(recordDocs.map((d) => d.$model).filter(Boolean))]) {
|
|
215
201
|
try {
|
package/src/commands/push.js
CHANGED
|
@@ -7,12 +7,11 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Each entity is an entity-content document (`$id` + `$model` + sections). The site
|
|
9
9
|
* holds exactly one identity: `site.yml::$uuid` (the site-content entity). A first
|
|
10
|
-
* push has none — it CREATEs the site (
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* the framework never holds a folder uuid. Records still round-trip their own `$uuid`
|
|
10
|
+
* push has none — it CREATEs the site (uuid-less), the backend mints + adopts it
|
|
11
|
+
* and returns the new uuid, which `push` records into `site.yml`. Later pushes
|
|
12
|
+
* UPDATE by that uuid. The folder lane is keyed by the SAME site-content uuid —
|
|
13
|
+
* the backend owns the site's `@uniweb/folder`, so the framework never holds a
|
|
14
|
+
* folder uuid. Records still round-trip their own `$uuid`
|
|
16
15
|
* (back-filled into their source files). site-content is pushed wholesale (no per-item
|
|
17
16
|
* uuids on the wire). Push-only, last-push-wins (`collision=force`) in v1.
|
|
18
17
|
*
|
|
@@ -32,8 +31,7 @@
|
|
|
32
31
|
* uniweb push --foundation <dir> Use this local foundation for the Model schema
|
|
33
32
|
* uniweb push --all Send every record (bypass the changed-only cache)
|
|
34
33
|
*
|
|
35
|
-
*
|
|
36
|
-
* (update), /dev/site/folder/push/{uuid}. origin from
|
|
34
|
+
* Backend: via BackendClient (the content + folder sync lanes). Origin from
|
|
37
35
|
* --registry > UNIWEB_REGISTER_URL > the local default.
|
|
38
36
|
* Auth: --token > UNIWEB_TOKEN > `uniweb login` session.
|
|
39
37
|
*/
|
|
@@ -45,13 +43,9 @@ import {
|
|
|
45
43
|
backfillEntityUuids,
|
|
46
44
|
writeSiteEntityUuid,
|
|
47
45
|
} from '@uniweb/build/uwx'
|
|
48
|
-
import {
|
|
46
|
+
import { BackendClient } from '../backend/client.js'
|
|
49
47
|
import { resolveSiteDir } from './deploy.js'
|
|
50
48
|
|
|
51
|
-
// Same backend host as `uniweb register`; only the path differs (the restore
|
|
52
|
-
// lane). Overridable via --registry / UNIWEB_REGISTER_URL (a URL; origin taken).
|
|
53
|
-
const DEFAULT_BACKEND_ORIGIN = 'http://localhost:8080'
|
|
54
|
-
|
|
55
49
|
const colors = {
|
|
56
50
|
reset: '\x1b[0m', bright: '\x1b[1m', dim: '\x1b[2m',
|
|
57
51
|
red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[36m',
|
|
@@ -95,7 +89,7 @@ function extractFinalized(payload) {
|
|
|
95
89
|
.filter((e) => Number.isInteger(e.index) && e.uuid)
|
|
96
90
|
}
|
|
97
91
|
|
|
98
|
-
// Pull the minted site-content uuid out of a CREATE
|
|
92
|
+
// Pull the minted site-content uuid out of a CREATE
|
|
99
93
|
// response. The exact shape is an open backend item — tolerant of a bare
|
|
100
94
|
// `{ siteContentUuid }` / `{ $uuid }` / `{ uuid }`, or the same `report.finalized[]`
|
|
101
95
|
// envelope the update/folder lanes return (the site entity is submitted alone, so its
|
|
@@ -124,45 +118,15 @@ function changedSummary(finalized) {
|
|
|
124
118
|
return parts.length ? parts.join(', ') : null
|
|
125
119
|
}
|
|
126
120
|
|
|
127
|
-
//
|
|
128
|
-
// /
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
export function
|
|
132
|
-
const m = /^@([^/]+)\/(.+)$/.exec(modelName)
|
|
133
|
-
if (m) return `/dev/registry/data-schemas/${encodeURIComponent(m[1])}/${encodeURIComponent(m[2])}`
|
|
134
|
-
return `/dev/registry/data-schemas/${encodeURIComponent(modelName)}`
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Resolve a Model NOT defined by the local foundation by fetching its declaration
|
|
138
|
-
// (the `@uniweb/data-schema` form) from the backend's Model-read route. Cached per
|
|
139
|
-
// run; HTTP 404 → null (the emitter then says "register it first"). The bearer is
|
|
140
|
-
// acquired lazily via getToken, so a fully-local sync never authenticates.
|
|
141
|
-
export function makeModelResolver({ apiBase, getToken, fetchImpl = fetch }) {
|
|
121
|
+
// Resolve a Model NOT defined by the local foundation by reading its declaration
|
|
122
|
+
// (the `@uniweb/data-schema` form) from the backend via the client. Cached per run;
|
|
123
|
+
// HTTP 404 → null (the emitter then says "register it first"). The bearer is acquired
|
|
124
|
+
// lazily by the client, so a fully-local sync never authenticates.
|
|
125
|
+
export function makeModelResolver({ client }) {
|
|
142
126
|
const cache = new Map()
|
|
143
127
|
return async (modelName) => {
|
|
144
128
|
if (cache.has(modelName)) return cache.get(modelName)
|
|
145
|
-
const
|
|
146
|
-
const token = await getToken()
|
|
147
|
-
let res
|
|
148
|
-
try {
|
|
149
|
-
res = await fetchImpl(url, { headers: { Authorization: `Bearer ${token}` } })
|
|
150
|
-
} catch (err) {
|
|
151
|
-
throw new Error(`could not reach the Model-read endpoint ${url}: ${err.message}`)
|
|
152
|
-
}
|
|
153
|
-
if (res.status === 404) {
|
|
154
|
-
cache.set(modelName, null)
|
|
155
|
-
return null
|
|
156
|
-
}
|
|
157
|
-
if (!res.ok) {
|
|
158
|
-
throw new Error(`Model-read ${url} failed: HTTP ${res.status} ${res.statusText}`)
|
|
159
|
-
}
|
|
160
|
-
let decl
|
|
161
|
-
try {
|
|
162
|
-
decl = await res.json()
|
|
163
|
-
} catch (err) {
|
|
164
|
-
throw new Error(`Model-read ${url} returned non-JSON: ${err.message}`)
|
|
165
|
-
}
|
|
129
|
+
const decl = await client.readDataSchema(modelName)
|
|
166
130
|
cache.set(modelName, decl)
|
|
167
131
|
return decl
|
|
168
132
|
}
|
|
@@ -196,30 +160,18 @@ export async function push(args = []) {
|
|
|
196
160
|
const asOrg = flagValue(args, '--as-org')
|
|
197
161
|
const foundationDir = flagValue(args, '--foundation')
|
|
198
162
|
const sendAll = args.includes('--all') // bypass the send-only-changed cache
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
163
|
+
// One front door. The bearer is resolved lazily on first need (a non-local Model
|
|
164
|
+
// read during the build, or the submit), so a fully-local sync — and --dry-run / -o
|
|
165
|
+
// — never authenticate when every Model is defined by the local foundation.
|
|
166
|
+
const client = new BackendClient({
|
|
167
|
+
originFlag: flagValue(args, '--registry'),
|
|
168
|
+
token: tokenFlag,
|
|
169
|
+
args,
|
|
170
|
+
command: 'Syncing',
|
|
171
|
+
})
|
|
209
172
|
|
|
210
173
|
const siteDir = await resolveSiteDir(args, 'push')
|
|
211
174
|
|
|
212
|
-
// Lazy bearer — acquired once, on first need (a non-local Model fetch during the
|
|
213
|
-
// build, or the submit). A fully-local sync never triggers it, so --dry-run / -o
|
|
214
|
-
// stay offline when every Model is defined by the local foundation.
|
|
215
|
-
let cachedToken = null
|
|
216
|
-
const getToken = async () => {
|
|
217
|
-
if (cachedToken) return cachedToken
|
|
218
|
-
cachedToken =
|
|
219
|
-
tokenFlag || process.env.UNIWEB_TOKEN || (await ensureRegistryAuth({ apiBase, command: 'Syncing', args }))
|
|
220
|
-
return cachedToken
|
|
221
|
-
}
|
|
222
|
-
|
|
223
175
|
// Build BOTH directional packages (the producer side). Each carries its own
|
|
224
176
|
// `index` — the per-entity source-file map for back-fill, correlated by submission
|
|
225
177
|
// position. Non-local Models are fetched from the registry on demand. `priorHashes`
|
|
@@ -229,7 +181,7 @@ export async function push(args = []) {
|
|
|
229
181
|
try {
|
|
230
182
|
pkg = await emitSyncPackages(siteDir, {
|
|
231
183
|
...(foundationDir ? { foundationDir } : {}),
|
|
232
|
-
resolveModel: makeModelResolver({
|
|
184
|
+
resolveModel: makeModelResolver({ client }),
|
|
233
185
|
priorHashes,
|
|
234
186
|
sendAll,
|
|
235
187
|
})
|
|
@@ -266,41 +218,30 @@ export async function push(args = []) {
|
|
|
266
218
|
}
|
|
267
219
|
if (dryRun) {
|
|
268
220
|
if (siteContent) {
|
|
269
|
-
const route = siteContentUuid ? `/dev/site/content/push/${siteContentUuid}` : '/dev/site/content'
|
|
270
221
|
const verb = siteContentUuid ? 'update' : 'create'
|
|
271
|
-
info(`Dry run — would ${verb} content at ${colors.dim}${
|
|
222
|
+
info(`Dry run — would ${verb} content at ${colors.dim}${client.origin}${colors.reset}`)
|
|
272
223
|
}
|
|
273
224
|
if (collections) {
|
|
274
|
-
|
|
275
|
-
info(`Dry run — would push the folder to ${colors.dim}${apiBase}/dev/site/folder/push/${key}${colors.reset}`)
|
|
225
|
+
info(`Dry run — would push the folder at ${colors.dim}${client.origin}${colors.reset}`)
|
|
276
226
|
}
|
|
277
227
|
return { exitCode: 0 }
|
|
278
228
|
}
|
|
279
229
|
|
|
280
|
-
const token = await getToken()
|
|
281
230
|
const wrote = []
|
|
282
231
|
let finalizedTotal = 0
|
|
283
232
|
|
|
284
|
-
// POST one lane
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
const params = new URLSearchParams({ collision: 'force' })
|
|
292
|
-
if (asOrg) params.set('as_org', asOrg)
|
|
293
|
-
const url = `${apiBase}${path}?${params.toString()}`
|
|
294
|
-
info(`Pushing ${label} to ${colors.dim}${url}${colors.reset} …`)
|
|
233
|
+
// POST one lane via the client and parse the JSON response. `doRequest` is a thunk
|
|
234
|
+
// returning the client's Response promise (so the "Pushing …" line prints before the
|
|
235
|
+
// request fires). The client carries `collision=force` (last-push-wins) + the optional
|
|
236
|
+
// `--as-org`. Returns the parsed payload, or null on any transport/HTTP/parse failure
|
|
237
|
+
// (already reported).
|
|
238
|
+
const postLane = async (label, doRequest) => {
|
|
239
|
+
info(`Pushing ${label} to ${colors.dim}${client.origin}${colors.reset} …`)
|
|
295
240
|
let res
|
|
296
241
|
try {
|
|
297
|
-
res = await
|
|
298
|
-
method: 'POST',
|
|
299
|
-
headers: { 'Content-Type': 'application/zip', Authorization: `Bearer ${token}` },
|
|
300
|
-
body: buffer,
|
|
301
|
-
})
|
|
242
|
+
res = await doRequest()
|
|
302
243
|
} catch (err) {
|
|
303
|
-
error(`Could not reach the backend at ${
|
|
244
|
+
error(`Could not reach the backend at ${client.origin}: ${err.message}`)
|
|
304
245
|
note('Set the origin with --registry <url> or UNIWEB_REGISTER_URL.')
|
|
305
246
|
return null
|
|
306
247
|
}
|
|
@@ -324,8 +265,8 @@ export async function push(args = []) {
|
|
|
324
265
|
// POST a lane that round-trips entity uuids (content UPDATE + the folder): parse the
|
|
325
266
|
// finalized list (for record back-fill + the changed summary). Returns the finalized
|
|
326
267
|
// array, or null on failure (already reported).
|
|
327
|
-
const pushLane = async (label,
|
|
328
|
-
const payload = await postLane(label,
|
|
268
|
+
const pushLane = async (label, doRequest) => {
|
|
269
|
+
const payload = await postLane(label, doRequest)
|
|
329
270
|
if (payload === null) return null
|
|
330
271
|
const finalized = extractFinalized(payload)
|
|
331
272
|
if (!finalized) {
|
|
@@ -347,13 +288,15 @@ export async function push(args = []) {
|
|
|
347
288
|
if (siteContentUuid) {
|
|
348
289
|
const finalized = await pushLane(
|
|
349
290
|
'site-content',
|
|
350
|
-
|
|
351
|
-
siteContent.buffer
|
|
291
|
+
() => client.updateSiteContent(siteContentUuid, siteContent.buffer, { asOrg })
|
|
352
292
|
)
|
|
353
293
|
if (!finalized) return { exitCode: 1 }
|
|
354
294
|
finalizedTotal += finalized.length
|
|
355
295
|
} else {
|
|
356
|
-
const payload = await postLane(
|
|
296
|
+
const payload = await postLane(
|
|
297
|
+
'site-content',
|
|
298
|
+
() => client.createSiteContent(siteContent.buffer, { asOrg })
|
|
299
|
+
)
|
|
357
300
|
if (payload === null) return { exitCode: 1 }
|
|
358
301
|
const minted = extractMintedSiteUuid(payload)
|
|
359
302
|
if (!minted) {
|
|
@@ -379,8 +322,7 @@ export async function push(args = []) {
|
|
|
379
322
|
}
|
|
380
323
|
const finalized = await pushLane(
|
|
381
324
|
'collections',
|
|
382
|
-
|
|
383
|
-
collections.buffer
|
|
325
|
+
() => client.pushFolder(boundSiteUuid, collections.buffer, { asOrg })
|
|
384
326
|
)
|
|
385
327
|
if (!finalized) return { exitCode: 1 }
|
|
386
328
|
const bf = backfillEntityUuids({ index: collections.index, finalized })
|