uniweb 0.62.0 → 0.63.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 +6 -6
- package/partials/agents.md +4 -3
- package/src/backend/client.js +231 -20
- package/src/backend/foundation-bring-along.js +12 -13
- package/src/backend/site-sync.js +235 -126
- package/src/commands/clone.js +35 -5
- package/src/commands/publish.js +33 -20
- package/src/commands/pull.js +55 -10
- package/src/commands/push.js +23 -17
- package/src/commands/register.js +136 -26
- package/src/commands/status.js +26 -5
- package/src/framework-index.json +6 -6
- package/src/index.js +14 -8
- package/src/utils/args.js +6 -14
- package/src/utils/flag-guard.js +17 -9
- package/src/utils/registry-orgs.js +3 -2
- package/src/utils/site-data-upload.js +13 -2
package/src/commands/register.js
CHANGED
|
@@ -21,8 +21,13 @@
|
|
|
21
21
|
* uniweb register Build the .uwx, submit it, then deliver
|
|
22
22
|
* the foundation's dist/ code (plan +
|
|
23
23
|
* upload — see utils/code-upload.js)
|
|
24
|
-
* uniweb register --scope @org
|
|
25
|
-
*
|
|
24
|
+
* uniweb register --scope @org A foundation whose name is bare: register it under
|
|
25
|
+
* @org (resolves @/x -> @org/x) and write the scope
|
|
26
|
+
* into its name in main.js. Refused when the name
|
|
27
|
+
* already carries another scope — a foundation's
|
|
28
|
+
* scope is the one in its name.
|
|
29
|
+
* A schemas-only package: publish under @org.
|
|
30
|
+
* Default: its package.json "uniweb.scope".
|
|
26
31
|
* uniweb register --schema-only Skip the code delivery (schemas land, no dist upload)
|
|
27
32
|
* uniweb register --dry-run Print the .uwx + the code file plan; submit nothing
|
|
28
33
|
* uniweb register -o foundation.uwx Write the .uwx to a file; submit nothing
|
|
@@ -71,7 +76,8 @@ import {
|
|
|
71
76
|
isSchemasPackage,
|
|
72
77
|
collectStandaloneSchemas,
|
|
73
78
|
readFoundationName,
|
|
74
|
-
checkFoundationName
|
|
79
|
+
checkFoundationName,
|
|
80
|
+
splitFoundationName
|
|
75
81
|
} from '@uniweb/build'
|
|
76
82
|
import {
|
|
77
83
|
suggestFoundationName,
|
|
@@ -143,8 +149,10 @@ function cliVersion() {
|
|
|
143
149
|
}
|
|
144
150
|
}
|
|
145
151
|
|
|
146
|
-
//
|
|
147
|
-
// (`{ "uniweb": { "scope": "@acme" } }`) — the default when `--scope` is absent.
|
|
152
|
+
// A SCHEMAS-ONLY package's recorded publish org, from its package.json `uniweb.scope`
|
|
153
|
+
// (`{ "uniweb": { "scope": "@acme" } }`) — the default when `--scope` is absent. It
|
|
154
|
+
// has no main.js whose name could carry it. ⛔ A foundation's scope is part of its name
|
|
155
|
+
// (2026-09-22); its `uniweb.scope` is refused by `readFoundationName`.
|
|
148
156
|
function readPkgScope(foundationDir) {
|
|
149
157
|
try {
|
|
150
158
|
const pkg = JSON.parse(
|
|
@@ -156,9 +164,10 @@ function readPkgScope(foundationDir) {
|
|
|
156
164
|
}
|
|
157
165
|
}
|
|
158
166
|
|
|
159
|
-
// Record the chosen publish scope in
|
|
160
|
-
// with the
|
|
161
|
-
// the file's existing JSON style.
|
|
167
|
+
// Record the chosen publish scope in a schemas-only package's package.json so it
|
|
168
|
+
// travels with the package (read back by readPkgScope on the next register). Preserves
|
|
169
|
+
// the file's existing JSON style. A foundation's goes into its name instead
|
|
170
|
+
// (`settleFoundationScope`).
|
|
162
171
|
async function writePkgScope(foundationDir, scope) {
|
|
163
172
|
const path = join(foundationDir, 'package.json')
|
|
164
173
|
const pkg = JSON.parse(readFileSync(path, 'utf8'))
|
|
@@ -293,6 +302,95 @@ export async function settleFoundationName(targetDir, { args, isPreview }) {
|
|
|
293
302
|
return true
|
|
294
303
|
}
|
|
295
304
|
|
|
305
|
+
/**
|
|
306
|
+
* Settle the scope a foundation registers under — the one in its name.
|
|
307
|
+
*
|
|
308
|
+
* ⭐ THE SCOPE IS PART OF THE NAME (2026-09-22): `name: '@acme/marketing'` in main.js.
|
|
309
|
+
* A scoped name decides it, and a `--scope` naming another org is refused — which org
|
|
310
|
+
* was meant is not ours to guess, and today's alternative was a split identity (the
|
|
311
|
+
* foundation under one org, its data schemas under the other). A bare name has not
|
|
312
|
+
* registered yet: it takes `--scope`, else one derived from your orgs, and the chosen
|
|
313
|
+
* scope is WRITTEN INTO THE NAME, so it is chosen once and travels with the code.
|
|
314
|
+
* ⛔ Until then it was written to `package.json::uniweb.scope`, which is refused now
|
|
315
|
+
* (`readFoundationName`, naming the line to write).
|
|
316
|
+
*
|
|
317
|
+
* A preview (`--dry-run`, `-o`) writes nothing: a bare name previews under `--scope`,
|
|
318
|
+
* or unscoped.
|
|
319
|
+
*
|
|
320
|
+
* @returns {Promise<{ scope: string|null, source: string|null } | { cancelled: true } | null>}
|
|
321
|
+
* null when refused (the reason is printed); `cancelled` when no org was chosen
|
|
322
|
+
*/
|
|
323
|
+
export async function settleFoundationScope(targetDir, { args, isPreview, flagScope, client }) {
|
|
324
|
+
let read
|
|
325
|
+
try {
|
|
326
|
+
read = await readFoundationName(targetDir)
|
|
327
|
+
} catch (err) {
|
|
328
|
+
error(err.message)
|
|
329
|
+
return null
|
|
330
|
+
}
|
|
331
|
+
const where = relative(process.cwd(), read.mainFile) || read.mainFile
|
|
332
|
+
const { scope: nameScope, bare } = splitFoundationName(read.name)
|
|
333
|
+
|
|
334
|
+
if (nameScope) {
|
|
335
|
+
if (flagScope && flagScope !== nameScope) {
|
|
336
|
+
error(
|
|
337
|
+
`${where} names this foundation ${read.name}, so it registers under ${nameScope} — ` +
|
|
338
|
+
`--scope ${flagScope} names another org.`
|
|
339
|
+
)
|
|
340
|
+
log(
|
|
341
|
+
` ${colors.dim}To register it under ${flagScope}, change its name in ${where}: ` +
|
|
342
|
+
`name: '${flagScope}/${bare}'${colors.reset}`
|
|
343
|
+
)
|
|
344
|
+
return null
|
|
345
|
+
}
|
|
346
|
+
return { scope: nameScope, source: 'main.js' }
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (isPreview) return { scope: flagScope, source: flagScope ? '--scope' : null }
|
|
350
|
+
|
|
351
|
+
let scope = flagScope
|
|
352
|
+
let source = flagScope ? '--scope' : null
|
|
353
|
+
if (!scope) {
|
|
354
|
+
scope = await deriveScopeFromLogin(client, args)
|
|
355
|
+
if (!scope) return { cancelled: true }
|
|
356
|
+
source = 'login'
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const scoped = `${scope}/${bare}`
|
|
360
|
+
const written = writeFoundationName(read.mainFile, scoped, {
|
|
361
|
+
replace: read.source === 'main.js' ? read.name : null
|
|
362
|
+
})
|
|
363
|
+
if (!written.ok) {
|
|
364
|
+
error(`Could not write the scope into the foundation's name: ${written.reason}.`)
|
|
365
|
+
log(` Name it in the default export in ${colors.bright}${where}${colors.reset}: name: '${scoped}',`)
|
|
366
|
+
return null
|
|
367
|
+
}
|
|
368
|
+
// Read it back through the build's own rule — the text edit is only a means.
|
|
369
|
+
const again = await readFoundationName(targetDir).catch(() => null)
|
|
370
|
+
if (again?.name !== scoped) {
|
|
371
|
+
error(`${where} now names the foundation '${scoped}', but it still reads as "${again?.name}".`)
|
|
372
|
+
return null
|
|
373
|
+
}
|
|
374
|
+
success(`This foundation registers as ${colors.bright}${scoped}${colors.reset} — kept in ${where}.`)
|
|
375
|
+
return { scope, source }
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* A publish scope derived from the login's org memberships (list → 1 use / 0 create /
|
|
380
|
+
* N pick), as `@handle` — or null when none was chosen.
|
|
381
|
+
*/
|
|
382
|
+
async function deriveScopeFromLogin(client, args) {
|
|
383
|
+
const token = await client.token()
|
|
384
|
+
const sess = await readRegistryAuth(client.origin)
|
|
385
|
+
const derived = await deriveScope({
|
|
386
|
+
apiBase: client.origin,
|
|
387
|
+
token,
|
|
388
|
+
accountHandle: sess?.handle || null,
|
|
389
|
+
args
|
|
390
|
+
})
|
|
391
|
+
return derived ? `@${derived}` : null
|
|
392
|
+
}
|
|
393
|
+
|
|
296
394
|
// Directories that are never foundation source. `dist` is the output we compare
|
|
297
395
|
// against; the rest are tooling. Dotted entries are skipped wholesale.
|
|
298
396
|
const NON_SOURCE_DIRS = new Set(['dist', 'node_modules'])
|
|
@@ -487,10 +585,13 @@ async function runRegister(args = []) {
|
|
|
487
585
|
? process.cwd()
|
|
488
586
|
: await resolveFoundationDir(args)
|
|
489
587
|
|
|
490
|
-
// Scope
|
|
491
|
-
//
|
|
492
|
-
//
|
|
493
|
-
|
|
588
|
+
// Scope. ⭐ A FOUNDATION's is the one in its name (`name: '@acme/marketing'` in
|
|
589
|
+
// main.js), settled below before the build; a bare name takes --scope, else one
|
|
590
|
+
// derived from your orgs, and register writes it into the name (2026-09-22).
|
|
591
|
+
// A SCHEMAS-ONLY package has no name to carry one: --scope, else package.json
|
|
592
|
+
// `uniweb.scope`, else (real submit only) derived from login membership. Either
|
|
593
|
+
// spelling, `@acme` or `acme`, and from here on the one form: `@acme` (publishScope).
|
|
594
|
+
const pkgScope = standalone ? readPkgScope(targetDir) : null
|
|
494
595
|
const givenScope = scopeFlag || pkgScope
|
|
495
596
|
let scope = publishScope(givenScope)
|
|
496
597
|
let scopeSource = scopeFlag
|
|
@@ -530,6 +631,19 @@ async function runRegister(args = []) {
|
|
|
530
631
|
if (!(await settleFoundationName(targetDir, { args, isPreview }))) {
|
|
531
632
|
return { exitCode: 2 }
|
|
532
633
|
}
|
|
634
|
+
// ⭐ BEFORE THE BUILD, so the build embeds the name the foundation registers as.
|
|
635
|
+
// Settled after it, a scope written into main.js would leave dist/ describing the
|
|
636
|
+
// bare name — stale the moment it was built.
|
|
637
|
+
const settled = await settleFoundationScope(targetDir, {
|
|
638
|
+
args,
|
|
639
|
+
isPreview,
|
|
640
|
+
flagScope: scope,
|
|
641
|
+
client
|
|
642
|
+
})
|
|
643
|
+
if (!settled) return { exitCode: 2 }
|
|
644
|
+
if (settled.cancelled) return { exitCode: 0 }
|
|
645
|
+
scope = settled.scope
|
|
646
|
+
scopeSource = settled.source
|
|
533
647
|
// Build-if-stale (`foundationNeedsBuild`): a missing or stale dist/ gets
|
|
534
648
|
// (re)built before we read its schema. Preview paths
|
|
535
649
|
// (--dry-run / -o) must not write to dist/, so they require a pre-built
|
|
@@ -578,24 +692,18 @@ async function runRegister(args = []) {
|
|
|
578
692
|
}
|
|
579
693
|
}
|
|
580
694
|
|
|
581
|
-
//
|
|
582
|
-
// 0 create / N pick)
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
const
|
|
586
|
-
const derived = await deriveScope({
|
|
587
|
-
apiBase: client.origin,
|
|
588
|
-
token,
|
|
589
|
-
accountHandle: sess?.handle || null,
|
|
590
|
-
args
|
|
591
|
-
})
|
|
695
|
+
// A schemas-only package with no scope, on a real submit → derive it from login
|
|
696
|
+
// membership (list → 1 use / 0 create / N pick) and persist it to package.json.
|
|
697
|
+
// (A foundation's was settled before its build — `settleFoundationScope`.)
|
|
698
|
+
if (standalone && !scope && !isPreview) {
|
|
699
|
+
const derived = await deriveScopeFromLogin(client, args)
|
|
592
700
|
if (!derived) return { exitCode: 0 }
|
|
593
|
-
scope =
|
|
701
|
+
scope = derived
|
|
594
702
|
scopeSource = 'login'
|
|
595
703
|
try {
|
|
596
704
|
await writePkgScope(targetDir, scope)
|
|
597
705
|
info(
|
|
598
|
-
`Saved ${colors.bright}${scope}${colors.reset} as this
|
|
706
|
+
`Saved ${colors.bright}${scope}${colors.reset} as this package's publish scope (package.json).`
|
|
599
707
|
)
|
|
600
708
|
} catch {
|
|
601
709
|
log(
|
|
@@ -692,7 +800,9 @@ async function runRegister(args = []) {
|
|
|
692
800
|
// Submit requires a concrete scope — the registry rejects @/… fail-closed.
|
|
693
801
|
if (!scope) {
|
|
694
802
|
error(
|
|
695
|
-
|
|
803
|
+
standalone
|
|
804
|
+
? 'No publish scope — set "uniweb.scope" in package.json, or pass --scope @org.'
|
|
805
|
+
: "No publish scope — name the foundation '@org/<name>' in main.js, or pass --scope @org."
|
|
696
806
|
)
|
|
697
807
|
log(
|
|
698
808
|
` ${colors.dim}Without a scope, names stay @/… and the registry rejects them.${colors.reset}`
|
package/src/commands/status.js
CHANGED
|
@@ -25,8 +25,14 @@ import { join } from 'node:path'
|
|
|
25
25
|
import yaml from 'js-yaml'
|
|
26
26
|
|
|
27
27
|
import { resolveSiteDir } from './deploy.js'
|
|
28
|
-
import { probeUnpushed } from '../backend/site-sync.js'
|
|
29
|
-
import {
|
|
28
|
+
import { probeUnpushed, nameSiteWorkspace, readSiteWorkspace } from '../backend/site-sync.js'
|
|
29
|
+
import { readOrgFlag } from '../utils/args.js'
|
|
30
|
+
import {
|
|
31
|
+
BackendClient,
|
|
32
|
+
resolveBackendOrigin,
|
|
33
|
+
workspaceHandle,
|
|
34
|
+
WorkspaceMismatchError
|
|
35
|
+
} from '../backend/client.js'
|
|
30
36
|
import { readBackendState } from '@uniweb/build/uwx'
|
|
31
37
|
import {
|
|
32
38
|
resolveLocalFoundation,
|
|
@@ -115,12 +121,22 @@ export async function status(args = []) {
|
|
|
115
121
|
let site = null
|
|
116
122
|
let fdnLatest = null
|
|
117
123
|
let foundationFresh = null // true/false when both digests are known; else null
|
|
124
|
+
let remoteError = null // a refusal about the workspace — the one remote failure worth saying
|
|
118
125
|
if (remote) {
|
|
119
126
|
try {
|
|
120
127
|
const client = new BackendClient({
|
|
121
128
|
args,
|
|
122
129
|
command: 'Status'
|
|
123
130
|
})
|
|
131
|
+
// The workspace the site's requests name — as pull names it. A stale record is
|
|
132
|
+
// re-adopted from the backend's answer; `--json` keeps stdout to the document.
|
|
133
|
+
const orgFlag = readOrgFlag(args)
|
|
134
|
+
nameSiteWorkspace(client, {
|
|
135
|
+
siteDir,
|
|
136
|
+
workspace: orgFlag ? workspaceHandle(orgFlag) : readSiteWorkspace(siteDir, client.origin),
|
|
137
|
+
explicit: Boolean(orgFlag),
|
|
138
|
+
note: jsonMode ? undefined : say.dim
|
|
139
|
+
})
|
|
124
140
|
if (uuid) site = await client.siteStatus(uuid)
|
|
125
141
|
// Foundation freshness: prefer the LOCAL foundation's scoped name (so a
|
|
126
142
|
// local-foundation site can be checked too); fall back to a scoped
|
|
@@ -134,8 +150,11 @@ export async function status(args = []) {
|
|
|
134
150
|
const localDigest = computeFoundationDigest(join(local.dir, 'dist'))
|
|
135
151
|
if (localDigest) foundationFresh = localDigest === fdnLatest.digest
|
|
136
152
|
}
|
|
137
|
-
} catch {
|
|
138
|
-
//
|
|
153
|
+
} catch (err) {
|
|
154
|
+
// Degrade silently — except a refusal about the workspace: `--org` named one the
|
|
155
|
+
// backend does not work on this site from, or the deployment has another. Saying
|
|
156
|
+
// nothing would read as "fine".
|
|
157
|
+
if (err instanceof WorkspaceMismatchError || err?.status === 409) remoteError = err.message
|
|
139
158
|
}
|
|
140
159
|
}
|
|
141
160
|
|
|
@@ -153,7 +172,8 @@ export async function status(args = []) {
|
|
|
153
172
|
remote: {
|
|
154
173
|
site,
|
|
155
174
|
foundation_latest: fdnLatest?.latest_version ?? null,
|
|
156
|
-
foundation_fresh: foundationFresh
|
|
175
|
+
foundation_fresh: foundationFresh,
|
|
176
|
+
...(remoteError ? { error: remoteError } : {})
|
|
157
177
|
}
|
|
158
178
|
}
|
|
159
179
|
: {})
|
|
@@ -202,6 +222,7 @@ export async function status(args = []) {
|
|
|
202
222
|
|
|
203
223
|
// Remote signals
|
|
204
224
|
if (remote) {
|
|
225
|
+
if (remoteError) say.warn(remoteError)
|
|
205
226
|
if (site) {
|
|
206
227
|
if (site.draft_dirty) {
|
|
207
228
|
say.info(
|
package/src/framework-index.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-09-
|
|
3
|
+
"generatedAt": "2026-09-23T15:24:08.497Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/api": {
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.5.0",
|
|
7
7
|
"path": "framework/api",
|
|
8
8
|
"deps": [
|
|
9
9
|
"@uniweb/core"
|
|
10
10
|
]
|
|
11
11
|
},
|
|
12
12
|
"@uniweb/build": {
|
|
13
|
-
"version": "0.
|
|
13
|
+
"version": "0.59.0",
|
|
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.3.
|
|
99
|
+
"version": "0.3.4",
|
|
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.1",
|
|
120
120
|
"path": "framework/templates",
|
|
121
121
|
"deps": []
|
|
122
122
|
},
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
"deps": []
|
|
127
127
|
},
|
|
128
128
|
"@uniweb/unipress": {
|
|
129
|
-
"version": "0.10.
|
|
129
|
+
"version": "0.10.2",
|
|
130
130
|
"path": "framework/unipress",
|
|
131
131
|
"deps": [
|
|
132
132
|
"@uniweb/build",
|
package/src/index.js
CHANGED
|
@@ -1456,11 +1456,12 @@ ${colors.bright}Options:${colors.reset}
|
|
|
1456
1456
|
bind to, and the app cannot open such a site.
|
|
1457
1457
|
--no-save Do not record this deploy in deploy.yml
|
|
1458
1458
|
--no-validate Skip the content-conformance check (it only warns)
|
|
1459
|
-
--org @org Publish under @org (membership-gated
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1459
|
+
--org @org Publish under @org (membership-gated). On a site's FIRST
|
|
1460
|
+
publish it decides which org owns the site, and whose storage
|
|
1461
|
+
its assets are charged to. Recorded in sync.json and named on
|
|
1462
|
+
every request after, so it is never re-typed. Without it, you
|
|
1463
|
+
are asked once. Later, it must match the workspace the backend
|
|
1464
|
+
works on the site from, or publish stops.
|
|
1464
1465
|
--personal Create the site under your personal account, deliberately.
|
|
1465
1466
|
Only needed on a first publish, and only to answer the owner
|
|
1466
1467
|
question without a prompt (CI, agents, scripts).
|
|
@@ -1725,13 +1726,18 @@ Auto-detects what you run it in:
|
|
|
1725
1726
|
• a schemas-only pkg just its data schemas, no foundation — e.g. @uniweb/schemas,
|
|
1726
1727
|
any @org/schemas package, or a bare schemas/*.yml folder
|
|
1727
1728
|
|
|
1728
|
-
|
|
1729
|
-
|
|
1729
|
+
A foundation's scope is part of its name — name: '@org/<name>' in main.js. A bare
|
|
1730
|
+
name takes --scope (or one of your orgs), and register writes it into the name.
|
|
1731
|
+
|
|
1732
|
+
Schema scopes:
|
|
1733
|
+
@/name your own schema, in the foundation's scope (@/x -> @org/x)
|
|
1730
1734
|
@std/name a shared standard schema (from @uniweb/schemas)
|
|
1731
1735
|
@org/name another org's published schema, referenced by name
|
|
1732
1736
|
|
|
1733
1737
|
${colors.bright}Options:${colors.reset}
|
|
1734
|
-
--scope @org
|
|
1738
|
+
--scope @org A foundation with a bare name: register under @org, and write it into
|
|
1739
|
+
the name (refused when the name has another scope). A schemas-only
|
|
1740
|
+
package: publish under @org; default: package.json uniweb.scope
|
|
1735
1741
|
--dry-run Print the .uwx; submit nothing
|
|
1736
1742
|
-o, --output <f> Write the .uwx to a file; submit nothing
|
|
1737
1743
|
--non-interactive Fail with usage info instead of prompting
|
package/src/utils/args.js
CHANGED
|
@@ -21,25 +21,17 @@
|
|
|
21
21
|
* @returns {string | null | undefined}
|
|
22
22
|
*/
|
|
23
23
|
/**
|
|
24
|
-
* The
|
|
25
|
-
* is
|
|
24
|
+
* The workspace a site command names — `--org @acme` — and, on a site's first push or
|
|
25
|
+
* publish, the org it is created under.
|
|
26
26
|
*
|
|
27
|
-
* `--as-org`
|
|
28
|
-
*
|
|
29
|
-
* it is also not the name anyone reaches for: asked in one day, the backend's docs
|
|
30
|
-
* said `--as-unit` and a second reader said `--org`; nobody produced `--as-org`.
|
|
31
|
-
* Since the flag's main job is answering *who owns this site*, `--org` is the name
|
|
32
|
-
* that matches the question, and the alias costs one `||` (the same shape
|
|
33
|
-
* `--backend` already uses).
|
|
34
|
-
*
|
|
35
|
-
* `||`, not `??`, on purpose: a valueless `--org` falls through to `--as-org`
|
|
36
|
-
* rather than shadowing it.
|
|
27
|
+
* ⛔ `--as-org` is RETIRED (2026-09-23). It mirrored the wire's `?as_org=`, which the
|
|
28
|
+
* `x-uniweb-workspace` header replaced; `flag-guard.js` refuses it and names `--org`.
|
|
37
29
|
*
|
|
38
30
|
* @param {string[]} args
|
|
39
31
|
* @returns {string|null|undefined}
|
|
40
32
|
*/
|
|
41
33
|
export function readOrgFlag(args) {
|
|
42
|
-
return readFlagValue(args, '--org')
|
|
34
|
+
return readFlagValue(args, '--org')
|
|
43
35
|
}
|
|
44
36
|
|
|
45
37
|
/**
|
|
@@ -97,7 +89,7 @@ function editDistance(a, b) {
|
|
|
97
89
|
|
|
98
90
|
/**
|
|
99
91
|
* The closest known flag to `flag`, or null when nothing is close enough.
|
|
100
|
-
* The threshold scales with length so `--
|
|
92
|
+
* The threshold scales with length so `--forcee` → `--force` is offered while two
|
|
101
93
|
* unrelated short flags are not.
|
|
102
94
|
* @param {string} flag
|
|
103
95
|
* @param {string[]} known
|
package/src/utils/flag-guard.js
CHANGED
|
@@ -50,12 +50,11 @@ const VIA_DEPLOY = ['--target', '--host', '--no-save']
|
|
|
50
50
|
/**
|
|
51
51
|
* Per-verb flag sets. Derived by scanning each command for dash-literals AND the
|
|
52
52
|
* helpers it calls — not from the help text, which has drifted from the parser in
|
|
53
|
-
* both directions (`--
|
|
54
|
-
* documented on `publish` and consumed two files away).
|
|
53
|
+
* both directions (`--yes` is documented on `publish` and consumed two files away).
|
|
55
54
|
*/
|
|
56
55
|
const VERBS = {
|
|
57
56
|
push: [
|
|
58
|
-
'--all', '--
|
|
57
|
+
'--all', '--org', '--dry-run', '--force',
|
|
59
58
|
'--foundation', '--output', '-o', '--personal',
|
|
60
59
|
// read in utils/conformance.js and backend/site-sync.js respectively —
|
|
61
60
|
// neither appears in push.js
|
|
@@ -69,7 +68,7 @@ const VERBS = {
|
|
|
69
68
|
'--no-release', ...VIA_DEPLOY
|
|
70
69
|
],
|
|
71
70
|
publish: [
|
|
72
|
-
'--
|
|
71
|
+
'--org', '--dry-run', '--force', '--foundation',
|
|
73
72
|
'--personal',
|
|
74
73
|
// read in utils/conformance.js, backend/site-sync.js, and
|
|
75
74
|
// backend/foundation-bring-along.js — none appear in publish.js
|
|
@@ -80,15 +79,15 @@ const VERBS = {
|
|
|
80
79
|
'--no-assets',
|
|
81
80
|
'--no-records', '--no-delete', '--no-prune',
|
|
82
81
|
// via backend/site-sync.js (the owner resolver) and utils/conformance.js
|
|
83
|
-
'--yes', '--org', '--
|
|
82
|
+
'--yes', '--org', '--no-validate', ...VIA_DEPLOY
|
|
84
83
|
],
|
|
85
84
|
clone: [
|
|
86
85
|
'--content-only', '--no-assets', '--no-records', '--path',
|
|
87
|
-
'--project', '--org'
|
|
86
|
+
'--project', '--org'
|
|
88
87
|
],
|
|
89
88
|
register: [
|
|
90
89
|
'--dry-run', '--json', '--output', '-o',
|
|
91
|
-
'--schema-only', '--scope', '--org'
|
|
90
|
+
'--schema-only', '--scope', '--org'
|
|
92
91
|
],
|
|
93
92
|
/**
|
|
94
93
|
* `forget` = remove one backend's records (`--backend <url>`), or everything a
|
|
@@ -99,7 +98,7 @@ const VERBS = {
|
|
|
99
98
|
forget: ['--backend', '--all'],
|
|
100
99
|
status: [
|
|
101
100
|
'--json', '--remote', '--dry-run',
|
|
102
|
-
'--force', '--no-verify', '--no-validate', '--yes', '--org',
|
|
101
|
+
'--force', '--no-verify', '--no-validate', '--yes', '--org',
|
|
103
102
|
// inert here, reachable through the bring-along module status imports for
|
|
104
103
|
// `resolveLocalFoundation` — listed per the over-approximation note above
|
|
105
104
|
'--no-release', ...VIA_DEPLOY
|
|
@@ -119,7 +118,7 @@ const VERBS = {
|
|
|
119
118
|
*/
|
|
120
119
|
refresh: [
|
|
121
120
|
'--no-backend', '--no-git',
|
|
122
|
-
'--
|
|
121
|
+
'--org', '--dry-run', '--no-validate', '--yes', ...VIA_DEPLOY
|
|
123
122
|
]
|
|
124
123
|
}
|
|
125
124
|
|
|
@@ -213,6 +212,15 @@ export function checkFlags(verb, args = []) {
|
|
|
213
212
|
].join('\n')
|
|
214
213
|
}
|
|
215
214
|
}
|
|
215
|
+
// ⭐ `--as-org` is RETIRED (2026-09-23) with the `?as_org=` it mirrored — the request
|
|
216
|
+
// names its workspace in a header now, and `--org` is the one spelling.
|
|
217
|
+
if (flag === '--as-org' && all.includes('--org')) {
|
|
218
|
+
return {
|
|
219
|
+
flag,
|
|
220
|
+
suggestion: '--org',
|
|
221
|
+
message: `\`--as-org\` is retired — use \`--org @org\` (e.g. \`uniweb ${verb} --org @acme\`).`
|
|
222
|
+
}
|
|
223
|
+
}
|
|
216
224
|
const suggestion = didYouMean(flag, all)
|
|
217
225
|
const lines = [`Unknown flag \`${flag}\` for \`uniweb ${verb}\`.`]
|
|
218
226
|
if (suggestion) lines.push(` Did you mean \`${suggestion}\`?`)
|
|
@@ -61,8 +61,9 @@ export function bareHandle(scope) {
|
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
63
|
* A publish scope in the one form the registry names things by — `@acme` — from any
|
|
64
|
-
* spelling `--scope` or `package.json::uniweb.scope` may carry
|
|
65
|
-
* `@acme/…`). Null when there is no handle in it.
|
|
64
|
+
* spelling `--scope` or a schemas-only package's `package.json::uniweb.scope` may carry
|
|
65
|
+
* it in (`@acme`, `acme`, `@acme/…`). Null when there is no handle in it. (A
|
|
66
|
+
* foundation's scope is part of its name, `@acme/marketing`, and needs no reading.)
|
|
66
67
|
*
|
|
67
68
|
* ⛔ Every name built from a scope goes through this. The `.uwx` assembly
|
|
68
69
|
* (`@uniweb/build`'s `buildRegistryPackage`) has always accepted `acme` as well as
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
* demonstrated.**
|
|
52
52
|
*/
|
|
53
53
|
|
|
54
|
+
import { WORKSPACE_HEADER } from '../backend/client.js'
|
|
54
55
|
import { createHash } from 'node:crypto'
|
|
55
56
|
import { humanBytes } from './bytes.js'
|
|
56
57
|
import { fetchWithRetry, isTransientStatus } from './fetch-retry.js'
|
|
@@ -62,6 +63,9 @@ import { fetchWithRetry, isTransientStatus } from './fetch-retry.js'
|
|
|
62
63
|
* @param {string} opts.apiBase - backend origin
|
|
63
64
|
* @param {string} opts.token - bearer, used on the direct arm only
|
|
64
65
|
* @param {string} opts.siteUuid - the site these files belong to (path segment)
|
|
66
|
+
* @param {string|null} [opts.workspace] - the workspace the requests name, `@handle`
|
|
67
|
+
* (`x-uniweb-workspace`) — on the plan and on every DIRECT PUT, never on a presigned
|
|
68
|
+
* one, which is self-authorizing and must carry nothing it was not signed with
|
|
65
69
|
* @param {{ data: Record<string, unknown> }|null} opts.ball - source of the set:
|
|
66
70
|
* `{ "<relpath under dist/data>": <json> }`, media refs already rewritten
|
|
67
71
|
* @param {(m: string) => void} [opts.onProgress]
|
|
@@ -72,9 +76,13 @@ export async function uploadSiteData({
|
|
|
72
76
|
apiBase,
|
|
73
77
|
token,
|
|
74
78
|
siteUuid,
|
|
79
|
+
workspace = null,
|
|
75
80
|
ball,
|
|
76
81
|
onProgress = () => {}
|
|
77
82
|
}) {
|
|
83
|
+
// A plan made with the header mints BARE PUT urls: the PUT names its workspace with
|
|
84
|
+
// the header like every other request (backend, 2026-09-23).
|
|
85
|
+
const workspaceHeader = workspace ? { [WORKSPACE_HEADER]: workspace } : {}
|
|
78
86
|
// ⛔ An EMPTY set still posts a plan, and that is the whole point of this lane
|
|
79
87
|
// being a manifest rather than a stream of files.
|
|
80
88
|
//
|
|
@@ -131,7 +139,8 @@ export async function uploadSiteData({
|
|
|
131
139
|
method: 'POST',
|
|
132
140
|
headers: {
|
|
133
141
|
'Content-Type': 'application/json',
|
|
134
|
-
Authorization: `Bearer ${token}
|
|
142
|
+
Authorization: `Bearer ${token}`,
|
|
143
|
+
...workspaceHeader
|
|
135
144
|
},
|
|
136
145
|
body: JSON.stringify({
|
|
137
146
|
files: files.map(({ path, content_type, size, sha256 }) => ({
|
|
@@ -172,7 +181,9 @@ export async function uploadSiteData({
|
|
|
172
181
|
// PUT is a bearer-authed backend route; a presigned URL is self-authorizing
|
|
173
182
|
// and must NOT carry a foreign bearer, which can break signature validation.
|
|
174
183
|
const authHeaders =
|
|
175
|
-
plan.mode === 'presigned'
|
|
184
|
+
plan.mode === 'presigned'
|
|
185
|
+
? {}
|
|
186
|
+
: { Authorization: `Bearer ${token}`, ...workspaceHeader }
|
|
176
187
|
|
|
177
188
|
const uploaded = []
|
|
178
189
|
const failed = []
|