uniweb 0.84.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 +6 -6
- package/partials/agents.md +126 -80
- package/src/backend/client.js +59 -6
- package/src/backend/foundation-bring-along.js +5 -3
- package/src/backend/service-request.js +91 -145
- package/src/backend/site-preview.js +1 -1
- package/src/backend/site-sync.js +161 -54
- package/src/commands/build.js +6 -2
- package/src/commands/clone.js +1 -1
- package/src/commands/deploy.js +3 -3
- package/src/commands/doctor.js +32 -36
- package/src/commands/forget.js +7 -7
- package/src/commands/publish.js +28 -30
- package/src/commands/pull.js +22 -11
- package/src/commands/push.js +19 -28
- 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/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
|
-
}
|