uniweb 1.16.0 → 1.16.2

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.2",
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
- "@uniweb/schemas": "~1.6.5",
44
+ "@uniweb/content-writer": "~1.3.0",
46
45
  "@uniweb/semantic-parser": "~1.12.2",
47
- "@uniweb/content-writer": "~1.3.0"
46
+ "@uniweb/core": "~1.4.0",
47
+ "@uniweb/schemas": "~1.6.5"
48
48
  },
49
49
  "peerDependencies": {
50
- "@uniweb/build": "~1.7.4",
51
- "@uniweb/semantic-parser": "~1.12.2",
52
- "@uniweb/content-reader": "~1.5.4"
50
+ "@uniweb/build": "~1.7.5",
51
+ "@uniweb/content-reader": "~1.5.4",
52
+ "@uniweb/semantic-parser": "~1.12.2"
53
53
  },
54
54
  "peerDependenciesMeta": {
55
55
  "@uniweb/build": {
@@ -131,7 +131,7 @@ my-project/
131
131
  └── pnpm-workspace.yaml
132
132
  ```
133
133
 
134
- A site is pure content. A foundation is the site's source code — that's why it lives in `src/`. The foundation's `package.json::name` is `src`, symmetric with `site` — a **workspace** name, how the site depends on it. What the foundation **registers as** (`@org/<name>`, the name sites pin) is `name` in its `main.js`, which `uniweb create` sets to the project's name. **`src` and `foundation` are never a foundation's name** — `uniweb register` refuses them, because every project in an org would register the same one.
134
+ A site is pure content. A foundation is the site's source code — that's why it lives in `src/`. The foundation's `package.json::name` is `src`, symmetric with `site` — a **workspace** name, how the site depends on it. What the foundation **registers as** (`@org/<name>`, the name sites pin) is `name` in its `main.js`, which `uniweb create` sets to the project's name. **`src` and `foundation` are never a foundation's name** — `uniweb register` refuses them, because every project in an org would register the same one. **Nor is any name under `@uniweb`**: that scope is the Uniweb framework's own, and `uniweb create`, `uniweb add` and `uniweb register` refuse it.
135
135
 
136
136
  - **Foundation** (`src/`): React components. Those in `sections/` and `layouts/` are *section types* — selectable by authors via `type:`, or used for layout areas. Everything in `components/` and `utils/` is ordinary React and JS: the developer's workbench, not visible to authors.
137
137
  - **Site** (`site/`): markdown content and configuration, plus optional records of structured content and the queries that reach them or an external data source.
@@ -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,
@@ -46,7 +46,8 @@ import {
46
46
  import { findWorkspaceRoot } from '../utils/workspace.js'
47
47
  import {
48
48
  normalizeFoundationName,
49
- ensureFoundationName
49
+ ensureFoundationName,
50
+ reservedScopeIn
50
51
  } from '../utils/foundation-name.js'
51
52
  import {
52
53
  detectPackageManager,
@@ -305,6 +306,21 @@ export async function add(rawArgs) {
305
306
  parsed = parseArgs(args)
306
307
  }
307
308
 
309
+ // ⛔ A FOUNDATION NAMED UNDER THE RESERVED SCOPE IS REFUSED before anything is placed
310
+ // [Diego, 2026-10-23]. The name read as `@scope/name` would otherwise be a folder
311
+ // path, and the foundation named `name` — the scope dropped in silence.
312
+ const reserved = ['project', 'foundation', 'extension'].includes(parsed.subcommand)
313
+ ? reservedScopeIn(parsed.name)
314
+ : null
315
+ if (reserved) {
316
+ error(`${reserved.problem}.`)
317
+ log(
318
+ `Add it without the scope — ${colors.cyan}${prefix} add ${parsed.subcommand} ${reserved.bare}${colors.reset} — and ` +
319
+ `${colors.cyan}${prefix} register${colors.reset} gives it the scope of the workspace you work in.`
320
+ )
321
+ process.exit(1)
322
+ }
323
+
308
324
  // Read root package.json for project name
309
325
  const rootPkg = JSON.parse(
310
326
  await readFile(join(rootDir, 'package.json'), 'utf-8').catch(() => '{}')
@@ -76,6 +76,7 @@ import {
76
76
  collectStandaloneSchemas,
77
77
  readFoundationName,
78
78
  checkFoundationName,
79
+ checkRegistrationScope,
79
80
  splitFoundationName
80
81
  } from '@uniweb/build'
81
82
  import {
@@ -90,7 +91,7 @@ import {
90
91
  } from '../utils/code-upload.js'
91
92
  import { deriveScope, publishScope, belongsToScope } from '../utils/registry-orgs.js'
92
93
  import { BackendClient } from '../backend/client.js'
93
- import { resolveWorkspace, scopeOfWorkspace, SOURCE_LABEL } from '../backend/workspace.js'
94
+ import { resolveWorkspace, scopeOfWorkspace, topOfChildHandle, SOURCE_LABEL } from '../backend/workspace.js'
94
95
  import { writeJsonPreservingStyleAsync } from '../utils/json-file.js'
95
96
  import {
96
97
  findWorkspaceRoot,
@@ -252,7 +253,12 @@ export async function settleFoundationName(targetDir, { args, isPreview }) {
252
253
  if (!problem) return true
253
254
 
254
255
  const where = relative(process.cwd(), read.mainFile) || read.mainFile
255
- const suggestion = suggestFoundationName(targetDir)
256
+ // A name refused for its reserved scope keeps the rest of itself: `@uniweb/marketing`
257
+ // suggests `marketing`, which registers under the scope of the workspace you work in.
258
+ const { scope: nameScope, bare } = splitFoundationName(read.name)
259
+ const suggestion =
260
+ (nameScope && checkRegistrationScope(nameScope) && !checkFoundationName(bare) ? bare : null) ||
261
+ suggestFoundationName(targetDir)
256
262
  const reason = `${problem[0].toUpperCase()}${problem.slice(1)}.`
257
263
 
258
264
  // Nobody to ask — a script, a preview (which writes nothing), or --json (whose
@@ -334,6 +340,7 @@ export async function settleFoundationScope(targetDir, { args, isPreview, flagSc
334
340
  const { scope: nameScope, bare } = splitFoundationName(read.name)
335
341
 
336
342
  if (nameScope) {
343
+ if (refusedReservedScope(nameScope, `${where} names this foundation ${read.name}, under`)) return null
337
344
  if (flagScope && flagScope !== nameScope) {
338
345
  error(
339
346
  `${where} names this foundation ${read.name}, so it registers under ${nameScope} — ` +
@@ -352,6 +359,7 @@ export async function settleFoundationScope(targetDir, { args, isPreview, flagSc
352
359
 
353
360
  let scope = flagScope
354
361
  let source = flagScope ? '--scope' : null
362
+ if (refusedReservedScope(scope, '--scope names')) return null
355
363
  if (!scope) {
356
364
  scope = await deriveScopeFromLogin(client, args)
357
365
  if (!scope) return null
@@ -392,26 +400,50 @@ export async function settleFoundationScope(targetDir, { args, isPreview, flagSc
392
400
  * the scope is derived from your orgs as before (`deriveScope`): your personal scope when
393
401
  * you belong to none, else a pick at a terminal, your personal scope without one.
394
402
  */
403
+ /**
404
+ * Refuse a reserved scope wherever it came from, before anything is written or sent —
405
+ * `@uniweb` names the framework's own packages and a system account on every Uniweb
406
+ * backend [Diego, 2026-10-23] (`checkRegistrationScope`).
407
+ *
408
+ * @param {string|null} scope
409
+ * @param {string} lead - where the scope came from, said before it: `--scope names`
410
+ * @returns {boolean} true when refused — the reason printed
411
+ */
412
+ function refusedReservedScope(scope, lead) {
413
+ const problem = scope ? checkRegistrationScope(scope) : null
414
+ if (!problem) return false
415
+ error(`${lead} ${scope}. ${problem}.`)
416
+ log(` ${colors.dim}Register under your own scope or an organization's: --scope @org.${colors.reset}`)
417
+ return true
418
+ }
419
+
395
420
  async function deriveScopeFromLogin(client, args) {
396
421
  const token = await client.token()
397
422
  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)
423
+ const live = !ws.refused && ws.source !== 'offline'
424
+ const personal = live && ws.workspace === null
425
+ // A unit below an organization — a sub-org, a project — names no scope; its organization's is
426
+ // the default for the organization's members (`scopeOfWorkspace`), read from the same answer.
427
+ const child = live && typeof ws.workspace === 'string' && ws.workspace.includes(':')
428
+ const envelope = personal || child ? await client.fetchOrgs() : null
429
+ const fromWorkspace = scopeOfWorkspace(ws, envelope?.account_handle ?? null, envelope?.orgs)
401
430
  if (fromWorkspace) {
431
+ if (refusedReservedScope(fromWorkspace, 'The workspace you work in is')) return null
402
432
  const named = `${colors.bright}${fromWorkspace}${colors.reset}`
403
433
  console.error(
404
434
  personal
405
435
  ? `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.`
436
+ : child
437
+ ? `Registering under ${named} — the organization of ${ws.workspace}, the workspace you work in (${SOURCE_LABEL[ws.source]}). For another scope: --scope @org.`
438
+ : `Registering under ${named} — the workspace you work in (${SOURCE_LABEL[ws.source]}). For another scope: --scope @org.`
407
439
  )
408
440
  return fromWorkspace
409
441
  }
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(':')) {
442
+ // Not a member of the unit's organization — a project's guest — so it gives no scope, and a bare
443
+ // name is asked for one as when no workspace names any.
444
+ if (child) {
413
445
  console.error(
414
- `${colors.dim}${ws.workspace}, the workspace you work in, is a unit below an organization, and names no scope.${colors.reset}`
446
+ `${colors.dim}${ws.workspace}, the workspace you work in, names no scope, and ${topOfChildHandle(ws.workspace)} is not among your organizations.${colors.reset}`
415
447
  )
416
448
  }
417
449
  const sess = await readRegistryAuth(client.origin)
@@ -421,6 +453,7 @@ async function deriveScopeFromLogin(client, args) {
421
453
  accountHandle: sess?.handle || null,
422
454
  args
423
455
  })
