uniweb 0.12.26 → 0.12.27
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 +17 -6
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.27",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -42,13 +42,13 @@
|
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
44
|
"@uniweb/core": "0.7.12",
|
|
45
|
-
"@uniweb/
|
|
46
|
-
"@uniweb/
|
|
45
|
+
"@uniweb/kit": "0.9.16",
|
|
46
|
+
"@uniweb/runtime": "0.8.16"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"@uniweb/
|
|
50
|
-
"@uniweb/
|
|
51
|
-
"@uniweb/
|
|
49
|
+
"@uniweb/build": "0.14.11",
|
|
50
|
+
"@uniweb/content-reader": "1.1.12",
|
|
51
|
+
"@uniweb/semantic-parser": "1.1.17"
|
|
52
52
|
},
|
|
53
53
|
"peerDependenciesMeta": {
|
|
54
54
|
"@uniweb/build": {
|
package/partials/agents.md
CHANGED
|
@@ -794,6 +794,7 @@ id: about # Stable identity (for page: links, survives moves)
|
|
|
794
794
|
order: 2 # Navigation sort position
|
|
795
795
|
pages: [team, history, ...] # Child page order (... = rest). Without ... = strict (hides unlisted)
|
|
796
796
|
redirect: academic # Redirect to child page (relative or absolute path, or URL)
|
|
797
|
+
slug: { fr: a-propos } # Localized URL segment per language (multilingual sites)
|
|
797
798
|
```
|
|
798
799
|
|
|
799
800
|
**site.yml:**
|
|
@@ -805,6 +806,8 @@ pages: [home, about] # Strict: only listed pages in nav
|
|
|
805
806
|
|
|
806
807
|
**Route mapping:** Folder structure maps 1:1 to routes. Every folder keeps its natural route — `pages:` controls **order only**, not which child "becomes" the parent. The only exception is the site root: `index:` (or first in `pages:`) in site.yml sets the homepage at `/`.
|
|
807
808
|
|
|
809
|
+
**Localized URLs:** On a multilingual site (`languages:` in site.yml), a page's `slug: { <lang>: <segment> }` gives it a native URL segment per language (`/about` → `/fr/a-propos`); the folder name stays the canonical route. Nested folders compose automatically, and localized URLs flow through navigation, the language switcher, and the sitemap.
|
|
810
|
+
|
|
808
811
|
**Content-less containers:** Folders with `page.yml` but no markdown are structural groups. They appear in `getPageHierarchy()` with `hasContent: false` and their own title/label. When visited directly, the runtime auto-redirects to the first descendant with content. This supports hierarchical navigation (courses → modules → lessons) at any depth.
|
|
809
812
|
|
|
810
813
|
### Lists as Navigation Menus
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BackendClient — the single front door to the Uniweb backend.
|
|
3
|
+
*
|
|
4
|
+
* Every CLI verb that talks to the backend (register, push, pull, clone, org,
|
|
5
|
+
* login — and, as the consolidation proceeds, publish/deploy/invite/handoff)
|
|
6
|
+
* goes through one instance of this client instead of hand-rolling
|
|
7
|
+
* `fetch(… Authorization: Bearer …)` against a per-command default origin. It
|
|
8
|
+
* owns the three things that were previously scattered across a dozen files:
|
|
9
|
+
*
|
|
10
|
+
* 1. ORIGIN — where the backend is. resolveBackendOrigin(): an explicit
|
|
11
|
+
* flag (`--backend` / `--registry`) > UNIWEB_REGISTER_URL >
|
|
12
|
+
* the local default. Any full URL is reduced to its origin.
|
|
13
|
+
* 2. AUTH — the opaque session bearer (utils/registry-auth.js). Resolved
|
|
14
|
+
* LAZILY on the first authed call, so dry-runs and fully-local
|
|
15
|
+
* work never trigger a login.
|
|
16
|
+
* 3. REQUESTS — one request() helper (auth header, query params, body
|
|
17
|
+
* content-type) plus a typed method per backend operation.
|
|
18
|
+
*
|
|
19
|
+
* The legacy platform paths (the old per-verb URL resolvers, the separate
|
|
20
|
+
* token-based auth, and the remote/local registry classes) are deliberately
|
|
21
|
+
* NOT represented here — consolidating every verb onto this client is what
|
|
22
|
+
* retires them.
|
|
23
|
+
*
|
|
24
|
+
* During Phase 1 the typed methods delegate to the existing, well-tested
|
|
25
|
+
* helpers (registry-auth.js, registry-orgs.js, code-upload.js); those modules
|
|
26
|
+
* move under backend/ as the consolidation lands, leaving this client as their
|
|
27
|
+
* single home.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { getRegistryApiBaseUrl } from '../utils/config.js'
|
|
31
|
+
import { ensureRegistryAuth, fetchMe } from '../utils/registry-auth.js'
|
|
32
|
+
import { fetchOrgs as fetchOrgsImpl, createOrg as createOrgImpl } from '../utils/registry-orgs.js'
|
|
33
|
+
import { uploadFoundationCode } from '../utils/code-upload.js'
|
|
34
|
+
import { uploadSiteAssets } from '../utils/asset-upload.js'
|
|
35
|
+
import { uploadRuntime } from '../utils/runtime-upload.js'
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolve the backend origin: an explicit flag value wins; otherwise defer to
|
|
39
|
+
* the shared origin resolver (env override > saved config > local default). A
|
|
40
|
+
* full URL is reduced to its origin, so callers may pass a whole endpoint URL.
|
|
41
|
+
*
|
|
42
|
+
* One resolver, one place to revisit when we settle the single-origin-input +
|
|
43
|
+
* discovery-handshake decision — kept delegating for now so this stays a pure,
|
|
44
|
+
* behavior-preserving consolidation.
|
|
45
|
+
*
|
|
46
|
+
* @param {string} [flag] - the raw value of --backend / --registry, if supplied
|
|
47
|
+
* @returns {string} a bare origin with no trailing slash
|
|
48
|
+
*/
|
|
49
|
+
export function resolveBackendOrigin(flag) {
|
|
50
|
+
if (flag) {
|
|
51
|
+
try { return new URL(flag).origin } catch { /* not a URL — fall through */ }
|
|
52
|
+
}
|
|
53
|
+
return getRegistryApiBaseUrl()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The fallback capability doc when `GET /dev/config` is absent or unreachable
|
|
58
|
+
* (an older backend, or no backend at all). Keeps the client non-breaking: the
|
|
59
|
+
* bases mirror a self-serve dev backend, `assetBase` falls back to the historical
|
|
60
|
+
* production CDN host so published-site asset resolution is unchanged, and
|
|
61
|
+
* `runtime.installed` is empty so runtime resolution requires an explicit pin.
|
|
62
|
+
*/
|
|
63
|
+
export const DISCOVERY_DEFAULTS = {
|
|
64
|
+
gatewayBase: '/gateway',
|
|
65
|
+
assetBase: 'https://assets.uniweb.app/',
|
|
66
|
+
auth: { loginPath: '/dev/auth/login', required: true },
|
|
67
|
+
delivery: { deploy: true, publish: true, broker: 'self-serve' },
|
|
68
|
+
assets: { supported: false },
|
|
69
|
+
runtime: { installed: [] },
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class BackendClient {
|
|
73
|
+
/**
|
|
74
|
+
* @param {object} [opts]
|
|
75
|
+
* @param {string} [opts.origin] - explicit origin (wins over originFlag/env)
|
|
76
|
+
* @param {string} [opts.originFlag] - raw --backend/--registry value to resolve
|
|
77
|
+
* @param {string} [opts.token] - explicit bearer (wins over env + stored session)
|
|
78
|
+
* @param {() => Promise<string>} [opts.getToken] - injected bearer resolver (tests, or
|
|
79
|
+
* callers with their own auth); used when no explicit token/env is present
|
|
80
|
+
* @param {string[]} [opts.args] - argv slice (checked for --non-interactive in auth)
|
|
81
|
+
* @param {string} [opts.command] - label for the login prompt ('Pushing', 'Registering', …)
|
|
82
|
+
* @param {typeof fetch} [opts.fetchImpl] - injectable fetch (tests)
|
|
83
|
+
*/
|
|
84
|
+
constructor({ origin, originFlag, token, getToken, args = [], command = 'This command', fetchImpl } = {}) {
|
|
85
|
+
this.origin = (origin || resolveBackendOrigin(originFlag)).replace(/\/+$/, '')
|
|
86
|
+
this._token = token || process.env.UNIWEB_TOKEN || null
|
|
87
|
+
this._getToken = getToken || null
|
|
88
|
+
this._args = args
|
|
89
|
+
this._command = command
|
|
90
|
+
this._fetch = fetchImpl || ((url, init) => globalThis.fetch(url, init))
|
|
91
|
+
this._discovery = null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The session bearer, resolved lazily and memoized. Order (matching the
|
|
96
|
+
* standalone verbs): explicit --token / UNIWEB_TOKEN (constructor) > stored
|
|
97
|
+
* session > interactive login (ensureRegistryAuth).
|
|
98
|
+
* @returns {Promise<string>}
|
|
99
|
+
*/
|
|
100
|
+
async token() {
|
|
101
|
+
if (this._token) return this._token
|
|
102
|
+
if (this._getToken) {
|
|
103
|
+
this._token = await this._getToken()
|
|
104
|
+
return this._token
|
|
105
|
+
}
|
|
106
|
+
this._token = await ensureRegistryAuth({ apiBase: this.origin, command: this._command, args: this._args })
|
|
107
|
+
return this._token
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Low-level request against the backend. Adds the bearer (unless auth:false),
|
|
112
|
+
* applies query params, and infers a content-type from the body when unset
|
|
113
|
+
* (string → application/json, Buffer/Uint8Array → application/zip). Returns
|
|
114
|
+
* the raw Response so callers branch on status themselves (409 resume,
|
|
115
|
+
* 404 → null, 401/403 messaging, …).
|
|
116
|
+
*
|
|
117
|
+
* @param {string} path - leading-slash path, e.g. '/dev/site/content'
|
|
118
|
+
* @param {object} [opts]
|
|
119
|
+
* @param {string} [opts.method='GET']
|
|
120
|
+
* @param {*} [opts.body]
|
|
121
|
+
* @param {Record<string,string>} [opts.headers]
|
|
122
|
+
* @param {Record<string,string|number|undefined>} [opts.query]
|
|
123
|
+
* @param {boolean} [opts.auth=true]
|
|
124
|
+
* @returns {Promise<Response>}
|
|
125
|
+
*/
|
|
126
|
+
async request(path, { method = 'GET', body, headers = {}, query, auth = true } = {}) {
|
|
127
|
+
const url = new URL(path, this.origin)
|
|
128
|
+
if (query) {
|
|
129
|
+
for (const [k, v] of Object.entries(query)) {
|
|
130
|
+
if (v != null) url.searchParams.set(k, String(v))
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const h = { ...headers }
|
|
134
|
+
if (auth) h.Authorization = `Bearer ${await this.token()}`
|
|
135
|
+
if (body != null && h['Content-Type'] == null) {
|
|
136
|
+
if (typeof body === 'string') h['Content-Type'] = 'application/json'
|
|
137
|
+
else if (body instanceof Uint8Array || Buffer.isBuffer(body)) h['Content-Type'] = 'application/zip'
|
|
138
|
+
}
|
|
139
|
+
return this._fetch(url.href, { method, headers: h, body })
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── Discovery ─────────────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* GET /dev/config — the anonymous capability/handshake document. The one route
|
|
146
|
+
* that answers before login (`auth: false`). Lazy + cached for the client's
|
|
147
|
+
* lifetime; a missing route or any transport/parse error falls back to
|
|
148
|
+
* DISCOVERY_DEFAULTS (non-breaking — an older backend still works). Lets the
|
|
149
|
+
* CLI hardcode nothing about a backend but its origin and discover the rest:
|
|
150
|
+
* `gatewayBase`/`assetBase` (serve/asset roots — relative ⇒ relative-to-origin),
|
|
151
|
+
* `auth`, `delivery` (deploy/publish? broker), `assets` (lane built yet?),
|
|
152
|
+
* `runtime.installed` (the default-runtime source replacing the old /runtime/latest).
|
|
153
|
+
* @returns {Promise<object>}
|
|
154
|
+
*/
|
|
155
|
+
async discover() {
|
|
156
|
+
if (this._discovery) return this._discovery
|
|
157
|
+
try {
|
|
158
|
+
const res = await this.request('/dev/config', { auth: false })
|
|
159
|
+
this._discovery = res.ok ? await res.json() : { ...DISCOVERY_DEFAULTS }
|
|
160
|
+
} catch {
|
|
161
|
+
this._discovery = { ...DISCOVERY_DEFAULTS }
|
|
162
|
+
}
|
|
163
|
+
return this._discovery
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Identity ────────────────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
/** GET /dev/auth/me → the account object ({ uuid, username, handle }) or null. */
|
|
169
|
+
async whoami() {
|
|
170
|
+
return fetchMe({ apiBase: this.origin, token: await this.token() })
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── Registry: foundations + data schemas ──────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* POST /dev/registry/register — submit a names-only .uwx document (a
|
|
177
|
+
* foundation schema + the data schemas it renders, or a standalone schemas
|
|
178
|
+
* package). Returns the raw Response (register branches on a 409 "already
|
|
179
|
+
* registered" to resume code delivery).
|
|
180
|
+
* @param {string} uwxJson - the serialized .uwx (a JSON string)
|
|
181
|
+
* @returns {Promise<Response>}
|
|
182
|
+
*/
|
|
183
|
+
async register(uwxJson) {
|
|
184
|
+
return this.request('/dev/registry/register', { method: 'POST', body: uwxJson })
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Deliver a built foundation's dist/ code (plan → PUT-per-file → verify).
|
|
189
|
+
* Thin pass-through to utils/code-upload.js with this client's origin + token.
|
|
190
|
+
* @param {object} opts - { name, version, distDir, files?, onProgress? }
|
|
191
|
+
*/
|
|
192
|
+
async uploadFoundationCode(opts) {
|
|
193
|
+
return uploadFoundationCode({ apiBase: this.origin, token: await this.token(), ...opts })
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* GET /dev/registry/data-schemas/{scope}/{name} — a Model declaration, or
|
|
198
|
+
* null on 404 (the caller then says "register it first"). Accepts `@scope/name`
|
|
199
|
+
* and bare `name`.
|
|
200
|
+
* @param {string} modelName
|
|
201
|
+
* @returns {Promise<object|null>}
|
|
202
|
+
*/
|
|
203
|
+
async readDataSchema(modelName) {
|
|
204
|
+
const res = await this.request(dataSchemaPath(modelName))
|
|
205
|
+
if (res.status === 404) return null
|
|
206
|
+
if (!res.ok) throw new Error(`Model-read ${modelName} failed: HTTP ${res.status} ${res.statusText}`)
|
|
207
|
+
return res.json()
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ── Orgs ──────────────────────────────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
/** GET /dev/orgs → { account_handle, personal_org_exists, orgs[] }. */
|
|
213
|
+
async fetchOrgs() {
|
|
214
|
+
return fetchOrgsImpl({ apiBase: this.origin, token: await this.token() })
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** POST /dev/orgs { handle } → { handle, uuid, is_primary }. Throws with the server's detail on 409/422. */
|
|
218
|
+
async createOrg(handle) {
|
|
219
|
+
return createOrgImpl({ apiBase: this.origin, token: await this.token(), handle })
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ── Site sync (push / pull) ─────────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
/** POST /dev/site/content — CREATE a site from its content lane (.uwx zip). */
|
|
225
|
+
async createSiteContent(buffer, { asOrg } = {}) {
|
|
226
|
+
return this.request('/dev/site/content', { method: 'POST', body: buffer, query: pushQuery(asOrg) })
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** POST /dev/site/content/push/{uuid} — UPDATE the content lane by site uuid (.uwx zip). */
|
|
230
|
+
async updateSiteContent(uuid, buffer, { asOrg } = {}) {
|
|
231
|
+
return this.request(`/dev/site/content/push/${encodeURIComponent(uuid)}`, {
|
|
232
|
+
method: 'POST', body: buffer, query: pushQuery(asOrg),
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** POST /dev/site/folder/push/{uuid} — push the folder lane, keyed by the site uuid (.uwx zip). */
|
|
237
|
+
async pushFolder(uuid, buffer, { asOrg } = {}) {
|
|
238
|
+
return this.request(`/dev/site/folder/push/${encodeURIComponent(uuid)}`, {
|
|
239
|
+
method: 'POST', body: buffer, query: pushQuery(asOrg),
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** GET /dev/site/content/pull/{uuid} — the content lane document. */
|
|
244
|
+
async pullSiteContent(uuid) {
|
|
245
|
+
return this.request(`/dev/site/content/pull/${encodeURIComponent(uuid)}`)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** GET /dev/site/folder/pull/{uuid} — the folder lane (folder + record documents). */
|
|
249
|
+
async pullFolder(uuid) {
|
|
250
|
+
return this.request(`/dev/site/folder/pull/${encodeURIComponent(uuid)}`)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ── Delivery: deploy + site publish ─────────────────────────────────────────────
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* POST /dev/deploy — dumb, file-built delivery. Body is the deploy payload (the
|
|
257
|
+
* runtime-init JSON `build-site-data.js` produces — foundation, runtimeVersion,
|
|
258
|
+
* theme, languages, locales, optional dataFiles/searchFiles) plus an optional
|
|
259
|
+
* `site_uuid`. First deploy of a never-synced site omits it → the backend mints
|
|
260
|
+
* a uuid and returns it for write-back to deploy.yml; later deploys resend it so
|
|
261
|
+
* `/gateway/site/{uuid}/` stays stable. Returns the raw Response so the caller
|
|
262
|
+
* messages its own errors and reads `{ site_uuid, url, locales }` on 200.
|
|
263
|
+
* @param {object} payload - the deploy payload (universal currency)
|
|
264
|
+
* @param {object} [opts]
|
|
265
|
+
* @param {string} [opts.siteUuid] - a previously-minted delivery uuid
|
|
266
|
+
* @returns {Promise<Response>}
|
|
267
|
+
*/
|
|
268
|
+
async deploy(payload, { siteUuid } = {}) {
|
|
269
|
+
const body = siteUuid ? { ...payload, site_uuid: siteUuid } : payload
|
|
270
|
+
return this.request('/dev/deploy', { method: 'POST', body: JSON.stringify(body) })
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* POST /dev/site/publish/{uuid} — CMS-publish a synced site (make its CURRENT
|
|
275
|
+
* backend state live; it does NOT push local files). `{uuid}` is the site-content
|
|
276
|
+
* uuid (`site.yml::$uuid`); a never-synced site 404s (sync first, or use deploy).
|
|
277
|
+
* The CLI knows the runtime from its build; the body carries it snake-cased per
|
|
278
|
+
* the route contract. Returns the raw Response ({ deploy_uuid, url,
|
|
279
|
+
* published_folder_uuid, status } on 200).
|
|
280
|
+
* @param {string} uuid - the site-content uuid
|
|
281
|
+
* @param {object} opts
|
|
282
|
+
* @param {string} opts.runtimeVersion
|
|
283
|
+
* @param {string[]} [opts.languages]
|
|
284
|
+
* @returns {Promise<Response>}
|
|
285
|
+
*/
|
|
286
|
+
async publishSite(uuid, { runtimeVersion, languages } = {}) {
|
|
287
|
+
// Runtime rides as a query param (?runtime=<version>) per the shipped /dev
|
|
288
|
+
// route (D3, "request-carried"), NOT the body. Languages, when present, go in
|
|
289
|
+
// the body; absent → no body (the route only requires the runtime).
|
|
290
|
+
return this.request(`/dev/site/publish/${encodeURIComponent(uuid)}`, {
|
|
291
|
+
method: 'POST',
|
|
292
|
+
query: { runtime: runtimeVersion },
|
|
293
|
+
...(languages ? { body: JSON.stringify({ languages }) } : {}),
|
|
294
|
+
})
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* POST /dev/site/unpublish/{uuid} — drop the published-folder gate so /gateway
|
|
299
|
+
* stops serving the site's dynamic content. Returns the raw Response ({ was_published }).
|
|
300
|
+
* @param {string} uuid - the site-content uuid
|
|
301
|
+
* @returns {Promise<Response>}
|
|
302
|
+
*/
|
|
303
|
+
async unpublishSite(uuid) {
|
|
304
|
+
return this.request(`/dev/site/unpublish/${encodeURIComponent(uuid)}`, { method: 'POST' })
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Deliver a site's processed assets (plan → PUT-per-file) to the backend's
|
|
309
|
+
* content-addressed store. Thin pass-through to utils/asset-upload.js with this
|
|
310
|
+
* client's origin + token; returns the `localUrl → { id, ext }` rewrite map the
|
|
311
|
+
* deploy step turns into durable `{assetBase}dist/{id}/base.{ext}` serve URLs.
|
|
312
|
+
* @param {object} opts - { distDir, files?, onProgress? }
|
|
313
|
+
*/
|
|
314
|
+
async uploadSiteAssets(opts) {
|
|
315
|
+
return uploadSiteAssets({ apiBase: this.origin, token: await this.token(), ...opts })
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Upload a built `@uniweb/runtime` to the runtime registry (plan → PUT-per-file),
|
|
320
|
+
* served at `/gateway/runtime/{version}`. @std-gated on the backend. Thin
|
|
321
|
+
* pass-through to utils/runtime-upload.js with this client's origin + token.
|
|
322
|
+
* @param {object} opts - { version, distDir, files?, onProgress? }
|
|
323
|
+
*/
|
|
324
|
+
async uploadRuntime(opts) {
|
|
325
|
+
return uploadRuntime({ apiBase: this.origin, token: await this.token(), ...opts })
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** `@scope/name` → /dev/registry/data-schemas/{scope}/{name}; a bare name → …/{name}. */
|
|
330
|
+
export function dataSchemaPath(modelName) {
|
|
331
|
+
const m = /^@([^/]+)\/(.+)$/.exec(modelName)
|
|
332
|
+
if (m) return `/dev/registry/data-schemas/${encodeURIComponent(m[1])}/${encodeURIComponent(m[2])}`
|
|
333
|
+
return `/dev/registry/data-schemas/${encodeURIComponent(modelName)}`
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** The shared push query: last-push-wins, plus an optional acting-org. */
|
|
337
|
+
function pushQuery(asOrg) {
|
|
338
|
+
return { collision: 'force', ...(asOrg ? { as_org: asOrg } : {}) }
|
|
339
|
+
}
|
package/src/commands/clone.js
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
* `npx uniweb clone`, same reason utils/workspace.js loads the classifier lazily).
|
|
13
13
|
* So clone does the minimum itself and delegates the heavy lifting:
|
|
14
14
|
*
|
|
15
|
-
* 1.
|
|
16
|
-
* ref out of that one document (no `@uniweb/build` needed for a
|
|
15
|
+
* 1. read the site-content document via the backend client — pull the `foundation`
|
|
16
|
+
* ref out of that one document (no `@uniweb/build` needed for a read);
|
|
17
17
|
* 2. scaffold the HARNESS — a full Vite site package whose foundation is
|
|
18
18
|
* REFERENCED (runtime-loaded), no local foundation sibling (scaffoldSite with
|
|
19
19
|
* foundationRef and no foundationPath) + AGENTS.md + deps pinned to this CLI's
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* uniweb clone <uuid> --project docs Co-located docs/site
|
|
40
40
|
* uniweb clone <uuid> --no-collections Pull pages only; skip collection records
|
|
41
41
|
*
|
|
42
|
-
*
|
|
42
|
+
* Backend: via BackendClient (the site-content pull lane). Origin from
|
|
43
43
|
* --registry > UNIWEB_REGISTER_URL > the local default (internal dev overrides;
|
|
44
44
|
* not the user-facing path — `uniweb login` determines the origin).
|
|
45
45
|
* Auth: --token > UNIWEB_TOKEN > `uniweb login` session.
|
|
@@ -53,12 +53,10 @@ import { resolvePlacement, SITE_KIND } from '../utils/placement.js'
|
|
|
53
53
|
import { findWorkspaceRoot } from '../utils/workspace.js'
|
|
54
54
|
import { addWorkspaceGlob } from '../utils/config.js'
|
|
55
55
|
import { detectWorkspacePm, installCmd } from '../utils/pm.js'
|
|
56
|
-
import {
|
|
56
|
+
import { BackendClient } from '../backend/client.js'
|
|
57
57
|
import { isNonInteractive, getCliPrefix } from '../utils/interactive.js'
|
|
58
58
|
import { extractFoundationRef } from '../utils/site-content-refs.js'
|
|
59
59
|
|
|
60
|
-
const DEFAULT_BACKEND_ORIGIN = 'http://localhost:8080'
|
|
61
|
-
|
|
62
60
|
const colors = {
|
|
63
61
|
reset: '\x1b[0m', bright: '\x1b[1m', dim: '\x1b[2m',
|
|
64
62
|
red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[36m', cyan: '\x1b[36m',
|
|
@@ -150,8 +148,6 @@ function pullExecArgv(pm, extra) {
|
|
|
150
148
|
* runPull(siteDir, pm, extraArgs).
|
|
151
149
|
*/
|
|
152
150
|
export async function clone(args = [], deps = {}) {
|
|
153
|
-
const fetchImpl = deps.fetch || globalThis.fetch
|
|
154
|
-
|
|
155
151
|
const positionals = args.filter((a) => !a.startsWith('-'))
|
|
156
152
|
const siteUuid = positionals[0]
|
|
157
153
|
const target = positionals[1] || null // [name|.]
|
|
@@ -167,31 +163,21 @@ export async function clone(args = [], deps = {}) {
|
|
|
167
163
|
const pathFlag = flagValue(args, '--path')
|
|
168
164
|
const projectFlag = flagValue(args, '--project')
|
|
169
165
|
const tokenFlag = flagValue(args, '--token')
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
let cachedToken = null
|
|
181
|
-
const getToken =
|
|
182
|
-
deps.getToken ||
|
|
183
|
-
(async () => {
|
|
184
|
-
if (cachedToken) return cachedToken
|
|
185
|
-
cachedToken = tokenFlag || process.env.UNIWEB_TOKEN || (await ensureRegistryAuth({ apiBase, command: 'Cloning', args }))
|
|
186
|
-
return cachedToken
|
|
187
|
-
})
|
|
166
|
+
const explicitRegistry = flagValue(args, '--registry')
|
|
167
|
+
const client = new BackendClient({
|
|
168
|
+
originFlag: explicitRegistry,
|
|
169
|
+
token: tokenFlag,
|
|
170
|
+
getToken: deps.getToken,
|
|
171
|
+
fetchImpl: deps.fetch,
|
|
172
|
+
args,
|
|
173
|
+
command: 'Cloning',
|
|
174
|
+
})
|
|
188
175
|
|
|
189
|
-
// 1. GET the site-content document (
|
|
190
|
-
|
|
191
|
-
info(`Reading site ${colors.bright}${siteUuid}${colors.reset} from ${colors.dim}${url}${colors.reset} …`)
|
|
176
|
+
// 1. GET the site-content document (no @uniweb/build needed for a read).
|
|
177
|
+
info(`Reading site ${colors.bright}${siteUuid}${colors.reset} from ${colors.dim}${client.origin}${colors.reset} …`)
|
|
192
178
|
let payload
|
|
193
179
|
try {
|
|
194
|
-
const res = await
|
|
180
|
+
const res = await client.pullSiteContent(siteUuid)
|
|
195
181
|
if (res.status === 404) {
|
|
196
182
|
error(`Site not found (404) — check the uuid, or you lack access.`)
|
|
197
183
|
return { exitCode: 1 }
|
|
@@ -203,7 +189,7 @@ export async function clone(args = [], deps = {}) {
|
|
|
203
189
|
}
|
|
204
190
|
payload = await res.json()
|
|
205
191
|
} catch (err) {
|
|
206
|
-
error(`Could not reach the backend at ${
|
|
192
|
+
error(`Could not reach the backend at ${client.origin}: ${err.message}`)
|
|
207
193
|
return { exitCode: 1 }
|
|
208
194
|
}
|
|
209
195
|
|
|
@@ -309,7 +295,7 @@ export async function clone(args = [], deps = {}) {
|
|
|
309
295
|
}
|
|
310
296
|
|
|
311
297
|
const pullExtra = []
|
|
312
|
-
if (
|
|
298
|
+
if (explicitRegistry) pullExtra.push('--registry', explicitRegistry)
|
|
313
299
|
if (tokenFlag) pullExtra.push('--token', tokenFlag)
|
|
314
300
|
if (noCollections) pullExtra.push('--no-collections')
|
|
315
301
|
|