uniweb 1.16.0 → 1.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uniweb",
3
- "version": "1.16.0",
3
+ "version": "1.16.1",
4
4
  "description": "Create structured Vite + React sites with content/code separation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -41,15 +41,15 @@
41
41
  "js-yaml": "^4.1.0",
42
42
  "prompts": "^2.4.2",
43
43
  "tar": "^7.0.0",
44
- "@uniweb/core": "~1.4.0",
45
44
  "@uniweb/schemas": "~1.6.5",
45
+ "@uniweb/core": "~1.4.0",
46
46
  "@uniweb/semantic-parser": "~1.12.2",
47
47
  "@uniweb/content-writer": "~1.3.0"
48
48
  },
49
49
  "peerDependencies": {
50
- "@uniweb/build": "~1.7.4",
51
50
  "@uniweb/semantic-parser": "~1.12.2",
52
- "@uniweb/content-reader": "~1.5.4"
51
+ "@uniweb/content-reader": "~1.5.4",
52
+ "@uniweb/build": "~1.7.4"
53
53
  },
54
54
  "peerDependenciesMeta": {
55
55
  "@uniweb/build": {
@@ -91,6 +91,33 @@ export function listedWorkspaces(envelope) {
91
91
  .filter((w) => w.name)
92
92
  }
93
93
 
94
+ /**
95
+ * The top-level organization of a unit named by its child handle — `@studio` for `@studio:acme`,
96
+ * the part before the colon (backend, 2026-10-22).
97
+ *
98
+ * @param {string} handle - `@top:part`
99
+ * @returns {string} `@top`
100
+ */
101
+ export function topOfChildHandle(handle) {
102
+ return `@${String(handle).replace(/^@/, '').split(':')[0]}`
103
+ }
104
+
105
+ /**
106
+ * The account's own handle, as the header would name it (`@jane`), from the orgs read — or null.
107
+ *
108
+ * ⛔ **An account's handle names no workspace** (backend, 2026-10-22, pinned by its tests). A
109
+ * handle names a person or an org, and only an org is a workspace: the header answers
110
+ * `@<account handle>` with `400`, never as the personal workspace, which a request names by
111
+ * naming none. So `--org @<your handle>` is refused, saying what names your own.
112
+ */
113
+ export function ownHandle(envelope) {
114
+ return envelope?.account_handle ? workspaceHandle(envelope.account_handle) : null
115
+ }
116
+
117
+ /** What naming your own handle as a workspace is told — `--personal`, or `UNIWEB_WORKSPACE=personal`. */
118
+ export const ownHandleNamesNone = (named, personal) =>
119
+ `${named} is your own handle, which names no workspace — your personal workspace is ${personal}.`
120
+
94
121
  /**
95
122
  * What a backend whose orgs read lists no workspaces is told. ⛔ The CLI does not check
96
123
  * against `orgs` in its place: that list leaves out every unit below an org.
@@ -185,22 +212,36 @@ export async function resolveWorkspace({ client, args = [], offline = false }) {
185
212
  * decides which SITE a command works on; here it only answers the question a bare name
186
213
  * asks the first time it registers.
187
214
  *
215
+ * ⭐ **A unit below an organization — a sub-org, a project — takes its organization's scope**
216
+ * *[Diego, 2026-10-22]*: its child handle (`@studio:acme`) names no scope (backend, the same day),
217
+ * and a bare name registered from it defaults to the top-level org's handle, `@studio`, when that
218
+ * org is among the account's organizations — the orgs read's `orgs`, the scopes. Not otherwise: a
219
+ * project's guest belongs to the project alone and has only their own scope, and so do the people
220
+ * of a sub-org whose belonging stops there — which the orgs read cannot tell apart, so membership is
221
+ * read from `orgs` and nothing else.
222
+ *
188
223
  * Null when the workspace names no scope, and the caller derives one as before
189
224
  * (`deriveScope`): none is chosen (you belong to organizations and none is named), the
190
225
  * 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).
226
+ * below an organization the account is not a member of, or the account has no handle (a
227
+ * service account).
193
228
  *
194
229
  * @param {{ workspace?: string|null, source?: string, refused?: boolean }} ws -
195
230
  * `resolveWorkspace`'s answer
196
231
  * @param {string|null} accountHandle - the account's own handle (`GET /dev/orgs`), read
197
232
  * only for the personal workspace
233
+ * @param {Array<{ handle?: string }>} [orgs] - the orgs read's `orgs`, read only for a unit
234
+ * below an organization
198
235
  * @returns {string|null} `@handle`
199
236
  */
200
- export function scopeOfWorkspace(ws, accountHandle) {
237
+ export function scopeOfWorkspace(ws, accountHandle, orgs = []) {
201
238
  if (!ws || ws.refused || ws.source === 'offline') return null
202
239
  if (typeof ws.workspace === 'string') {
203
- return ws.workspace.startsWith('@') && !ws.workspace.includes(':') ? ws.workspace : null
240
+ if (!ws.workspace.startsWith('@')) return null
241
+ if (!ws.workspace.includes(':')) return ws.workspace
242
+ const top = topOfChildHandle(ws.workspace)
243
+ const member = (Array.isArray(orgs) ? orgs : []).some((o) => `@${bareHandle(o?.handle)}` === top)
244
+ return member ? top : null
204
245
  }
205
246
  const h = bareHandle(accountHandle || '')
206
247
  return h && !validateHandle(h) ? `@${h}` : null
@@ -228,12 +269,14 @@ export async function chooseWorkspace({ apiBase, token, args = [] }) {
228
269
  const flag = readOrgFlag(args)
229
270
  if (!flag && args.includes('--personal')) return { choice: PERSONAL }
230
271
 
231
- const listed = listedWorkspaces(await fetchOrgs({ apiBase, token }))
272
+ const envelope = await fetchOrgs({ apiBase, token })
273
+ const listed = listedWorkspaces(envelope)
232
274
  if (!listed) return { refused: true, reason: noWorkspaceList(apiBase) }
233
275
  const mine = listed.map((w) => w.name)
234
276
  if (flag) {
235
277
  const named = namedWorkspace(flag)
236
278
  if (!named) return { refused: true, reason: `--org ${flag} names no workspace — give @org, or --personal.` }
279
+ if (named === ownHandle(envelope)) return { refused: true, reason: ownHandleNamesNone(named, '--personal') }
237
280
  if (!mine.includes(named)) {
238
281
  return {
239
282
  refused: true,
@@ -90,7 +90,7 @@ import {
90
90
  } from '../utils/code-upload.js'
91
91
  import { deriveScope, publishScope, belongsToScope } from '../utils/registry-orgs.js'
92
92
  import { BackendClient } from '../backend/client.js'
93
- import { resolveWorkspace, scopeOfWorkspace, SOURCE_LABEL } from '../backend/workspace.js'
93
+ import { resolveWorkspace, scopeOfWorkspace, topOfChildHandle, SOURCE_LABEL } from '../backend/workspace.js'
94
94
  import { writeJsonPreservingStyleAsync } from '../utils/json-file.js'
95
95
  import {
96
96
  findWorkspaceRoot,
@@ -395,23 +395,29 @@ export async function settleFoundationScope(targetDir, { args, isPreview, flagSc
395
395
  async function deriveScopeFromLogin(client, args) {
396
396
  const token = await client.token()
397
397
  const ws = await resolveWorkspace({ client, args })
398
- const personal = !ws.refused && ws.source !== 'offline' && ws.workspace === null
399
- const accountHandle = personal ? (await client.fetchOrgs()).account_handle : null
400
- const fromWorkspace = scopeOfWorkspace(ws, accountHandle)
398
+ const live = !ws.refused && ws.source !== 'offline'
399
+ const personal = live && ws.workspace === null
400
+ // A unit below an organization — a sub-org, a project — names no scope; its organization's is
401
+ // the default for the organization's members (`scopeOfWorkspace`), read from the same answer.
402
+ const child = live && typeof ws.workspace === 'string' && ws.workspace.includes(':')
403
+ const envelope = personal || child ? await client.fetchOrgs() : null
404
+ const fromWorkspace = scopeOfWorkspace(ws, envelope?.account_handle ?? null, envelope?.orgs)
401
405
  if (fromWorkspace) {
402
406
  const named = `${colors.bright}${fromWorkspace}${colors.reset}`
403
407
  console.error(
404
408
  personal
405
409
  ? `Registering under your personal scope ${named} — you work in your personal workspace (${SOURCE_LABEL[ws.source]}). For an org: --scope @org.`
406
- : `Registering under ${named} — the workspace you work in (${SOURCE_LABEL[ws.source]}). For another scope: --scope @org.`
410
+ : child
411
+ ? `Registering under ${named} — the organization of ${ws.workspace}, the workspace you work in (${SOURCE_LABEL[ws.source]}). For another scope: --scope @org.`
412
+ : `Registering under ${named} — the workspace you work in (${SOURCE_LABEL[ws.source]}). For another scope: --scope @org.`
407
413
  )
408
414
  return fromWorkspace
409
415
  }
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(':')) {
416
+ // Not a member of the unit's organization — a project's guest — so it gives no scope, and a bare
417
+ // name is asked for one as when no workspace names any.
418
+ if (child) {
413
419
  console.error(
414
- `${colors.dim}${ws.workspace}, the workspace you work in, is a unit below an organization, and names no scope.${colors.reset}`
420
+ `${colors.dim}${ws.workspace}, the workspace you work in, names no scope, and ${topOfChildHandle(ws.workspace)} is not among your organizations.${colors.reset}`
415
421
  )
416
422
  }
417
423
  const sess = await readRegistryAuth(client.origin)
package/src/index.js CHANGED
@@ -1759,7 +1759,9 @@ ${colors.bright}Options:${colors.reset}
1759
1759
  org's — and write it into the name (refused when the name has another
1760
1760
  scope). A schemas-only package: publish under @scope; default:
1761
1761
  package.json uniweb.scope
1762
- --org @org Work in @org for this command: a bare name registers under it
1762
+ --org @org Work in @org for this command: a bare name registers under it —
1763
+ a sub-org's or project's (@studio:acme): under its organization's,
1764
+ when you are a member of it
1763
1765
  --personal Work in your personal workspace: a bare name registers under your
1764
1766
  personal scope
1765
1767
  --dry-run Print the .uwx; submit nothing
@@ -952,8 +952,16 @@ async function reportTokenProcess({ named, token }) {
952
952
  // Their workspace is UNIWEB_WORKSPACE's — never the stored session's — and, with none
953
953
  // named, personal for an account in no organization or project (resolveWorkspace,
954
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 } =
956
- await import('../backend/workspace.js')
955
+ const {
956
+ WORKSPACE_ENV,
957
+ PERSONAL,
958
+ parseWorkspaceChoice,
959
+ headerOf,
960
+ describeWorkspace,
961
+ listedWorkspaces,
962
+ noWorkspaceList,
963
+ ownHandle
964
+ } = await import('../backend/workspace.js')
957
965
  const unsettled = (reason, { commandsRefuse }) => {
958
966
  console.error(`\x1b[31m✗\x1b[0m ${reason}`)
959
967
  if (commandsRefuse) {
@@ -968,18 +976,27 @@ async function reportTokenProcess({ named, token }) {
968
976
  commandsRefuse: true
969
977
  })
970
978
  }
971
- let listed
979
+ let envelope
972
980
  try {
973
- listed = listedWorkspaces(await (await import('./registry-orgs.js')).fetchOrgs({ apiBase: origin, token }))
981
+ envelope = await (await import('./registry-orgs.js')).fetchOrgs({ apiBase: origin, token })
974
982
  } catch (err) {
975
983
  console.error(`\x1b[33m⚠\x1b[0m ${err.message} — the workspace is not checked.`)
976
984
  return
977
985
  }
986
+ const listed = listedWorkspaces(envelope)
978
987
  if (!listed) {
979
988
  console.error(`\x1b[33m⚠\x1b[0m ${noWorkspaceList(origin)}`)
980
989
  return
981
990
  }
982
991
  const mine = listed.map((w) => w.name)
992
+ if (choice && choice === ownHandle(envelope)) {
993
+ // As a login refuses `--org @<own handle>` (chooseWorkspace): a handle names a person or an org,
994
+ // and only an org is a workspace.
995
+ unsettled(
996
+ `${WORKSPACE_ENV}=${raw} names the account's own handle, which names no workspace — its personal workspace is ${WORKSPACE_ENV}=personal.`,
997
+ { commandsRefuse: false }
998
+ )
999
+ }
983
1000
  if (choice && choice !== PERSONAL && !mine.includes(choice)) {
984
1001
  // As a login refuses `--org` for one (chooseWorkspace).
985
1002
  unsettled(`${WORKSPACE_ENV}=${raw} is not one of this account's workspaces on ${origin}.`, {