uniweb 1.21.3 → 1.21.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uniweb",
3
- "version": "1.21.3",
3
+ "version": "1.21.5",
4
4
  "description": "Create structured Vite + React sites with content/code separation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -41,15 +41,15 @@
41
41
  "js-yaml": "^4.3.2",
42
42
  "prompts": "^2.4.2",
43
43
  "tar": "^7.5.21",
44
- "@uniweb/semantic-parser": "~1.13.0",
45
- "@uniweb/core": "~1.4.7",
46
44
  "@uniweb/content-writer": "~1.3.1",
47
- "@uniweb/schemas": "~1.6.5"
45
+ "@uniweb/schemas": "~1.6.5",
46
+ "@uniweb/semantic-parser": "~1.13.0",
47
+ "@uniweb/core": "~1.4.7"
48
48
  },
49
49
  "peerDependencies": {
50
+ "@uniweb/build": "~1.10.5",
50
51
  "@uniweb/semantic-parser": "~1.13.0",
51
- "@uniweb/content-reader": "~1.5.7",
52
- "@uniweb/build": "~1.10.4"
52
+ "@uniweb/content-reader": "~1.5.7"
53
53
  },
54
54
  "peerDependenciesMeta": {
55
55
  "@uniweb/build": {
@@ -67,7 +67,7 @@ https://uniweb.io/llms.txt
67
67
 
68
68
  It lists every documentation page with a one-line description and a fetchable link. Go there whenever your question isn't answered in this file — and *especially* when you don't know whether a feature exists at all. That's where guessing does the most damage: **a 404 on a path you invented tells you your guess was wrong, not that the feature is missing.** Concluding "the framework doesn't support X" from a failed fetch is a mistake worth actively guarding against; the index answers the existence question directly.
69
69
 
70
- Then **follow the link the index gives you** rather than constructing one yourself. The index is the source of truth for where pages live, so it stays correct when the site's URL conventions change. (Pages also serve markdown to any client that sends `Accept: text/markdown`.)
70
+ Then **follow the link the index gives you** rather than constructing one yourself. The index is the source of truth for where pages live, so it stays correct when the site's URL conventions change. Each link it gives is the page as Markdown.
71
71
 
72
72
  **Clone the repo instead** when you expect several lookups, or may lose network access later:
73
73
 
@@ -1515,6 +1515,8 @@ vars:
1515
1515
 
1516
1516
  Each color generates 11 OKLCH shades (50–950); shade 500 is your exact input. `neutral` takes a named preset rather than hex. Context override keys match token names: `section:` not `bg:`, `primary:` not `btn-primary-bg:`.
1517
1517
 
1518
+ **Quote a color, and any value with one inside it — here, in `site.yml`, in a section's frontmatter.** In YAML a `#` after a space starts a comment: `primary: #3b82f6` leaves `primary` empty, and `gradient: linear-gradient(165deg, #0c1226 0%, #05060f 100%)` keeps only `linear-gradient(165deg,`, which the browser drops. The YAML is valid, so nothing fails — the color is just not there. The build warns when it can tell (*is cut short*, *is empty*) and gives the quoted value to write.
1519
+
1518
1520
  **How colors reach components:** your hex → 11 shades → semantic tokens → components. In light/medium, `--primary` uses shade 600, `--link` 600, `--ring` 500; in dark, `--primary` uses 500 and `--link` 400.
1519
1521
 
1520
1522
  **Buttons use shade 600 — darker than your input color.** That's an accessibility choice for contrast with white text. For brand-exact buttons:
@@ -2910,7 +2912,7 @@ A site can be in more than one place — usually while it moves from one host to
2910
2912
  - **Git is the reviewed, durable record** — you `pull` content back, read it with `git diff`, and commit.
2911
2913
  - **Authors never push or pull.** For them, content simply updates, whether the change came from another author or from a developer's CLI.
2912
2914
 
2913
- **Working on a site others edit — run `uniweb status` when you begin, when you come back to it, before you push, publish or deploy, and after any refusal.** It reads the backend without taking anything: what changed there since this copy last synced, file by file — as `uniweb pull` would write it, translations and records included; what you hold that it lacks; where the site is live, and what services it has; who has it open in the app right now, and on which file. Then it lists the commands to run, in an order that works from the top — `uniweb pull` before the push it would otherwise refuse. Three habits go with it: take what changed (`pull`, or `refresh`) before you edit; never `--force` past a refusal; and when someone has a page open in the app, tell the person before you change that page. A step marked *ask the person first* — going live, which may cost money — is theirs to decide.
2915
+ **Working on a site others edit — run `uniweb status` when you begin, when you come back to it, before you push, publish or deploy, and after any refusal.** It reads the backend without taking anything: what changed there since this copy last synced, file by file — as `uniweb pull` would write it, translations and records included; what you hold that it lacks; where the site is live, and what services it has; who has it open in the app right now, and on which file; and what else in the project is not committed — an update, the foundation's source — which a push neither sends nor waits for, though your team builds what git holds and a push or publish releases a changed foundation from the files. Then it lists the commands to run, in an order that works from the top — `uniweb pull` before the push it would otherwise refuse. Three habits go with it: take what changed (`pull`, or `refresh`) before you edit; never `--force` past a refusal; and when someone has a page open in the app, tell the person before you change that page. A step marked *ask the person first* — a commit, sharing commits, going live, which may cost money — is theirs to decide.
2914
2916
 
2915
2917
  **Nobody's work is overwritten without asking.** `uniweb push` is refused if the site changed on the backend since your last pull — usually an author editing in the app — and reports what changed; nothing is written. Edits to different sections never collide. **In a git repository `uniweb pull` merges, like `git pull`:** it commits the backend's content (*"The site's content on …"*) and merges that commit into your branch, against the last state of the site your history shares with the backend. Your work, committed or not, and a teammate's arriving through git are kept; a file the backend never had is never deleted; changes to different parts of a file combine silently; and only a genuine overlap leaves conflict markers and a non-zero exit — so `uniweb pull && uniweb push` never ships markers. Settle a conflict as in any merge, then `git commit`; where your uncommitted changes overlap what the pull brought, the markers are theirs — settle them and `git add` each file, then `git stash drop` (git keeps them in the stash too). A push or publish is refused while a file under the site is in conflict. **In a git repository a push sends what you have committed:** with the site's content not committed it is refused before anything is sent, naming the files — commit them, or run `uniweb push --commit -m "…"` (it commits the site's content only, and leaves anything else staged as it is); `publish` and `sync` take `--commit` too. That commit is where the next pull merges from, which is how the pull tells your changes from the backend's. What the push writes back into the files — the ids the backend assigned — it commits itself, as `uniweb`. When the backend has nothing new, a pull says so and lists the files where yours hold changes it lacks. `uniweb refresh` runs `git pull` first, then the same pull. A push is also refused when your branch does not hold what this copy last took from the backend — after `git merge --abort`, or in a fresh clone that has not pulled — so pull, then push. Outside git — which includes a folder the repository around it ignores, such as a scratch directory — `uniweb pull` takes the backend's version over your files, asking first. `--force` is the deliberate overwrite (on `push` it replaces the backend's changes, on `pull` it discards yours); don't use it to get past a refusal. `uniweb sync` is `refresh` then `push`; neither publishes.
2916
2918
 
@@ -2965,7 +2967,7 @@ npx uniweb@latest update --dry-run # preview exactly what update would change
2965
2967
  npx uniweb@latest update # apply: align @uniweb/* deps AND refresh AGENTS.md
2966
2968
  ```
2967
2969
 
2968
- When `status` names a newer release, take it: `npx uniweb@latest update`, then build and look at the pages. Releases rarely ask anything of a project.
2970
+ When `status` names a newer release, take it: `npx uniweb@latest update`, then build, look at the pages, and commit what the update changed — `status` lists those files until you do. Releases rarely ask anything of a project.
2969
2971
 
2970
2972
  **`npx uniweb@latest update` is the command for bringing a project up to date.** It aligns the project's `@uniweb/*` dependencies *and* this AGENTS.md to the version matrix of the CLI that runs it. Deps and documentation move together — that's the whole point of the verb.
2971
2973
 
@@ -7,7 +7,8 @@
7
7
  * with the steps to take, in an order that works when followed from the top
8
8
  * (`utils/status-next.js`):
9
9
  *
10
- * git conflicts, a merge waiting, the site's files not committed, the git remote
10
+ * git conflicts, a merge waiting, the site's content and the project's other files
11
+ * not committed, the git remote
11
12
  * app the session, the site, what changed on the backend since this copy last
12
13
  * synced, what is not pushed, whether the draft is live, who has the site open,
13
14
  * the foundation
@@ -42,8 +43,8 @@
42
43
  */
43
44
 
44
45
  import { execFileSync } from 'node:child_process'
45
- import { existsSync, readFileSync } from 'node:fs'
46
- import { basename, join, relative, resolve } from 'node:path'
46
+ import { existsSync, readFileSync, statSync } from 'node:fs'
47
+ import { basename, dirname, join, relative, resolve, sep } from 'node:path'
47
48
  import yaml from 'js-yaml'
48
49
 
49
50
  import {
@@ -80,7 +81,7 @@ import { aheadBehind, remoteMoved, upstreamOf } from '../utils/git.js'
80
81
  import { getLatestVersion, isBehind } from '../utils/update-check.js'
81
82
  import { syncedElsewhere } from '../utils/site-identity.js'
82
83
  import { DEFAULT_BACKEND_ORIGIN } from '../utils/config.js'
83
- import { classifyPackage, findSites, findWorkspaceRoot } from '../utils/workspace.js'
84
+ import { classifyPackage, findSites, findWorkspaceRoot, isWorkspaceRoot } from '../utils/workspace.js'
84
85
  import { getCliVersion } from '../versions.js'
85
86
  import { planNext } from '../utils/status-next.js'
86
87
 
@@ -236,6 +237,63 @@ const count = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`
236
237
 
237
238
  // ─── the checks ─────────────────────────────────────────────────────────────
238
239
 
240
+ /**
241
+ * The project's files that git holds changes to and that are not the site's content — a
242
+ * dependency update, the foundation's source, `deploy.yml`, `AGENTS.md` — relative to the
243
+ * site, as every file list here is. The project is the workspace the site is in, or the site
244
+ * without one; a folder in it that is a workspace of its own is a project of its own.
245
+ *
246
+ * ⭐ A PUSH SENDS ONLY THE CONTENT, SO NOTHING STOPS FOR THESE. A teammate builds what git
247
+ * holds, and a push or publish releases a changed foundation from the files as they are: left
248
+ * uncommitted, an update or a foundation change is in this copy only, and in a release no
249
+ * commit holds. ⛔ Until 2026-10-26 status said "Nothing to do" over an `uniweb update` it
250
+ * had not seen committed.
251
+ *
252
+ * @returns {string[]|null} null when git could not be asked
253
+ */
254
+ function otherUncommitted(siteDir, repo) {
255
+ const root = findWorkspaceRoot(siteDir) || siteDir
256
+ // Only the repository's part of it: a workspace above the repository's top is read from there.
257
+ const scope = relative(repo.top, root).startsWith('..') ? repo.top : root
258
+ let out
259
+ try {
260
+ out = execFileSync('git', ['status', '--porcelain', '-z', '--untracked-files=normal', '--', scope], {
261
+ cwd: repo.top,
262
+ encoding: 'utf8',
263
+ stdio: ['ignore', 'pipe', 'ignore'],
264
+ maxBuffer: 64 * 1024 * 1024
265
+ })
266
+ } catch {
267
+ return null
268
+ }
269
+ const parts = out.split('\0').filter(Boolean)
270
+ const paths = []
271
+ for (let i = 0; i < parts.length; i++) {
272
+ paths.push(parts[i].slice(3))
273
+ if (/[RC]/.test(parts[i].slice(0, 2))) paths.push(parts[++i]) // a rename's source follows it
274
+ }
275
+ const content = sentRoots(siteDir).map((r) => join(siteDir, r))
276
+ const isContent = (abs) => content.some((r) => abs === r || abs.startsWith(r + sep))
277
+ const isDir = (p) => {
278
+ try {
279
+ return statSync(p).isDirectory()
280
+ } catch {
281
+ return false
282
+ }
283
+ }
284
+ // A folder from the file up to the project's root that is a workspace of its own.
285
+ const inOwnProject = (abs) => {
286
+ for (let d = isDir(abs) ? abs : dirname(abs); d !== scope && d.startsWith(scope + sep); d = dirname(d)) {
287
+ if (isWorkspaceRoot(d)) return true
288
+ }
289
+ return false
290
+ }
291
+ return [...new Set(paths)]
292
+ .map((p) => join(repo.top, p.replace(/\/$/, '')))
293
+ .filter((abs) => !isContent(abs) && !inOwnProject(abs))
294
+ .map((abs) => relative(siteDir, abs) || '.')
295
+ }
296
+
239
297
  /**
240
298
  * Git: conflicts, a merge waiting, what is not committed, and the git remote.
241
299
  * @returns {Promise<{ checks: object[], proposals: object[], localFiles: string[]|null }>}
@@ -292,9 +350,31 @@ async function gitChecks(siteDir, { offline, origin }) {
292
350
  files: uncommitted.paths,
293
351
  blocks: ['push', 'publish']
294
352
  }
295
- : { id: 'git.uncommitted', state: 'ok', summary: "The site's files are committed." }
353
+ : { id: 'git.uncommitted', state: 'ok', summary: "The site's content is committed." }
296
354
  )
297
355
 
356
+ // What else in the project is not committed — a merge holds it until its commit, and a
357
+ // repository with no commit yet has said so above.
358
+ const others = merging || !repo.head ? null : otherUncommitted(siteDir, repo)
359
+ if (others?.length) {
360
+ const n = others.length
361
+ const fnd = resolveLocalFoundation(siteDir, readSiteYml(siteDir))
362
+ const ofFoundation = fnd && others.some((f) => {
363
+ const abs = resolve(siteDir, f)
364
+ return abs === fnd.dir || abs.startsWith(fnd.dir + sep)
365
+ })
366
+ checks.push({
367
+ id: 'git.other',
368
+ state: 'attention',
369
+ summary: `${count(n, 'other file')} here ${n === 1 ? 'is' : 'are'} not committed — a push sends only the site's content, and your team builds what git holds.`,
370
+ files: others,
371
+ lines: ofFoundation ? ["The foundation's files are among them: when a push or publish releases it, it is built from these files as they are, so commit them first."] : []
372
+ })
373
+ proposals.push({ step: 'commit', command: null, why: `commit the ${count(n, 'other file')} here: \`git add\` ${n === 1 ? 'it' : 'them'}, then \`git commit -m "<what changed>"\``, ask: true })
374
+ } else if (others) {
375
+ checks.push({ id: 'git.other', state: 'ok', summary: 'Nothing else here is uncommitted.', quiet: true })
376
+ }
377
+
298
378
  // What this copy changed since its last sync, file by file — while its branch holds the
