uniweb 0.59.2 → 0.61.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 +24 -32
- package/src/backend/asset-download.js +11 -9
- package/src/backend/client.js +38 -91
- package/src/backend/foundation-bring-along.js +19 -14
- package/src/backend/service-request.js +14 -9
- package/src/backend/site-media.js +6 -6
- package/src/backend/site-sync.js +323 -221
- package/src/commands/clone.js +42 -39
- package/src/commands/deploy.js +45 -27
- package/src/commands/doctor.js +6 -7
- package/src/commands/forget.js +197 -0
- package/src/commands/i18n.js +19 -10
- package/src/commands/publish.js +112 -77
- package/src/commands/pull.js +63 -49
- package/src/commands/push.js +76 -60
- package/src/commands/refresh.js +22 -29
- package/src/commands/register.js +37 -23
- package/src/commands/status.js +13 -11
- package/src/commands/sync.js +0 -1
- package/src/framework-index.json +7 -7
- package/src/index.js +101 -76
- package/src/utils/args.js +3 -4
- package/src/utils/asset-upload.js +18 -5
- package/src/utils/config.js +79 -42
- package/src/utils/flag-guard.js +70 -27
- package/src/utils/git.js +1 -1
- package/src/utils/interactive.js +38 -0
- package/src/utils/records-guard.js +38 -30
- package/src/utils/registry-auth.js +150 -28
- package/src/utils/registry-orgs.js +19 -0
- package/src/utils/scaffold.js +11 -0
- package/src/utils/session-file.js +95 -0
- package/src/utils/site-copies.js +123 -0
- package/src/utils/site-data-upload.js +4 -3
- package/src/utils/site-identity.js +89 -299
- package/templates/site/site.yml.hbs +11 -9
package/src/commands/register.js
CHANGED
|
@@ -30,12 +30,11 @@
|
|
|
30
30
|
* uniweb register --json Porcelain: ONE compact JSON line on stdout
|
|
31
31
|
* ({ok,scope,origin,entities:[{name,uuid,version,unchanged}]}),
|
|
32
32
|
* all human output to stderr — for scripted callers
|
|
33
|
-
* uniweb register --backend <url> Override the backend origin
|
|
34
|
-
* uniweb register --token <bearer> Submit with this bearer; skips `uniweb login`
|
|
35
33
|
*
|
|
36
|
-
* Endpoint
|
|
37
|
-
*
|
|
38
|
-
*
|
|
34
|
+
* Endpoint: UNIWEB_REGISTER_URL > the backend you are logged in to > the default
|
|
35
|
+
* (~/.uniweb/config.json, else uniweb.app) — resolveBackendOrigin. No `--backend`:
|
|
36
|
+
* switching is `uniweb login --backend <url>`.
|
|
37
|
+
* Auth (submit only): UNIWEB_TOKEN > the stored session > `uniweb login`.
|
|
39
38
|
*/
|
|
40
39
|
|
|
41
40
|
// DEFERRED foundation-registration capabilities (the legacy `uniweb publish` had
|
|
@@ -78,7 +77,7 @@ import {
|
|
|
78
77
|
computeFoundationDigest,
|
|
79
78
|
readRuntimePin
|
|
80
79
|
} from '../utils/code-upload.js'
|
|
81
|
-
import { deriveScope } from '../utils/registry-orgs.js'
|
|
80
|
+
import { deriveScope, publishScope } from '../utils/registry-orgs.js'
|
|
82
81
|
import { BackendClient } from '../backend/client.js'
|
|
83
82
|
import { writeJsonPreservingStyleAsync } from '../utils/json-file.js'
|
|
84
83
|
import {
|
|
@@ -388,12 +387,9 @@ async function runRegister(args = []) {
|
|
|
388
387
|
const dryRun = args.includes('--dry-run')
|
|
389
388
|
const output = flagValue(args, '-o') || flagValue(args, '--output')
|
|
390
389
|
const scopeFlag = flagValue(args, '--scope')
|
|
391
|
-
|
|
392
|
-
//
|
|
393
|
-
// origin-selection convention); either overrides UNIWEB_REGISTER_URL / default.
|
|
390
|
+
// The backend you are logged in to (UNIWEB_REGISTER_URL overrides it for a script) —
|
|
391
|
+
// resolveBackendOrigin, like every backend command.
|
|
394
392
|
const client = new BackendClient({
|
|
395
|
-
originFlag: flagValue(args, '--backend'),
|
|
396
|
-
token: tokenFlag,
|
|
397
393
|
args,
|
|
398
394
|
command: 'Registering'
|
|
399
395
|
})
|
|
@@ -408,14 +404,24 @@ async function runRegister(args = []) {
|
|
|
408
404
|
: await resolveFoundationDir(args)
|
|
409
405
|
|
|
410
406
|
// Scope: --scope flag, else package.json `uniweb.scope`, else (real submit
|
|
411
|
-
// only) derived from login membership in the bootstrap below.
|
|
407
|
+
// only) derived from login membership in the bootstrap below. Either spelling,
|
|
408
|
+
// `@acme` or `acme`, and from here on the one form: `@acme` (publishScope).
|
|
412
409
|
const pkgScope = readPkgScope(targetDir)
|
|
413
|
-
|
|
410
|
+
const givenScope = scopeFlag || pkgScope
|
|
411
|
+
let scope = publishScope(givenScope)
|
|
414
412
|
let scopeSource = scopeFlag
|
|
415
413
|
? '--scope'
|
|
416
414
|
: pkgScope
|
|
417
415
|
? 'package.json uniweb.scope'
|
|
418
416
|
: null
|
|
417
|
+
// A scope that was GIVEN but names no org (`--scope @`) is refused, never read as
|
|
418
|
+
// absent: absent derives one from the login, which would register under an org the
|
|
419
|
+
// caller did not name.
|
|
420
|
+
if (givenScope && !scope) {
|
|
421
|
+
error(`Not an org scope: ${givenScope} (from ${scopeSource})`)
|
|
422
|
+
log(` ${colors.dim}Pass --scope @org.${colors.reset}`)
|
|
423
|
+
return { exitCode: 2 }
|
|
424
|
+
}
|
|
419
425
|
const isPreview = !!output || dryRun
|
|
420
426
|
|
|
421
427
|
// Each path supplies a different schema source: the standalone path discovers
|
|
@@ -489,7 +495,7 @@ async function runRegister(args = []) {
|
|
|
489
495
|
// 0 create / N pick), persist to package.json, and reuse the session token.
|
|
490
496
|
if (!scope && !isPreview) {
|
|
491
497
|
const token = await client.token()
|
|
492
|
-
const sess = await readRegistryAuth()
|
|
498
|
+
const sess = await readRegistryAuth(client.origin)
|
|
493
499
|
const derived = await deriveScope({
|
|
494
500
|
apiBase: client.origin,
|
|
495
501
|
token,
|
|
@@ -606,7 +612,7 @@ async function runRegister(args = []) {
|
|
|
606
612
|
)
|
|
607
613
|
return { exitCode: 2 }
|
|
608
614
|
}
|
|
609
|
-
// Submit — the client carries the bearer (
|
|
615
|
+
// Submit — the client carries the bearer (UNIWEB_TOKEN › stored
|
|
610
616
|
// session › login), resolved lazily on this first authed call.
|
|
611
617
|
info(`Submitting to ${colors.dim}${client.origin}${colors.reset} …`)
|
|
612
618
|
let res
|
|
@@ -615,7 +621,7 @@ async function runRegister(args = []) {
|
|
|
615
621
|
} catch (err) {
|
|
616
622
|
error(`Could not reach the registry at ${client.origin}: ${err.message}`)
|
|
617
623
|
log(
|
|
618
|
-
` ${colors.dim}
|
|
624
|
+
` ${colors.dim}Is that the backend you meant? Switch with: uniweb login --backend <url>${colors.reset}`
|
|
619
625
|
)
|
|
620
626
|
return { exitCode: 2 }
|
|
621
627
|
}
|
|
@@ -650,7 +656,7 @@ async function runRegister(args = []) {
|
|
|
650
656
|
` ${colors.dim}The registry didn't accept your credentials — it may use different ones than \`uniweb login\`.${colors.reset}`
|
|
651
657
|
)
|
|
652
658
|
log(
|
|
653
|
-
` ${colors.dim}
|
|
659
|
+
` ${colors.dim}Log in again (uniweb login --backend <url>), or check UNIWEB_TOKEN — an existing one may be wrong or expired.${colors.reset}`
|
|
654
660
|
)
|
|
655
661
|
}
|
|
656
662
|
// ⭐ **Surface `detail` as a sentence, not as a JSON dump.** The registry
|
|
@@ -675,11 +681,17 @@ async function runRegister(args = []) {
|
|
|
675
681
|
return { exitCode: 1 }
|
|
676
682
|
}
|
|
677
683
|
}
|
|
684
|
+
// The foundation's name as the registry holds it (`@org/name`) — read from the
|
|
685
|
+
// document just submitted, never composed again. See phase 2.
|
|
686
|
+
const registeredName = standalone
|
|
687
|
+
? null
|
|
688
|
+
: doc.entities.find((e) => e.model === '@uniweb/foundation-schema')?.info
|
|
689
|
+
?.name
|
|
678
690
|
if (!alreadyRegistered) {
|
|
679
691
|
success(
|
|
680
692
|
standalone
|
|
681
693
|
? `Registered ${defined.length} data schema(s)${scope ? ` under ${scope}` : ''}`
|
|
682
|
-
: `Registered ${
|
|
694
|
+
: `Registered ${registeredName}@${schema._self.version}${defined.length ? ` + ${defined.length} data schema(s)` : ''}`
|
|
683
695
|
)
|
|
684
696
|
}
|
|
685
697
|
|
|
@@ -688,11 +700,13 @@ async function runRegister(args = []) {
|
|
|
688
700
|
// only packages have no dist; --schema-only skips deliberately.
|
|
689
701
|
if (!standalone && !args.includes('--schema-only')) {
|
|
690
702
|
const distDir = join(targetDir, 'dist')
|
|
691
|
-
// The
|
|
692
|
-
//
|
|
693
|
-
//
|
|
694
|
-
|
|
695
|
-
|
|
703
|
+
// ⭐ The plan authorizes against the REGISTERED (name, version), so the name here
|
|
704
|
+
// is the one phase 1 registered — the `.uwx`'s own `info.name`, where a scoped
|
|
705
|
+
// package name passes through and a bare one gets the scope. ⛔ Not composed a
|
|
706
|
+
// second time: this line composed it from the raw scope until 2026-09-21, so
|
|
707
|
+
// `--scope std` registered `@std/src` and then asked to deliver `std/src`, which
|
|
708
|
+
// the registry refuses. One name, read from the one place it was decided.
|
|
709
|
+
const name = registeredName
|
|
696
710
|
const version = schema._self.version
|
|
697
711
|
info(
|
|
698
712
|
`Delivering code for ${colors.bright}${name}@${version}${colors.reset} …`
|
package/src/commands/status.js
CHANGED
|
@@ -24,14 +24,13 @@ import { existsSync, readFileSync } from 'node:fs'
|
|
|
24
24
|
import { join } from 'node:path'
|
|
25
25
|
import yaml from 'js-yaml'
|
|
26
26
|
|
|
27
|
-
import { resolveSiteDir
|
|
27
|
+
import { resolveSiteDir } from './deploy.js'
|
|
28
28
|
import { probeUnpushed } from '../backend/site-sync.js'
|
|
29
|
-
import { BackendClient } from '../backend/client.js'
|
|
30
|
-
import {
|
|
29
|
+
import { BackendClient, resolveBackendOrigin } from '../backend/client.js'
|
|
30
|
+
import { readBackendState } from '@uniweb/build/uwx'
|
|
31
31
|
import { resolveLocalFoundation } from '../backend/foundation-bring-along.js'
|
|
32
32
|
import { computeFoundationDigest } from '../utils/code-upload.js'
|
|
33
33
|
import { checkFlags } from '../utils/flag-guard.js'
|
|
34
|
-
import { readSiteIdentity } from '../utils/site-identity.js'
|
|
35
34
|
|
|
36
35
|
const c = {
|
|
37
36
|
reset: '\x1b[0m',
|
|
@@ -87,15 +86,23 @@ export async function status(args = []) {
|
|
|
87
86
|
const remote = args.includes('--remote')
|
|
88
87
|
const siteDir = await resolveSiteDir(args, 'status')
|
|
89
88
|
const siteYml = readSiteYml(siteDir)
|
|
90
|
-
const uuid = siteYml.$uuid || null
|
|
91
89
|
const fnd = foundationRef(siteYml)
|
|
92
90
|
const { scope: fndScope, version: fndVersion } = splitFoundationRef(fnd)
|
|
93
91
|
|
|
94
92
|
// Local content diff — builds the sync packages, never authenticates.
|
|
93
|
+
//
|
|
94
|
+
// ⭐ The ORIGIN is resolved here rather than taken from the client below, because
|
|
95
|
+
// this probe is deliberately offline and the client may never be built. It decides
|
|
96
|
+
// whose asset ids the comparison reads: against the wrong backend every media ref
|
|
97
|
+
// reads as changed.
|
|
98
|
+
const probeBackend = resolveBackendOrigin()
|
|
99
|
+
// ⭐ Identity is this backend's, from sync.json. It read `site.yml::$uuid`, so after
|
|
100
|
+
// step 4 moved the key every project reported itself as never synced.
|
|
101
|
+
const uuid = readBackendState(siteDir, probeBackend).site?.uuid || null
|
|
95
102
|
let probe = null
|
|
96
103
|
let probeErr = null
|
|
97
104
|
try {
|
|
98
|
-
probe = await probeUnpushed(siteDir)
|
|
105
|
+
probe = await probeUnpushed(siteDir, { backend: probeBackend })
|
|
99
106
|
} catch (err) {
|
|
100
107
|
probeErr = err.message
|
|
101
108
|
}
|
|
@@ -108,11 +115,6 @@ export async function status(args = []) {
|
|
|
108
115
|
if (remote) {
|
|
109
116
|
try {
|
|
110
117
|
const client = new BackendClient({
|
|
111
|
-
originFlag:
|
|
112
|
-
readFlagValue(args, '--backend'),
|
|
113
|
-
siteScope: readSiteIdentity(siteDir).backend,
|
|
114
|
-
siteBackend: await resolveSiteBackend(siteDir),
|
|
115
|
-
token: readFlagValue(args, '--token') || undefined,
|
|
116
118
|
args,
|
|
117
119
|
command: 'Status'
|
|
118
120
|
})
|
package/src/commands/sync.js
CHANGED
|
@@ -35,7 +35,6 @@
|
|
|
35
35
|
* uniweb sync refresh, then push
|
|
36
36
|
* uniweb sync --no-git skip the git remote half of the refresh
|
|
37
37
|
* uniweb sync --force forwarded to push (overwrite upstream changes)
|
|
38
|
-
* uniweb sync --backend <url> override the backend origin
|
|
39
38
|
*/
|
|
40
39
|
|
|
41
40
|
import { checkFlags } from '../utils/flag-guard.js'
|
package/src/framework-index.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-09-
|
|
3
|
+
"generatedAt": "2026-09-21T21:47:05.925Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/api": {
|
|
6
6
|
"version": "0.4.1",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
]
|
|
11
11
|
},
|
|
12
12
|
"@uniweb/build": {
|
|
13
|
-
"version": "0.
|
|
13
|
+
"version": "0.57.0",
|
|
14
14
|
"path": "framework/build",
|
|
15
15
|
"deps": [
|
|
16
16
|
"@uniweb/content-reader",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"deps": []
|
|
36
36
|
},
|
|
37
37
|
"@uniweb/core": {
|
|
38
|
-
"version": "0.29.
|
|
38
|
+
"version": "0.29.7",
|
|
39
39
|
"path": "framework/core",
|
|
40
40
|
"deps": [
|
|
41
41
|
"@uniweb/semantic-parser",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
]
|
|
56
56
|
},
|
|
57
57
|
"@uniweb/kit": {
|
|
58
|
-
"version": "0.19.
|
|
58
|
+
"version": "0.19.4",
|
|
59
59
|
"path": "framework/kit",
|
|
60
60
|
"deps": [
|
|
61
61
|
"@uniweb/core",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
]
|
|
84
84
|
},
|
|
85
85
|
"@uniweb/runtime": {
|
|
86
|
-
"version": "0.27.
|
|
86
|
+
"version": "0.27.3",
|
|
87
87
|
"path": "framework/runtime",
|
|
88
88
|
"deps": [
|
|
89
89
|
"@uniweb/core",
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
"deps": []
|
|
117
117
|
},
|
|
118
118
|
"@uniweb/templates": {
|
|
119
|
-
"version": "0.
|
|
119
|
+
"version": "0.15.0",
|
|
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.
|
|
129
|
+
"version": "0.10.0",
|
|
130
130
|
"path": "framework/unipress",
|
|
131
131
|
"deps": [
|
|
132
132
|
"@uniweb/build",
|
package/src/index.js
CHANGED
|
@@ -805,6 +805,15 @@ async function main() {
|
|
|
805
805
|
process.exit(result?.exitCode ?? 0)
|
|
806
806
|
}
|
|
807
807
|
|
|
808
|
+
// Handle forget — remove one backend's local records (sync.json, cache, deploy
|
|
809
|
+
// records), or with --all everything a copied project inherited. Local only: every
|
|
810
|
+
// site still exists on its backend. Needs @uniweb/build (the store).
|
|
811
|
+
if (command === 'forget') {
|
|
812
|
+
const { forget } = await importProjectCommand('./commands/forget.js')
|
|
813
|
+
const result = await forget(args.slice(1))
|
|
814
|
+
process.exit(result?.exitCode ?? 0)
|
|
815
|
+
}
|
|
816
|
+
|
|
808
817
|
// Handle status command (dynamic import — offline emit via @uniweb/build)
|
|
809
818
|
if (command === 'status') {
|
|
810
819
|
const { status } = await importProjectCommand('./commands/status.js')
|
|
@@ -879,70 +888,59 @@ async function main() {
|
|
|
879
888
|
}
|
|
880
889
|
|
|
881
890
|
// Handle login command — the backend (username/password · paste a token ·
|
|
882
|
-
// --token <bearer>).
|
|
883
|
-
//
|
|
884
|
-
// the
|
|
891
|
+
// --token <bearer>). ⭐ `--backend <url>` names it; without the flag it is the DEFAULT
|
|
892
|
+
// backend — UNIWEB_REGISTER_URL, else ~/.uniweb/config.json, else https://uniweb.app —
|
|
893
|
+
// and never the project's backend or the current session *[Diego, 2026-09-21: "the
|
|
894
|
+
// default backend for login, if not specified, is uniweb.app"]*. The backend logged in
|
|
895
|
+
// to becomes CURRENT, and every backend command goes there (only UNIWEB_REGISTER_URL
|
|
896
|
+
// outranks it) — which is why this is the one place a backend is chosen.
|
|
897
|
+
//
|
|
898
|
+
// ⛔ Until 2026-09-21 a bare login went to the backend of the project in the cwd, and
|
|
899
|
+
// asked when the machine knew several backends. Both are gone: the default is the
|
|
900
|
+
// default, and any other backend is named.
|
|
885
901
|
if (command === 'login') {
|
|
886
902
|
const loginArgs = args.slice(1)
|
|
887
|
-
const { resolveBackendOrigin } = await import('./backend/client.js')
|
|
888
903
|
const { readFlagValue } = await import('./utils/args.js')
|
|
889
904
|
const { runRegistryLogin } = await import('./utils/registry-auth.js')
|
|
890
|
-
const
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
// does not remove the failure, it MOVES it — you log into the default, and the next
|
|
900
|
-
// push/pull/publish resolves to `$backend` and warns about the mismatch. That is the
|
|
901
|
-
// routed-not-nagged case `$backend` was added for, missing at the one command a
|
|
902
|
-
// teammate runs FIRST after cloning.
|
|
903
|
-
//
|
|
904
|
-
// ⛔ Silent when the origin was named explicitly (--backend /
|
|
905
|
-
// UNIWEB_REGISTER_URL). A deliberate aim is not a mistake to warn about; a genuinely
|
|
906
|
-
// wrong one is still caught by the session-mismatch guard in BackendClient.token().
|
|
907
|
-
if (!originFlag && !process.env.UNIWEB_REGISTER_URL) {
|
|
908
|
-
try {
|
|
909
|
-
const { findNearbySiteBackend } = await import(
|
|
910
|
-
'./utils/site-identity.js'
|
|
911
|
-
)
|
|
912
|
-
const nearby = findNearbySiteBackend(process.cwd())
|
|
913
|
-
if (nearby && nearby.backend !== apiBase) {
|
|
914
|
-
console.error(
|
|
915
|
-
`\x1b[33m⚠\x1b[0m This project syncs with ${nearby.backend} (site.yml::$backend), but login is targeting ${apiBase}.`
|
|
916
|
-
)
|
|
917
|
-
console.error(
|
|
918
|
-
` Log in where the project belongs: uniweb login --backend ${nearby.backend}`
|
|
919
|
-
)
|
|
920
|
-
console.error(
|
|
921
|
-
` Continuing with ${apiBase} — a session is machine-wide, so this is a heads-up, not a block.\n`
|
|
922
|
-
)
|
|
923
|
-
}
|
|
924
|
-
} catch {
|
|
925
|
-
// Advisory only. A malformed site.yml, an unreadable directory or anything else
|
|
926
|
-
// here must never be the reason someone cannot log in.
|
|
927
|
-
}
|
|
905
|
+
const { resolveLoginOrigin } = await import('./utils/config.js')
|
|
906
|
+
let apiBase
|
|
907
|
+
try {
|
|
908
|
+
apiBase = resolveLoginOrigin(readFlagValue(loginArgs, '--backend'))
|
|
909
|
+
} catch (err) {
|
|
910
|
+
console.error(
|
|
911
|
+
`\x1b[31m✗\x1b[0m ${err.message} — e.g. uniweb login --backend http://localhost:8080`
|
|
912
|
+
)
|
|
913
|
+
process.exit(2)
|
|
928
914
|
}
|
|
929
|
-
|
|
930
915
|
await runRegistryLogin({ apiBase, args: loginArgs })
|
|
931
916
|
return
|
|
932
917
|
}
|
|
933
918
|
|
|
934
|
-
// Handle logout
|
|
919
|
+
// Handle logout — THE session. ⭐ There is at most one *[Diego, 2026-09-21: "`uniweb
|
|
920
|
+
// logout` always logs you out of the one backend you may be logged in"]*, so logout
|
|
921
|
+
// takes no options: `--backend` and `--all` existed only while sessions were kept per
|
|
922
|
+
// backend. ⚠️ Local only — the token is forgotten here, not revoked on the backend.
|
|
935
923
|
if (command === 'logout') {
|
|
936
|
-
const
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
)
|
|
924
|
+
const stray = args.slice(1).find((a) => a.startsWith('-') && a !== '--non-interactive')
|
|
925
|
+
if (stray) {
|
|
926
|
+
console.error(
|
|
927
|
+
`\x1b[31m✗\x1b[0m \`uniweb logout\` takes no options (got ${stray}) — it logs you out of the backend you are logged in to.`
|
|
928
|
+
)
|
|
929
|
+
process.exit(2)
|
|
930
|
+
}
|
|
931
|
+
const { clearRegistryAuth } = await import('./utils/registry-auth.js')
|
|
932
|
+
const { loggedInOrigin } = await import('./utils/config.js')
|
|
933
|
+
const origin = loggedInOrigin()
|
|
934
|
+
const cleared = await clearRegistryAuth()
|
|
935
|
+
if (!cleared.length) {
|
|
936
|
+
console.log('Not logged in — nothing to clear.')
|
|
937
|
+
return
|
|
938
|
+
}
|
|
939
|
+
console.log(`\x1b[32m✓\x1b[0m Logged out${origin ? ` of ${origin}` : ''}.`)
|
|
940
|
+
// A file from before sessions were single can hold several; all of them go.
|
|
941
|
+
const others = cleared.filter((o) => o !== origin)
|
|
942
|
+
if (others.length)
|
|
943
|
+
console.log(`\x1b[2mAlso removed stored sessions for: ${others.join(', ')}\x1b[0m`)
|
|
946
944
|
return
|
|
947
945
|
}
|
|
948
946
|
|
|
@@ -1392,7 +1390,7 @@ ${colors.bright}Options:${colors.reset}
|
|
|
1392
1390
|
--host <name> The host to ship to (no value → the wizard, TTY only)
|
|
1393
1391
|
--target <name> Pick a target from deploy.yml (default: deploy.yml's \`default:\`)
|
|
1394
1392
|
--dry-run Resolve the target + adapter; print summary; upload nothing
|
|
1395
|
-
--no-save
|
|
1393
|
+
--no-save Do not record this deploy in deploy.yml
|
|
1396
1394
|
--no-validate Skip the content-conformance check (it only warns)
|
|
1397
1395
|
--non-interactive Fail with usage info instead of prompting
|
|
1398
1396
|
|
|
@@ -1429,18 +1427,16 @@ ${colors.bright}Options:${colors.reset}
|
|
|
1429
1427
|
when only the content should go live. Refused if the
|
|
1430
1428
|
foundation has never been released — there is nothing to
|
|
1431
1429
|
bind to, and the app cannot open such a site.
|
|
1432
|
-
--no-save
|
|
1430
|
+
--no-save Do not record this deploy in deploy.yml
|
|
1433
1431
|
--no-validate Skip the content-conformance check (it only warns)
|
|
1434
1432
|
--org @org Publish under @org (membership-gated; alias: --as-org). Read
|
|
1435
1433
|
only on a site's FIRST publish — that create decides which org
|
|
1436
1434
|
owns the site, and whose storage its assets are charged to.
|
|
1437
|
-
Recorded
|
|
1435
|
+
Recorded in sync.json and replayed, so it is never
|
|
1438
1436
|
re-typed. Without it, you are asked once.
|
|
1439
1437
|
--personal Create the site under your personal account, deliberately.
|
|
1440
1438
|
Only needed on a first publish, and only to answer the owner
|
|
1441
1439
|
question without a prompt (CI, agents, scripts).
|
|
1442
|
-
--backend <url> Backend origin (default: \$UNIWEB_REGISTER_URL or built-in)
|
|
1443
|
-
--token <bearer> Auth bearer (skips \`uniweb login\`)
|
|
1444
1440
|
`,
|
|
1445
1441
|
create: `
|
|
1446
1442
|
${colors.cyan}${colors.bright}uniweb create${colors.reset} ${colors.dim}— Create a new project${colors.reset}
|
|
@@ -1691,7 +1687,7 @@ ${colors.bright}Usage:${colors.reset}
|
|
|
1691
1687
|
uniweb release [options] ${colors.dim}(synonym — reads naturally for updates)${colors.reset}
|
|
1692
1688
|
|
|
1693
1689
|
Builds one \`.uwx\` document and submits it to the registry over HTTP. Run
|
|
1694
|
-
\`uniweb login\` first
|
|
1690
|
+
\`uniweb login\` first. \`register\`/\`release\` are for
|
|
1695
1691
|
FOUNDATIONS (and schemas) — code only; \`uniweb publish\` makes a synced SITE
|
|
1696
1692
|
live (and brings its foundation along); \`uniweb deploy\` hosts on a third-party
|
|
1697
1693
|
host. \`register\` and \`release\` are the same act: \`register\` reads naturally
|
|
@@ -1711,8 +1707,6 @@ ${colors.bright}Options:${colors.reset}
|
|
|
1711
1707
|
--scope @org Publish under @org (resolves @/x -> @org/x); default: package.json uniweb.scope
|
|
1712
1708
|
--dry-run Print the .uwx; submit nothing
|
|
1713
1709
|
-o, --output <f> Write the .uwx to a file; submit nothing
|
|
1714
|
-
--backend <url> Backend origin (default: \$UNIWEB_REGISTER_URL or a local URL)
|
|
1715
|
-
--token <bearer> Submit with this bearer; skips \`uniweb login\` (or set UNIWEB_TOKEN)
|
|
1716
1710
|
--non-interactive Fail with usage info instead of prompting
|
|
1717
1711
|
|
|
1718
1712
|
Run from a foundation, a schemas-only package, or a workspace with a single foundation.
|
|
@@ -1753,19 +1747,26 @@ ${colors.cyan}${colors.bright}uniweb login${colors.reset} ${colors.dim}— Log i
|
|
|
1753
1747
|
${colors.bright}Usage:${colors.reset}
|
|
1754
1748
|
uniweb login [options]
|
|
1755
1749
|
|
|
1756
|
-
Authenticates with
|
|
1757
|
-
~/.uniweb/registry-auth.json.
|
|
1758
|
-
|
|
1750
|
+
Authenticates with a backend and stores its session in
|
|
1751
|
+
~/.uniweb/registry-auth.json. ${colors.bright}One backend at a time:${colors.reset} logging in to another one
|
|
1752
|
+
logs you out of the first (once the new login succeeds). The backend you are
|
|
1753
|
+
logged in to is where the backend commands go (push, pull, publish, status,
|
|
1754
|
+
register, clone) — so this is how you switch. Already logged in to it, login
|
|
1755
|
+
does nothing; add --password, --browser, --token-paste or --token to log in again.
|
|
1756
|
+
|
|
1757
|
+
Without --backend: https://uniweb.app (or \$UNIWEB_REGISTER_URL). No command talks
|
|
1758
|
+
to a backend you are not logged in to — run one before logging in and it asks first.
|
|
1759
1759
|
|
|
1760
1760
|
${colors.bright}Options:${colors.reset}
|
|
1761
|
-
--backend <url>
|
|
1761
|
+
--backend <url> The backend to log in to
|
|
1762
1762
|
--token <bearer> Seed + verify a session from a bearer token (non-interactive)
|
|
1763
|
+
--browser Force the browser method
|
|
1763
1764
|
--password Force the username/password method
|
|
1764
1765
|
--token-paste Force the paste-a-token prompt
|
|
1765
1766
|
|
|
1766
1767
|
In non-interactive mode (CI / no TTY), pass \`--token <bearer>\`, or set
|
|
1767
1768
|
\`UNIWEB_USERNAME\` + \`UNIWEB_PASSWORD\`, or set \`UNIWEB_TOKEN\` (used per-command,
|
|
1768
|
-
not stored).
|
|
1769
|
+
not stored). \`uniweb logout\` logs you out.
|
|
1769
1770
|
`,
|
|
1770
1771
|
refresh: `
|
|
1771
1772
|
${colors.cyan}${colors.bright}uniweb refresh${colors.reset} ${colors.dim}— Catch up with teammates AND app authors${colors.reset}
|
|
@@ -1784,8 +1785,6 @@ reflexively. Exits non-zero if a merge leaves conflicts.
|
|
|
1784
1785
|
${colors.bright}Options:${colors.reset}
|
|
1785
1786
|
--no-git Skip the git remote; backend only
|
|
1786
1787
|
--no-backend Skip the backend; git only
|
|
1787
|
-
--backend <url> Override the backend origin
|
|
1788
|
-
--token <bearer> Read with this bearer; skips \`uniweb login\`
|
|
1789
1788
|
|
|
1790
1789
|
\`--force\` and \`--merge\` are NOT accepted here: this verb builds the delegated
|
|
1791
1790
|
pull's arguments itself, and to pull \`--force\` means "discard my local work".
|
|
@@ -1813,11 +1812,32 @@ ${colors.bright}Options:${colors.reset}
|
|
|
1813
1812
|
|
|
1814
1813
|
--no-git Skip the git remote half of the refresh
|
|
1815
1814
|
--force Overwrite upstream changes (reaches the PUSH half only)
|
|
1816
|
-
--backend <url> Override the backend origin
|
|
1817
|
-
--token <bearer> Auth bearer; skips \`uniweb login\`
|
|
1818
1815
|
|
|
1819
1816
|
\`--force\` means "overwrite upstream" to push but "discard my local work" to pull,
|
|
1820
1817
|
so it is deliberately kept away from the refresh half.
|
|
1818
|
+
`,
|
|
1819
|
+
forget: `
|
|
1820
|
+
${colors.cyan}${colors.bright}uniweb forget${colors.reset} ${colors.dim}— Remove what this project recorded about where it synced${colors.reset}
|
|
1821
|
+
|
|
1822
|
+
${colors.bright}Usage:${colors.reset}
|
|
1823
|
+
uniweb forget --backend <url>
|
|
1824
|
+
uniweb forget --all
|
|
1825
|
+
|
|
1826
|
+
Local files only. Every site stays where it is on its backend.
|
|
1827
|
+
|
|
1828
|
+
${colors.bright}Options:${colors.reset}
|
|
1829
|
+
--backend <url> Forget one backend: its entries in sync.json and in the local
|
|
1830
|
+
cache, and its deploy records in deploy.yml. Its targets stay,
|
|
1831
|
+
so the next publish there creates a new site.
|
|
1832
|
+
--all For a COPY of a project that should become a new one. Deletes
|
|
1833
|
+
sync.json, deploy.yml and the local cache — everything naming
|
|
1834
|
+
the original's sites and destinations. Without it, the copy's
|
|
1835
|
+
first push updates the original's site.
|
|
1836
|
+
|
|
1837
|
+
Record files keep their \`$uuid\`: it is the record's own id, not a backend's.
|
|
1838
|
+
|
|
1839
|
+
To duplicate a project:
|
|
1840
|
+
cp -r my-site my-site-2 && cd my-site-2 && uniweb forget --all
|
|
1821
1841
|
`,
|
|
1822
1842
|
invite: `
|
|
1823
1843
|
${colors.cyan}${colors.bright}uniweb invite${colors.reset} ${colors.dim}— (reserved; not available on the new backend yet)${colors.reset}
|
|
@@ -1986,6 +2006,7 @@ ${colors.bright}Commands:${colors.reset}
|
|
|
1986
2006
|
refresh Catch up with the git remote AND the backend (never pushes)
|
|
1987
2007
|
sync Catch up, then push (refresh + push)
|
|
1988
2008
|
status Show a site's sync state (unpushed content, foundation)
|
|
2009
|
+
forget Forget a backend, or --all to make a copied project a new one
|
|
1989
2010
|
inspect <path> Inspect parsed content shape of a markdown file or folder
|
|
1990
2011
|
docs Generate component documentation
|
|
1991
2012
|
families List the standard section families (for meta.js family:)
|
|
@@ -1994,7 +2015,7 @@ ${colors.bright}Commands:${colors.reset}
|
|
|
1994
2015
|
update Align workspace deps + AGENTS.md to the running CLI
|
|
1995
2016
|
i18n <cmd> Internationalization (extract, sync, status)
|
|
1996
2017
|
login Log in to your Uniweb account
|
|
1997
|
-
logout
|
|
2018
|
+
logout Log out of the backend you are logged in to
|
|
1998
2019
|
|
|
1999
2020
|
${colors.bright}Create Options:${colors.reset}
|
|
2000
2021
|
--template <type> Project template (default: starter)
|
|
@@ -2015,16 +2036,20 @@ ${colors.bright}Global Options:${colors.reset}
|
|
|
2015
2036
|
--non-interactive Fail with usage info instead of prompting
|
|
2016
2037
|
Auto-detected when CI=true or no TTY (pipes, agents)
|
|
2017
2038
|
|
|
2039
|
+
Backend commands (push, pull, publish, status, register, clone) go to the
|
|
2040
|
+
backend you are logged in to, with its session. Switch with
|
|
2041
|
+
\`uniweb login --backend <url>\` (add \`--token <bearer>\` to sign in with a token);
|
|
2042
|
+
a script can aim and authenticate one process with UNIWEB_REGISTER_URL and
|
|
2043
|
+
UNIWEB_TOKEN instead.
|
|
2044
|
+
|
|
2018
2045
|
${colors.bright}Publish Options:${colors.reset}
|
|
2019
2046
|
--dry-run Resolve everything; release/sync/POST nothing
|
|
2020
2047
|
--yes Skip confirmations (CI); never block on a prompt
|
|
2021
2048
|
--no-release Ship content against the already-released code; release nothing
|
|
2022
2049
|
--org @org Publish under @org (first publish only; then remembered)
|
|
2023
2050
|
--personal Own the new site personally, deliberately (first publish only)
|
|
2024
|
-
--no-save
|
|
2051
|
+
--no-save Do not record this deploy in deploy.yml
|
|
2025
2052
|
--no-validate Skip the content-conformance check (it only warns)
|
|
2026
|
-
--backend <url> Backend origin (default: \$UNIWEB_REGISTER_URL or built-in)
|
|
2027
|
-
--token <bearer> Auth bearer (skips \`uniweb login\`)
|
|
2028
2053
|
|
|
2029
2054
|
uniweb publish is the smart Uniweb-hosting path: it brings the site's
|
|
2030
2055
|
foundation along, syncs, and goes live. To register a foundation on its own
|
|
@@ -2037,7 +2062,7 @@ ${colors.bright}Deploy Options:${colors.reset}
|
|
|
2037
2062
|
s3-cloudfront, or \`uniweb\` (delegates to \`uniweb publish\`).
|
|
2038
2063
|
--target <name> Pick a target from deploy.yml (default: deploy.yml's \`default:\`)
|
|
2039
2064
|
--dry-run Resolve the target + adapter; print summary; upload nothing
|
|
2040
|
-
--no-save
|
|
2065
|
+
--no-save Do not record this deploy in deploy.yml
|
|
2041
2066
|
--no-validate Skip the content-conformance check (it only warns)
|
|
2042
2067
|
To deploy on every push instead, see \`uniweb add ci --help\`.
|
|
2043
2068
|
|
package/src/utils/args.js
CHANGED
|
@@ -47,10 +47,9 @@ export function readOrgFlag(args) {
|
|
|
47
47
|
*
|
|
48
48
|
* The CLI reads flags by scanning argv for a literal, so an unrecognized flag is
|
|
49
49
|
* not an error — it is *invisible*, and whatever it was meant to change silently
|
|
50
|
-
* keeps its default. The sharp
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* same way, to a stored session belonging to someone else.
|
|
50
|
+
* keeps its default. The sharp cases were `--backend` and `--token`, which aimed a
|
|
51
|
+
* command and chose its identity; both left the backend commands on 2026-09-21
|
|
52
|
+
* (the login does both now), and the guard names the login when either is passed.
|
|
54
53
|
*
|
|
55
54
|
* Scanning rules, chosen to avoid false positives (a wrong rejection is worse than
|
|
56
55
|
* a missed one — it breaks a working command):
|
|
@@ -19,11 +19,24 @@
|
|
|
19
19
|
* its id+ext for the rewrite. Absent `present` ⇒ false — an older
|
|
20
20
|
* backend without the flag uploads everything (today's behavior).
|
|
21
21
|
*
|
|
22
|
-
* Assets are
|
|
23
|
-
* across sites and idempotent re-deploys (a
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* what the deploy step rewrites
|
|
22
|
+
* Assets are content-addressed and global WITHIN ONE BACKEND: identical bytes →
|
|
23
|
+
* same id → dedup across that backend's sites and idempotent re-deploys (a
|
|
24
|
+
* re-PUT is a cheap no-op). This mirrors the foundation code lane
|
|
25
|
+
* (utils/code-upload.js); the one structural difference is that the backend
|
|
26
|
+
* MINTS the per-asset id, so the plan response is what the deploy step rewrites
|
|
27
|
+
* content references to.
|
|
28
|
+
*
|
|
29
|
+
* ⛔ **"Global" is the DEDUP SCOPE, not a claim about other backends.** An id,
|
|
30
|
+
* and the presence of the bytes behind it, are facts about the backend that
|
|
31
|
+
* answered the PLAN and about nothing else — where a backend keeps them is its
|
|
32
|
+
* own business and not a shape this lane models. ⚠️ This said only "GLOBAL" until
|
|
33
|
+
* 2026-09-20 and was read as "the same id works on any backend", which made
|
|
34
|
+
* `assets.json` look portable in a multi-backend design. It is not: `sync.json`
|
|
35
|
+
* keeps asset ids per backend (`backends.<origin>.assets`), and the backend ORIGIN
|
|
36
|
+
* is the segregation value the CLI has.
|
|
37
|
+
*
|
|
38
|
+
* ⚠️ Do not infer portability from `id` looking like the sha256 either — we read
|
|
39
|
+
* it from the response precisely so the derivation is never load-bearing here.
|
|
27
40
|
*/
|
|
28
41
|
|
|
29
42
|
import { createHash } from 'node:crypto'
|