uniweb 0.63.2 → 0.64.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 +7 -7
- package/partials/agents.md +16 -12
- package/src/backend/client.js +1 -1
- package/src/backend/site-sync.js +7 -8
- package/src/commands/org.js +26 -8
- package/src/commands/register.js +4 -4
- package/src/framework-index.json +5 -5
- package/src/index.js +8 -5
- package/src/utils/registry-orgs.js +61 -184
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.64.0",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,17 +41,17 @@
|
|
|
41
41
|
"js-yaml": "^4.1.0",
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
|
-
"@uniweb/kit": "^0.19.4",
|
|
45
|
-
"@uniweb/semantic-parser": "^1.4.1",
|
|
46
|
-
"@uniweb/schemas": "^0.3.4",
|
|
47
44
|
"@uniweb/content-writer": "^0.3.4",
|
|
48
45
|
"@uniweb/core": "^0.29.8",
|
|
49
|
-
"@uniweb/
|
|
46
|
+
"@uniweb/schemas": "^0.4.0",
|
|
47
|
+
"@uniweb/semantic-parser": "^1.4.1",
|
|
48
|
+
"@uniweb/runtime": "^0.27.4",
|
|
49
|
+
"@uniweb/kit": "^0.19.4"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"@uniweb/content-reader": "^1.2.5",
|
|
53
|
-
"@uniweb/
|
|
54
|
-
"@uniweb/
|
|
53
|
+
"@uniweb/build": "^0.59.1",
|
|
54
|
+
"@uniweb/semantic-parser": "^1.4.1"
|
|
55
55
|
},
|
|
56
56
|
"peerDependenciesMeta": {
|
|
57
57
|
"@uniweb/build": {
|
package/partials/agents.md
CHANGED
|
@@ -2455,15 +2455,19 @@ await writer.remove(itemId)
|
|
|
2455
2455
|
|
|
2456
2456
|
### ⛔ Permissions are the SERVER'S, and your UI is only a courtesy
|
|
2457
2457
|
|
|
2458
|
-
Gate your controls on what the viewer may do — `viewer.
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2458
|
+
Gate your controls on what the viewer may do — `viewer.roles` (the site's operator
|
|
2459
|
+
holds `system_admin`), or a record's own `can_edit` from a single-entity read — but
|
|
2460
|
+
**never rely on that for safety**. A foundation runs with exactly the viewer's own
|
|
2461
|
+
authority, so hiding a button hides a button.
|
|
2462
2462
|
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2463
|
+
What protects content is the **entry**: a member writes their own entries and not
|
|
2464
|
+
another's, and the store enforces it. **Models are open** — anyone signed in may
|
|
2465
|
+
create entries of any Model, so a data schema does not restrict who creates
|
|
2466
|
+
(`creatable_by` is retired and refused). What a schema can protect is how an item
|
|
2467
|
+
changes:
|
|
2466
2468
|
|
|
2469
|
+
```yaml
|
|
2470
|
+
# foundation/schemas/attendance.yml
|
|
2467
2471
|
sections:
|
|
2468
2472
|
checkins:
|
|
2469
2473
|
many: true
|
|
@@ -2489,8 +2493,8 @@ export default createMockBackend({ seed }).fetch
|
|
|
2489
2493
|
```
|
|
2490
2494
|
|
|
2491
2495
|
`uniweb dev` mounts it at your `api:` address, same-origin, so cookies and your
|
|
2492
|
-
site's configuration behave exactly as they will in production. It **enforces**
|
|
2493
|
-
|
|
2496
|
+
site's configuration behave exactly as they will in production. It **enforces** who
|
|
2497
|
+
may edit an entry and `append_only`, so a permission you are relying on fails on your
|
|
2494
2498
|
machine rather than in front of a user. State is in memory; restart to reset.
|
|
2495
2499
|
|
|
2496
2500
|
`uniweb create my-event --template conference` is a worked example of all of this.
|
|
@@ -2515,9 +2519,9 @@ uniweb add ci --target foundation # Publish a foundation for free at permanent v
|
|
|
2515
2519
|
uniweb push / pull / clone / status # Git-style content sync with the Uniweb backend
|
|
2516
2520
|
uniweb refresh / sync # Catch up (git + backend, never pushes) / catch up, then push
|
|
2517
2521
|
uniweb push --org @acme # First push/publish of a site: who owns it (see below)
|
|
2518
|
-
uniweb register [--scope @
|
|
2522
|
+
uniweb register [--scope @scope] # Register a foundation + its data schemas to the registry
|
|
2519
2523
|
uniweb login / logout # One backend at a time: log in (--backend <url>) or out
|
|
2520
|
-
uniweb org list / create <handle> #
|
|
2524
|
+
uniweb org list / create <handle> # Your personal scope, and the orgs you belong to
|
|
2521
2525
|
uniweb content export [dir] # Package a site (or a built foundation's schema) as .uwx
|
|
2522
2526
|
|
|
2523
2527
|
uniweb rename <foundation|site|extension> <old> <new> # Rename across the whole workspace
|
|
@@ -2634,7 +2638,7 @@ Platform-specific configuration that doesn't belong in npm-standard fields. All
|
|
|
2634
2638
|
|---|---|---|---|
|
|
2635
2639
|
| `runtimePolicy` | `dist/runtime-pin.json` | unset | Declares how far past the recorded runtime version a host may move a site. |
|
|
2636
2640
|
|
|
2637
|
-
**The foundation's name is not here — it is `name` in `main.js`** (else `package.json`'s `name`), **and so is the
|
|
2641
|
+
**The foundation's name is not here — it is `name` in `main.js`** (else `package.json`'s `name`), **and so is the scope it registers under: the scope is part of the name**, `name: '@acme/marketing'`. A scope is a namespace — your personal one, `@<your handle>`, which needs no org, or an org's. A name with no scope has not been registered yet: the first `uniweb register` takes `--scope`, else your personal scope (asking only if you belong to orgs), and writes it into the name; a `--scope` naming another scope than the name's is refused. The foundation's own data schemas register under the same scope (`@/article` → `@acme/article`). ⛔ `uniweb.id` and `uniweb.scope` are no longer read for a foundation: `uniweb register` refuses them and prints the `main.js` line to write instead. (A schemas-only package has no `main.js`, and still records its scope in `uniweb.scope`.)
|
|
2638
2642
|
|
|
2639
2643
|
**Runtime updates — handled for you.** Your foundation's code links against the runtime: it externalizes `react`, `react-dom`, `react-dom/server`, both JSX runtimes and `@uniweb/core`, and the runtime supplies all of them at load time. So a build is bound to *that* React and *that* core API.
|
|
2640
2644
|
|
package/src/backend/client.js
CHANGED
|
@@ -569,7 +569,7 @@ export class BackendClient {
|
|
|
569
569
|
|
|
570
570
|
// ── Orgs ──────────────────────────────────────────────────────────────────────
|
|
571
571
|
|
|
572
|
-
/** GET /dev/orgs → { account_handle,
|
|
572
|
+
/** GET /dev/orgs → { account_handle, orgs[] }. */
|
|
573
573
|
async fetchOrgs() {
|
|
574
574
|
return fetchOrgsImpl({ apiBase: this.origin, token: await this.token() })
|
|
575
575
|
}
|
package/src/backend/site-sync.js
CHANGED
|
@@ -787,10 +787,10 @@ export const EXPLICIT_OWNER = new Set(['flag', 'personal', 'picked'])
|
|
|
787
787
|
* they never named.
|
|
788
788
|
*
|
|
789
789
|
* `register` resolves a foundation's scope in the same order — recorded (the scope
|
|
790
|
-
* in its name, `@acme/marketing` in main.js), else explicit (`--scope`), else
|
|
791
|
-
* (`deriveScope`) — with one difference: non-interactive, it
|
|
792
|
-
*
|
|
793
|
-
*
|
|
790
|
+
* in its name, `@acme/marketing` in main.js), else explicit (`--scope`), else derived
|
|
791
|
+
* (`deriveScope`) — with one difference: non-interactive, it takes your personal
|
|
792
|
+
* scope and says so. This refuses instead, because a site's owner is the one-shot
|
|
793
|
+
* decision above, and a scope is only a namespace (2026-09-23). ⭐ And the two are
|
|
794
794
|
* separate decisions: a foundation may be registered under any org its author
|
|
795
795
|
* belongs to, whoever owns the site (2026-09-22) — a site's `@/x` refs take the
|
|
796
796
|
* FOUNDATION's scope, never this one. ⚠️ Until 2026-09-21 this said
|
|
@@ -861,10 +861,9 @@ export async function resolveSiteOrgForCreate({
|
|
|
861
861
|
}
|
|
862
862
|
}
|
|
863
863
|
|
|
864
|
-
// Interactive: offer the real choice. Deliberately NOT `deriveScope` — that one
|
|
865
|
-
//
|
|
866
|
-
//
|
|
867
|
-
// and reusing deriveScope would quietly turn it into an org creation.
|
|
864
|
+
// Interactive: offer the real choice. Deliberately NOT `deriveScope` — that one picks
|
|
865
|
+
// a NAMESPACE for a foundation, where `@<your handle>` is your personal scope. Here
|
|
866
|
+
// the answer is an OWNER: your personal workspace (named by naming none) or an org.
|
|
868
867
|
const { fetchOrgs, createOrg, validateHandle, bareHandle } =
|
|
869
868
|
await import('../utils/registry-orgs.js')
|
|
870
869
|
let envelope
|
package/src/commands/org.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* uniweb org —
|
|
2
|
+
* uniweb org — the orgs you belong to, on the backend you are logged in to.
|
|
3
3
|
*
|
|
4
|
-
* uniweb org list
|
|
4
|
+
* uniweb org list Your personal scope, and the orgs you're a member of.
|
|
5
5
|
* uniweb org create <handle> Create an org; you become a member.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* ⭐ You need no org to register a foundation: `@<your handle>` is your personal scope
|
|
8
|
+
* (2026-09-23). An org is for a scope — and sites — you share, and it needs a handle
|
|
9
|
+
* of its own: a backend refuses one named after an account, yours included.
|
|
10
|
+
*
|
|
11
|
+
* Auth: the session of the backend you are logged in to (`uniweb login`) / UNIWEB_TOKEN.
|
|
9
12
|
*/
|
|
10
13
|
|
|
11
14
|
import { BackendClient } from '../backend/client.js'
|
|
@@ -26,10 +29,17 @@ export async function org(args = []) {
|
|
|
26
29
|
|
|
27
30
|
if (sub === 'list') {
|
|
28
31
|
const client = new BackendClient({ args, command: 'Listing orgs' })
|
|
29
|
-
const { orgs } = await client.fetchOrgs()
|
|
32
|
+
const { account_handle: mine, orgs: all } = await client.fetchOrgs()
|
|
33
|
+
// An org named after you — made before 2026-09-23 — is your personal scope's `@`.
|
|
34
|
+
const orgs = all.filter((u) => u.handle !== mine)
|
|
35
|
+
if (mine) {
|
|
36
|
+
console.log(
|
|
37
|
+
`Your personal scope: ${colors.bright}@${mine}${colors.reset}${colors.dim} — register under it with no org${colors.reset}`
|
|
38
|
+
)
|
|
39
|
+
}
|
|
30
40
|
if (!orgs.length) {
|
|
31
41
|
console.log(
|
|
32
|
-
|
|
42
|
+
`You belong to no org. To share a scope and sites with others: uniweb org create <handle>`
|
|
33
43
|
)
|
|
34
44
|
return { exitCode: 0 }
|
|
35
45
|
}
|
|
@@ -55,12 +65,20 @@ export async function org(args = []) {
|
|
|
55
65
|
}
|
|
56
66
|
const client = new BackendClient({ args, command: 'Creating an org' })
|
|
57
67
|
try {
|
|
68
|
+
// Named after you, it is your personal scope already — and a backend refuses it.
|
|
69
|
+
const { account_handle: mine } = await client.fetchOrgs()
|
|
70
|
+
if (mine && handle === mine) {
|
|
71
|
+
error(
|
|
72
|
+
`@${handle} is your personal scope already — you can register under it with no org. An org needs a handle of its own.`
|
|
73
|
+
)
|
|
74
|
+
return { exitCode: 2 }
|
|
75
|
+
}
|
|
58
76
|
const org = await client.createOrg(handle)
|
|
59
77
|
success(
|
|
60
78
|
`Created ${colors.bright}@${org.handle}${colors.reset} — you're a member${org.is_primary ? ' (primary)' : ''}.`
|
|
61
79
|
)
|
|
62
80
|
console.log(
|
|
63
|
-
`${colors.dim}
|
|
81
|
+
`${colors.dim}Register under it: uniweb register --scope @${org.handle}${colors.reset}`
|
|
64
82
|
)
|
|
65
83
|
return { exitCode: 0 }
|
|
66
84
|
} catch (err) {
|
|
@@ -70,7 +88,7 @@ export async function org(args = []) {
|
|
|
70
88
|
}
|
|
71
89
|
|
|
72
90
|
console.log('uniweb org <command>')
|
|
73
|
-
console.log(' list
|
|
91
|
+
console.log(' list Your personal scope, and the orgs you belong to')
|
|
74
92
|
console.log(' create <handle> Create an org (you become a member)')
|
|
75
93
|
return { exitCode: sub ? 2 : 0 }
|
|
76
94
|
}
|
package/src/commands/register.js
CHANGED
|
@@ -376,8 +376,8 @@ export async function settleFoundationScope(targetDir, { args, isPreview, flagSc
|
|
|
376
376
|
}
|
|
377
377
|
|
|
378
378
|
/**
|
|
379
|
-
* A
|
|
380
|
-
*
|
|
379
|
+
* A scope derived from the login (`deriveScope`): your personal scope when you belong
|
|
380
|
+
* to no org, else a pick — as `@handle`, or null when none was chosen.
|
|
381
381
|
*/
|
|
382
382
|
async function deriveScopeFromLogin(client, args) {
|
|
383
383
|
const token = await client.token()
|
|
@@ -692,8 +692,8 @@ async function runRegister(args = []) {
|
|
|
692
692
|
}
|
|
693
693
|
}
|
|
694
694
|
|
|
695
|
-
// A schemas-only package with no scope, on a real submit → derive it from login
|
|
696
|
-
//
|
|
695
|
+
// A schemas-only package with no scope, on a real submit → derive it from the login
|
|
696
|
+
// (your personal scope, or a pick among it and your orgs) and persist it to package.json.
|
|
697
697
|
// (A foundation's was settled before its build — `settleFoundationScope`.)
|
|
698
698
|
if (standalone && !scope && !isPreview) {
|
|
699
699
|
const derived = await deriveScopeFromLogin(client, args)
|
package/src/framework-index.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-09-
|
|
3
|
+
"generatedAt": "2026-09-23T16:32:05.892Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/api": {
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.6.0",
|
|
7
7
|
"path": "framework/api",
|
|
8
8
|
"deps": [
|
|
9
9
|
"@uniweb/core"
|
|
10
10
|
]
|
|
11
11
|
},
|
|
12
12
|
"@uniweb/build": {
|
|
13
|
-
"version": "0.59.
|
|
13
|
+
"version": "0.59.1",
|
|
14
14
|
"path": "framework/build",
|
|
15
15
|
"deps": [
|
|
16
16
|
"@uniweb/content-reader",
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
"deps": []
|
|
97
97
|
},
|
|
98
98
|
"@uniweb/schemas": {
|
|
99
|
-
"version": "0.
|
|
99
|
+
"version": "0.4.0",
|
|
100
100
|
"path": "framework/schemas",
|
|
101
101
|
"deps": []
|
|
102
102
|
},
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
"deps": []
|
|
117
117
|
},
|
|
118
118
|
"@uniweb/templates": {
|
|
119
|
-
"version": "0.16.
|
|
119
|
+
"version": "0.16.2",
|
|
120
120
|
"path": "framework/templates",
|
|
121
121
|
"deps": []
|
|
122
122
|
},
|
package/src/index.js
CHANGED
|
@@ -1726,8 +1726,10 @@ Auto-detects what you run it in:
|
|
|
1726
1726
|
• a schemas-only pkg just its data schemas, no foundation — e.g. @uniweb/schemas,
|
|
1727
1727
|
any @org/schemas package, or a bare schemas/*.yml folder
|
|
1728
1728
|
|
|
1729
|
-
A foundation's scope is part of its name — name: '@
|
|
1730
|
-
|
|
1729
|
+
A foundation's scope is part of its name — name: '@scope/<name>' in main.js. A scope
|
|
1730
|
+
is a namespace: your personal one (@<your handle>, no org needed) or an org's. A bare
|
|
1731
|
+
name takes --scope, else your personal scope (or a pick, if you belong to orgs), and
|
|
1732
|
+
register writes it into the name.
|
|
1731
1733
|
|
|
1732
1734
|
Schema scopes:
|
|
1733
1735
|
@/name your own schema, in the foundation's scope (@/x -> @org/x)
|
|
@@ -1735,9 +1737,10 @@ Schema scopes:
|
|
|
1735
1737
|
@org/name another org's published schema, referenced by name
|
|
1736
1738
|
|
|
1737
1739
|
${colors.bright}Options:${colors.reset}
|
|
1738
|
-
--scope @
|
|
1739
|
-
the name (refused when the name has another
|
|
1740
|
-
package: publish under @
|
|
1740
|
+
--scope @scope A foundation with a bare name: register under @scope — yours or an
|
|
1741
|
+
org's — and write it into the name (refused when the name has another
|
|
1742
|
+
scope). A schemas-only package: publish under @scope; default:
|
|
1743
|
+
package.json uniweb.scope
|
|
1741
1744
|
--dry-run Print the .uwx; submit nothing
|
|
1742
1745
|
-o, --output <f> Write the .uwx to a file; submit nothing
|
|
1743
1746
|
--non-interactive Fail with usage info instead of prompting
|
|
@@ -26,17 +26,19 @@
|
|
|
26
26
|
*
|
|
27
27
|
* GET /dev/orgs → {
|
|
28
28
|
* account_handle: string|null, // the caller's account handle
|
|
29
|
-
* personal_org_exists: bool, // does @<account_handle> exist as an org
|
|
30
29
|
* orgs: [{ handle, is_primary }] // memberships, primary first;
|
|
31
30
|
* } // handle-less units filtered server-side
|
|
32
31
|
*
|
|
33
32
|
* POST /dev/orgs { handle } → { handle, uuid, is_primary }
|
|
34
33
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
34
|
+
* ⭐ A SCOPE IS A NAMESPACE, NOT AN ORG (2026-09-23). `@<account handle>` is the
|
|
35
|
+
* account's own scope and needs no org; an org's scope is published into by its
|
|
36
|
+
* members. Handles live in ONE global namespace across accounts and orgs, so an org
|
|
37
|
+
* needs a handle of its own — a backend refuses an org named after an account, its
|
|
38
|
+
* owner's included. ⛔ Until 2026-09-23 the CLI created a "personal org" `@jane` for
|
|
39
|
+
* account `jane` to have a scope at all; `personal_org_exists` served that and is no
|
|
40
|
+
* longer read. Org creation requires NO second factor on any lane (the 2FA gate lives
|
|
41
|
+
* at escalation points, not here).
|
|
40
42
|
*
|
|
41
43
|
* Failure shapes (branch on STATUS; details are human display, not contract):
|
|
42
44
|
* 422 = handle grammar; 409 = taken / reserved / belongs to another account
|
|
@@ -95,7 +97,7 @@ export function validateHandle(handle) {
|
|
|
95
97
|
/**
|
|
96
98
|
* The picker read: memberships + the caller's account handle + whether the
|
|
97
99
|
* personal org already exists.
|
|
98
|
-
* @returns {Promise<{account_handle: string|null,
|
|
100
|
+
* @returns {Promise<{account_handle: string|null, orgs: Array<{handle: string, is_primary: boolean}>}>}
|
|
99
101
|
*/
|
|
100
102
|
export async function fetchOrgs({ apiBase, token }) {
|
|
101
103
|
const res = await fetch(`${apiBase.replace(/\/$/, '')}${ORGS_PATH}`, {
|
|
@@ -108,7 +110,6 @@ export async function fetchOrgs({ apiBase, token }) {
|
|
|
108
110
|
const data = await res.json().catch(() => null)
|
|
109
111
|
return {
|
|
110
112
|
account_handle: data?.account_handle ?? null,
|
|
111
|
-
personal_org_exists: data?.personal_org_exists === true,
|
|
112
113
|
orgs: Array.isArray(data?.orgs) ? data.orgs : []
|
|
113
114
|
}
|
|
114
115
|
}
|
|
@@ -119,8 +120,8 @@ export async function listOrgs(opts) {
|
|
|
119
120
|
}
|
|
120
121
|
|
|
121
122
|
/**
|
|
122
|
-
* Create an org
|
|
123
|
-
*
|
|
123
|
+
* Create an org. Its handle must be its own: a backend refuses an account's handle,
|
|
124
|
+
* your own included — your personal scope needs no org.
|
|
124
125
|
* @returns {Promise<{handle: string, uuid?: string, is_primary?: boolean}>}
|
|
125
126
|
*/
|
|
126
127
|
export async function createOrg({ apiBase, token, handle }) {
|
|
@@ -156,11 +157,23 @@ export async function createOrg({ apiBase, token, handle }) {
|
|
|
156
157
|
}
|
|
157
158
|
|
|
158
159
|
/**
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
160
|
+
* The scope to register under, from the login, when none was named — a bare handle,
|
|
161
|
+
* or null when none was chosen. Persists nothing; the caller records it (in the
|
|
162
|
+
* foundation's name, or a schemas-only package's `package.json`).
|
|
163
|
+
*
|
|
164
|
+
* ⭐ A SCOPE IS A NAMESPACE (2026-09-23): your account's own, `@<handle>`, needs no org.
|
|
165
|
+
*
|
|
166
|
+
* no org → your personal scope — said, never asked, in CI too
|
|
167
|
+
* orgs → pick: your personal scope first, then each org;
|
|
168
|
+
* non-interactive, your personal scope, said
|
|
169
|
+
* no account handle → (a service account) its one org, said; several are asked,
|
|
170
|
+
* or refused in CI; none is a pointer to `--scope`
|
|
171
|
+
*
|
|
172
|
+
* ⛔ Until 2026-09-23 a login with no org was offered a "personal org" `@jane`, created
|
|
173
|
+
* on the spot — a scope was taken to need an org — and refused in CI.
|
|
174
|
+
*
|
|
175
|
+
* An org named after the account — a personal org made before then — is the same
|
|
176
|
+
* `@jane`, and is listed once, as your personal scope.
|
|
164
177
|
*
|
|
165
178
|
* @param {Object} p
|
|
166
179
|
* @param {string} p.apiBase
|
|
@@ -177,161 +190,56 @@ export async function deriveScope({
|
|
|
177
190
|
args = []
|
|
178
191
|
}) {
|
|
179
192
|
const envelope = await fetchOrgs({ apiBase, token })
|
|
180
|
-
const
|
|
181
|
-
const personal =
|
|
182
|
-
|
|
183
|
-
(accountHandle ? bareHandle(accountHandle) : null)
|
|
184
|
-
const personalOrgExists = envelope.personal_org_exists
|
|
193
|
+
const handle = bareHandle(envelope.account_handle || accountHandle || '')
|
|
194
|
+
const personal = handle && !validateHandle(handle) ? handle : null
|
|
195
|
+
const orgs = envelope.orgs.filter((o) => o.handle !== personal)
|
|
185
196
|
const { isNonInteractive } = await import('./interactive.js')
|
|
186
197
|
const nonInteractive = isNonInteractive(args)
|
|
187
|
-
const
|
|
198
|
+
const bold = (h) => `\x1b[1m@${h}\x1b[0m`
|
|
188
199
|
|
|
189
|
-
if (orgs.length
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
)
|
|
196
|
-
return
|
|
200
|
+
if (personal && !orgs.length) {
|
|
201
|
+
console.error(`Registering under your personal scope ${bold(personal)}.`)
|
|
202
|
+
return personal
|
|
203
|
+
}
|
|
204
|
+
if (!personal && orgs.length <= 1) {
|
|
205
|
+
if (orgs.length === 1) {
|
|
206
|
+
console.error(`Registering under your org ${bold(orgs[0].handle)}.`)
|
|
207
|
+
return orgs[0].handle
|
|
197
208
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
type: 'confirm',
|
|
202
|
-
name: 'ok',
|
|
203
|
-
message: `Publish under ${label}?`,
|
|
204
|
-
initial: true
|
|
205
|
-
},
|
|
206
|
-
{
|
|
207
|
-
onCancel: () => {
|
|
208
|
-
console.error('\nCancelled.')
|
|
209
|
-
process.exit(0)
|
|
210
|
-
}
|
|
211
|
-
}
|
|
209
|
+
console.error(
|
|
210
|
+
"\x1b[31m✗\x1b[0m This account has no handle (service accounts don't get one), so it has no personal scope, and it belongs to no org.\n" +
|
|
211
|
+
' Pass --scope @org, or create one with `uniweb org create <handle>`.'
|
|
212
212
|
)
|
|
213
|
-
|
|
214
|
-
console.error(
|
|
215
|
-
'Pass --scope @org, or create another with `uniweb org create <handle>`.'
|
|
216
|
-
)
|
|
217
|
-
return null
|
|
218
|
-
}
|
|
219
|
-
return h
|
|
213
|
+
return null
|
|
220
214
|
}
|
|
221
215
|
|
|
222
|
-
if (
|
|
223
|
-
|
|
224
|
-
const ordered = [...orgs].sort(
|
|
225
|
-
(a, b) => (isPersonal(b.handle) ? 1 : 0) - (isPersonal(a.handle) ? 1 : 0)
|
|
226
|
-
)
|
|
227
|
-
if (nonInteractive) {
|
|
228
|
-
const pick =
|
|
229
|
-
ordered.find((u) => isPersonal(u.handle)) ||
|
|
230
|
-
orgs.find((u) => u.is_primary) ||
|
|
231
|
-
orgs[0]
|
|
216
|
+
if (nonInteractive) {
|
|
217
|
+
if (!personal) {
|
|
232
218
|
console.error(
|
|
233
|
-
|
|
219
|
+
'\x1b[31m✗\x1b[0m This account belongs to several orgs and has no personal scope. Pass --scope @org.'
|
|
234
220
|
)
|
|
235
|
-
return
|
|
221
|
+
return null
|
|
236
222
|
}
|
|
237
|
-
const prompts = (await import('prompts')).default
|
|
238
|
-
const { choice } = await prompts(
|
|
239
|
-
{
|
|
240
|
-
type: 'select',
|
|
241
|
-
name: 'choice',
|
|
242
|
-
message: 'Publish under which org?',
|
|
243
|
-
choices: ordered.map((u) => ({
|
|
244
|
-
title: `@${u.handle}${isPersonal(u.handle) ? ' — your personal org' : u.is_primary ? ' (primary)' : ''}`,
|
|
245
|
-
value: u.handle
|
|
246
|
-
})),
|
|
247
|
-
initial: 0
|
|
248
|
-
},
|
|
249
|
-
{
|
|
250
|
-
onCancel: () => {
|
|
251
|
-
console.error('\nCancelled.')
|
|
252
|
-
process.exit(0)
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
)
|
|
256
|
-
return choice || null
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// 0 orgs → the first-publish picker.
|
|
260
|
-
if (nonInteractive) {
|
|
261
223
|
console.error(
|
|
262
|
-
|
|
224
|
+
`Registering under your personal scope ${bold(personal)} (non-interactive). Pass --scope @org for an org.`
|
|
263
225
|
)
|
|
264
|
-
|
|
226
|
+
return personal
|
|
265
227
|
}
|
|
266
|
-
return offerCreateOrg({
|
|
267
|
-
apiBase,
|
|
268
|
-
token,
|
|
269
|
-
accountHandle: personal,
|
|
270
|
-
personalOrgExists
|
|
271
|
-
})
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Cold-start (0 orgs) — the first-publish org choice. Every account handle
|
|
276
|
-
* is a reserved, ready-to-go org handle (globally unique across accounts +
|
|
277
|
-
* orgs; only the owner can claim it), so the default is one keystroke:
|
|
278
|
-
*
|
|
279
|
-
* ? Publish under which org?
|
|
280
|
-
* ❯ @jane — your personal org (created on first publish)
|
|
281
|
-
* A new organization…
|
|
282
|
-
*
|
|
283
|
-
* The personal org is materialized LAZILY here — at first publish, never at
|
|
284
|
-
* signup. Two guards:
|
|
285
|
-
* - no account handle (Service/System accounts — real signups always mint
|
|
286
|
-
* one) → a crisp pointer instead of a prompt;
|
|
287
|
-
* - the personal org exists but the caller is no longer a member
|
|
288
|
-
* (created-then-left) → the lazy claim would 409; don't offer it.
|
|
289
|
-
* Returns the chosen bare handle, or null if cancelled/failed.
|
|
290
|
-
*/
|
|
291
|
-
export async function offerCreateOrg({
|
|
292
|
-
apiBase,
|
|
293
|
-
token,
|
|
294
|
-
accountHandle = null,
|
|
295
|
-
personalOrgExists = false
|
|
296
|
-
}) {
|
|
297
228
|
const prompts = (await import('prompts')).default
|
|
298
|
-
const personal =
|
|
299
|
-
accountHandle && !validateHandle(accountHandle)
|
|
300
|
-
? bareHandle(accountHandle)
|
|
301
|
-
: null
|
|
302
|
-
|
|
303
|
-
if (!personal) {
|
|
304
|
-
console.error(
|
|
305
|
-
"\x1b[31m✗\x1b[0m This account has no handle (service accounts don't get one), so there is no ready-to-go org.\n" +
|
|
306
|
-
' Log in with a personal account or set a handle in the app — or pass --scope @org / `uniweb org create <handle>`.'
|
|
307
|
-
)
|
|
308
|
-
return null
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
const canClaimPersonal = !personalOrgExists
|
|
312
|
-
const choices = [
|
|
313
|
-
...(canClaimPersonal
|
|
314
|
-
? [
|
|
315
|
-
{
|
|
316
|
-
title: `@${personal} — your personal org (created on first publish)`,
|
|
317
|
-
value: personal
|
|
318
|
-
}
|
|
319
|
-
]
|
|
320
|
-
: []),
|
|
321
|
-
{ title: 'A new organization…', value: ':new' }
|
|
322
|
-
]
|
|
323
|
-
if (!canClaimPersonal) {
|
|
324
|
-
console.error(
|
|
325
|
-
`\x1b[2m@${personal} exists but you're not a member of it — ask its admin, or create another org.\x1b[0m`
|
|
326
|
-
)
|
|
327
|
-
}
|
|
328
|
-
|
|
329
229
|
const { choice } = await prompts(
|
|
330
230
|
{
|
|
331
231
|
type: 'select',
|
|
332
232
|
name: 'choice',
|
|
333
|
-
message: '
|
|
334
|
-
choices
|
|
233
|
+
message: 'Register under which scope?',
|
|
234
|
+
choices: [
|
|
235
|
+
...(personal
|
|
236
|
+
? [{ title: `@${personal} — your personal scope`, value: personal }]
|
|
237
|
+
: []),
|
|
238
|
+
...orgs.map((o) => ({
|
|
239
|
+
title: `@${o.handle}${o.is_primary ? ' (primary org)' : ' (org)'}`,
|
|
240
|
+
value: o.handle
|
|
241
|
+
}))
|
|
242
|
+
],
|
|
335
243
|
initial: 0
|
|
336
244
|
},
|
|
337
245
|
{
|
|
@@ -341,36 +249,5 @@ export async function offerCreateOrg({
|
|
|
341
249
|
}
|
|
342
250
|
}
|
|
343
251
|
)
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
let handle = choice
|
|
347
|
-
if (choice === ':new') {
|
|
348
|
-
const answer = await prompts(
|
|
349
|
-
{
|
|
350
|
-
type: 'text',
|
|
351
|
-
name: 'handle',
|
|
352
|
-
message: 'Org handle (e.g. acme):',
|
|
353
|
-
validate: (v) => validateHandle(v) || true
|
|
354
|
-
},
|
|
355
|
-
{
|
|
356
|
-
onCancel: () => {
|
|
357
|
-
console.error('\nCancelled.')
|
|
358
|
-
process.exit(0)
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
)
|
|
362
|
-
if (!answer.handle) return null
|
|
363
|
-
handle = answer.handle
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
try {
|
|
367
|
-
const org = await createOrg({ apiBase, token, handle })
|
|
368
|
-
console.error(
|
|
369
|
-
`\x1b[32m✓\x1b[0m Created \x1b[1m@${org.handle}\x1b[0m — you're a member${org.is_primary ? ' (primary)' : ''}.`
|
|
370
|
-
)
|
|
371
|
-
return org.handle
|
|
372
|
-
} catch (err) {
|
|
373
|
-
console.error(`\x1b[31m✗\x1b[0m ${err.message}`)
|
|
374
|
-
return null
|
|
375
|
-
}
|
|
252
|
+
return choice || null
|
|
376
253
|
}
|