uniweb 1.15.5 → 1.16.0
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 +4 -4
- package/partials/agents.md +5 -4
- package/src/backend/client.js +15 -14
- package/src/backend/workspace.js +86 -30
- package/src/commands/publish.js +21 -14
- package/src/commands/register.js +7 -0
- package/src/framework-index.json +3 -3
- package/src/index.js +7 -6
- package/src/utils/registry-auth.js +13 -8
- package/src/utils/registry-orgs.js +23 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -42,14 +42,14 @@
|
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
44
|
"@uniweb/core": "~1.4.0",
|
|
45
|
+
"@uniweb/schemas": "~1.6.5",
|
|
45
46
|
"@uniweb/semantic-parser": "~1.12.2",
|
|
46
|
-
"@uniweb/schemas": "~1.6.4",
|
|
47
47
|
"@uniweb/content-writer": "~1.3.0"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@uniweb/
|
|
50
|
+
"@uniweb/build": "~1.7.4",
|
|
51
51
|
"@uniweb/semantic-parser": "~1.12.2",
|
|
52
|
-
"@uniweb/
|
|
52
|
+
"@uniweb/content-reader": "~1.5.4"
|
|
53
53
|
},
|
|
54
54
|
"peerDependenciesMeta": {
|
|
55
55
|
"@uniweb/build": {
|
package/partials/agents.md
CHANGED
|
@@ -2851,11 +2851,12 @@ Foundations have their own free path too: `uniweb add ci --target foundation` pu
|
|
|
2851
2851
|
**Content authors work visually in the Uniweb App.** They compose the same extended markdown and set the same component params you defined, through a visual editor — never touching code, git, or the CLI. They see exactly the section types your foundation offers and exactly the knobs each one exposes, because **every `meta.js` is registered as the foundation's schema** when you publish. Your `meta.js` is the app's UI (see *meta.js* in Part 4).
|
|
2852
2852
|
|
|
2853
2853
|
> ⚠️ **Every push, pull and publish works in ONE workspace — the one chosen at `uniweb login`.**
|
|
2854
|
-
> A workspace is your personal one
|
|
2854
|
+
> A workspace is your personal one, an organization's, or a sub-organization's or project's in one
|
|
2855
|
+
> (`@studio:acme`). A site a push or publish **creates** is
|
|
2855
2856
|
> created — owned, and its storage billed — there, and there is no CLI command to move it
|
|
2856
2857
|
> afterwards. A site the backend keeps in **another** workspace stops the command, which says how
|
|
2857
|
-
> to switch. With no organization, the login works in your personal workspace;
|
|
2858
|
-
>
|
|
2858
|
+
> to switch. With no organization or project, the login works in your personal workspace;
|
|
2859
|
+
> otherwise the login asks, or you name it:
|
|
2859
2860
|
>
|
|
2860
2861
|
> ```bash
|
|
2861
2862
|
> uniweb login --org @acme # work in an organization
|
|
@@ -2865,7 +2866,7 @@ Foundations have their own free path too: `uniweb add ci --target foundation` pu
|
|
|
2865
2866
|
>
|
|
2866
2867
|
> Already logged in, `uniweb login --org @other` (or `--personal`) switches the workspace on the
|
|
2867
2868
|
> backend you are logged in to, without logging in again. **Without a
|
|
2868
|
-
> terminal — you, an agent — a login with organizations and no workspace named signs you in but
|
|
2869
|
+
> terminal — you, an agent — a login with organizations or projects and no workspace named signs you in but
|
|
2869
2870
|
> exits 2**: `uniweb login --org @acme` (or `--personal`) finishes it without signing in again.
|
|
2870
2871
|
> Until then every command that works on a site refuses, and so does registering a foundation whose
|
|
2871
2872
|
> name has no scope. A process logged in with `UNIWEB_TOKEN` names its workspace with
|
package/src/backend/client.js
CHANGED
|
@@ -165,6 +165,11 @@ export function workspaceHandle(value) {
|
|
|
165
165
|
|
|
166
166
|
const UNIT_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
167
167
|
|
|
168
|
+
/** Whether `value` is shaped like a unit's uuid — how the header names a unit with no handle. */
|
|
169
|
+
export function isUnitUuid(value) {
|
|
170
|
+
return typeof value === 'string' && UNIT_UUID.test(value.trim())
|
|
171
|
+
}
|
|
172
|
+
|
|
168
173
|
/**
|
|
169
174
|
* A workspace as a sentence names it: `@acme`, `the unit <uuid>`, or `your personal
|
|
170
175
|
* workspace` — for null, and for a login's stored `personal`.
|
|
@@ -180,8 +185,8 @@ export function describeWorkspace(workspace) {
|
|
|
180
185
|
*
|
|
181
186
|
* ⚠️ A bare value is taken for a uuid only when it is shaped like one, and our own
|
|
182
187
|
* handle grammar admits uuid-shaped handles (3–39 lowercase letters, digits, hyphens).
|
|
183
|
-
* So a HANDLE reaches this already dressed — `--org` through `
|
|
184
|
-
* login's choice as stored — and a bare uuid here is a unit.
|
|
188
|
+
* So a HANDLE reaches this already dressed — `--org` through `namedWorkspace`
|
|
189
|
+
* (`workspace.js`), a login's choice as stored — and a bare uuid here is a unit.
|
|
185
190
|
*
|
|
186
191
|
* @param {unknown} value
|
|
187
192
|
* @returns {string|null}
|
|
@@ -214,20 +219,16 @@ export class WorkspaceMismatchError extends Error {
|
|
|
214
219
|
constructor({ named, answer, nowhere = false, source = 'login', origin = null, message = null }) {
|
|
215
220
|
const where = (w) =>
|
|
216
221
|
!w ? 'your personal workspace' : w.startsWith('@') ? w : `the unit ${w}`
|
|
217
|
-
// How to work in the site's workspace, said for the way this one was chosen.
|
|
222
|
+
// How to work in the site's workspace, said for the way this one was chosen. A unit with
|
|
223
|
+
// no handle is named by its uuid, as the header names it. ⛔ *Until 2026-10-22 this said
|
|
224
|
+
// "It has no handle, so the CLI cannot work in it": a login could choose only what the
|
|
225
|
+
// orgs read listed, and that list left handle-less units out (`workspace.js`).*
|
|
218
226
|
const switchTo = !answer
|
|
219
227
|
? { flag: 'pass --personal', env: 'set UNIWEB_WORKSPACE=personal' }
|
|
220
|
-
: answer
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
? `${loginCommand(origin)} --personal`
|
|
225
|
-
: answer.startsWith('@')
|
|
226
|
-
? `${loginCommand(origin)} --org ${answer}`
|
|
227
|
-
: null
|
|
228
|
-
const fix = !switchTo
|
|
229
|
-
? 'It has no handle, so the CLI cannot work in it.'
|
|
230
|
-
: source === 'flag'
|
|
228
|
+
: { flag: `pass --org ${answer}`, env: `set UNIWEB_WORKSPACE=${answer}` }
|
|
229
|
+
const login = !answer ? `${loginCommand(origin)} --personal` : `${loginCommand(origin)} --org ${answer}`
|
|
230
|
+
const fix =
|
|
231
|
+
source === 'flag'
|
|
231
232
|
? `To work on it, ${switchTo.flag}.`
|
|
232
233
|
: source === 'env'
|
|
233
234
|
? `To work on it, ${switchTo.env}.`
|
package/src/backend/workspace.js
CHANGED
|
@@ -23,13 +23,19 @@
|
|
|
23
23
|
*
|
|
24
24
|
* ⛔ **The personal workspace is named by naming NONE.** `@<your handle>` is your personal
|
|
25
25
|
* SCOPE — a registry namespace for foundations and data schemas — not a workspace.
|
|
26
|
+
*
|
|
27
|
+
* ⭐ **A workspace is any unit the backend lets a request name** (backend, 2026-10-22): an
|
|
28
|
+
* organization, and the units below one — a sub-org, a project — each under its child handle,
|
|
29
|
+
* `@top:part` (`@studio:acme`); a unit with no handle by its uuid. The login checks and offers
|
|
30
|
+
* them from the orgs read's `workspaces`, never its `orgs`, which are the SCOPES and leave every
|
|
31
|
+
* child unit out (`registry-orgs.js`). A child handle names a workspace and no scope.
|
|
26
32
|
*/
|
|
27
33
|
|
|
28
34
|
import { readOrgFlag } from '../utils/args.js'
|
|
29
35
|
import { loginCommand } from '../utils/config.js'
|
|
30
36
|
import { fetchOrgs, bareHandle, validateHandle } from '../utils/registry-orgs.js'
|
|
31
37
|
import { readRegistryAuth } from '../utils/registry-auth.js'
|
|
32
|
-
import { workspaceHandle, describeWorkspace } from './client.js'
|
|
38
|
+
import { workspaceHandle, describeWorkspace, isUnitUuid } from './client.js'
|
|
33
39
|
|
|
34
40
|
export { describeWorkspace }
|
|
35
41
|
|
|
@@ -40,18 +46,58 @@ export const PERSONAL = 'personal'
|
|
|
40
46
|
export const WORKSPACE_ENV = 'UNIWEB_WORKSPACE'
|
|
41
47
|
|
|
42
48
|
/**
|
|
43
|
-
* A workspace
|
|
44
|
-
* the
|
|
49
|
+
* A workspace as `--org` names it, in the header's form: a unit's uuid when the value is
|
|
50
|
+
* written bare and shaped like one — the header's own rule, where the `@` tells the two
|
|
51
|
+
* apart — else a handle, `@acme` or `@studio:acme`. Null when the value names none.
|
|
52
|
+
*
|
|
53
|
+
* @param {unknown} value
|
|
54
|
+
* @returns {string|null}
|
|
55
|
+
*/
|
|
56
|
+
export function namedWorkspace(value) {
|
|
57
|
+
if (typeof value !== 'string' || !value.trim()) return null
|
|
58
|
+
const v = value.trim()
|
|
59
|
+
return isUnitUuid(v) ? v.toLowerCase() : workspaceHandle(v)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A workspace choice as it may be written — `@acme`, `acme`, `@studio:acme`, a unit's uuid,
|
|
64
|
+
* `personal` — or null when the value names none.
|
|
45
65
|
*
|
|
46
66
|
* @param {unknown} value
|
|
47
|
-
* @returns {string|null} `@acme`, `PERSONAL`, or null
|
|
67
|
+
* @returns {string|null} `@acme`, a uuid, `PERSONAL`, or null
|
|
48
68
|
*/
|
|
49
69
|
export function parseWorkspaceChoice(value) {
|
|
50
70
|
if (typeof value !== 'string' || !value.trim()) return null
|
|
51
71
|
const v = value.trim()
|
|
52
|
-
return v === PERSONAL ? PERSONAL :
|
|
72
|
+
return v === PERSONAL ? PERSONAL : namedWorkspace(v)
|
|
53
73
|
}
|
|
54
74
|
|
|
75
|
+
/**
|
|
76
|
+
* The workspaces the orgs read lists — what the header admits — each by the name a request
|
|
77
|
+
* gives it: `@handle`, or the unit's uuid when it has none. Null when the answer carries no
|
|
78
|
+
* list, which a workspace reader refuses (`noWorkspaceList`).
|
|
79
|
+
*
|
|
80
|
+
* @param {{ workspaces?: Array<{ unit_uuid?: string, handle?: string|null, kind?: string, is_primary?: boolean }>|null }|null} envelope
|
|
81
|
+
* @returns {Array<{ name: string, kind: string|null, primary: boolean }>|null}
|
|
82
|
+
*/
|
|
83
|
+
export function listedWorkspaces(envelope) {
|
|
84
|
+
if (!Array.isArray(envelope?.workspaces)) return null
|
|
85
|
+
return envelope.workspaces
|
|
86
|
+
.map((w) => ({
|
|
87
|
+
name: w?.handle ? workspaceHandle(w.handle) : isUnitUuid(w?.unit_uuid) ? w.unit_uuid.trim().toLowerCase() : null,
|
|
88
|
+
kind: typeof w?.kind === 'string' ? w.kind : null,
|
|
89
|
+
primary: w?.is_primary === true
|
|
90
|
+
}))
|
|
91
|
+
.filter((w) => w.name)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* What a backend whose orgs read lists no workspaces is told. ⛔ The CLI does not check
|
|
96
|
+
* against `orgs` in its place: that list leaves out every unit below an org.
|
|
97
|
+
*/
|
|
98
|
+
export const noWorkspaceList = (origin) =>
|
|
99
|
+
`${origin} lists no workspaces, so the CLI cannot check or offer one — that backend is older than this CLI.`
|
|
100
|
+
|
|
55
101
|
/** A choice in the header's form: `@acme`, or null for the personal workspace. */
|
|
56
102
|
export function headerOf(choice) {
|
|
57
103
|
return choice === PERSONAL ? null : choice
|
|
@@ -62,7 +108,7 @@ export const SOURCE_LABEL = {
|
|
|
62
108
|
flag: 'named on this command',
|
|
63
109
|
env: WORKSPACE_ENV,
|
|
64
110
|
login: 'your login',
|
|
65
|
-
personal: 'you belong to no organization',
|
|
111
|
+
personal: 'you belong to no organization or project',
|
|
66
112
|
offline: 'not resolved in a preview'
|
|
67
113
|
}
|
|
68
114
|
|
|
@@ -88,7 +134,7 @@ const choose = (origin) =>
|
|
|
88
134
|
*/
|
|
89
135
|
export async function resolveWorkspace({ client, args = [], offline = false }) {
|
|
90
136
|
const flag = readOrgFlag(args)
|
|
91
|
-
if (flag) return { workspace:
|
|
137
|
+
if (flag) return { workspace: namedWorkspace(flag), source: 'flag' }
|
|
92
138
|
if (args.includes('--personal')) return { workspace: null, source: 'flag' }
|
|
93
139
|
|
|
94
140
|
const raw = process.env[WORKSPACE_ENV]
|
|
@@ -118,11 +164,14 @@ export async function resolveWorkspace({ client, args = [], offline = false }) {
|
|
|
118
164
|
const now = await fromSession()
|
|
119
165
|
if (now) return { workspace: headerOf(now), source: 'login' }
|
|
120
166
|
|
|
121
|
-
|
|
122
|
-
|
|
167
|
+
// Personal only when it is the one workspace there is — a project counts as much as an
|
|
168
|
+
// organization (`listedWorkspaces`).
|
|
169
|
+
const listed = listedWorkspaces(await client.fetchOrgs())
|
|
170
|
+
if (!listed) return { refused: true, reason: noWorkspaceList(client.origin) }
|
|
171
|
+
if (!listed.length) return { workspace: null, source: 'personal' }
|
|
123
172
|
return {
|
|
124
173
|
refused: true,
|
|
125
|
-
reason: `You belong to organizations, so no workspace is assumed.\n ${choose(client.origin)}`
|
|
174
|
+
reason: `You belong to organizations or projects, so no workspace is assumed.\n ${choose(client.origin)}`
|
|
126
175
|
}
|
|
127
176
|
}
|
|
128
177
|
|
|
@@ -138,8 +187,9 @@ export async function resolveWorkspace({ client, args = [], offline = false }) {
|
|
|
138
187
|
*
|
|
139
188
|
* Null when the workspace names no scope, and the caller derives one as before
|
|
140
189
|
* (`deriveScope`): none is chosen (you belong to organizations and none is named), the
|
|
141
|
-
* command is a preview, the workspace is a unit without a handle,
|
|
142
|
-
*
|
|
190
|
+
* command is a preview, the workspace is a unit without a handle, the workspace is a unit
|
|
191
|
+
* below an organization — a sub-org or a project, whose child handle (`@studio:acme`) names
|
|
192
|
+
* no scope (backend, 2026-10-22) — or the account has no handle (a service account).
|
|
143
193
|
*
|
|
144
194
|
* @param {{ workspace?: string|null, source?: string, refused?: boolean }} ws -
|
|
145
195
|
* `resolveWorkspace`'s answer
|
|
@@ -149,7 +199,9 @@ export async function resolveWorkspace({ client, args = [], offline = false }) {
|
|
|
149
199
|
*/
|
|
150
200
|
export function scopeOfWorkspace(ws, accountHandle) {
|
|
151
201
|
if (!ws || ws.refused || ws.source === 'offline') return null
|
|
152
|
-
if (typeof ws.workspace === 'string')
|
|
202
|
+
if (typeof ws.workspace === 'string') {
|
|
203
|
+
return ws.workspace.startsWith('@') && !ws.workspace.includes(':') ? ws.workspace : null
|
|
204
|
+
}
|
|
153
205
|
const h = bareHandle(accountHandle || '')
|
|
154
206
|
return h && !validateHandle(h) ? `@${h}` : null
|
|
155
207
|
}
|
|
@@ -158,42 +210,46 @@ export function scopeOfWorkspace(ws, accountHandle) {
|
|
|
158
210
|
* The workspace a login works in — asked once, at `uniweb login`, and stored with the
|
|
159
211
|
* session.
|
|
160
212
|
*
|
|
161
|
-
* `--org @acme` must be an organization
|
|
213
|
+
* `--org @acme` must be a workspace the orgs read lists — an organization, or
|
|
214
|
+
* a sub-org or a project under its child handle (`@studio:acme`),
|
|
215
|
+
* or a unit with no handle by its uuid
|
|
162
216
|
* `--personal` your personal workspace
|
|
163
|
-
* neither,
|
|
164
|
-
* neither,
|
|
217
|
+
* neither, none listed your personal workspace — said, not asked
|
|
218
|
+
* neither, some listed a pick at a terminal; refused without one
|
|
165
219
|
*
|
|
166
220
|
* @param {object} p
|
|
167
221
|
* @param {string} p.apiBase
|
|
168
222
|
* @param {string} p.token
|
|
169
223
|
* @param {string[]} [p.args]
|
|
170
224
|
* @returns {Promise<{ choice: string, note?: string } | { refused: true, reason: string }>}
|
|
171
|
-
* `choice` is `@acme
|
|
225
|
+
* `choice` is `@acme`, a unit's uuid, or `PERSONAL`
|
|
172
226
|
*/
|
|
173
227
|
export async function chooseWorkspace({ apiBase, token, args = [] }) {
|
|
174
228
|
const flag = readOrgFlag(args)
|
|
175
229
|
if (!flag && args.includes('--personal')) return { choice: PERSONAL }
|
|
176
230
|
|
|
177
|
-
const
|
|
178
|
-
|
|
231
|
+
const listed = listedWorkspaces(await fetchOrgs({ apiBase, token }))
|
|
232
|
+
if (!listed) return { refused: true, reason: noWorkspaceList(apiBase) }
|
|
233
|
+
const mine = listed.map((w) => w.name)
|
|
179
234
|
if (flag) {
|
|
180
|
-
const
|
|
181
|
-
if (!
|
|
235
|
+
const named = namedWorkspace(flag)
|
|
236
|
+
if (!named) return { refused: true, reason: `--org ${flag} names no workspace — give @org, or --personal.` }
|
|
237
|
+
if (!mine.includes(named)) {
|
|
182
238
|
return {
|
|
183
239
|
refused: true,
|
|
184
240
|
reason:
|
|
185
|
-
|
|
241
|
+
`${describeWorkspace(named)} is not a workspace you can open on ${apiBase}. ` +
|
|
186
242
|
(mine.length
|
|
187
243
|
? `Your workspaces: ${[...mine, `--personal`].join(', ')}.`
|
|
188
|
-
: 'You belong to no organization — use --personal.')
|
|
244
|
+
: 'You belong to no organization or project — use --personal.')
|
|
189
245
|
}
|
|
190
246
|
}
|
|
191
|
-
return { choice:
|
|
247
|
+
return { choice: named }
|
|
192
248
|
}
|
|
193
|
-
if (!
|
|
249
|
+
if (!listed.length) {
|
|
194
250
|
return {
|
|
195
251
|
choice: PERSONAL,
|
|
196
|
-
note: 'You belong to no organization, so you work in your personal workspace.'
|
|
252
|
+
note: 'You belong to no organization or project, so you work in your personal workspace.'
|
|
197
253
|
}
|
|
198
254
|
}
|
|
199
255
|
|
|
@@ -207,7 +263,7 @@ export async function chooseWorkspace({ apiBase, token, args = [] }) {
|
|
|
207
263
|
if (isNonInteractive(args)) {
|
|
208
264
|
return {
|
|
209
265
|
refused: true,
|
|
210
|
-
reason: `You belong to organizations, so no workspace is assumed. ${howToChoose}`
|
|
266
|
+
reason: `You belong to organizations or projects, so no workspace is assumed. ${howToChoose}`
|
|
211
267
|
}
|
|
212
268
|
}
|
|
213
269
|
const prompts = (await import('prompts')).default
|
|
@@ -218,9 +274,9 @@ export async function chooseWorkspace({ apiBase, token, args = [] }) {
|
|
|
218
274
|
message: 'Which workspace do you work in?',
|
|
219
275
|
choices: [
|
|
220
276
|
{ title: 'Personal — your own sites', value: PERSONAL },
|
|
221
|
-
...
|
|
222
|
-
title:
|
|
223
|
-
value:
|
|
277
|
+
...listed.map((w) => ({
|
|
278
|
+
title: `${describeWorkspace(w.name)}${w.kind === 'project' ? ' — a project' : ''}${w.primary ? ' (primary)' : ''}`,
|
|
279
|
+
value: w.name
|
|
224
280
|
}))
|
|
225
281
|
],
|
|
226
282
|
initial: 0
|
package/src/commands/publish.js
CHANGED
|
@@ -573,9 +573,14 @@ export async function publish(args = []) {
|
|
|
573
573
|
say.dim(`This publish creates the site in ${describeWorkspace(ws.workspace)} (${SOURCE_LABEL[ws.source]}).`)
|
|
574
574
|
}
|
|
575
575
|
|
|
576
|
-
// 2. Build the site data (link mode): dist/site-content.json (+ per-locale)
|
|
577
|
-
// dist/data
|
|
578
|
-
// build can't resolve to
|
|
576
|
+
// 2. Build the site data (link mode): dist/site-content.json (+ per-locale)
|
|
577
|
+
// and dist/data/*. No media is processed: 4b uploads it from the site's
|
|
578
|
+
// own files. Spawn the SAME CLI binary so the inner build can't resolve to
|
|
579
|
+
// a different installed version.
|
|
580
|
+
//
|
|
581
|
+
// ⛔ *This listed `dist/assets/*` until 2026-10-22; the link build has made
|
|
582
|
+
// no media copies since 2026-10-19 (`site/build-site-data.js`, "Does NOT
|
|
583
|
+
// emit").*
|
|
579
584
|
//
|
|
580
585
|
// ⛔ NO SEARCH INDEX. This listed `dist/_search/*` until 2026-08-26; the
|
|
581
586
|
// link lane stopped emitting one on 2026-08-01 (`@uniweb/build`
|
|
@@ -619,8 +624,9 @@ export async function publish(args = []) {
|
|
|
619
624
|
if (!guard.ok) return { exitCode: 1 }
|
|
620
625
|
}
|
|
621
626
|
|
|
622
|
-
// 3. Partition
|
|
623
|
-
// —
|
|
627
|
+
// 3. Partition the queries by schema presence (a first emit reads `schemaless`
|
|
628
|
+
// — the queries with no data schema, whose compiled files 4c uploads; no
|
|
629
|
+
// ball carries them since 2026-08-18).
|
|
624
630
|
let probe
|
|
625
631
|
try {
|
|
626
632
|
probe = await emitSyncPackages(siteDir, {
|
|
@@ -846,13 +852,14 @@ export async function publish(args = []) {
|
|
|
846
852
|
siteDir,
|
|
847
853
|
note: (m) => say.dim(m)
|
|
848
854
|
})
|
|
849
|
-
// Stamp
|
|
850
|
-
//
|
|
851
|
-
// Delivery is version-pinned end-to-end (
|
|
852
|
-
// by a concrete version
|
|
853
|
-
//
|
|
854
|
-
//
|
|
855
|
-
//
|
|
855
|
+
// Stamp the PINNED foundation ref (`@scope/name@version`) from the bring-along
|
|
856
|
+
// on the site-content entity — the one key `injectInfo` carries since the
|
|
857
|
+
// data-bundle URL went (2026-08-18). Delivery is version-pinned end-to-end (a
|
|
858
|
+
// foundation is delivered only by a concrete version, agreed with backend and
|
|
859
|
+
// measured end to end on 2026-06-25), so pinning the released version on the
|
|
860
|
+
// wire is required when site.yml uses an unversioned local ref; injectInfo
|
|
861
|
+
// overrides info.foundation. A registry/URL ref → fnd.ref is null → the
|
|
862
|
+
// site.yml ref is forwarded verbatim (already pinned).
|
|
856
863
|
// ⛔ DO NOT STAMP A FILE MAP INTO `info` HERE. `injectInfo` WINS the merge
|
|
857
864
|
// (`sync-package.js`: `{...siteDoc.info, ...injectInfo}`), so any name stamped
|
|
858
865
|
// here silently replaces what the author's document carries under it, and the
|
|
@@ -861,8 +868,8 @@ export async function publish(args = []) {
|
|
|
861
868
|
// that moved to `settings.fetch`, and the warning holds for any name.
|
|
862
869
|
//
|
|
863
870
|
// A file map needs a name nothing else claims (`static_data` / `data_files`
|
|
864
|
-
// were proposed) AND a consumer that reads it — neither settled.
|
|
865
|
-
// one object per file, no bundle.
|
|
871
|
+
// were proposed) AND a consumer that reads it — neither settled. Until then
|
|
872
|
+
// the schema-less data goes as 4c sends it: one object per file, no bundle.
|
|
866
873
|
const injectInfo = {
|
|
867
874
|
...(fnd.ref ? { foundation: fnd.ref } : {})
|
|
868
875
|
}
|
package/src/commands/register.js
CHANGED
|
@@ -407,6 +407,13 @@ async function deriveScopeFromLogin(client, args) {
|
|
|
407
407
|
)
|
|
408
408
|
return fromWorkspace
|
|
409
409
|
}
|
|
410
|
+
// A unit below an organization — a sub-org, a project — is a workspace and no scope (backend,
|
|
411
|
+
// 2026-10-22), so a bare name is asked for one as when no workspace names any.
|
|
412
|
+
if (typeof ws.workspace === 'string' && ws.workspace.includes(':')) {
|
|
413
|
+
console.error(
|
|
414
|
+
`${colors.dim}${ws.workspace}, the workspace you work in, is a unit below an organization, and names no scope.${colors.reset}`
|
|
415
|
+
)
|
|
416
|
+
}
|
|
410
417
|
const sess = await readRegistryAuth(client.origin)
|
|
411
418
|
const derived = await deriveScope({
|
|
412
419
|
apiBase: client.origin,
|
package/src/framework-index.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-10-
|
|
3
|
+
"generatedAt": "2026-10-22T17:15:55.292Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/api": {
|
|
6
|
-
"version": "1.6.
|
|
6
|
+
"version": "1.6.4"
|
|
7
7
|
},
|
|
8
8
|
"@uniweb/build": {
|
|
9
9
|
"version": "1.7.4"
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"version": "1.0.1"
|
|
43
43
|
},
|
|
44
44
|
"@uniweb/schemas": {
|
|
45
|
-
"version": "1.6.
|
|
45
|
+
"version": "1.6.5"
|
|
46
46
|
},
|
|
47
47
|
"@uniweb/scholar": {
|
|
48
48
|
"version": "1.1.0"
|
package/src/index.js
CHANGED
|
@@ -1815,16 +1815,17 @@ Without --server: https://uniweb.app (or \$UNIWEB_SERVER). No command talks
|
|
|
1815
1815
|
to a backend you are not logged in to — run one before logging in and it asks first.
|
|
1816
1816
|
|
|
1817
1817
|
${colors.bright}The workspace you work in.${colors.reset} A login works in ONE workspace — your
|
|
1818
|
-
personal one,
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
or name it. Already logged in, \`uniweb login --org @other\` (or \`--personal\`) switches
|
|
1818
|
+
personal one, an organization's, or a sub-organization's or project's in one
|
|
1819
|
+
(@studio:acme) — and every push, pull and publish works in it: a site it creates is
|
|
1820
|
+
created there, and a site kept in another workspace is refused. With no organization
|
|
1821
|
+
or project it is your personal workspace; with some you are asked, or name it. Already logged in, \`uniweb login --org @other\` (or \`--personal\`) switches
|
|
1822
1822
|
the workspace on the backend you are logged in to, without logging in again.
|
|
1823
1823
|
|
|
1824
1824
|
${colors.bright}Options:${colors.reset}
|
|
1825
1825
|
--server <url> The backend to log in to (without it: the default backend — or, for
|
|
1826
1826
|
--org / --personal alone, the one you are logged in to)
|
|
1827
|
-
--org @org Work in @org
|
|
1827
|
+
--org @org Work in @org — an organization, sub-organization or project you
|
|
1828
|
+
belong to (@studio:acme); a unit with no handle, by its uuid
|
|
1828
1829
|
--personal Work in your personal workspace
|
|
1829
1830
|
--token <bearer> Seed + verify a session from a bearer token (non-interactive)
|
|
1830
1831
|
--browser Force the browser method
|
|
@@ -1833,7 +1834,7 @@ ${colors.bright}Options:${colors.reset}
|
|
|
1833
1834
|
|
|
1834
1835
|
In non-interactive mode (no TTY — an agent, a script), pass \`--token <bearer>\` with
|
|
1835
1836
|
\`--org @org\` or \`--personal\`, or set \`UNIWEB_USERNAME\` + \`UNIWEB_PASSWORD\`.
|
|
1836
|
-
A login with organizations that names no workspace signs you in but exits 2;
|
|
1837
|
+
A login with organizations or projects that names no workspace signs you in but exits 2;
|
|
1837
1838
|
\`uniweb login --org @org\` (or \`--personal\`) finishes it without signing in again.
|
|
1838
1839
|
\`uniweb logout\` logs you out.
|
|
1839
1840
|
|
|
@@ -950,8 +950,9 @@ async function reportTokenProcess({ named, token }) {
|
|
|
950
950
|
)
|
|
951
951
|
|
|
952
952
|
// Their workspace is UNIWEB_WORKSPACE's — never the stored session's — and, with none
|
|
953
|
-
// named, personal for an account in no organization (resolveWorkspace,
|
|
954
|
-
|
|
953
|
+
// named, personal for an account in no organization or project (resolveWorkspace,
|
|
954
|
+
// workspace.js). Checked, as a login's `--org` is, against the workspaces the orgs read lists.
|
|
955
|
+
const { WORKSPACE_ENV, PERSONAL, parseWorkspaceChoice, headerOf, describeWorkspace, listedWorkspaces, noWorkspaceList } =
|
|
955
956
|
await import('../backend/workspace.js')
|
|
956
957
|
const unsettled = (reason, { commandsRefuse }) => {
|
|
957
958
|
console.error(`\x1b[31m✗\x1b[0m ${reason}`)
|
|
@@ -967,28 +968,32 @@ async function reportTokenProcess({ named, token }) {
|
|
|
967
968
|
commandsRefuse: true
|
|
968
969
|
})
|
|
969
970
|
}
|
|
970
|
-
let
|
|
971
|
+
let listed
|
|
971
972
|
try {
|
|
972
|
-
|
|
973
|
+
listed = listedWorkspaces(await (await import('./registry-orgs.js')).fetchOrgs({ apiBase: origin, token }))
|
|
973
974
|
} catch (err) {
|
|
974
975
|
console.error(`\x1b[33m⚠\x1b[0m ${err.message} — the workspace is not checked.`)
|
|
975
976
|
return
|
|
976
977
|
}
|
|
977
|
-
|
|
978
|
+
if (!listed) {
|
|
979
|
+
console.error(`\x1b[33m⚠\x1b[0m ${noWorkspaceList(origin)}`)
|
|
980
|
+
return
|
|
981
|
+
}
|
|
982
|
+
const mine = listed.map((w) => w.name)
|
|
978
983
|
if (choice && choice !== PERSONAL && !mine.includes(choice)) {
|
|
979
984
|
// As a login refuses `--org` for one (chooseWorkspace).
|
|
980
|
-
unsettled(`${WORKSPACE_ENV}=${raw} is not one of this account's
|
|
985
|
+
unsettled(`${WORKSPACE_ENV}=${raw} is not one of this account's workspaces on ${origin}.`, {
|
|
981
986
|
commandsRefuse: false
|
|
982
987
|
})
|
|
983
988
|
}
|
|
984
989
|
if (!choice && mine.length) {
|
|
985
990
|
unsettled(
|
|
986
|
-
`The account belongs to organizations, so no workspace is assumed: set ${WORKSPACE_ENV}=${mine[0]} (or another, or personal).`,
|
|
991
|
+
`The account belongs to organizations or projects, so no workspace is assumed: set ${WORKSPACE_ENV}=${mine[0]} (or another, or personal).`,
|
|
987
992
|
{ commandsRefuse: true }
|
|
988
993
|
)
|
|
989
994
|
}
|
|
990
995
|
const where = describeWorkspace(headerOf(choice || PERSONAL))
|
|
991
996
|
console.error(
|
|
992
|
-
` They work in \x1b[1m${where}\x1b[0m${choice ? ` (${WORKSPACE_ENV})` : ' — the account belongs to no organization'}.`
|
|
997
|
+
` They work in \x1b[1m${where}\x1b[0m${choice ? ` (${WORKSPACE_ENV})` : ' — the account belongs to no organization or project'}.`
|
|
993
998
|
)
|
|
994
999
|
}
|
|
@@ -26,8 +26,20 @@
|
|
|
26
26
|
*
|
|
27
27
|
* GET /dev/orgs → {
|
|
28
28
|
* account_handle: string|null, // the caller's account handle
|
|
29
|
-
* orgs: [{ handle, is_primary }]
|
|
30
|
-
*
|
|
29
|
+
* orgs: [{ handle, is_primary }], // the SCOPES: top-level orgs, primary first;
|
|
30
|
+
* // handle-less units filtered server-side
|
|
31
|
+
* workspaces: [{ unit_uuid, handle, kind, is_primary }]
|
|
32
|
+
* } // what `x-uniweb-workspace` admits
|
|
33
|
+
*
|
|
34
|
+
* ⭐ TWO LISTS, BECAUSE A WORKSPACE IS NOT A SCOPE (backend, 2026-10-22). `orgs` are the
|
|
35
|
+
* scopes a foundation registers under — the login's scope picker (`deriveScope`) and the
|
|
36
|
+
* membership read (`belongsToScope`) stay on it. `workspaces` are where a command may work:
|
|
37
|
+
* the organizations AND the units below them — a sub-org, a project (`kind: "project"`) —
|
|
38
|
+
* each under its child handle, `top:part` (`studio:acme`), and a unit with no handle by its
|
|
39
|
+
* `unit_uuid`. They differ exactly where a workspace is no scope: a child handle names none.
|
|
40
|
+
* The login's `--org` and its pick read `workspaces` (`backend/workspace.js`). ⛔ *Until
|
|
41
|
+
* 2026-10-22 both read `orgs`, which listed sub-orgs then; since that day it lists none, so a
|
|
42
|
+
* login refused a project or a sub-org even when a `409` had just named it.*
|
|
31
43
|
*
|
|
32
44
|
* POST /dev/orgs { handle } → { handle, uuid, is_primary }
|
|
33
45
|
*
|
|
@@ -114,9 +126,13 @@ export function validateHandle(handle) {
|
|
|
114
126
|
}
|
|
115
127
|
|
|
116
128
|
/**
|
|
117
|
-
* The
|
|
118
|
-
*
|
|
119
|
-
*
|
|
129
|
+
* The orgs read: the caller's account handle, the scopes (`orgs`) and the workspaces.
|
|
130
|
+
*
|
|
131
|
+
* `workspaces` is null when the answer carries no list — a backend older than the list
|
|
132
|
+
* (2026-10-22). The workspace readers refuse then, saying so (`backend/workspace.js`);
|
|
133
|
+
* they never stand `orgs` in for it, which leaves the units below an org out.
|
|
134
|
+
*
|
|
135
|
+
* @returns {Promise<{account_handle: string|null, orgs: Array<{handle: string, is_primary: boolean}>, workspaces: Array<{unit_uuid?: string, handle?: string|null, kind?: string, is_primary?: boolean}>|null}>}
|
|
120
136
|
*/
|
|
121
137
|
export async function fetchOrgs({ apiBase, token, fetchImpl = globalThis.fetch }) {
|
|
122
138
|
const res = await fetchImpl(`${apiBase.replace(/\/$/, '')}${ORGS_PATH}`, {
|
|
@@ -129,7 +145,8 @@ export async function fetchOrgs({ apiBase, token, fetchImpl = globalThis.fetch }
|
|
|
129
145
|
const data = await res.json().catch(() => null)
|
|
130
146
|
return {
|
|
131
147
|
account_handle: data?.account_handle ?? null,
|
|
132
|
-
orgs: Array.isArray(data?.orgs) ? data.orgs : []
|
|
148
|
+
orgs: Array.isArray(data?.orgs) ? data.orgs : [],
|
|
149
|
+
workspaces: Array.isArray(data?.workspaces) ? data.workspaces : null
|
|
133
150
|
}
|
|
134
151
|
}
|
|
135
152
|
|