456
+ if (derived && refusedReservedScope(`@${derived}`, 'The scope chosen is')) return null
424
457
  return derived ? `@${derived}` : null
425
458
  }
426
459
 
@@ -642,6 +675,7 @@ async function runRegister(args = []) {
642
675
  log(` ${colors.dim}Pass --scope @org.${colors.reset}`)
643
676
  return { exitCode: 2 }
644
677
  }
678
+ if (refusedReservedScope(scope, `${scopeSource} names`)) return { exitCode: 2 }
645
679
  const isPreview = !!output || dryRun
646
680
 
647
681
  // Each path supplies a different schema source: the standalone path discovers
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-10-22T17:15:55.292Z",
3
+ "generatedAt": "2026-10-23T16:20:14.706Z",
4
4
  "packages": {
5
5
  "@uniweb/api": {
6
6
  "version": "1.6.4"
7
7
  },
8
8
  "@uniweb/build": {
9
- "version": "1.7.4"
9
+ "version": "1.7.5"
10
10
  },
11
11
  "@uniweb/content-reader": {
12
12
  "version": "1.5.4"
@@ -27,10 +27,10 @@
27
27
  "version": "1.6.0"
28
28
  },
29
29
  "@uniweb/loom": {
30
- "version": "1.1.0"
30
+ "version": "1.1.1"
31
31
  },
32
32
  "@uniweb/press": {
33
- "version": "1.1.3"
33
+ "version": "1.1.4"
34
34
  },
35
35
  "@uniweb/projections": {
36
36
  "version": "1.1.6"
@@ -54,13 +54,13 @@
54
54
  "version": "1.1.0"
55
55
  },
56
56
  "@uniweb/templates": {
57
- "version": "1.1.4"
57
+ "version": "1.1.5"
58
58
  },
59
59
  "@uniweb/theming": {
60
60
  "version": "1.2.1"
61
61
  },
62
62
  "@uniweb/unipress": {
63
- "version": "1.1.11"
63
+ "version": "1.1.12"
64
64
  }
65
65
  },
