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.
Files changed (129) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +532 -3
  3. package/package.json +60 -11
  4. package/partials/adding-pages.hbs +77 -0
  5. package/partials/agents.md +3000 -0
  6. package/partials/ai-assistance.hbs +28 -0
  7. package/partials/components-docs.hbs +13 -0
  8. package/partials/config-reference.hbs +66 -0
  9. package/partials/deployment.hbs +13 -0
  10. package/partials/learn-more.hbs +6 -0
  11. package/partials/quick-start.hbs +8 -0
  12. package/partials/search-docs.hbs +110 -0
  13. package/src/backend/asset-download.js +222 -0
  14. package/src/backend/client.js +986 -0
  15. package/src/backend/foundation-bring-along.js +692 -0
  16. package/src/backend/payment-handoff.js +362 -0
  17. package/src/backend/service-request.js +210 -0
  18. package/src/backend/site-media.js +255 -0
  19. package/src/backend/site-preview.js +77 -0
  20. package/src/backend/site-sync.js +2515 -0
  21. package/src/backend/workspace.js +234 -0
  22. package/src/commands/add.js +1900 -0
  23. package/src/commands/build.js +1214 -0
  24. package/src/commands/clone.js +466 -0
  25. package/src/commands/content.js +202 -0
  26. package/src/commands/deploy.js +537 -0
  27. package/src/commands/dev.js +202 -0
  28. package/src/commands/docs.js +530 -0
  29. package/src/commands/doctor.js +1982 -0
  30. package/src/commands/export.js +116 -0
  31. package/src/commands/families.js +82 -0
  32. package/src/commands/forget.js +197 -0
  33. package/src/commands/handoff.js +21 -0
  34. package/src/commands/i18n.js +1707 -0
  35. package/src/commands/inspect.js +375 -0
  36. package/src/commands/invite.js +24 -0
  37. package/src/commands/org.js +96 -0
  38. package/src/commands/publish.js +1087 -0
  39. package/src/commands/pull.js +1257 -0
  40. package/src/commands/push.js +663 -0
  41. package/src/commands/refresh.js +212 -0
  42. package/src/commands/register.js +1092 -0
  43. package/src/commands/rename.js +647 -0
  44. package/src/commands/site.js +358 -0
  45. package/src/commands/snapshot.js +461 -0
  46. package/src/commands/starter.js +206 -0
  47. package/src/commands/status.js +302 -0
  48. package/src/commands/sync.js +93 -0
  49. package/src/commands/template.js +107 -0
  50. package/src/commands/update.js +887 -0
  51. package/src/commands/validate.js +376 -0
  52. package/src/framework-index.json +248 -0
  53. package/src/index.js +2241 -0
  54. package/src/templates/fetchers/github.js +113 -0
  55. package/src/templates/fetchers/npm.js +85 -0
  56. package/src/templates/fetchers/release.js +92 -0
  57. package/src/templates/index.js +190 -0
  58. package/src/templates/processor.js +333 -0
  59. package/src/templates/resolver.js +232 -0
  60. package/src/templates/validator.js +155 -0
  61. package/src/utils/agents-stamp.js +110 -0
  62. package/src/utils/args.js +126 -0
  63. package/src/utils/asset-upload.js +270 -0
  64. package/src/utils/bytes.js +30 -0
  65. package/src/utils/code-upload.js +394 -0
  66. package/src/utils/config.js +417 -0
  67. package/src/utils/conformance.js +274 -0
  68. package/src/utils/dep-survey.js +176 -0
  69. package/src/utils/destination-prompt.js +146 -0
  70. package/src/utils/discover.js +75 -0
  71. package/src/utils/env.js +22 -0
  72. package/src/utils/exit.js +30 -0
  73. package/src/utils/fetch-retry.js +109 -0
  74. package/src/utils/flag-guard.js +246 -0
  75. package/src/utils/foundation-name.js +179 -0
  76. package/src/utils/git.js +342 -0
  77. package/src/utils/host-prompt.js +51 -0
  78. package/src/utils/install-integrity.js +210 -0
  79. package/src/utils/interactive.js +118 -0
  80. package/src/utils/json-file.js +72 -0
  81. package/src/utils/names.js +91 -0
  82. package/src/utils/placement.js +151 -0
  83. package/src/utils/pm.js +165 -0
  84. package/src/utils/preview.js +20 -0
  85. package/src/utils/pull-written.js +132 -0
  86. package/src/utils/records-guard.js +88 -0
  87. package/src/utils/registry-auth.js +994 -0
  88. package/src/utils/registry-orgs.js +290 -0
  89. package/src/utils/scaffold.js +483 -0
  90. package/src/utils/schemaless-report.js +30 -0
  91. package/src/utils/semver-precedence.js +80 -0
  92. package/src/utils/session-file.js +95 -0
  93. package/src/utils/site-content-refs.js +23 -0
  94. package/src/utils/site-copies.js +123 -0
  95. package/src/utils/site-data-upload.js +302 -0
  96. package/src/utils/site-identity.js +137 -0
  97. package/src/utils/snapshot-settings.js +112 -0
  98. package/src/utils/update-check.js +302 -0
  99. package/src/utils/uwx-read.js +157 -0
  100. package/src/utils/workspace.js +223 -0
  101. package/src/versions.js +467 -0
  102. package/starter/foundation/main.js +50 -0
  103. package/starter/foundation/sections/Section/index.jsx +95 -0
  104. package/starter/foundation/sections/Section/meta.js +44 -0
  105. package/starter/site/pages/about/1-about.md.hbs +13 -0
  106. package/starter/site/pages/about/page.yml +3 -0
  107. package/starter/site/pages/home/1-welcome.md.hbs +14 -0
  108. package/starter/site/pages/home/page.yml +3 -0
  109. package/templates/foundation/_gitignore +5 -0
  110. package/templates/foundation/components/.gitkeep +0 -0
  111. package/templates/foundation/main.js.hbs +33 -0
  112. package/templates/foundation/package.json.hbs +51 -0
  113. package/templates/foundation/sections/.gitkeep +0 -0
  114. package/templates/foundation/styles.css +6 -0
  115. package/templates/foundation/vite.config.js +3 -0
  116. package/templates/site/_gitignore +12 -0
  117. package/templates/site/entry.js +7 -0
  118. package/templates/site/index.html.hbs +13 -0
  119. package/templates/site/package.json.hbs +27 -0
  120. package/templates/site/public/favicon.svg +7 -0
  121. package/templates/site/site.yml.hbs +91 -0
  122. package/templates/site/theme.yml +99 -0
  123. package/templates/site/vite.config.js +7 -0
  124. package/templates/workspace/AGENTS.md.hbs +2 -0
  125. package/templates/workspace/README.md.hbs +87 -0
  126. package/templates/workspace/_gitignore +29 -0
  127. package/templates/workspace/_vscode/settings.json +6 -0
  128. package/templates/workspace/package.json.hbs +20 -0
  129. 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
+ }