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,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⛔ **HUMAN OUTPUT IN THIS FILE GOES TO STDERR, NEVER STDOUT.**
|
|
3
|
+
*
|
|
4
|
+
* `stdout` is the CLI's DATA channel. `uniweb register --json` promises a single
|
|
5
|
+
* parseable JSON line there, and `register` diverts its own output accordingly —
|
|
6
|
+
* but it calls into this module, which wrote prose to stdout directly, so the
|
|
7
|
+
* promise broke for any caller that piped it.
|
|
8
|
+
*
|
|
9
|
+
* ⭐ Measured by the `flows` lane at `uniweb@0.37.1`: two stray lines ahead of
|
|
10
|
+
* the JSON on the cold path — down from ~35 before the delegated builder's
|
|
11
|
+
* stdout was redirected to fd 2, which is why the two defects share one symptom
|
|
12
|
+
* and only one of them was fixed. Every line here is prose, a prompt, or a
|
|
13
|
+
* cancellation notice; none of it is anything a machine reads. On stderr it is
|
|
14
|
+
* equally visible to a human and invisible to a pipe.
|
|
15
|
+
*
|
|
16
|
+
* ⚖️ This is not a `--json` special case. A utility that cannot see the flag
|
|
17
|
+
* should not be choosing the stream at all — which is exactly how it got the
|
|
18
|
+
* choice wrong. `cli/test/porcelain-stdout.test.js` walks `register`'s import
|
|
19
|
+
* graph and fails on a new `console.log`.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* New-backend org operations for the publish-scope bootstrap — used by
|
|
23
|
+
* `uniweb register`'s scope resolution and the `uniweb org` command.
|
|
24
|
+
*
|
|
25
|
+
* One resource (Bearer auth, the CLI's /dev lane); the verb selects the op:
|
|
26
|
+
*
|
|
27
|
+
* GET /dev/orgs → {
|
|
28
|
+
* account_handle: string|null, // the caller's account handle
|
|
29
|
+
* orgs: [{ handle, is_primary }] // memberships, primary first;
|
|
30
|
+
* } // handle-less units filtered server-side
|
|
31
|
+
*
|
|
32
|
+
* POST /dev/orgs { handle } → { handle, uuid, is_primary }
|
|
33
|
+
*
|
|
34
|
+
* ⭐ A SCOPE IS A NAMESPACE, NOT AN ORG (2026-09-23). `@<account handle>` is the
|
|
35
|
+
* account's own scope and needs no org; an org's scope is published into by its
|
|
36
|
+
* members. Handles live in ONE global namespace across accounts and orgs, so an org
|
|
37
|
+
* needs a handle of its own — a backend refuses an org named after an account, its
|
|
38
|
+
* owner's included. ⛔ Until 2026-09-23 the CLI created a "personal org" `@jane` for
|
|
39
|
+
* account `jane` to have a scope at all; `personal_org_exists` served that and is no
|
|
40
|
+
* longer read. Org creation requires NO second factor on any lane (the 2FA gate lives
|
|
41
|
+
* at escalation points, not here).
|
|
42
|
+
*
|
|
43
|
+
* Failure shapes (branch on STATUS; details are human display, not contract):
|
|
44
|
+
* 422 = handle grammar; 409 = taken / reserved / belongs to another account
|
|
45
|
+
* (one status, server detail says which). Reserved names are SERVER-curated —
|
|
46
|
+
* deliberately not replicated here; the 409 detail carries the answer.
|
|
47
|
+
*
|
|
48
|
+
* Handle grammar (pre-validated for a fast prompt): lowercase alphanumerics +
|
|
49
|
+
* hyphens, 3–39 chars, no leading/trailing hyphen (consecutive hyphens are
|
|
50
|
+
* allowed).
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
const ORGS_PATH = '/dev/orgs'
|
|
54
|
+
|
|
55
|
+
const HANDLE_RE = /^[a-z0-9][a-z0-9-]{1,37}[a-z0-9]$/
|
|
56
|
+
|
|
57
|
+
/** Strip a leading `@` and any `/suffix`, returning the bare handle segment. */
|
|
58
|
+
export function bareHandle(scope) {
|
|
59
|
+
return String(scope || '')
|
|
60
|
+
.replace(/^@/, '')
|
|
61
|
+
.replace(/\/.*$/, '')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A publish scope in the one form the registry names things by — `@acme` — from any
|
|
66
|
+
* spelling `--scope` or a schemas-only package's `package.json::uniweb.scope` may carry
|
|
67
|
+
* it in (`@acme`, `acme`, `@acme/…`). Null when there is no handle in it. (A
|
|
68
|
+
* foundation's scope is part of its name, `@acme/marketing`, and needs no reading.)
|
|
69
|
+
*
|
|
70
|
+
* ⛔ Every name built from a scope goes through this. The `.uwx` assembly
|
|
71
|
+
* (`@uniweb/build`'s `buildRegistryPackage`) has always accepted `acme` as well as
|
|
72
|
+
* `@acme`, so a name composed from the RAW value disagreed with the one it registered:
|
|
73
|
+
* until 2026-09-21 `register --scope std` registered `@std/src` and then asked to
|
|
74
|
+
* deliver code for `std/src`, which the registry refuses (a scoped name is `@org/name`).
|
|
75
|
+
*
|
|
76
|
+
* @param {unknown} value
|
|
77
|
+
* @returns {string|null}
|
|
78
|
+
*/
|
|
79
|
+
export function publishScope(value) {
|
|
80
|
+
const handle = bareHandle(value)
|
|
81
|
+
return handle ? `@${handle}` : null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Whether the orgs read (`GET /dev/orgs`) says this account may release into `scope`:
|
|
86
|
+
* the account's own handle, or an org it belongs to. True or false; null when there is
|
|
87
|
+
* nothing to say (no scope, no read).
|
|
88
|
+
*
|
|
89
|
+
* ⛔ For EXPLAINING a refusal only, never to gate a release: the registry decides who
|
|
90
|
+
* may publish into a scope, and this knows only the memberships that read lists.
|
|
91
|
+
*
|
|
92
|
+
* @param {string|null} scope - `@acme`, `acme` or `@acme/…`
|
|
93
|
+
* @param {{ account_handle?: string|null, orgs?: Array<{ handle?: string }> }|null} envelope
|
|
94
|
+
* @returns {boolean|null}
|
|
95
|
+
*/
|
|
96
|
+
export function belongsToScope(scope, envelope) {
|
|
97
|
+
const h = bareHandle(scope)
|
|
98
|
+
if (!h || !envelope || typeof envelope !== 'object') return null
|
|
99
|
+
if (bareHandle(envelope.account_handle || '') === h) return true
|
|
100
|
+
return (Array.isArray(envelope.orgs) ? envelope.orgs : []).some((o) => bareHandle(o?.handle) === h)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Validate a handle's GRAMMAR client-side (reserved names are the server's
|
|
105
|
+
* call — a 409 carries the verdict). Returns an error string, or null.
|
|
106
|
+
*/
|
|
107
|
+
export function validateHandle(handle) {
|
|
108
|
+
const h = bareHandle(handle)
|
|
109
|
+
if (!h) return 'A handle is required.'
|
|
110
|
+
if (!HANDLE_RE.test(h)) {
|
|
111
|
+
return 'Use 3–39 lowercase letters, digits, and hyphens (no leading/trailing hyphen).'
|
|
112
|
+
}
|
|
113
|
+
return null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The picker read: memberships + the caller's account handle + whether the
|
|
118
|
+
* personal org already exists.
|
|
119
|
+
* @returns {Promise<{account_handle: string|null, orgs: Array<{handle: string, is_primary: boolean}>}>}
|
|
120
|
+
*/
|
|
121
|
+
export async function fetchOrgs({ apiBase, token, fetchImpl = globalThis.fetch }) {
|
|
122
|
+
const res = await fetchImpl(`${apiBase.replace(/\/$/, '')}${ORGS_PATH}`, {
|
|
123
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
124
|
+
})
|
|
125
|
+
if (!res.ok)
|
|
126
|
+
throw new Error(
|
|
127
|
+
`Could not list your orgs: HTTP ${res.status} ${res.statusText}`
|
|
128
|
+
)
|
|
129
|
+
const data = await res.json().catch(() => null)
|
|
130
|
+
return {
|
|
131
|
+
account_handle: data?.account_handle ?? null,
|
|
132
|
+
orgs: Array.isArray(data?.orgs) ? data.orgs : []
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Back-compat alias: the membership rows only. */
|
|
137
|
+
export async function listOrgs(opts) {
|
|
138
|
+
return (await fetchOrgs(opts)).orgs
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Create an org. Its handle must be its own: a backend refuses an account's handle,
|
|
143
|
+
* your own included — your personal scope needs no org.
|
|
144
|
+
* @returns {Promise<{handle: string, uuid?: string, is_primary?: boolean}>}
|
|
145
|
+
*/
|
|
146
|
+
export async function createOrg({ apiBase, token, handle }) {
|
|
147
|
+
const h = bareHandle(handle)
|
|
148
|
+
const res = await fetch(`${apiBase.replace(/\/$/, '')}${ORGS_PATH}`, {
|
|
149
|
+
method: 'POST',
|
|
150
|
+
headers: {
|
|
151
|
+
'Content-Type': 'application/json',
|
|
152
|
+
Authorization: `Bearer ${token}`
|
|
153
|
+
},
|
|
154
|
+
body: JSON.stringify({ handle: h })
|
|
155
|
+
})
|
|
156
|
+
if (res.ok) return res.json()
|
|
157
|
+
|
|
158
|
+
// Surface the server's human detail — a 409 distinguishes taken vs
|
|
159
|
+
// reserved vs belongs-to-another-account; a 422 names the grammar rule.
|
|
160
|
+
let detail = ''
|
|
161
|
+
try {
|
|
162
|
+
const body = await res.json()
|
|
163
|
+
detail = body?.detail || ''
|
|
164
|
+
} catch {
|
|
165
|
+
/* non-JSON body — keep the generic line */
|
|
166
|
+
}
|
|
167
|
+
const fallback =
|
|
168
|
+
res.status === 409
|
|
169
|
+
? `@${h} is not available.`
|
|
170
|
+
: res.status === 422
|
|
171
|
+
? `@${h} is not a valid handle.`
|
|
172
|
+
: `Could not create @${h}: HTTP ${res.status}`
|
|
173
|
+
const e = new Error(detail || fallback)
|
|
174
|
+
e.status = res.status
|
|
175
|
+
throw e
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The scope to register under, from the login's orgs, when none was named and the
|
|
180
|
+
* workspace the command works in names none either — a bare handle, or null when none
|
|
181
|
+
* was chosen. Persists nothing; the caller records it (in the foundation's name, or a
|
|
182
|
+
* schemas-only package's `package.json`).
|
|
183
|
+
*
|
|
184
|
+
* ⚠️ The FALLBACK since 2026-10-06: a bare name first takes the workspace the command
|
|
185
|
+
* works in (`register.js::deriveScopeFromLogin`, `workspace.js::scopeOfWorkspace`). This
|
|
186
|
+
* answers only when no workspace is chosen — you belong to orgs and named none — or the
|
|
187
|
+
* one chosen is a unit without a handle.
|
|
188
|
+
*
|
|
189
|
+
* ⭐ A SCOPE IS A NAMESPACE (2026-09-23): your account's own, `@<handle>`, needs no org.
|
|
190
|
+
*
|
|
191
|
+
* no org → your personal scope — said, never asked, in CI too
|
|
192
|
+
* orgs → pick: your personal scope first, then each org;
|
|
193
|
+
* non-interactive, REFUSED — choose a workspace, or --scope
|
|
194
|
+
* (until 2026-10-06: your personal scope, said)
|
|
195
|
+
* no account handle → (a service account) its one org, said; several are asked,
|
|
196
|
+
* or refused in CI; none is a pointer to `--scope`
|
|
197
|
+
*
|
|
198
|
+
* ⛔ Until 2026-09-23 a login with no org was offered a "personal org" `@jane`, created
|
|
199
|
+
* on the spot — a scope was taken to need an org — and refused in CI.
|
|
200
|
+
*
|
|
201
|
+
* An org named after the account — a personal org made before then — is the same
|
|
202
|
+
* `@jane`, and is listed once, as your personal scope.
|
|
203
|
+
*
|
|
204
|
+
* @param {Object} p
|
|
205
|
+
* @param {string} p.apiBase
|
|
206
|
+
* @param {string} p.token
|
|
207
|
+
* @param {string|null} [p.accountHandle] - fallback only; the server's
|
|
208
|
+
* account_handle from the orgs read is authoritative
|
|
209
|
+
* @param {string[]} [p.args] - argv slice; checked for --non-interactive
|
|
210
|
+
* @returns {Promise<string|null>}
|
|
211
|
+
*/
|
|
212
|
+
export async function deriveScope({
|
|
213
|
+
apiBase,
|
|
214
|
+
token,
|
|
215
|
+
accountHandle = null,
|
|
216
|
+
args = []
|
|
217
|
+
}) {
|
|
218
|
+
const envelope = await fetchOrgs({ apiBase, token })
|
|
219
|
+
const handle = bareHandle(envelope.account_handle || accountHandle || '')
|
|
220
|
+
const personal = handle && !validateHandle(handle) ? handle : null
|
|
221
|
+
const orgs = envelope.orgs.filter((o) => o.handle !== personal)
|
|
222
|
+
const { isNonInteractive } = await import('./interactive.js')
|
|
223
|
+
const nonInteractive = isNonInteractive(args)
|
|
224
|
+
const bold = (h) => `\x1b[1m@${h}\x1b[0m`
|
|
225
|
+
|
|
226
|
+
if (personal && !orgs.length) {
|
|
227
|
+
console.error(`Registering under your personal scope ${bold(personal)}.`)
|
|
228
|
+
return personal
|
|
229
|
+
}
|
|
230
|
+
if (!personal && orgs.length <= 1) {
|
|
231
|
+
if (orgs.length === 1) {
|
|
232
|
+
console.error(`Registering under your org ${bold(orgs[0].handle)}.`)
|
|
233
|
+
return orgs[0].handle
|
|
234
|
+
}
|
|
235
|
+
console.error(
|
|
236
|
+
"\x1b[31m✗\x1b[0m This account has no handle (service accounts don't get one), so it has no personal scope, and it belongs to no org.\n" +
|
|
237
|
+
' Pass --scope @org, or create one with `uniweb org create <handle>`.'
|
|
238
|
+
)
|
|
239
|
+
return null
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (nonInteractive) {
|
|
243
|
+
if (!personal) {
|
|
244
|
+
console.error(
|
|
245
|
+
'\x1b[31m✗\x1b[0m This account belongs to several orgs and has no personal scope. Pass --scope @org.'
|
|
246
|
+
)
|
|
247
|
+
return null
|
|
248
|
+
}
|
|
249
|
+
// ⛔ NO DEFAULT FOR A MEMBER OF ORGANIZATIONS *[Diego, 2026-10-06: "someone in an org,
|
|
250
|
+
// with no workspace chosen must choose one. we should not default to personal"]*. Only
|
|
251
|
+
// reached when the workspace the command works in names no scope — none is chosen — so
|
|
252
|
+
// the choice is theirs. ⚠️ Until then this answered your personal scope, said: a team's
|
|
253
|
+
// foundation registered as one member's.
|
|
254
|
+
const { getCliPrefix } = await import('./interactive.js')
|
|
255
|
+
const { loginCommand } = await import('./config.js')
|
|
256
|
+
const first = orgs[0].handle
|
|
257
|
+
console.error(
|
|
258
|
+
`\x1b[31m✗\x1b[0m You belong to organizations (${orgs.map((o) => `@${o.handle}`).join(', ')}), so no scope is assumed.\n` +
|
|
259
|
+
` Choose the workspace you work in — ${loginCommand(apiBase, getCliPrefix())} --org @${first} (or --personal) — and the name takes its scope;\n` +
|
|
260
|
+
` or name the scope here: --scope @${first}, or --scope @${personal} for your own.`
|
|
261
|
+
)
|
|
262
|
+
return null
|
|
263
|
+
}
|
|
264
|
+
const prompts = (await import('prompts')).default
|
|
265
|
+
const { choice } = await prompts(
|
|
266
|
+
{
|
|
267
|
+
type: 'select',
|
|
268
|
+
name: 'choice',
|
|
269
|
+
message: 'Register under which scope?',
|
|
270
|
+
choices: [
|
|
271
|
+
...(personal
|
|
272
|
+
? [{ title: `@${personal} — your personal scope`, value: personal }]
|
|
273
|
+
: []),
|
|
274
|
+
...orgs.map((o) => ({
|
|
275
|
+
title: `@${o.handle}${o.is_primary ? ' (primary org)' : ' (org)'}`,
|
|
276
|
+
value: o.handle
|
|
277
|
+
}))
|
|
278
|
+
],
|
|
279
|
+
initial: 0
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
onCancel: () => {
|
|
283
|
+
console.error('\nCancelled.')
|
|
284
|
+
process.exit(0)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
)
|
|
288
|
+
if (!choice) console.error('\x1b[31m✗\x1b[0m No scope chosen.')
|
|
289
|
+
return choice || null
|
|
290
|
+
}
|