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
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `uniweb site` — the sites in a workspace, on the backend you are logged in to.
|
|
3
|
+
*
|
|
4
|
+
* uniweb site list [--json] each site's name, uuid, and whether it is live
|
|
5
|
+
* uniweb site unpublish [<site-uuid>] [--yes] take a site offline; its content stays
|
|
6
|
+
* uniweb site delete [<site-uuid>] [--yes] delete a site — final, no restore
|
|
7
|
+
*
|
|
8
|
+
* Without a uuid, `unpublish` and `delete` act on the site this project is synced to on
|
|
9
|
+
* that backend. The workspace is the command's (`--org @x` / `--personal`), else the
|
|
10
|
+
* login's — as for every site request.
|
|
11
|
+
*
|
|
12
|
+
* ⭐ A live site is not deleted: the backend refuses while it is published, so `delete`
|
|
13
|
+
* says to unpublish it first, and anything else still active (a plan, a domain, stored
|
|
14
|
+
* form messages) is named in the backend's own words and resolved in the app. A delete
|
|
15
|
+
* of the project's own site also drops the project's record of it, as `uniweb forget`
|
|
16
|
+
* does, so its next push creates a new site.
|
|
17
|
+
*
|
|
18
|
+
* ⛔ Both write verbs ask first, and `--yes` is the only way past the question. Without
|
|
19
|
+
* a terminal they refuse rather than print what they would have done: a script that
|
|
20
|
+
* reads exit 0 as "deleted" must not be told 0 for nothing.
|
|
21
|
+
*
|
|
22
|
+
* Auth: the session of the backend you are logged in to (`uniweb login`) / UNIWEB_TOKEN.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { existsSync } from 'node:fs'
|
|
26
|
+
import { join, resolve } from 'node:path'
|
|
27
|
+
|
|
28
|
+
import { BackendClient, WorkspaceMismatchError, describeWorkspace } from '../backend/client.js'
|
|
29
|
+
import { resolveWorkspace } from '../backend/workspace.js'
|
|
30
|
+
import { checkFlags } from '../utils/flag-guard.js'
|
|
31
|
+
import { confirm, isNonInteractive } from '../utils/interactive.js'
|
|
32
|
+
import { readSiteIdentity } from '../utils/site-identity.js'
|
|
33
|
+
import { findSites, findWorkspaceRoot } from '../utils/workspace.js'
|
|
34
|
+
|
|
35
|
+
const c = {
|
|
36
|
+
reset: '\x1b[0m',
|
|
37
|
+
bold: '\x1b[1m',
|
|
38
|
+
dim: '\x1b[2m',
|
|
39
|
+
red: '\x1b[31m',
|
|
40
|
+
green: '\x1b[32m',
|
|
41
|
+
yellow: '\x1b[33m',
|
|
42
|
+
cyan: '\x1b[36m'
|
|
43
|
+
}
|
|
44
|
+
// As in every verb: errors on stderr, the rest on stdout — and under `--json` nothing but
|
|
45
|
+
// the JSON on stdout.
|
|
46
|
+
const say = {
|
|
47
|
+
ok: (m) => console.log(`${c.green}✓${c.reset} ${m}`),
|
|
48
|
+
info: (m) => console.log(`${c.cyan}→${c.reset} ${m}`),
|
|
49
|
+
warn: (m) => console.log(`${c.yellow}⚠${c.reset} ${m}`),
|
|
50
|
+
err: (m) => console.error(`${c.red}✗${c.reset} ${m}`),
|
|
51
|
+
dim: (m) => console.log(` ${c.dim}${m}${c.reset}`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const USAGE = [
|
|
55
|
+
'uniweb site <command>',
|
|
56
|
+
' list [--json] The sites in the workspace: name, uuid, whether live',
|
|
57
|
+
' unpublish [<site-uuid>] [--yes] Take a site offline (its content stays)',
|
|
58
|
+
' delete [<site-uuid>] [--yes] Delete a site — final',
|
|
59
|
+
'',
|
|
60
|
+
'Without a uuid, unpublish and delete act on the site this project is synced to.',
|
|
61
|
+
'The workspace is --org @x or --personal, else the one chosen at login.'
|
|
62
|
+
].join('\n')
|
|
63
|
+
|
|
64
|
+
/** A site-content uuid, loosely: enough to tell one from a site's name. */
|
|
65
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
66
|
+
|
|
67
|
+
/** The arguments that are not flags — skipping `--org`'s value. */
|
|
68
|
+
function positionals(args) {
|
|
69
|
+
const out = []
|
|
70
|
+
for (let i = 0; i < args.length; i++) {
|
|
71
|
+
const a = args[i]
|
|
72
|
+
if (a === '--org') {
|
|
73
|
+
i++
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
if (a.startsWith('-')) continue
|
|
77
|
+
out.push(a)
|
|
78
|
+
}
|
|
79
|
+
return out
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The site directory this command runs in — the site itself (it holds `site.yml`), or a
|
|
84
|
+
* workspace's only site. ⛔ Never a stop: outside a project — or from an install without
|
|
85
|
+
* `@uniweb/build`, which finding a workspace's sites may load — there is simply none.
|
|
86
|
+
*/
|
|
87
|
+
async function projectSiteDir() {
|
|
88
|
+
try {
|
|
89
|
+
const cwd = process.cwd()
|
|
90
|
+
if (existsSync(join(cwd, 'site.yml'))) return { dir: cwd }
|
|
91
|
+
const root = findWorkspaceRoot(cwd)
|
|
92
|
+
if (!root) return {}
|
|
93
|
+
const sites = await findSites(root)
|
|
94
|
+
if (sites.length === 1) return { dir: resolve(root, sites[0]) }
|
|
95
|
+
return sites.length > 1 ? { many: sites } : {}
|
|
96
|
+
} catch {
|
|
97
|
+
return {}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** A client on the backend you are logged in to, working in the command's workspace. */
|
|
102
|
+
async function connect(args, command) {
|
|
103
|
+
const client = new BackendClient({ args, command })
|
|
104
|
+
const ws = await resolveWorkspace({ client, args })
|
|
105
|
+
if (ws.refused) return { refused: ws.reason }
|
|
106
|
+
client.setWorkspace(ws.workspace, { source: ws.source })
|
|
107
|
+
return { client, workspace: ws.workspace }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Every site in the workspace, page by page. */
|
|
111
|
+
async function allSites(client) {
|
|
112
|
+
const limit = 1000
|
|
113
|
+
const sites = []
|
|
114
|
+
for (let offset = 0; ; offset += limit) {
|
|
115
|
+
const res = await client.listSites({ limit, offset })
|
|
116
|
+
if (!res.ok) throw new Error(`Could not list the sites: HTTP ${res.status} ${res.statusText}`)
|
|
117
|
+
const page = (await res.json().catch(() => null))?.sites
|
|
118
|
+
if (!Array.isArray(page)) throw new Error('Could not list the sites: the backend sent no list.')
|
|
119
|
+
sites.push(...page)
|
|
120
|
+
if (page.length < limit) return sites
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function list(args) {
|
|
125
|
+
const json = args.includes('--json')
|
|
126
|
+
const conn = await connect(args, 'Listing sites')
|
|
127
|
+
if (conn.refused) {
|
|
128
|
+
say.err(conn.refused)
|
|
129
|
+
return { exitCode: 2 }
|
|
130
|
+
}
|
|
131
|
+
const { client, workspace } = conn
|
|
132
|
+
const sites = await allSites(client)
|
|
133
|
+
const rows = sites.map((s) => ({
|
|
134
|
+
uuid: s.uuid,
|
|
135
|
+
name: s.name ?? null,
|
|
136
|
+
status: s.deployment?.status ?? null,
|
|
137
|
+
published: s.deployment?.status === 'published',
|
|
138
|
+
url: s.deployment?.published_url ?? null,
|
|
139
|
+
updated_at: s.updated_at ?? null
|
|
140
|
+
}))
|
|
141
|
+
if (json) {
|
|
142
|
+
console.log(JSON.stringify({ backend: client.origin, workspace: workspace ?? null, sites: rows }))
|
|
143
|
+
return { exitCode: 0 }
|
|
144
|
+
}
|
|
145
|
+
if (!rows.length) {
|
|
146
|
+
say.info(`No sites in ${describeWorkspace(workspace)} on ${client.origin}.`)
|
|
147
|
+
return { exitCode: 0 }
|
|
148
|
+
}
|
|
149
|
+
console.log(`${c.bold}Sites in ${describeWorkspace(workspace)}${c.reset} ${c.dim}on ${client.origin}${c.reset}`)
|
|
150
|
+
for (const r of rows) {
|
|
151
|
+
const state = r.published ? `${c.green}published${c.reset}` : `${c.dim}${r.status ?? 'never published'}${c.reset}`
|
|
152
|
+
const name = r.name ?? `${c.dim}(no name)${c.reset}`
|
|
153
|
+
console.log(` ${name} ${c.dim}${r.uuid}${c.reset} ${state}${r.published && r.url ? ` ${r.url}` : ''}`)
|
|
154
|
+
}
|
|
155
|
+
return { exitCode: 0 }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The site a write verb acts on: the uuid given, else this project's site on the
|
|
160
|
+
* backend the client goes to. Null, with the reason said, when there is neither.
|
|
161
|
+
*/
|
|
162
|
+
async function targetSite(args, client, verb) {
|
|
163
|
+
const [, given] = positionals(args)
|
|
164
|
+
if (given) {
|
|
165
|
+
if (!UUID.test(given)) {
|
|
166
|
+
say.err(`\`${given}\` is not a site uuid.`)
|
|
167
|
+
say.dim('`uniweb site list` shows each site with its uuid.')
|
|
168
|
+
return null
|
|
169
|
+
}
|
|
170
|
+
const project = await projectSiteDir()
|
|
171
|
+
const mine = project.dir ? readSiteIdentity(project.dir, client.origin).uuid : null
|
|
172
|
+
return { uuid: given, siteDir: mine === given ? project.dir : null }
|
|
173
|
+
}
|
|
174
|
+
const project = await projectSiteDir()
|
|
175
|
+
if (project.many) {
|
|
176
|
+
say.err(`This workspace has ${project.many.length} sites — name one: uniweb site ${verb} <site-uuid>, or run it inside the site.`)
|
|
177
|
+
return null
|
|
178
|
+
}
|
|
179
|
+
const uuid = project.dir ? readSiteIdentity(project.dir, client.origin).uuid : null
|
|
180
|
+
if (!uuid) {
|
|
181
|
+
say.err(
|
|
182
|
+
project.dir
|
|
183
|
+
? `This project has no site on ${client.origin} — name one: uniweb site ${verb} <site-uuid>`
|
|
184
|
+
: `No site named, and this is not a Uniweb project — name one: uniweb site ${verb} <site-uuid>`
|
|
185
|
+
)
|
|
186
|
+
say.dim('`uniweb site list` shows each site with its uuid.')
|
|
187
|
+
return null
|
|
188
|
+
}
|
|
189
|
+
return { uuid, siteDir: project.dir }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** The site's row in the workspace's list, or null when the workspace does not list it. */
|
|
193
|
+
async function findSite(client, uuid) {
|
|
194
|
+
try {
|
|
195
|
+
return (await allSites(client)).find((s) => s.uuid === uuid) ?? null
|
|
196
|
+
} catch {
|
|
197
|
+
return null
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Ask, unless `--yes`. The exit code when the answer is not yes: 0 for a person who
|
|
203
|
+
* declined, 2 where nobody could be asked — so a script never reads 0 for nothing done.
|
|
204
|
+
*/
|
|
205
|
+
async function confirmed(args, question, verb) {
|
|
206
|
+
if (args.includes('--yes')) return { yes: true }
|
|
207
|
+
if (isNonInteractive(args)) {
|
|
208
|
+
say.err(`\`uniweb site ${verb}\` asks before it acts — pass --yes to confirm it here.`)
|
|
209
|
+
return { yes: false, exitCode: 2 }
|
|
210
|
+
}
|
|
211
|
+
if (await confirm(question, false)) return { yes: true }
|
|
212
|
+
say.info('Cancelled — nothing changed.')
|
|
213
|
+
return { yes: false, exitCode: 0 }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** What the backend said about a refused write, in its own words where it gave them. */
|
|
217
|
+
async function sayRefusal(res, { uuid, verb, origin }) {
|
|
218
|
+
const body = await res.json().catch(() => null)
|
|
219
|
+
if (res.status === 409 && Array.isArray(body?.blockers) && body.blockers.length) {
|
|
220
|
+
say.err(`The site cannot be deleted yet:`)
|
|
221
|
+
for (const b of body.blockers) {
|
|
222
|
+
// ⭐ `detail` is written for the site's owner; the set of blockers is open, so an
|
|
223
|
+
// unknown one is shown by its detail, never dropped.
|
|
224
|
+
console.error(` • ${b.detail || b.resource}`)
|
|
225
|
+
if (b.resource === 'published') console.error(` ${c.dim}→ uniweb site unpublish ${uuid}${c.reset}`)
|
|
226
|
+
}
|
|
227
|
+
if (body.blockers.some((b) => b.resource !== 'published')) {
|
|
228
|
+
say.dim('Anything else listed is resolved in the app.')
|
|
229
|
+
}
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
if (res.status === 401) {
|
|
233
|
+
say.err(`Your session on ${origin} is not accepted — sign in again: uniweb login`)
|
|
234
|
+
} else if (res.status === 403) {
|
|
235
|
+
say.err(`You may not ${verb} this site — only its owner or an admin of its workspace can.`)
|
|
236
|
+
} else if (res.status === 404) {
|
|
237
|
+
say.err(`${origin} has no site ${uuid}.`)
|
|
238
|
+
} else {
|
|
239
|
+
say.err(`The backend refused: HTTP ${res.status}${body?.detail ? ` — ${body.detail}` : ''}`)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* The project's record of a site it no longer has — what `uniweb forget` removes for one
|
|
245
|
+
* backend. Loaded here, not at the top: it needs `@uniweb/build`, which a command run
|
|
246
|
+
* outside a project does not have.
|
|
247
|
+
*/
|
|
248
|
+
async function forgetDeletedSite(siteDir, origin) {
|
|
249
|
+
try {
|
|
250
|
+
const { clearBackend } = await import('@uniweb/build/uwx')
|
|
251
|
+
const { forgetBackendCache } = await import('../backend/site-sync.js')
|
|
252
|
+
const { forgetDeploys } = await import('@uniweb/build/site')
|
|
253
|
+
clearBackend(siteDir, origin)
|
|
254
|
+
forgetBackendCache(siteDir, origin)
|
|
255
|
+
await forgetDeploys(siteDir, origin)
|
|
256
|
+
return true
|
|
257
|
+
} catch {
|
|
258
|
+
return false
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function unpublish(args) {
|
|
263
|
+
const conn = await connect(args, 'Unpublishing a site')
|
|
264
|
+
if (conn.refused) {
|
|
265
|
+
say.err(conn.refused)
|
|
266
|
+
return { exitCode: 2 }
|
|
267
|
+
}
|
|
268
|
+
const { client } = conn
|
|
269
|
+
const target = await targetSite(args, client, 'unpublish')
|
|
270
|
+
if (!target) return { exitCode: 2 }
|
|
271
|
+
const site = await findSite(client, target.uuid)
|
|
272
|
+
const label = site?.name ? `“${site.name}” (${target.uuid})` : target.uuid
|
|
273
|
+
const url = site?.deployment?.published_url
|
|
274
|
+
const ok = await confirmed(
|
|
275
|
+
args,
|
|
276
|
+
`Unpublish ${label} on ${client.origin}?${url ? ` Visitors get nothing at ${url} until it is published again.` : ''}`,
|
|
277
|
+
'unpublish'
|
|
278
|
+
)
|
|
279
|
+
if (!ok.yes) return { exitCode: ok.exitCode }
|
|
280
|
+
|
|
281
|
+
const res = await client.unpublishSite(target.uuid)
|
|
282
|
+
if (!res.ok) {
|
|
283
|
+
await sayRefusal(res, { uuid: target.uuid, verb: 'unpublish', origin: client.origin })
|
|
284
|
+
return { exitCode: 1 }
|
|
285
|
+
}
|
|
286
|
+
const body = await res.json().catch(() => null)
|
|
287
|
+
if (body?.was_published === false) {
|
|
288
|
+
say.info(`${label} was not published — nothing to take down.`)
|
|
289
|
+
} else {
|
|
290
|
+
say.ok(`Unpublished ${label}. Its content stays; \`uniweb publish\` puts it back online.`)
|
|
291
|
+
}
|
|
292
|
+
return { exitCode: 0 }
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function remove(args) {
|
|
296
|
+
const conn = await connect(args, 'Deleting a site')
|
|
297
|
+
if (conn.refused) {
|
|
298
|
+
say.err(conn.refused)
|
|
299
|
+
return { exitCode: 2 }
|
|
300
|
+
}
|
|
301
|
+
const { client, workspace } = conn
|
|
302
|
+
const target = await targetSite(args, client, 'delete')
|
|
303
|
+
if (!target) return { exitCode: 2 }
|
|
304
|
+
const site = await findSite(client, target.uuid)
|
|
305
|
+
const label = site?.name ? `“${site.name}” (${target.uuid})` : target.uuid
|
|
306
|
+
if (site?.deployment?.status === 'published') {
|
|
307
|
+
say.err(`${label} is published — unpublish it first: uniweb site unpublish ${target.uuid}`)
|
|
308
|
+
return { exitCode: 1 }
|
|
309
|
+
}
|
|
310
|
+
const ok = await confirmed(
|
|
311
|
+
args,
|
|
312
|
+
`Delete ${label} on ${client.origin}, in ${describeWorkspace(workspace)}? ` +
|
|
313
|
+
'Its pages, settings and snapshots go with it, and it cannot be undone. Records it pushed stay in the workspace.',
|
|
314
|
+
'delete'
|
|
315
|
+
)
|
|
316
|
+
if (!ok.yes) return { exitCode: ok.exitCode }
|
|
317
|
+
|
|
318
|
+
const res = await client.deleteSite(target.uuid)
|
|
319
|
+
if (!res.ok) {
|
|
320
|
+
await sayRefusal(res, { uuid: target.uuid, verb: 'delete', origin: client.origin })
|
|
321
|
+
return { exitCode: 1 }
|
|
322
|
+
}
|
|
323
|
+
say.ok(`Deleted ${label}.`)
|
|
324
|
+
if (target.siteDir) {
|
|
325
|
+
if (await forgetDeletedSite(target.siteDir, client.origin)) {
|
|
326
|
+
say.dim(`This project no longer records a site on ${client.origin}; its next push creates a new one.`)
|
|
327
|
+
} else {
|
|
328
|
+
say.dim(`This project still records it — run \`uniweb forget --server ${client.origin}\` in it.`)
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return { exitCode: 0 }
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export async function site(args = []) {
|
|
335
|
+
const badFlag = checkFlags('site', args)
|
|
336
|
+
if (badFlag) {
|
|
337
|
+
say.err(badFlag.message)
|
|
338
|
+
return { exitCode: 2 }
|
|
339
|
+
}
|
|
340
|
+
const sub = args[0]
|
|
341
|
+
try {
|
|
342
|
+
if (sub === 'list') return await list(args)
|
|
343
|
+
if (sub === 'unpublish') return await unpublish(args)
|
|
344
|
+
if (sub === 'delete') return await remove(args)
|
|
345
|
+
} catch (err) {
|
|
346
|
+
say.err(err instanceof WorkspaceMismatchError ? err.message : err?.message || String(err))
|
|
347
|
+
return { exitCode: 1 }
|
|
348
|
+
}
|
|
349
|
+
if (sub) {
|
|
350
|
+
say.err(`Unknown command: uniweb site ${sub}`)
|
|
351
|
+
console.error(USAGE)
|
|
352
|
+
return { exitCode: 2 }
|
|
353
|
+
}
|
|
354
|
+
console.log(USAGE)
|
|
355
|
+
return { exitCode: 0 }
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export default site
|
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.
|
|
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}
|