299
379
  // sync point that sync recorded.
300
380
  let localFiles = null
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-10-26T19:09:19.484Z",
3
+ "generatedAt": "2026-10-26T19:45:51.544Z",
4
4
  "packages": {
5
5
  "@uniweb/api": {
6
6
  "version": "1.6.4"
7
7
  },
8
8
  "@uniweb/build": {
9
- "version": "1.10.4"
9
+ "version": "1.10.5"
10
10
  },
11
11
  "@uniweb/content-reader": {
12
12
  "version": "1.5.7"
package/src/index.js CHANGED
@@ -1784,7 +1784,8 @@ where, live where, deployed where — and checks:
1784
1784
  project the workspace's sites and foundations, the foundation this site is built
1785
1785
  with and the other sites it reaches, pages from outside the site's folder,
1786
1786
  the languages
1787
- git conflicts, a merge waiting, the site's files not committed, the git remote
1787
+ git conflicts, a merge waiting, the site's content not committed, the project's
1788
+ other files not committed (an update, the foundation's source), the git remote
1788
1789
  app the session, what changed on the backend since this copy last synced —
1789
1790
  the files a pull would change — what is not pushed, who has the site
1790
1791
  open in the app, the foundation
@@ -7,13 +7,19 @@
7
7
  * then sends the reader back up. So the order is one table, not each check's own idea:
8
8
  *
9
9
  * sign in → settle conflicts → finish a merge → take what others did (refresh, or pull)
10
- * → push → push to git → go live → deploy to another host → update Uniweb → read doctor's findings
10
+ * → commit what else changed → push → push to git → go live → deploy to another host
11
+ * → update Uniweb → read doctor's findings
12
+ *
13
+ * A commit of the project's other files needs nothing above it — refresh's `git pull` keeps
14
+ * uncommitted work (`--autostash`, `utils/git.js::pullRemote`) — and comes before the push,
15
+ * which with the publish releases a changed foundation from the files as they are, and before a
16
+ * push to git, which shares only commits.
11
17
  *
12
18
  * Each check proposes steps by name (`step`); this sorts them, folds the ones another step
13
19
  * already does, and drops duplicates.
14
20
  */
15
21
 
16
- const ORDER = ['login', 'conflicts', 'merge', 'refresh', 'pull', 'push', 'gitpush', 'publish', 'deploy', 'update', 'doctor']
22
+ const ORDER = ['login', 'conflicts', 'merge', 'refresh', 'pull', 'commit', 'push', 'gitpush', 'publish', 'deploy', 'update', 'doctor']
17
23
 
18
24
  /**
19
25
  * @param {Array<{ step: string, command: string|null, why: string, ask?: boolean }>} proposals