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,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
+ }