uniweb 0.85.0 → 0.86.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 +44 -28
- package/src/backend/client.js +59 -6
- package/src/backend/foundation-bring-along.js +5 -3
- package/src/backend/service-request.js +6 -2
- package/src/backend/site-preview.js +1 -1
- package/src/backend/site-sync.js +7 -7
- package/src/commands/build.js +6 -2
- package/src/commands/clone.js +1 -1
- package/src/commands/deploy.js +3 -3
- package/src/commands/forget.js +7 -7
- package/src/commands/publish.js +14 -7
- package/src/commands/pull.js +9 -9
- package/src/commands/push.js +13 -7
- package/src/commands/refresh.js +2 -1
- package/src/commands/register.js +6 -6
- package/src/commands/rename.js +6 -6
- package/src/commands/site.js +358 -0
- package/src/commands/snapshot.js +14 -5
- package/src/commands/status.js +1 -1
- package/src/framework-index.json +11 -11
- package/src/index.js +41 -11
- package/src/utils/config.js +13 -13
- package/src/utils/flag-guard.js +30 -16
- package/src/utils/site-identity.js +3 -3
- package/src/utils/yaml-edit.js +0 -115
package/src/commands/snapshot.js
CHANGED
|
@@ -33,7 +33,7 @@ import { createRequire } from 'node:module'
|
|
|
33
33
|
import { isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
34
34
|
import { pathToFileURL } from 'node:url'
|
|
35
35
|
import yaml from 'js-yaml'
|
|
36
|
-
import {
|
|
36
|
+
import { writeSiteConfig } from '@uniweb/build/uwx'
|
|
37
37
|
|
|
38
38
|
import { didYouMean } from '../utils/args.js'
|
|
39
39
|
import { humanBytes } from '../utils/bytes.js'
|
|
@@ -431,14 +431,23 @@ export async function snapshot(args = []) {
|
|
|
431
431
|
return
|
|
432
432
|
}
|
|
433
433
|
const current = siteYml.data.preview
|
|
434
|
+
// Edited in place, its comments kept (`writeSiteConfig`) — and not at all when site.yml
|
|
435
|
+
// does not parse: that is said, and the file left as the author has it.
|
|
436
|
+
const setPreview = () => {
|
|
437
|
+
try {
|
|
438
|
+
writeSiteConfig(siteDir, { preview: value })
|
|
439
|
+
return true
|
|
440
|
+
} catch (err) {
|
|
441
|
+
console.log(` ${YELLOW}site.yml not changed:${RESET} ${DIM}${err.message}${RESET}`)
|
|
442
|
+
return false
|
|
443
|
+
}
|
|
444
|
+
}
|
|
434
445
|
switch (previewDecision(current, value)) {
|
|
435
446
|
case 'set':
|
|
436
|
-
|
|
437
|
-
console.log(` site.yml: ${CYAN}preview: ${value}${RESET}`)
|
|
447
|
+
if (setPreview()) console.log(` site.yml: ${CYAN}preview: ${value}${RESET}`)
|
|
438
448
|
break
|
|
439
449
|
case 'replace':
|
|
440
|
-
|
|
441
|
-
console.log(` site.yml: ${CYAN}preview: ${value}${RESET} ${DIM}(replaces the app-generated preview)${RESET}`)
|
|
450
|
+
if (setPreview()) console.log(` site.yml: ${CYAN}preview: ${value}${RESET} ${DIM}(replaces the app-generated preview)${RESET}`)
|
|
442
451
|
break
|
|
443
452
|
case 'unchanged':
|
|
444
453
|
console.log(` ${DIM}site.yml already has preview: ${value}${RESET}`)
|
package/src/commands/status.js
CHANGED
|
@@ -243,7 +243,7 @@ export async function status(args = []) {
|
|
|
243
243
|
if (siteState?.state === 'gone') {
|
|
244
244
|
say.warn(`The backend has no site ${uuid} — it was deleted there, or the backend was rebuilt.`)
|
|
245
245
|
say.dim(
|
|
246
|
-
`To push this as a new site: uniweb forget --
|
|
246
|
+
`To push this as a new site: uniweb forget --server ${probeBackend}, then uniweb push.`
|
|
247
247
|
)
|
|
248
248
|
} else if (siteState?.state === 'unknown') {
|
|
249
249
|
say.dim(
|
package/src/framework-index.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-10-
|
|
3
|
+
"generatedAt": "2026-10-07T18:16:06.785Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/api": {
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.7.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.79.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.
|
|
38
|
+
"version": "0.38.0",
|
|
39
39
|
"path": "framework/core",
|
|
40
40
|
"deps": [
|
|
41
41
|
"@uniweb/semantic-parser",
|
|
@@ -48,14 +48,14 @@
|
|
|
48
48
|
"deps": []
|
|
49
49
|
},
|
|
50
50
|
"@uniweb/icons": {
|
|
51
|
-
"version": "0.4.
|
|
51
|
+
"version": "0.4.29",
|
|
52
52
|
"path": "framework/icons",
|
|
53
53
|
"deps": [
|
|
54
54
|
"@uniweb/core"
|
|
55
55
|
]
|
|
56
56
|
},
|
|
57
57
|
"@uniweb/kit": {
|
|
58
|
-
"version": "0.
|
|
58
|
+
"version": "0.20.0",
|
|
59
59
|
"path": "framework/kit",
|
|
60
60
|
"deps": [
|
|
61
61
|
"@uniweb/core",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"deps": []
|
|
77
77
|
},
|
|
78
78
|
"@uniweb/projections": {
|
|
79
|
-
"version": "0.7.
|
|
79
|
+
"version": "0.7.15",
|
|
80
80
|
"path": "framework/projections",
|
|
81
81
|
"deps": [
|
|
82
82
|
"@uniweb/content-writer",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
]
|
|
85
85
|
},
|
|
86
86
|
"@uniweb/runtime": {
|
|
87
|
-
"version": "0.29.
|
|
87
|
+
"version": "0.29.3",
|
|
88
88
|
"path": "framework/runtime",
|
|
89
89
|
"deps": [
|
|
90
90
|
"@uniweb/core",
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
"deps": []
|
|
98
98
|
},
|
|
99
99
|
"@uniweb/schemas": {
|
|
100
|
-
"version": "0.
|
|
100
|
+
"version": "0.13.0",
|
|
101
101
|
"path": "framework/schemas",
|
|
102
102
|
"deps": []
|
|
103
103
|
},
|
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
"deps": []
|
|
118
118
|
},
|
|
119
119
|
"@uniweb/templates": {
|
|
120
|
-
"version": "0.18.
|
|
120
|
+
"version": "0.18.1",
|
|
121
121
|
"path": "framework/templates",
|
|
122
122
|
"deps": []
|
|
123
123
|
},
|
|
@@ -127,7 +127,7 @@
|
|
|
127
127
|
"deps": []
|
|
128
128
|
},
|
|
129
129
|
"@uniweb/unipress": {
|
|
130
|
-
"version": "0.10.
|
|
130
|
+
"version": "0.10.23",
|
|
131
131
|
"path": "framework/unipress",
|
|
132
132
|
"deps": [
|
|
133
133
|
"@uniweb/build",
|
package/src/index.js
CHANGED
|
@@ -645,6 +645,15 @@ async function main() {
|
|
|
645
645
|
const command = args[0]
|
|
646
646
|
const pm = detectPackageManager()
|
|
647
647
|
|
|
648
|
+
// ⛔ THE VARIABLE THAT AIMS A PROCESS IS UNIWEB_SERVER — UNIWEB_REGISTER_URL until 2026-10-07.
|
|
649
|
+
// Set ALONE, the old name is refused rather than ignored: ignored, a script that meant a
|
|
650
|
+
// local or staging backend would go to the default one, uniweb.app, and say nothing. Set
|
|
651
|
+
// beside the new one it is ignored, so a script can carry both while CLIs of both ages run it.
|
|
652
|
+
if (process.env.UNIWEB_REGISTER_URL && !process.env.UNIWEB_SERVER) {
|
|
653
|
+
error('UNIWEB_REGISTER_URL is now UNIWEB_SERVER — rename it, and the command goes to the same backend.')
|
|
654
|
+
process.exit(2)
|
|
655
|
+
}
|
|
656
|
+
|
|
648
657
|
// Handle --version / -v
|
|
649
658
|
//
|
|
650
659
|
// Output convention: the version goes to stdout (parseable, scriptable —
|
|
@@ -915,13 +924,13 @@ async function main() {
|
|
|
915
924
|
}
|
|
916
925
|
|
|
917
926
|
// Handle login command — the backend (username/password · paste a token ·
|
|
918
|
-
// --token <bearer>). ⭐ `--
|
|
919
|
-
// backend —
|
|
927
|
+
// --token <bearer>). ⭐ `--server <url>` names it; without the flag it is the DEFAULT
|
|
928
|
+
// backend — UNIWEB_SERVER, else ~/.uniweb/config.json, else https://uniweb.app —
|
|
920
929
|
// and never the project's backend *[Diego, 2026-09-21: "the default backend for login, if
|
|
921
930
|
// not specified, is uniweb.app"]*. ⭐ Except a WORKSPACE SWITCH — `--org` / `--personal`
|
|
922
931
|
// and no way of signing in — which acts on the backend you are logged in to *[Diego,
|
|
923
932
|
// 2026-10-06]* (`resolveLoginOrigin`). The backend logged in to becomes CURRENT, and
|
|
924
|
-
// every backend command goes there (only
|
|
933
|
+
// every backend command goes there (only UNIWEB_SERVER outranks it) — which is why
|
|
925
934
|
// this is the one place a backend is chosen.
|
|
926
935
|
//
|
|
927
936
|
// ⛔ Until 2026-09-21 a bare login went to the backend of the project in the cwd, and
|
|
@@ -932,12 +941,19 @@ async function main() {
|
|
|
932
941
|
const { readFlagValue } = await import('./utils/args.js')
|
|
933
942
|
const { runRegistryLogin } = await import('./utils/registry-auth.js')
|
|
934
943
|
const { resolveLoginOrigin } = await import('./utils/config.js')
|
|
944
|
+
// `--backend` was this flag's name until 2026-10-07 — renamed so it is not read as a
|
|
945
|
+
// site's `backend` service. Refused with the new name, never treated as unknown and dropped:
|
|
946
|
+
// dropped, the login would go to the default backend.
|
|
947
|
+
if (loginArgs.some((a) => a === '--backend' || a.startsWith('--backend='))) {
|
|
948
|
+
console.error('\x1b[31m✗\x1b[0m `--backend` is now `--server`: uniweb login --server <url>')
|
|
949
|
+
process.exit(2)
|
|
950
|
+
}
|
|
935
951
|
let apiBase
|
|
936
952
|
try {
|
|
937
|
-
apiBase = resolveLoginOrigin(readFlagValue(loginArgs, '--
|
|
953
|
+
apiBase = resolveLoginOrigin(readFlagValue(loginArgs, '--server'), loginArgs)
|
|
938
954
|
} catch (err) {
|
|
939
955
|
console.error(
|
|
940
|
-
`\x1b[31m✗\x1b[0m ${err.message} — e.g. uniweb login --
|
|
956
|
+
`\x1b[31m✗\x1b[0m ${err.message} — e.g. uniweb login --server http://localhost:8080`
|
|
941
957
|
)
|
|
942
958
|
process.exit(2)
|
|
943
959
|
}
|
|
@@ -973,6 +989,19 @@ async function main() {
|
|
|
973
989
|
return
|
|
974
990
|
}
|
|
975
991
|
|
|
992
|
+
// Handle site command — the sites in a workspace: list them, unpublish or delete one.
|
|
993
|
+
// Runs anywhere: inside a project it can act on the site the project is synced to.
|
|
994
|
+
if (command === 'site') {
|
|
995
|
+
const { site } = await import('./commands/site.js')
|
|
996
|
+
const result = await site(args.slice(1))
|
|
997
|
+
// ⛔ EXIT ONLY ONCE STDOUT HAS DRAINED. To a pipe Node writes asynchronously, so an exit
|
|
998
|
+
// straight after a long list cut it at 8192 bytes — measured 2026-10-07: `site list --json`
|
|
999
|
+
// of 79 sites reached a reading process as half a JSON document, while the same command
|
|
1000
|
+
// into a file (synchronous) was whole. Pinned by test/site-list-pipe.test.js.
|
|
1001
|
+
await new Promise((resolve) => process.stdout.write('', resolve))
|
|
1002
|
+
process.exit(result?.exitCode ?? 0)
|
|
1003
|
+
}
|
|
1004
|
+
|
|
976
1005
|
// Handle org command (new-backend orgs/units — publish-scope management)
|
|
977
1006
|
if (command === 'org') {
|
|
978
1007
|
const { org } = await import('./commands/org.js')
|
|
@@ -1796,7 +1825,7 @@ logged in to is where the backend commands go (push, pull, publish, status,
|
|
|
1796
1825
|
register, clone) — so this is how you switch. Already logged in to it, login
|
|
1797
1826
|
does nothing; add --password, --browser, --token-paste or --token to log in again.
|
|
1798
1827
|
|
|
1799
|
-
Without --
|
|
1828
|
+
Without --server: https://uniweb.app (or \$UNIWEB_SERVER). No command talks
|
|
1800
1829
|
to a backend you are not logged in to — run one before logging in and it asks first.
|
|
1801
1830
|
|
|
1802
1831
|
${colors.bright}The workspace you work in.${colors.reset} A login works in ONE workspace — your
|
|
@@ -1807,7 +1836,7 @@ or name it. Already logged in, \`uniweb login --org @other\` (or \`--personal\`)
|
|
|
1807
1836
|
the workspace on the backend you are logged in to, without logging in again.
|
|
1808
1837
|
|
|
1809
1838
|
${colors.bright}Options:${colors.reset}
|
|
1810
|
-
--
|
|
1839
|
+
--server <url> The backend to log in to (without it: the default backend — or, for
|
|
1811
1840
|
--org / --personal alone, the one you are logged in to)
|
|
1812
1841
|
--org @org Work in @org (an organization you belong to)
|
|
1813
1842
|
--personal Work in your personal workspace
|
|
@@ -1875,13 +1904,13 @@ so it is deliberately kept away from the refresh half.
|
|
|
1875
1904
|
${colors.cyan}${colors.bright}uniweb forget${colors.reset} ${colors.dim}— Remove what this project recorded about where it synced${colors.reset}
|
|
1876
1905
|
|
|
1877
1906
|
${colors.bright}Usage:${colors.reset}
|
|
1878
|
-
uniweb forget --
|
|
1907
|
+
uniweb forget --server <url>
|
|
1879
1908
|
uniweb forget --all
|
|
1880
1909
|
|
|
1881
1910
|
Local files only. Every site stays where it is on its backend.
|
|
1882
1911
|
|
|
1883
1912
|
${colors.bright}Options:${colors.reset}
|
|
1884
|
-
--
|
|
1913
|
+
--server <url> Forget one backend: its entries in sync.json and in the local
|
|
1885
1914
|
cache, and its deploy records in deploy.yml. Its targets stay,
|
|
1886
1915
|
so the next publish there creates a new site.
|
|
1887
1916
|
--all For a COPY of a project that should become a new one. Deletes
|
|
@@ -2062,6 +2091,7 @@ ${colors.bright}Commands:${colors.reset}
|
|
|
2062
2091
|
sync Catch up, then push (refresh + push)
|
|
2063
2092
|
status Show a site's sync state (unpushed content, foundation)
|
|
2064
2093
|
forget Forget a backend, or --all to make a copied project a new one
|
|
2094
|
+
site <cmd> The sites in a workspace: list, unpublish, delete
|
|
2065
2095
|
inspect <path> Inspect parsed content shape of a markdown file or folder
|
|
2066
2096
|
docs Generate component documentation
|
|
2067
2097
|
families List the standard section families (for meta.js family:)
|
|
@@ -2093,8 +2123,8 @@ ${colors.bright}Global Options:${colors.reset}
|
|
|
2093
2123
|
|
|
2094
2124
|
Backend commands (push, pull, publish, status, register, clone) go to the
|
|
2095
2125
|
backend you are logged in to, with its session. Switch with
|
|
2096
|
-
\`uniweb login --
|
|
2097
|
-
a script can aim and authenticate one process with
|
|
2126
|
+
\`uniweb login --server <url>\` (add \`--token <bearer>\` to sign in with a token);
|
|
2127
|
+
a script can aim and authenticate one process with UNIWEB_SERVER and
|
|
2098
2128
|
UNIWEB_TOKEN instead.
|
|
2099
2129
|
|
|
2100
2130
|
${colors.bright}Push Options:${colors.reset}
|
package/src/utils/config.js
CHANGED
|
@@ -53,7 +53,7 @@ function readCliConfig() {
|
|
|
53
53
|
/**
|
|
54
54
|
* The origin the LAST `uniweb login` authenticated against — persisted on the
|
|
55
55
|
* session record so subsequent verbs default to the backend you logged into
|
|
56
|
-
* (no
|
|
56
|
+
* (no backend flag per command). Sync read; null when there's no session or it
|
|
57
57
|
* carries no origin (older sessions). Read directly (not via registry-auth.js)
|
|
58
58
|
* to keep this module off the optional-peer / import-cycle path.
|
|
59
59
|
* @returns {string|null}
|
|
@@ -91,7 +91,7 @@ function originOrNull(value) {
|
|
|
91
91
|
* **The default backend** — where a bare `uniweb login` goes, and where a backend command
|
|
92
92
|
* goes when nobody is logged in (the login it asks for is then this one).
|
|
93
93
|
*
|
|
94
|
-
* `
|
|
94
|
+
* `UNIWEB_SERVER`, else `~/.uniweb/config.json` `registryApiUrl`, else
|
|
95
95
|
* https://uniweb.app. ⛔ **Never the current session** *[Diego, 2026-09-21: "the default
|
|
96
96
|
* backend for login, if not specified, is uniweb.app"]* — a bare `uniweb login` means the
|
|
97
97
|
* default backend, not "the one I am already on".
|
|
@@ -100,7 +100,7 @@ function originOrNull(value) {
|
|
|
100
100
|
*/
|
|
101
101
|
export function getDefaultBackendOrigin() {
|
|
102
102
|
return (
|
|
103
|
-
originOrNull(process.env.
|
|
103
|
+
originOrNull(process.env.UNIWEB_SERVER) ||
|
|
104
104
|
originOrNull(readCliConfig().registryApiUrl) ||
|
|
105
105
|
DEFAULT_BACKEND_ORIGIN
|
|
106
106
|
)
|
|
@@ -108,7 +108,7 @@ export function getDefaultBackendOrigin() {
|
|
|
108
108
|
|
|
109
109
|
/**
|
|
110
110
|
* The `uniweb login` that SWITCHES the workspace on `origin` — the caller appends `--org @x`
|
|
111
|
-
* or `--personal` — with `--
|
|
111
|
+
* or `--personal` — with `--server` unless a switch without it reaches `origin`
|
|
112
112
|
* (`resolveLoginOrigin`): the backend you are logged in to, or, logged in nowhere, the
|
|
113
113
|
* default backend.
|
|
114
114
|
*
|
|
@@ -118,12 +118,12 @@ export function getDefaultBackendOrigin() {
|
|
|
118
118
|
*
|
|
119
119
|
* @param {string} origin - the backend the hint is about
|
|
120
120
|
* @param {string} [prefix='uniweb'] - how the user runs the CLI (`getCliPrefix`)
|
|
121
|
-
* @returns {string} e.g. `uniweb login`, or `uniweb login --
|
|
121
|
+
* @returns {string} e.g. `uniweb login`, or `uniweb login --server http://localhost:8080`
|
|
122
122
|
*/
|
|
123
123
|
export function loginCommand(origin, prefix = 'uniweb') {
|
|
124
124
|
const o = originOrNull(origin)
|
|
125
125
|
const reached = originOrNull(loggedInOrigin()) || getDefaultBackendOrigin()
|
|
126
|
-
return o && o !== reached ? `${prefix} login --
|
|
126
|
+
return o && o !== reached ? `${prefix} login --server ${o}` : `${prefix} login`
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
/** The flags with which `uniweb login` SIGNS IN — a method, or a credential. */
|
|
@@ -145,7 +145,7 @@ export function isWorkspaceSwitch(args = []) {
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
/**
|
|
148
|
-
* The backend `uniweb login` logs in to: `--
|
|
148
|
+
* The backend `uniweb login` logs in to: `--server`; else, for a workspace switch, the
|
|
149
149
|
* backend you are logged in to; else the default backend.
|
|
150
150
|
*
|
|
151
151
|
* ⭐ A SWITCH ACTS ON YOUR SESSION *[Diego, 2026-10-06: "`uniweb login --org @x` or
|
|
@@ -157,14 +157,14 @@ export function isWorkspaceSwitch(args = []) {
|
|
|
157
157
|
* uniweb.app"]*. ⛔ Until 2026-10-06 a switch went to the default backend too, so on any
|
|
158
158
|
* other backend it began a new login there, logging you out of the one you were on.
|
|
159
159
|
*
|
|
160
|
-
* ⛔ A mistyped `--
|
|
160
|
+
* ⛔ A mistyped `--server` is an error, never a fallback — it would log you in, and so
|
|
161
161
|
* point every command, somewhere you did not name.
|
|
162
162
|
*
|
|
163
163
|
* @param {string|null|undefined} flag - `readFlagValue`'s answer: undefined when the
|
|
164
164
|
* flag is absent, null when it was given with no value
|
|
165
165
|
* @param {string[]} [args] - the login's argv, to tell a switch from a sign-in
|
|
166
166
|
* @returns {string}
|
|
167
|
-
* @throws {Error} when --
|
|
167
|
+
* @throws {Error} when --server was given and is not a URL
|
|
168
168
|
*/
|
|
169
169
|
export function resolveLoginOrigin(flag, args = []) {
|
|
170
170
|
if (flag === undefined) {
|
|
@@ -172,7 +172,7 @@ export function resolveLoginOrigin(flag, args = []) {
|
|
|
172
172
|
return session || getDefaultBackendOrigin()
|
|
173
173
|
}
|
|
174
174
|
const origin = originOrNull(flag)
|
|
175
|
-
if (!origin) throw new Error(flag ? `Not a URL: ${flag}` : '--
|
|
175
|
+
if (!origin) throw new Error(flag ? `Not a URL: ${flag}` : '--server needs a URL')
|
|
176
176
|
return origin
|
|
177
177
|
}
|
|
178
178
|
|
|
@@ -180,12 +180,12 @@ export function resolveLoginOrigin(flag, args = []) {
|
|
|
180
180
|
* **The backend a command talks to** — the base of every `/dev/*` route (`register`
|
|
181
181
|
* POSTs to {origin}/dev/registry/register, and so on).
|
|
182
182
|
*
|
|
183
|
-
* `
|
|
183
|
+
* `UNIWEB_SERVER` > **the backend the user is logged in to** > the default
|
|
184
184
|
* (`getDefaultBackendOrigin`). ⭐ Nothing talks to a backend the user is not logged in
|
|
185
185
|
* to *[Diego, 2026-09-21]*: when this falls through to the default, the command's first
|
|
186
186
|
* request asks for that login — so the default is where the login goes, never a
|
|
187
187
|
* backend reached without one. `resolveBackendOrigin` (backend/client.js) returns this
|
|
188
|
-
* unchanged: no command takes a
|
|
188
|
+
* unchanged: no command takes a backend flag. ⛔ *Until 2026-10-05 this said "when no
|
|
189
189
|
* `--backend` is given" and that `resolveBackendOrigin` put `--backend` on top — read as
|
|
190
190
|
* a per-command flag outranking the login. The verbs lost that flag on 2026-09-21.*
|
|
191
191
|
*
|
|
@@ -193,7 +193,7 @@ export function resolveLoginOrigin(flag, args = []) {
|
|
|
193
193
|
*/
|
|
194
194
|
export function getRegistryApiBaseUrl() {
|
|
195
195
|
return (
|
|
196
|
-
originOrNull(process.env.
|
|
196
|
+
originOrNull(process.env.UNIWEB_SERVER) ||
|
|
197
197
|
originOrNull(loggedInOrigin()) ||
|
|
198
198
|
getDefaultBackendOrigin()
|
|
199
199
|
)
|
package/src/utils/flag-guard.js
CHANGED
|
@@ -9,14 +9,15 @@
|
|
|
9
9
|
* Tolerable for a cosmetic flag; dangerous for any flag that aims the command or picks
|
|
10
10
|
* its identity. This turns that class into one sentence.
|
|
11
11
|
*
|
|
12
|
-
* ⛔
|
|
12
|
+
* ⛔ **A backend flag and `--token` are NOT flags of these verbs** *(2026-09-21)*. Every
|
|
13
13
|
* backend verb goes to the backend you are logged in to, with that login's session:
|
|
14
|
-
* switching and signing in are `uniweb login` (`--
|
|
15
|
-
* aims one process with
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
14
|
+
* switching and signing in are `uniweb login` (`--server`, `--token`), and a script
|
|
15
|
+
* aims one process with UNIWEB_SERVER + UNIWEB_TOKEN. Passed to a verb, each is an
|
|
16
|
+
* unknown flag — and this guard is what makes that a loud error, with a pointer to the
|
|
17
|
+
* login, instead of a command that quietly runs against wherever and as whoever you
|
|
18
|
+
* happen to be. `--server` stays on `forget`, where it SELECTS which backend's records
|
|
19
|
+
* to remove. ⛔ *The flag was `--backend` until 2026-10-07, renamed so it is not read as
|
|
20
|
+
* a site's `backend` service; `--backend` is refused everywhere, naming `--server`.*
|
|
20
21
|
*
|
|
21
22
|
* ⚠️ A wrong rejection is worse than a missed one — it breaks an invocation that
|
|
22
23
|
* works — so the per-command lists must be complete, INCLUDING flags read by
|
|
@@ -93,12 +94,17 @@ const VERBS = {
|
|
|
93
94
|
'--personal'
|
|
94
95
|
],
|
|
95
96
|
/**
|
|
96
|
-
* `forget` = remove one backend's records (`--
|
|
97
|
+
* `forget` = remove one backend's records (`--server <url>`), or everything a
|
|
97
98
|
* copied project inherited (`--all`). One of the two is required — the verb refuses
|
|
98
99
|
* without it — and they exclude each other. `--non-interactive` reaches it through
|
|
99
100
|
* resolveSiteDir in a workspace of several sites.
|
|
100
101
|
*/
|
|
101
|
-
forget: ['--
|
|
102
|
+
forget: ['--server', '--all'],
|
|
103
|
+
/**
|
|
104
|
+
* `site list | unpublish | delete` — a workspace's sites. `--yes` confirms a write;
|
|
105
|
+
* `--json` is `list`'s porcelain; the workspace is `--org` / `--personal`.
|
|
106
|
+
*/
|
|
107
|
+
site: ['--json', '--yes', '--org', '--personal'],
|
|
102
108
|
status: [
|
|
103
109
|
'--json', '--remote', '--dry-run',
|
|
104
110
|
'--force', '--no-verify', '--no-validate', '--yes', '--org', '--personal',
|
|
@@ -191,16 +197,24 @@ export function checkFlags(verb, args = []) {
|
|
|
191
197
|
if (!unknown.length) return null
|
|
192
198
|
|
|
193
199
|
const flag = unknown[0]
|
|
194
|
-
// ⭐ `--backend` and `--token` are not typos on these verbs — they
|
|
195
|
-
// (2026-09-21), and
|
|
196
|
-
|
|
200
|
+
// ⭐ `--backend`, `--server` and `--token` are not typos on these verbs — they aim a
|
|
201
|
+
// command or pick its identity, which only the login does (2026-09-21), and `--backend`
|
|
202
|
+
// is `--server` since 2026-10-07. The useful answer is where each went, not "run --help".
|
|
203
|
+
if (flag === '--backend' && verb === 'forget') {
|
|
204
|
+
return {
|
|
205
|
+
flag,
|
|
206
|
+
suggestion: '--server',
|
|
207
|
+
message: '`--backend` is now `--server`: uniweb forget --server <url>'
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (flag === '--backend' || flag === '--server') {
|
|
197
211
|
return {
|
|
198
212
|
flag,
|
|
199
213
|
suggestion: null,
|
|
200
214
|
message: [
|
|
201
|
-
`\`uniweb ${verb}\` has no
|
|
202
|
-
' Switch with: uniweb login --
|
|
203
|
-
' (A script can aim one process with
|
|
215
|
+
`\`uniweb ${verb}\` has no \`${flag}\`: it goes to the backend you are logged in to.`,
|
|
216
|
+
' Switch with: uniweb login --server <url>',
|
|
217
|
+
' (A script can aim one process with UNIWEB_SERVER instead.)'
|
|
204
218
|
].join('\n')
|
|
205
219
|
}
|
|
206
220
|
}
|
|
@@ -210,7 +224,7 @@ export function checkFlags(verb, args = []) {
|
|
|
210
224
|
suggestion: null,
|
|
211
225
|
message: [
|
|
212
226
|
`\`uniweb ${verb}\` has no \`--token\`: it uses the session of the backend you are logged in to.`,
|
|
213
|
-
' Sign in with a token: uniweb login --
|
|
227
|
+
' Sign in with a token: uniweb login --server <url> --token <bearer>',
|
|
214
228
|
' (A script can authenticate one process with UNIWEB_TOKEN instead.)'
|
|
215
229
|
].join('\n')
|
|
216
230
|
}
|
|
@@ -116,8 +116,8 @@ export function syncedElsewhere(siteDir, origin) {
|
|
|
116
116
|
|
|
117
117
|
/**
|
|
118
118
|
* The heads-up for `syncedElsewhere`, as lines — each verb prints them with its own
|
|
119
|
-
* reporter. Only worth saying when the verb was not TOLD where to go: a
|
|
120
|
-
*
|
|
119
|
+
* reporter. Only worth saying when the verb was not TOLD where to go: a backend named with
|
|
120
|
+
* UNIWEB_SERVER is already a decision.
|
|
121
121
|
*
|
|
122
122
|
* ⚖️ Worded for where the verb GOES, not for why: it is the logged-in backend, or — logged
|
|
123
123
|
* in nowhere — the default one, where the login it is about to ask for will be.
|
|
@@ -130,7 +130,7 @@ export function describeSyncedElsewhere(known, origin, verb) {
|
|
|
130
130
|
const where = known.length === 1 ? `site is on ${known[0]}` : `sites are on ${known.join(', ')}`
|
|
131
131
|
return [
|
|
132
132
|
`This project's ${where} — not on ${origin}, where this ${verb} goes.`,
|
|
133
|
-
`It creates a new site there. To ${verb} to ${known.length === 1 ? 'that one' : 'one of those'} instead: uniweb login --
|
|
133
|
+
`It creates a new site there. To ${verb} to ${known.length === 1 ? 'that one' : 'one of those'} instead: uniweb login --server <url>`
|
|
134
134
|
]
|
|
135
135
|
}
|
|
136
136
|
|
package/src/utils/yaml-edit.js
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* In-place edits to a YAML file a person wrote (`site.yml`), for a command that
|
|
3
|
-
* must change one value and leave everything else as it was: comments, key
|
|
4
|
-
* order, blank lines, quoting, a flow list written on one line.
|
|
5
|
-
*
|
|
6
|
-
* ⛔ Never load a hand-written file and dump it back. js-yaml's `dump` drops every
|
|
7
|
-
* comment and re-flows lists and long strings — `scaffold.js` records what that
|
|
8
|
-
* did to the templates whose comments are the point of them.
|
|
9
|
-
*
|
|
10
|
-
* ⭐ Every edit is VERIFIED: the edited text must parse to exactly the old data
|
|
11
|
-
* with that one value changed. A value written in a form the line-level edit
|
|
12
|
-
* cannot reach (a block scalar, an entry split across lines) returns null
|
|
13
|
-
* rather than a guess, so the caller can refuse before it changes anything.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { isDeepStrictEqual } from 'node:util'
|
|
17
|
-
import yaml from 'js-yaml'
|
|
18
|
-
|
|
19
|
-
const escapeRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
20
|
-
|
|
21
|
-
/** A scalar as written after `key: `, quoted only when YAML needs it (`@scope/name` does). */
|
|
22
|
-
const scalar = (value) => yaml.dump(value, { lineWidth: -1 }).trim()
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Where a trailing ` # comment` begins in the text after `key:`, outside quotes,
|
|
26
|
-
* including the whitespace before it. -1 when the line has none.
|
|
27
|
-
*/
|
|
28
|
-
function commentStart(rest) {
|
|
29
|
-
let quote = null
|
|
30
|
-
for (let i = 0; i < rest.length; i++) {
|
|
31
|
-
const ch = rest[i]
|
|
32
|
-
if (quote) {
|
|
33
|
-
if (ch === quote) quote = null
|
|
34
|
-
} else if (ch === '"' || ch === "'") {
|
|
35
|
-
quote = ch
|
|
36
|
-
} else if (ch === '#' && i > 0 && /\s/.test(rest[i - 1])) {
|
|
37
|
-
let start = i
|
|
38
|
-
while (start > 0 && /\s/.test(rest[start - 1])) start--
|
|
39
|
-
return start
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return -1
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** The edited text when it parses to `expected`, else null. */
|
|
46
|
-
function verified(after, expected) {
|
|
47
|
-
try {
|
|
48
|
-
return isDeepStrictEqual(yaml.load(after) ?? {}, expected) ? after : null
|
|
49
|
-
} catch {
|
|
50
|
-
return null
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function load(text) {
|
|
55
|
-
try {
|
|
56
|
-
const data = yaml.load(text) ?? {}
|
|
57
|
-
return data && typeof data === 'object' && !Array.isArray(data) ? data : null
|
|
58
|
-
} catch {
|
|
59
|
-
return null
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Set a top-level, one-line scalar `key` to `value`, keeping the line's inline
|
|
65
|
-
* comment and every other line as it was.
|
|
66
|
-
*
|
|
67
|
-
* @param {string} text - the file's contents
|
|
68
|
-
* @param {string} key - a top-level key, e.g. `foundation`
|
|
69
|
-
* @param {string} value
|
|
70
|
-
* @returns {string|null} the new contents, or null when the edit cannot be made in place
|
|
71
|
-
*/
|
|
72
|
-
export function setTopLevelScalar(text, key, value) {
|
|
73
|
-
const data = load(text)
|
|
74
|
-
if (!data) return null
|
|
75
|
-
const match = new RegExp(`^${escapeRegex(key)}:([^\\n]*)$`, 'm').exec(text)
|
|
76
|
-
if (!match) return null
|
|
77
|
-
const at = commentStart(match[1])
|
|
78
|
-
const comment = at === -1 ? '' : match[1].slice(at)
|
|
79
|
-
const line = `${key}: ${scalar(value)}${comment}`
|
|
80
|
-
const after = text.slice(0, match.index) + line + text.slice(match.index + match[0].length)
|
|
81
|
-
return verified(after, { ...data, [key]: value })
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Replace entries of a top-level list `key` — each `from` value becomes its `to`
|
|
86
|
-
* — wherever an entry is written on a line of its own or inside a one-line flow
|
|
87
|
-
* list, plain or quoted. A value that only CONTAINS `from` is left alone, and so
|
|
88
|
-
* is a comment line.
|
|
89
|
-
*
|
|
90
|
-
* @param {string} text - the file's contents
|
|
91
|
-
* @param {string} key - a top-level list key, e.g. `extensions`
|
|
92
|
-
* @param {Map<string, string>} replacements - old entry → new entry
|
|
93
|
-
* @returns {string|null} the new contents, or null when the edit cannot be made in place
|
|
94
|
-
*/
|
|
95
|
-
export function replaceInTopLevelList(text, key, replacements) {
|
|
96
|
-
const data = load(text)
|
|
97
|
-
if (!data || !Array.isArray(data[key])) return null
|
|
98
|
-
const expected = { ...data, [key]: data[key].map((v) => (replacements.has(v) ? replacements.get(v) : v)) }
|
|
99
|
-
|
|
100
|
-
const after = text
|
|
101
|
-
.split('\n')
|
|
102
|
-
.map((line) => {
|
|
103
|
-
if (/^\s*#/.test(line)) return line
|
|
104
|
-
for (const [from, to] of replacements) {
|
|
105
|
-
const f = escapeRegex(from)
|
|
106
|
-
line = line
|
|
107
|
-
.replace(new RegExp(`'${f}'`, 'g'), () => `'${to.replace(/'/g, "''")}'`)
|
|
108
|
-
.replace(new RegExp(`"${f}"`, 'g'), () => JSON.stringify(to))
|
|
109
|
-
.replace(new RegExp(`(^|[\\s\\[,])${f}(?=$|[\\s,\\]])`, 'g'), (_, lead) => lead + to)
|
|
110
|
-
}
|
|
111
|
-
return line
|
|
112
|
-
})
|
|
113
|
-
.join('\n')
|
|
114
|
-
return verified(after, expected)
|
|
115
|
-
}
|