uniweb 0.82.2 → 0.84.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 +35 -14
- package/src/backend/client.js +84 -13
- package/src/backend/foundation-bring-along.js +81 -11
- package/src/backend/service-request.js +204 -173
- package/src/backend/site-sync.js +72 -3
- package/src/backend/workspace.js +52 -17
- package/src/commands/publish.js +39 -159
- package/src/commands/push.js +28 -4
- package/src/commands/register.js +64 -19
- package/src/commands/status.js +32 -3
- package/src/framework-index.json +6 -6
- package/src/index.js +28 -10
- package/src/utils/config.js +61 -6
- package/src/utils/registry-auth.js +18 -5
- package/src/utils/registry-orgs.js +43 -6
package/src/index.js
CHANGED
|
@@ -917,10 +917,12 @@ async function main() {
|
|
|
917
917
|
// Handle login command — the backend (username/password · paste a token ·
|
|
918
918
|
// --token <bearer>). ⭐ `--backend <url>` names it; without the flag it is the DEFAULT
|
|
919
919
|
// backend — UNIWEB_REGISTER_URL, else ~/.uniweb/config.json, else https://uniweb.app —
|
|
920
|
-
// and never the project's backend
|
|
921
|
-
//
|
|
922
|
-
//
|
|
923
|
-
//
|
|
920
|
+
// and never the project's backend *[Diego, 2026-09-21: "the default backend for login, if
|
|
921
|
+
// not specified, is uniweb.app"]*. ⭐ Except a WORKSPACE SWITCH — `--org` / `--personal`
|
|
922
|
+
// and no way of signing in — which acts on the backend you are logged in to *[Diego,
|
|
923
|
+
// 2026-10-06]* (`resolveLoginOrigin`). The backend logged in to becomes CURRENT, and
|
|
924
|
+
// every backend command goes there (only UNIWEB_REGISTER_URL outranks it) — which is why
|
|
925
|
+
// this is the one place a backend is chosen.
|
|
924
926
|
//
|
|
925
927
|
// ⛔ Until 2026-09-21 a bare login went to the backend of the project in the cwd, and
|
|
926
928
|
// asked when the machine knew several backends. Both are gone: the default is the
|
|
@@ -932,7 +934,7 @@ async function main() {
|
|
|
932
934
|
const { resolveLoginOrigin } = await import('./utils/config.js')
|
|
933
935
|
let apiBase
|
|
934
936
|
try {
|
|
935
|
-
apiBase = resolveLoginOrigin(readFlagValue(loginArgs, '--backend'))
|
|
937
|
+
apiBase = resolveLoginOrigin(readFlagValue(loginArgs, '--backend'), loginArgs)
|
|
936
938
|
} catch (err) {
|
|
937
939
|
console.error(
|
|
938
940
|
`\x1b[31m✗\x1b[0m ${err.message} — e.g. uniweb login --backend http://localhost:8080`
|
|
@@ -1729,8 +1731,8 @@ Auto-detects what you run it in:
|
|
|
1729
1731
|
|
|
1730
1732
|
A foundation's scope is part of its name — name: '@scope/<name>' in main.js. A scope
|
|
1731
1733
|
is a namespace: your personal one (@<your handle>, no org needed) or an org's. A bare
|
|
1732
|
-
name takes --scope, else
|
|
1733
|
-
register writes it into the name.
|
|
1734
|
+
name takes --scope, else the workspace you work in (an org's scope, or your personal
|
|
1735
|
+
one), and register writes it into the name.
|
|
1734
1736
|
|
|
1735
1737
|
Schema scopes:
|
|
1736
1738
|
@/name your own schema, in the foundation's scope (@/x -> @org/x)
|
|
@@ -1742,6 +1744,9 @@ ${colors.bright}Options:${colors.reset}
|
|
|
1742
1744
|
org's — and write it into the name (refused when the name has another
|
|
1743
1745
|
scope). A schemas-only package: publish under @scope; default:
|
|
1744
1746
|
package.json uniweb.scope
|
|
1747
|
+
--org @org Work in @org for this command: a bare name registers under it
|
|
1748
|
+
--personal Work in your personal workspace: a bare name registers under your
|
|
1749
|
+
personal scope
|
|
1745
1750
|
--dry-run Print the .uwx; submit nothing
|
|
1746
1751
|
-o, --output <f> Write the .uwx to a file; submit nothing
|
|
1747
1752
|
--non-interactive Fail with usage info instead of prompting
|
|
@@ -1798,11 +1803,12 @@ ${colors.bright}The workspace you work in.${colors.reset} A login works in ONE w
|
|
|
1798
1803
|
personal one, or an organization's — and every push, pull and publish works in it:
|
|
1799
1804
|
a site it creates is created there, and a site kept in another workspace is refused.
|
|
1800
1805
|
With no organization it is your personal workspace; with organizations you are asked,
|
|
1801
|
-
or name it. Already logged in, \`uniweb login --org @other\`
|
|
1802
|
-
in again.
|
|
1806
|
+
or name it. Already logged in, \`uniweb login --org @other\` (or \`--personal\`) switches
|
|
1807
|
+
the workspace on the backend you are logged in to, without logging in again.
|
|
1803
1808
|
|
|
1804
1809
|
${colors.bright}Options:${colors.reset}
|
|
1805
|
-
--backend <url> The backend to log in to
|
|
1810
|
+
--backend <url> The backend to log in to (without it: the default backend — or, for
|
|
1811
|
+
--org / --personal alone, the one you are logged in to)
|
|
1806
1812
|
--org @org Work in @org (an organization you belong to)
|
|
1807
1813
|
--personal Work in your personal workspace
|
|
1808
1814
|
--token <bearer> Seed + verify a session from a bearer token (non-interactive)
|
|
@@ -1813,6 +1819,8 @@ ${colors.bright}Options:${colors.reset}
|
|
|
1813
1819
|
In non-interactive mode (no TTY — an agent, a script), pass \`--token <bearer>\` with
|
|
1814
1820
|
\`--org @org\` or \`--personal\`, or set \`UNIWEB_USERNAME\` + \`UNIWEB_PASSWORD\`, or set
|
|
1815
1821
|
\`UNIWEB_TOKEN\` (per process, not stored) with \`UNIWEB_WORKSPACE=@org\` or \`personal\`.
|
|
1822
|
+
A login with organizations that names no workspace signs you in but exits 2;
|
|
1823
|
+
\`uniweb login --org @org\` (or \`--personal\`) finishes it without signing in again.
|
|
1816
1824
|
\`uniweb logout\` logs you out.
|
|
1817
1825
|
`,
|
|
1818
1826
|
refresh: `
|
|
@@ -2089,6 +2097,16 @@ ${colors.bright}Global Options:${colors.reset}
|
|
|
2089
2097
|
a script can aim and authenticate one process with UNIWEB_REGISTER_URL and
|
|
2090
2098
|
UNIWEB_TOKEN instead.
|
|
2091
2099
|
|
|
2100
|
+
${colors.bright}Push Options:${colors.reset}
|
|
2101
|
+
--dry-run Report what would be pushed; send nothing (-o <file> writes the .uwx)
|
|
2102
|
+
--no-release Send content against the already-released code; release nothing
|
|
2103
|
+
--bump Release above a newer registered foundation version
|
|
2104
|
+
--force Overwrite upstream changes (drop the staleness gate)
|
|
2105
|
+
--all Send every record (bypass the changed-only cache)
|
|
2106
|
+
--org @org Work in @org for this push, not your login's workspace
|
|
2107
|
+
--personal Work in your personal workspace for this push
|
|
2108
|
+
--no-validate Skip the content-conformance check (it stops a push)
|
|
2109
|
+
|
|
2092
2110
|
${colors.bright}Publish Options:${colors.reset}
|
|
2093
2111
|
--dry-run Resolve everything; release/sync/POST nothing
|
|
2094
2112
|
--yes Skip confirmations (CI); never block on a prompt
|
package/src/utils/config.js
CHANGED
|
@@ -107,32 +107,87 @@ export function getDefaultBackendOrigin() {
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
/**
|
|
110
|
-
* The
|
|
110
|
+
* The `uniweb login` that SWITCHES the workspace on `origin` — the caller appends `--org @x`
|
|
111
|
+
* or `--personal` — with `--backend` unless a switch without it reaches `origin`
|
|
112
|
+
* (`resolveLoginOrigin`): the backend you are logged in to, or, logged in nowhere, the
|
|
113
|
+
* default backend.
|
|
114
|
+
*
|
|
115
|
+
* ⚠️ *Measured 2026-10-06 against a local backend, before switches acted on the session:*
|
|
116
|
+
* `uniweb login --org @acme` went to https://uniweb.app and began a new login there — the
|
|
117
|
+
* reason a hint about any other backend names it.
|
|
118
|
+
*
|
|
119
|
+
* @param {string} origin - the backend the hint is about
|
|
120
|
+
* @param {string} [prefix='uniweb'] - how the user runs the CLI (`getCliPrefix`)
|
|
121
|
+
* @returns {string} e.g. `uniweb login`, or `uniweb login --backend http://localhost:8080`
|
|
122
|
+
*/
|
|
123
|
+
export function loginCommand(origin, prefix = 'uniweb') {
|
|
124
|
+
const o = originOrNull(origin)
|
|
125
|
+
const reached = originOrNull(loggedInOrigin()) || getDefaultBackendOrigin()
|
|
126
|
+
return o && o !== reached ? `${prefix} login --backend ${o}` : `${prefix} login`
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The flags with which `uniweb login` SIGNS IN — a method, or a credential. */
|
|
130
|
+
const SIGN_IN_FLAGS = ['--token', '--browser', '--password', '--token-paste']
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Whether a `uniweb login` is a WORKSPACE SWITCH: `--org` or `--personal`, and nothing that
|
|
134
|
+
* signs in (`SIGN_IN_FLAGS`).
|
|
135
|
+
*
|
|
136
|
+
* @param {string[]} [args]
|
|
137
|
+
* @returns {boolean}
|
|
138
|
+
*/
|
|
139
|
+
export function isWorkspaceSwitch(args = []) {
|
|
140
|
+
const names = args.map((a) => String(a).split('=')[0])
|
|
141
|
+
return (
|
|
142
|
+
(names.includes('--org') || names.includes('--personal')) &&
|
|
143
|
+
!SIGN_IN_FLAGS.some((f) => names.includes(f))
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The backend `uniweb login` logs in to: `--backend`; else, for a workspace switch, the
|
|
149
|
+
* backend you are logged in to; else the default backend.
|
|
150
|
+
*
|
|
151
|
+
* ⭐ A SWITCH ACTS ON YOUR SESSION *[Diego, 2026-10-06: "`uniweb login --org @x` or
|
|
152
|
+
* `--personal`, with no `--backend`, should switch to the workspace on the backend you're
|
|
153
|
+
* already logged in to so we can fullfil the promise 'switches without logging in again'"]*.
|
|
154
|
+
* Logged in nowhere, it is a first login, and goes where any first login goes. A login that
|
|
155
|
+
* signs in — a method or a credential named (`isWorkspaceSwitch`) — still goes to the
|
|
156
|
+
* default backend *[Diego, 2026-09-21: "the default backend for login, if not specified, is
|
|
157
|
+
* uniweb.app"]*. ⛔ Until 2026-10-06 a switch went to the default backend too, so on any
|
|
158
|
+
* other backend it began a new login there, logging you out of the one you were on.
|
|
111
159
|
*
|
|
112
160
|
* ⛔ A mistyped `--backend` is an error, never a fallback — it would log you in, and so
|
|
113
161
|
* point every command, somewhere you did not name.
|
|
114
162
|
*
|
|
115
163
|
* @param {string|null|undefined} flag - `readFlagValue`'s answer: undefined when the
|
|
116
164
|
* flag is absent, null when it was given with no value
|
|
165
|
+
* @param {string[]} [args] - the login's argv, to tell a switch from a sign-in
|
|
117
166
|
* @returns {string}
|
|
118
167
|
* @throws {Error} when --backend was given and is not a URL
|
|
119
168
|
*/
|
|
120
|
-
export function resolveLoginOrigin(flag) {
|
|
121
|
-
if (flag === undefined)
|
|
169
|
+
export function resolveLoginOrigin(flag, args = []) {
|
|
170
|
+
if (flag === undefined) {
|
|
171
|
+
const session = isWorkspaceSwitch(args) ? originOrNull(loggedInOrigin()) : null
|
|
172
|
+
return session || getDefaultBackendOrigin()
|
|
173
|
+
}
|
|
122
174
|
const origin = originOrNull(flag)
|
|
123
175
|
if (!origin) throw new Error(flag ? `Not a URL: ${flag}` : '--backend needs a URL')
|
|
124
176
|
return origin
|
|
125
177
|
}
|
|
126
178
|
|
|
127
179
|
/**
|
|
128
|
-
* **The backend a command talks to**
|
|
129
|
-
*
|
|
180
|
+
* **The backend a command talks to** — the base of every `/dev/*` route (`register`
|
|
181
|
+
* POSTs to {origin}/dev/registry/register, and so on).
|
|
130
182
|
*
|
|
131
183
|
* `UNIWEB_REGISTER_URL` > **the backend the user is logged in to** > the default
|
|
132
184
|
* (`getDefaultBackendOrigin`). ⭐ Nothing talks to a backend the user is not logged in
|
|
133
185
|
* to *[Diego, 2026-09-21]*: when this falls through to the default, the command's first
|
|
134
186
|
* request asks for that login — so the default is where the login goes, never a
|
|
135
|
-
* backend reached without one. `resolveBackendOrigin`
|
|
187
|
+
* backend reached without one. `resolveBackendOrigin` (backend/client.js) returns this
|
|
188
|
+
* unchanged: no command takes a `--backend`. ⛔ *Until 2026-10-05 this said "when no
|
|
189
|
+
* `--backend` is given" and that `resolveBackendOrigin` put `--backend` on top — read as
|
|
190
|
+
* a per-command flag outranking the login. The verbs lost that flag on 2026-09-21.*
|
|
136
191
|
*
|
|
137
192
|
* @returns {string}
|
|
138
193
|
*/
|
|
@@ -98,7 +98,8 @@ export function getRegistryAuthPath() {
|
|
|
98
98
|
* always logs you out of the one backend you may be logged in"]*. A login REPLACES the
|
|
99
99
|
* file — once it succeeds, so a cancelled or failed login leaves you where you were —
|
|
100
100
|
* and a logout deletes it. So "the backend you are logged in to" is exactly one thing
|
|
101
|
-
* or nothing, and logout needs no selector.
|
|
101
|
+
* or nothing, and logout needs no selector. ⚠️ One exception, and it says so: a login that
|
|
102
|
+
* signs in but cannot choose a workspace keeps the session and exits 2 (`finishLogin`).
|
|
102
103
|
*
|
|
103
104
|
* ⚖️ The v2 map is kept, holding one entry: the file then has one reader
|
|
104
105
|
* (utils/session-file.js), and it still understands what came before — a v1 flat
|
|
@@ -720,7 +721,12 @@ export async function runRegistryLogin({ apiBase, args = [] } = {}) {
|
|
|
720
721
|
if (wants || !session.workspace) {
|
|
721
722
|
const settled = await settleWorkspace(session, args)
|
|
722
723
|
if (settled.refused) {
|
|
723
|
-
|
|
724
|
+
// Say what the session works in now: unchanged, or — one from before workspaces —
|
|
725
|
+
// nothing yet (`finishLogin`).
|
|
726
|
+
const where = session.workspace
|
|
727
|
+
? `${await workspaceTail(session)} — unchanged`
|
|
728
|
+
: ' — with no workspace chosen yet'
|
|
729
|
+
console.error(`\x1b[32m✓\x1b[0m Logged in to ${key}${who ? ` as \x1b[1m${who}\x1b[0m` : ''}${where}.`)
|
|
724
730
|
console.error(`\x1b[31m✗\x1b[0m ${settled.refused}`)
|
|
725
731
|
process.exit(2)
|
|
726
732
|
}
|
|
@@ -839,15 +845,22 @@ export async function runRegistryLogin({ apiBase, args = [] } = {}) {
|
|
|
839
845
|
|
|
840
846
|
/**
|
|
841
847
|
* A login succeeded and its session is stored: choose its workspace, and say where the
|
|
842
|
-
* login works.
|
|
843
|
-
*
|
|
848
|
+
* login works.
|
|
849
|
+
*
|
|
850
|
+
* ⭐ WITHOUT A WORKSPACE THE SESSION STAYS, AND THE LOGIN SAYS SO *[Diego, 2026-10-06]*. No
|
|
851
|
+
* terminal, organizations, neither `--org` nor `--personal` — or a pick cancelled at a
|
|
852
|
+
* terminal: you are logged in, with no workspace, and the login exits 2 naming the one
|
|
853
|
+
* command that finishes it without signing in again. The credential was good, and a
|
|
854
|
+
* second sign-in can be a second browser round trip. ⚠️ So this is the one login that
|
|
855
|
+
* exits non-zero having replaced your session — the reason it says so in as many words.
|
|
844
856
|
*/
|
|
845
857
|
async function finishLogin(record, apiBase, args) {
|
|
846
858
|
const settled = await settleWorkspace(record, args)
|
|
847
859
|
const who = settled.record.username ? ` as \x1b[1m${settled.record.username}\x1b[0m` : ''
|
|
848
860
|
if (settled.refused) {
|
|
849
|
-
console.error(`\x1b[32m✓\x1b[0m Logged in${
|
|
861
|
+
console.error(`\x1b[32m✓\x1b[0m Logged in to ${normOrigin(apiBase)}${who} — with no workspace chosen yet.`)
|
|
850
862
|
console.error(`\x1b[31m✗\x1b[0m ${settled.refused}`)
|
|
863
|
+
console.error('\x1b[2m Until one is chosen, the commands that work on a site refuse.\x1b[0m')
|
|
851
864
|
process.exit(2)
|
|
852
865
|
}
|
|
853
866
|
console.error(`\x1b[32m✓\x1b[0m Logged in${who} (${apiBase})${await workspaceTail(settled.record)}.`)
|
|
@@ -81,6 +81,25 @@ export function publishScope(value) {
|
|
|
81
81
|
return handle ? `@${handle}` : null
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Whether the orgs read (`GET /dev/orgs`) says this account may release into `scope`:
|
|
86
|
+
* the account's own handle, or an org it belongs to. True or false; null when there is
|
|
87
|
+
* nothing to say (no scope, no read).
|
|
88
|
+
*
|
|
89
|
+
* ⛔ For EXPLAINING a refusal only, never to gate a release: the registry decides who
|
|
90
|
+
* may publish into a scope, and this knows only the memberships that read lists.
|
|
91
|
+
*
|
|
92
|
+
* @param {string|null} scope - `@acme`, `acme` or `@acme/…`
|
|
93
|
+
* @param {{ account_handle?: string|null, orgs?: Array<{ handle?: string }> }|null} envelope
|
|
94
|
+
* @returns {boolean|null}
|
|
95
|
+
*/
|
|
96
|
+
export function belongsToScope(scope, envelope) {
|
|
97
|
+
const h = bareHandle(scope)
|
|
98
|
+
if (!h || !envelope || typeof envelope !== 'object') return null
|
|
99
|
+
if (bareHandle(envelope.account_handle || '') === h) return true
|
|
100
|
+
return (Array.isArray(envelope.orgs) ? envelope.orgs : []).some((o) => bareHandle(o?.handle) === h)
|
|
101
|
+
}
|
|
102
|
+
|
|
84
103
|
/**
|
|
85
104
|
* Validate a handle's GRAMMAR client-side (reserved names are the server's
|
|
86
105
|
* call — a 409 carries the verdict). Returns an error string, or null.
|
|
@@ -157,15 +176,22 @@ export async function createOrg({ apiBase, token, handle }) {
|
|
|
157
176
|
}
|
|
158
177
|
|
|
159
178
|
/**
|
|
160
|
-
* The scope to register under, from the login, when none was named
|
|
161
|
-
*
|
|
162
|
-
* foundation's name, or a
|
|
179
|
+
* The scope to register under, from the login's orgs, when none was named and the
|
|
180
|
+
* workspace the command works in names none either — a bare handle, or null when none
|
|
181
|
+
* was chosen. Persists nothing; the caller records it (in the foundation's name, or a
|
|
182
|
+
* schemas-only package's `package.json`).
|
|
183
|
+
*
|
|
184
|
+
* ⚠️ The FALLBACK since 2026-10-06: a bare name first takes the workspace the command
|
|
185
|
+
* works in (`register.js::deriveScopeFromLogin`, `workspace.js::scopeOfWorkspace`). This
|
|
186
|
+
* answers only when no workspace is chosen — you belong to orgs and named none — or the
|
|
187
|
+
* one chosen is a unit without a handle.
|
|
163
188
|
*
|
|
164
189
|
* ⭐ A SCOPE IS A NAMESPACE (2026-09-23): your account's own, `@<handle>`, needs no org.
|
|
165
190
|
*
|
|
166
191
|
* no org → your personal scope — said, never asked, in CI too
|
|
167
192
|
* orgs → pick: your personal scope first, then each org;
|
|
168
|
-
* non-interactive,
|
|
193
|
+
* non-interactive, REFUSED — choose a workspace, or --scope
|
|
194
|
+
* (until 2026-10-06: your personal scope, said)
|
|
169
195
|
* no account handle → (a service account) its one org, said; several are asked,
|
|
170
196
|
* or refused in CI; none is a pointer to `--scope`
|
|
171
197
|
*
|
|
@@ -220,10 +246,20 @@ export async function deriveScope({
|
|
|
220
246
|
)
|
|
221
247
|
return null
|
|
222
248
|
}
|
|
249
|
+
// ⛔ NO DEFAULT FOR A MEMBER OF ORGANIZATIONS *[Diego, 2026-10-06: "someone in an org,
|
|
250
|
+
// with no workspace chosen must choose one. we should not default to personal"]*. Only
|
|
251
|
+
// reached when the workspace the command works in names no scope — none is chosen — so
|
|
252
|
+
// the choice is theirs. ⚠️ Until then this answered your personal scope, said: a team's
|
|
253
|
+
// foundation registered as one member's.
|
|
254
|
+
const { getCliPrefix } = await import('./interactive.js')
|
|
255
|
+
const { loginCommand } = await import('./config.js')
|
|
256
|
+
const first = orgs[0].handle
|
|
223
257
|
console.error(
|
|
224
|
-
|
|
258
|
+
`\x1b[31m✗\x1b[0m You belong to organizations (${orgs.map((o) => `@${o.handle}`).join(', ')}), so no scope is assumed.\n` +
|
|
259
|
+
` Choose the workspace you work in — ${loginCommand(apiBase, getCliPrefix())} --org @${first} (or --personal) — and the name takes its scope;\n` +
|
|
260
|
+
` or name the scope here: --scope @${first}, or --scope @${personal} for your own.`
|
|
225
261
|
)
|
|
226
|
-
return
|
|
262
|
+
return null
|
|
227
263
|
}
|
|
228
264
|
const prompts = (await import('prompts')).default
|
|
229
265
|
const { choice } = await prompts(
|
|
@@ -249,5 +285,6 @@ export async function deriveScope({
|
|
|
249
285
|
}
|
|
250
286
|
}
|
|
251
287
|
)
|
|
288
|
+
if (!choice) console.error('\x1b[31m✗\x1b[0m No scope chosen.')
|
|
252
289
|
return choice || null
|
|
253
290
|
}
|