66
66
  "templates": {
@@ -198,7 +198,7 @@
198
198
  },
199
199
  "business-docs": {
200
200
  "name": "Business Documents",
201
- "description": "A complete business-document site: invoice and SOW records, pages listing them, and a filtered engagement report with XLSX export. Built on @uniweb/business-docs and demonstrates the same foundation that ships the unipress invoice template.",
201
+ "description": "A complete business-document site: invoice and SOW records, pages listing them, and a filtered engagement report with XLSX export. Built on @unipress/business-docs and demonstrates the same foundation that ships the unipress invoice template.",
202
202
  "tags": [
203
203
  "business-docs",
204
204
  "invoice",
package/src/index.js CHANGED
@@ -54,7 +54,8 @@ import {
54
54
  } from './utils/scaffold.js'
55
55
  import {
56
56
  normalizeFoundationName,
57
- ensureFoundationName
57
+ ensureFoundationName,
58
+ reservedScopeIn
58
59
  } from './utils/foundation-name.js'
59
60
  import {
60
61
  detectPackageManager,
@@ -1171,6 +1172,21 @@ async function main() {
1171
1172
  await exitWhenDrained(1)
1172
1173
  }
1173
1174
 
1175
+ // ⛔ A NAME UNDER THE RESERVED SCOPE IS REFUSED before anything is asked or written
1176
+ // [Diego, 2026-10-23]. The foundation's name is the project's without its scope, so
1177
+ // `@uniweb/marketing` would be dropped to `marketing` in silence — read as granted
1178
+ // until `register` refused it.
1179
+ const reserved = reservedScopeIn(projectName)
1180
+ if (reserved) {
1181
+ error(`${reserved.problem}.`)
1182
+ const again = inPlace ? `create . --name ${reserved.bare}` : `create ${reserved.bare}`
1183
+ log(
1184
+ `Create it without the scope — ${colors.cyan}${prefix} ${again}${colors.reset} — and ` +
1185
+ `${colors.cyan}${prefix} register${colors.reset} gives its foundation the scope of the workspace you work in.`
1186
+ )
1187
+ await exitWhenDrained(1)
1188
+ }
1189
+
1174
1190
  // Prompt for template if not specified via --template or --blank
1175
1191
  if (!templateType && !isBlank) {
1176
1192
  const templateResponse = await prompts(
@@ -1759,7 +1775,9 @@ ${colors.bright}Options:${colors.reset}
1759
1775
  org's — and write it into the name (refused when the name has another
1760
1776
  scope). A schemas-only package: publish under @scope; default:
1761
1777
  package.json uniweb.scope
1762
- --org @org Work in @org for this command: a bare name registers under it
1778
+ --org @org Work in @org for this command: a bare name registers under it —
1779
+ a sub-org's or project's (@studio:acme): under its organization's,
1780
+ when you are a member of it
1763
1781
  --personal Work in your personal workspace: a bare name registers under your
1764
1782
  personal scope
1765
1783
  --dry-run Print the .uwx; submit nothing
@@ -2,9 +2,10 @@
2
2
  * Giving a foundation its name — `name` in `main.js`'s default export.
3
3
  *
4
4
  * The rule itself (main.js `name`, else package.json `name`; `src` and `foundation`
5
- * refused) is `@uniweb/build`'s `foundation-name.js`, shared by the build, `register`
6
- * and `push`. This module only WRITES the name: the scaffold at `create` / `add`, and
7
- * `register` when it asks for one.
5
+ * refused, and so is the `@uniweb` scope) is `@uniweb/build`'s `foundation-name.js`,
6
+ * shared by the build, `register` and `push`. This module only WRITES the name: the
7
+ * scaffold at `create` / `add`, and `register` when it asks for one — and refuses, at
8
+ * `create` / `add`, a name that asks for the reserved scope (`reservedScopeIn`).
8
9
  *
9
10
  * ⛔ NO `@uniweb/build` IMPORT, AND NO IMPORT OF main.js. `uniweb create` runs before
10
11
  * any project exists: `@uniweb/build` is an optional peer that may not be installed,
@@ -21,6 +22,35 @@ import { join, resolve, basename, dirname } from 'node:path'
21
22
  // (`@uniweb/build`'s FORBIDDEN_FOUNDATION_NAMES; the test keeps them equal).
22
23
  export const SCAFFOLD_FOLDER_NAMES = new Set(['src', 'foundation'])
23
24
 
25
+ // Scopes no foundation is named under — `@uniweb`, the framework's own (`@uniweb/build`'s
26
+ // RESERVED_SCOPES and its reason, which the test keeps equal).
27
+ export const RESERVED_SCOPES = new Set(['uniweb'])
28
+
29
+ /**
30
+ * The reserved scope a name asks for — `@uniweb/marketing` asks for `@uniweb` — with why
31
+ * it is refused and the name without it. Null when the name asks for no reserved scope.
32
+ *
33
+ * `create` and `add` drop any scope from the name they write, so this is for refusing
34
+ * what was ASKED for, before anything is written: a scope dropped in silence would read
35
+ * as granted until `register` refused it.
36
+ *
37
+ * @param {unknown} raw - a name as typed
38
+ * @returns {{ scope: string, bare: string, problem: string } | null}
39
+ */
40
+ export function reservedScopeIn(raw) {
41
+ const text = String(raw ?? '').trim()
42
+ const scoped = /^@([^/]+)\/(.+)$/.exec(text)
43
+ if (!scoped || !RESERVED_SCOPES.has(scoped[1].toLowerCase())) return null
44
+ const scope = `@${scoped[1].toLowerCase()}`
45
+ return {
46
+ scope,
47
+ bare: scoped[2],
48
+ problem:
49
+ `"${text}" is named under ${scope}, which is reserved: it names the Uniweb framework's ` +
50
+ 'own packages and the system account of every Uniweb backend, so nothing registers under it'
51
+ }
52
+ }
53
+
24
54
  // Written above the name wherever the CLI adds one — the scaffold's main.js
25
55
  // template says the same.
26
56
  const NAME_COMMENT = '// What this foundation registers as — @<org>/<name> — and what sites pin.'
@@ -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}.`, {