uniweb 1.21.2 → 1.21.4
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 +5 -5
- package/partials/agents.md +4 -2
- package/src/commands/status.js +85 -5
- package/src/framework-index.json +3 -3
- package/src/index.js +2 -1
- package/src/utils/status-next.js +8 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.4",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -42,14 +42,14 @@
|
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
43
|
"tar": "^7.5.21",
|
|
44
44
|
"@uniweb/content-writer": "~1.3.1",
|
|
45
|
-
"@uniweb/schemas": "~1.6.5",
|
|
46
45
|
"@uniweb/core": "~1.4.7",
|
|
47
|
-
"@uniweb/semantic-parser": "~1.13.0"
|
|
46
|
+
"@uniweb/semantic-parser": "~1.13.0",
|
|
47
|
+
"@uniweb/schemas": "~1.6.5"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@uniweb/build": "~1.10.4",
|
|
51
50
|
"@uniweb/content-reader": "~1.5.7",
|
|
52
|
-
"@uniweb/semantic-parser": "~1.13.0"
|
|
51
|
+
"@uniweb/semantic-parser": "~1.13.0",
|
|
52
|
+
"@uniweb/build": "~1.10.5"
|
|
53
53
|
},
|
|
54
54
|
"peerDependenciesMeta": {
|
|
55
55
|
"@uniweb/build": {
|
package/partials/agents.md
CHANGED
|
@@ -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
|
|
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
|
|
package/src/commands/status.js
CHANGED
|
@@ -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
|
|
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
|
|
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 source is among them: the next push or publish releases it from these files, 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
|
package/src/framework-index.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-10-
|
|
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.
|
|
9
|
+
"version": "1.10.5"
|
|
10
10
|
},
|
|
11
11
|
"@uniweb/content-reader": {
|
|
12
12
|
"version": "1.5.7"
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"version": "1.0.6"
|
|
25
25
|
},
|
|
26
26
|
"@uniweb/kit": {
|
|
27
|
-
"version": "1.6.
|
|
27
|
+
"version": "1.6.6"
|
|
28
28
|
},
|
|
29
29
|
"@uniweb/loom": {
|
|
30
30
|
"version": "1.1.2"
|
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
|
|
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
|
package/src/utils/status-next.js
CHANGED
|
@@ -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
|
|
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
|