uniweb 1.0.0 → 1.0.2
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/LICENSE +201 -0
- package/README.md +532 -3
- package/package.json +60 -11
- package/partials/adding-pages.hbs +77 -0
- package/partials/agents.md +3000 -0
- package/partials/ai-assistance.hbs +28 -0
- package/partials/components-docs.hbs +13 -0
- package/partials/config-reference.hbs +66 -0
- package/partials/deployment.hbs +13 -0
- package/partials/learn-more.hbs +6 -0
- package/partials/quick-start.hbs +8 -0
- package/partials/search-docs.hbs +110 -0
- package/src/backend/asset-download.js +222 -0
- package/src/backend/client.js +986 -0
- package/src/backend/foundation-bring-along.js +692 -0
- package/src/backend/payment-handoff.js +362 -0
- package/src/backend/service-request.js +210 -0
- package/src/backend/site-media.js +255 -0
- package/src/backend/site-preview.js +77 -0
- package/src/backend/site-sync.js +2515 -0
- package/src/backend/workspace.js +234 -0
- package/src/commands/add.js +1900 -0
- package/src/commands/build.js +1214 -0
- package/src/commands/clone.js +466 -0
- package/src/commands/content.js +202 -0
- package/src/commands/deploy.js +537 -0
- package/src/commands/dev.js +202 -0
- package/src/commands/docs.js +530 -0
- package/src/commands/doctor.js +1982 -0
- package/src/commands/export.js +116 -0
- package/src/commands/families.js +82 -0
- package/src/commands/forget.js +197 -0
- package/src/commands/handoff.js +21 -0
- package/src/commands/i18n.js +1707 -0
- package/src/commands/inspect.js +375 -0
- package/src/commands/invite.js +24 -0
- package/src/commands/org.js +96 -0
- package/src/commands/publish.js +1087 -0
- package/src/commands/pull.js +1257 -0
- package/src/commands/push.js +663 -0
- package/src/commands/refresh.js +212 -0
- package/src/commands/register.js +1092 -0
- package/src/commands/rename.js +647 -0
- package/src/commands/site.js +358 -0
- package/src/commands/snapshot.js +461 -0
- package/src/commands/starter.js +206 -0
- package/src/commands/status.js +302 -0
- package/src/commands/sync.js +93 -0
- package/src/commands/template.js +107 -0
- package/src/commands/update.js +887 -0
- package/src/commands/validate.js +376 -0
- package/src/framework-index.json +248 -0
- package/src/index.js +2241 -0
- package/src/templates/fetchers/github.js +113 -0
- package/src/templates/fetchers/npm.js +85 -0
- package/src/templates/fetchers/release.js +92 -0
- package/src/templates/index.js +190 -0
- package/src/templates/processor.js +333 -0
- package/src/templates/resolver.js +232 -0
- package/src/templates/validator.js +155 -0
- package/src/utils/agents-stamp.js +110 -0
- package/src/utils/args.js +126 -0
- package/src/utils/asset-upload.js +270 -0
- package/src/utils/bytes.js +30 -0
- package/src/utils/code-upload.js +394 -0
- package/src/utils/config.js +417 -0
- package/src/utils/conformance.js +274 -0
- package/src/utils/dep-survey.js +176 -0
- package/src/utils/destination-prompt.js +146 -0
- package/src/utils/discover.js +75 -0
- package/src/utils/env.js +22 -0
- package/src/utils/exit.js +30 -0
- package/src/utils/fetch-retry.js +109 -0
- package/src/utils/flag-guard.js +246 -0
- package/src/utils/foundation-name.js +179 -0
- package/src/utils/git.js +342 -0
- package/src/utils/host-prompt.js +51 -0
- package/src/utils/install-integrity.js +210 -0
- package/src/utils/interactive.js +118 -0
- package/src/utils/json-file.js +72 -0
- package/src/utils/names.js +91 -0
- package/src/utils/placement.js +151 -0
- package/src/utils/pm.js +165 -0
- package/src/utils/preview.js +20 -0
- package/src/utils/pull-written.js +132 -0
- package/src/utils/records-guard.js +88 -0
- package/src/utils/registry-auth.js +994 -0
- package/src/utils/registry-orgs.js +290 -0
- package/src/utils/scaffold.js +483 -0
- package/src/utils/schemaless-report.js +30 -0
- package/src/utils/semver-precedence.js +80 -0
- package/src/utils/session-file.js +95 -0
- package/src/utils/site-content-refs.js +23 -0
- package/src/utils/site-copies.js +123 -0
- package/src/utils/site-data-upload.js +302 -0
- package/src/utils/site-identity.js +137 -0
- package/src/utils/snapshot-settings.js +112 -0
- package/src/utils/update-check.js +302 -0
- package/src/utils/uwx-read.js +157 -0
- package/src/utils/workspace.js +223 -0
- package/src/versions.js +467 -0
- package/starter/foundation/main.js +50 -0
- package/starter/foundation/sections/Section/index.jsx +95 -0
- package/starter/foundation/sections/Section/meta.js +44 -0
- package/starter/site/pages/about/1-about.md.hbs +13 -0
- package/starter/site/pages/about/page.yml +3 -0
- package/starter/site/pages/home/1-welcome.md.hbs +14 -0
- package/starter/site/pages/home/page.yml +3 -0
- package/templates/foundation/_gitignore +5 -0
- package/templates/foundation/components/.gitkeep +0 -0
- package/templates/foundation/main.js.hbs +33 -0
- package/templates/foundation/package.json.hbs +51 -0
- package/templates/foundation/sections/.gitkeep +0 -0
- package/templates/foundation/styles.css +6 -0
- package/templates/foundation/vite.config.js +3 -0
- package/templates/site/_gitignore +12 -0
- package/templates/site/entry.js +7 -0
- package/templates/site/index.html.hbs +13 -0
- package/templates/site/package.json.hbs +27 -0
- package/templates/site/public/favicon.svg +7 -0
- package/templates/site/site.yml.hbs +91 -0
- package/templates/site/theme.yml +99 -0
- package/templates/site/vite.config.js +7 -0
- package/templates/workspace/AGENTS.md.hbs +2 -0
- package/templates/workspace/README.md.hbs +87 -0
- package/templates/workspace/_gitignore +29 -0
- package/templates/workspace/_vscode/settings.json +6 -0
- package/templates/workspace/package.json.hbs +20 -0
- package/templates/workspace/pnpm-workspace.yaml.hbs +26 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE WORKSPACE A COMMAND WORKS IN — one at a time, chosen with the login.
|
|
3
|
+
*
|
|
4
|
+
* ⭐ **[Diego, 2026-09-23]** — *"when a user has workspace in the backend, I expected them
|
|
5
|
+
* to explicitly login into one"*, and a site the backend keeps in ANOTHER workspace is
|
|
6
|
+
* refused rather than worked on anyway: *"that's the intent of the workspace concept."*
|
|
7
|
+
* The app keeps one workspace per tab; the CLI keeps one per login, and every site
|
|
8
|
+
* request names it (`x-uniweb-workspace`, `client.js`). A site outside it answers `409
|
|
9
|
+
* wrong_workspace`, and the command stops, saying how to switch — nothing is adopted.
|
|
10
|
+
*
|
|
11
|
+
* Where a command's workspace comes from — the first answer wins:
|
|
12
|
+
*
|
|
13
|
+
* 1. `--org @acme` / `--personal` this command only
|
|
14
|
+
* 2. `UNIWEB_WORKSPACE=@acme|personal` a process logged in with `UNIWEB_TOKEN`, which has
|
|
15
|
+
* no `uniweb login` to choose one (a template
|
|
16
|
+
* installer, an agent driving several backends)
|
|
17
|
+
* 3. the one chosen at `uniweb login` the session's
|
|
18
|
+
* 4. none chosen your personal workspace when you belong to no
|
|
19
|
+
* organization; otherwise refused, naming 1–3
|
|
20
|
+
*
|
|
21
|
+
* New sites are created in it: a create names the workspace, and the workspace is the
|
|
22
|
+
* site's owner.
|
|
23
|
+
*
|
|
24
|
+
* ⛔ **The personal workspace is named by naming NONE.** `@<your handle>` is your personal
|
|
25
|
+
* SCOPE — a registry namespace for foundations and data schemas — not a workspace.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { readOrgFlag } from '../utils/args.js'
|
|
29
|
+
import { loginCommand } from '../utils/config.js'
|
|
30
|
+
import { fetchOrgs, bareHandle, validateHandle } from '../utils/registry-orgs.js'
|
|
31
|
+
import { readRegistryAuth } from '../utils/registry-auth.js'
|
|
32
|
+
import { workspaceHandle, describeWorkspace } from './client.js'
|
|
33
|
+
|
|
34
|
+
export { describeWorkspace }
|
|
35
|
+
|
|
36
|
+
/** The personal workspace, as a login stores the choice (a handle is stored with its `@`). */
|
|
37
|
+
export const PERSONAL = 'personal'
|
|
38
|
+
|
|
39
|
+
/** Names a process's workspace when it is logged in with `UNIWEB_TOKEN`. */
|
|
40
|
+
export const WORKSPACE_ENV = 'UNIWEB_WORKSPACE'
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A workspace choice as it may be written — `@acme`, `acme`, `personal` — or null when
|
|
44
|
+
* the value names none.
|
|
45
|
+
*
|
|
46
|
+
* @param {unknown} value
|
|
47
|
+
* @returns {string|null} `@acme`, `PERSONAL`, or null
|
|
48
|
+
*/
|
|
49
|
+
export function parseWorkspaceChoice(value) {
|
|
50
|
+
if (typeof value !== 'string' || !value.trim()) return null
|
|
51
|
+
const v = value.trim()
|
|
52
|
+
return v === PERSONAL ? PERSONAL : workspaceHandle(v)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A choice in the header's form: `@acme`, or null for the personal workspace. */
|
|
56
|
+
export function headerOf(choice) {
|
|
57
|
+
return choice === PERSONAL ? null : choice
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** What a source is called in a sentence — "(… )" after the workspace. */
|
|
61
|
+
export const SOURCE_LABEL = {
|
|
62
|
+
flag: 'named on this command',
|
|
63
|
+
env: WORKSPACE_ENV,
|
|
64
|
+
login: 'your login',
|
|
65
|
+
personal: 'you belong to no organization',
|
|
66
|
+
offline: 'not resolved in a preview'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** How to choose, for a command on `origin` — its login names the backend when it must (`loginCommand`). */
|
|
70
|
+
const choose = (origin) =>
|
|
71
|
+
[
|
|
72
|
+
'Choose the workspace you work in:',
|
|
73
|
+
` ${loginCommand(origin)} --org @acme (or --personal) — for every command after`,
|
|
74
|
+
' --org @acme / --personal — for this command only',
|
|
75
|
+
` ${WORKSPACE_ENV}=@acme — for a process logged in with UNIWEB_TOKEN`
|
|
76
|
+
].join('\n')
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The workspace this command works in.
|
|
80
|
+
*
|
|
81
|
+
* @param {object} p
|
|
82
|
+
* @param {import('./client.js').BackendClient} p.client - its origin and token
|
|
83
|
+
* @param {string[]} [p.args]
|
|
84
|
+
* @param {boolean} [p.offline=false] - a preview (`--dry-run`, `-o`): never authenticates,
|
|
85
|
+
* so step 4 answers "not resolved" instead of asking the backend
|
|
86
|
+
* @returns {Promise<{ workspace: string|null, source: string } | { refused: true, reason: string }>}
|
|
87
|
+
* `workspace` in the header's form — `@acme`, or null for the personal workspace
|
|
88
|
+
*/
|
|
89
|
+
export async function resolveWorkspace({ client, args = [], offline = false }) {
|
|
90
|
+
const flag = readOrgFlag(args)
|
|
91
|
+
if (flag) return { workspace: workspaceHandle(flag), source: 'flag' }
|
|
92
|
+
if (args.includes('--personal')) return { workspace: null, source: 'flag' }
|
|
93
|
+
|
|
94
|
+
const raw = process.env[WORKSPACE_ENV]
|
|
95
|
+
if (raw !== undefined && raw !== '') {
|
|
96
|
+
const env = parseWorkspaceChoice(raw)
|
|
97
|
+
if (!env) {
|
|
98
|
+
return {
|
|
99
|
+
refused: true,
|
|
100
|
+
reason: `${WORKSPACE_ENV}=${JSON.stringify(raw)} names no workspace — give @org, or ${PERSONAL}.`
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { workspace: headerOf(env), source: 'env' }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ⛔ A process logged in with UNIWEB_TOKEN is not the session's login: the token may be
|
|
107
|
+
// another account's, so the session's workspace is not its to use.
|
|
108
|
+
const useSession = !process.env.UNIWEB_TOKEN
|
|
109
|
+
const fromSession = async () =>
|
|
110
|
+
useSession ? parseWorkspaceChoice((await readRegistryAuth(client.origin))?.workspace) : null
|
|
111
|
+
|
|
112
|
+
const chosen = await fromSession()
|
|
113
|
+
if (chosen) return { workspace: headerOf(chosen), source: 'login' }
|
|
114
|
+
if (offline) return { workspace: null, source: 'offline' }
|
|
115
|
+
|
|
116
|
+
// Authenticate — a login this triggers chooses a workspace, so ask the session again.
|
|
117
|
+
await client.token()
|
|
118
|
+
const now = await fromSession()
|
|
119
|
+
if (now) return { workspace: headerOf(now), source: 'login' }
|
|
120
|
+
|
|
121
|
+
const { orgs } = await client.fetchOrgs()
|
|
122
|
+
if (!orgs.length) return { workspace: null, source: 'personal' }
|
|
123
|
+
return {
|
|
124
|
+
refused: true,
|
|
125
|
+
reason: `You belong to organizations, so no workspace is assumed.\n ${choose(client.origin)}`
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The scope a bare name registers under by default — the workspace the command works in
|
|
131
|
+
* *[Diego, 2026-10-06]*: an organization's handle, or your own handle for your personal
|
|
132
|
+
* workspace.
|
|
133
|
+
*
|
|
134
|
+
* ⭐ A default, asked once: a name that carries a scope keeps it, and `--scope` names
|
|
135
|
+
* another. The workspace still never decides anything else about a foundation — it
|
|
136
|
+
* decides which SITE a command works on; here it only answers the question a bare name
|
|
137
|
+
* asks the first time it registers.
|
|
138
|
+
*
|
|
139
|
+
* Null when the workspace names no scope, and the caller derives one as before
|
|
140
|
+
* (`deriveScope`): none is chosen (you belong to organizations and none is named), the
|
|
141
|
+
* command is a preview, the workspace is a unit without a handle, or the account has no
|
|
142
|
+
* handle (a service account).
|
|
143
|
+
*
|
|
144
|
+
* @param {{ workspace?: string|null, source?: string, refused?: boolean }} ws -
|
|
145
|
+
* `resolveWorkspace`'s answer
|
|
146
|
+
* @param {string|null} accountHandle - the account's own handle (`GET /dev/orgs`), read
|
|
147
|
+
* only for the personal workspace
|
|
148
|
+
* @returns {string|null} `@handle`
|
|
149
|
+
*/
|
|
150
|
+
export function scopeOfWorkspace(ws, accountHandle) {
|
|
151
|
+
if (!ws || ws.refused || ws.source === 'offline') return null
|
|
152
|
+
if (typeof ws.workspace === 'string') return ws.workspace.startsWith('@') ? ws.workspace : null
|
|
153
|
+
const h = bareHandle(accountHandle || '')
|
|
154
|
+
return h && !validateHandle(h) ? `@${h}` : null
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The workspace a login works in — asked once, at `uniweb login`, and stored with the
|
|
159
|
+
* session.
|
|
160
|
+
*
|
|
161
|
+
* `--org @acme` must be an organization you belong to
|
|
162
|
+
* `--personal` your personal workspace
|
|
163
|
+
* neither, no organization your personal workspace — said, not asked
|
|
164
|
+
* neither, organizations a pick at a terminal; refused without one
|
|
165
|
+
*
|
|
166
|
+
* @param {object} p
|
|
167
|
+
* @param {string} p.apiBase
|
|
168
|
+
* @param {string} p.token
|
|
169
|
+
* @param {string[]} [p.args]
|
|
170
|
+
* @returns {Promise<{ choice: string, note?: string } | { refused: true, reason: string }>}
|
|
171
|
+
* `choice` is `@acme` or `PERSONAL`
|
|
172
|
+
*/
|
|
173
|
+
export async function chooseWorkspace({ apiBase, token, args = [] }) {
|
|
174
|
+
const flag = readOrgFlag(args)
|
|
175
|
+
if (!flag && args.includes('--personal')) return { choice: PERSONAL }
|
|
176
|
+
|
|
177
|
+
const { orgs } = await fetchOrgs({ apiBase, token })
|
|
178
|
+
const mine = orgs.map((o) => `@${o.handle}`)
|
|
179
|
+
if (flag) {
|
|
180
|
+
const h = workspaceHandle(flag)
|
|
181
|
+
if (!mine.includes(h)) {
|
|
182
|
+
return {
|
|
183
|
+
refused: true,
|
|
184
|
+
reason:
|
|
185
|
+
`You are not a member of ${h} on ${apiBase}. ` +
|
|
186
|
+
(mine.length
|
|
187
|
+
? `Your workspaces: ${[...mine, `--personal`].join(', ')}.`
|
|
188
|
+
: 'You belong to no organization — use --personal.')
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return { choice: h }
|
|
192
|
+
}
|
|
193
|
+
if (!orgs.length) {
|
|
194
|
+
return {
|
|
195
|
+
choice: PERSONAL,
|
|
196
|
+
note: 'You belong to no organization, so you work in your personal workspace.'
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const { isNonInteractive, getCliPrefix } = await import('../utils/interactive.js')
|
|
201
|
+
// ⭐ How to choose, said whole: the session is stored already (`registry-auth.js`, the
|
|
202
|
+
// login methods), so the login that finishes this signs nobody in again.
|
|
203
|
+
const others = [...mine.slice(1).map((h) => `--org ${h}`), '--personal']
|
|
204
|
+
const howToChoose =
|
|
205
|
+
`Choose one: ${loginCommand(apiBase, getCliPrefix())} --org ${mine[0]} (or ${others.join(', ')}) — ` +
|
|
206
|
+
'it will not ask you to sign in again.'
|
|
207
|
+
if (isNonInteractive(args)) {
|
|
208
|
+
return {
|
|
209
|
+
refused: true,
|
|
210
|
+
reason: `You belong to organizations, so no workspace is assumed. ${howToChoose}`
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const prompts = (await import('prompts')).default
|
|
214
|
+
const { choice } = await prompts(
|
|
215
|
+
{
|
|
216
|
+
type: 'select',
|
|
217
|
+
name: 'choice',
|
|
218
|
+
message: 'Which workspace do you work in?',
|
|
219
|
+
choices: [
|
|
220
|
+
{ title: 'Personal — your own sites', value: PERSONAL },
|
|
221
|
+
...orgs.map((o) => ({
|
|
222
|
+
title: `@${o.handle}${o.is_primary ? ' (primary)' : ''}`,
|
|
223
|
+
value: `@${o.handle}`
|
|
224
|
+
}))
|
|
225
|
+
],
|
|
226
|
+
initial: 0
|
|
227
|
+
},
|
|
228
|
+
// ⛔ A cancelled pick is NOT a cancelled login: the session is stored already, so the
|
|
229
|
+
// login says so and how to finish it (`finishLogin`), and exits 2. Until 2026-10-06 this
|
|
230
|
+
// printed "Cancelled." and exited 0 — with the new session in place of the old one.
|
|
231
|
+
{ onCancel: () => false }
|
|
232
|
+
)
|
|
233
|
+
return choice ? { choice } : { refused: true, reason: `No workspace chosen. ${howToChoose}` }
|
|
234
|
+
}
|