uniweb 0.17.0 → 0.18.1
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 +4 -4
- package/partials/agents.md +21 -0
- package/src/backend/site-sync.js +41 -4
- package/src/commands/pull.js +1 -1
- package/src/framework-index.json +2 -2
- package/src/utils/flag-guard.js +51 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.1",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -43,12 +43,12 @@
|
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
44
|
"@uniweb/kit": "^0.11.3",
|
|
45
45
|
"@uniweb/core": "^0.8.5",
|
|
46
|
-
"@uniweb/runtime": "^0.11.
|
|
46
|
+
"@uniweb/runtime": "^0.11.7"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"@uniweb/
|
|
49
|
+
"@uniweb/content-reader": "^1.2.2",
|
|
50
50
|
"@uniweb/semantic-parser": "^1.2.2",
|
|
51
|
-
"@uniweb/
|
|
51
|
+
"@uniweb/build": "^0.18.5"
|
|
52
52
|
},
|
|
53
53
|
"peerDependenciesMeta": {
|
|
54
54
|
"@uniweb/build": {
|
package/partials/agents.md
CHANGED
|
@@ -1906,6 +1906,7 @@ uniweb add ci --target foundation # Publish a foundation for free at permanent v
|
|
|
1906
1906
|
# (GitHub Pages → foundations/<name>/<version>/entry.js)
|
|
1907
1907
|
|
|
1908
1908
|
uniweb push / pull / clone / status # Git-style content sync with the Uniweb backend
|
|
1909
|
+
uniweb push --org @acme # First push/publish of a site: who owns it (see below)
|
|
1909
1910
|
uniweb register [--scope @org] # Register a foundation + its data schemas to the registry
|
|
1910
1911
|
uniweb login / logout # Start or clear the backend session the verbs above reuse
|
|
1911
1912
|
uniweb org list / create <handle> # Publish orgs you belong to — the @org in a scoped ref
|
|
@@ -1947,6 +1948,26 @@ Foundations have their own free path too: `uniweb add ci --target foundation` pu
|
|
|
1947
1948
|
|
|
1948
1949
|
**Content authors work visually in the Uniweb App.** They compose the same extended markdown and set the same component params you defined, through a visual editor — never touching code, git, or the CLI. They see exactly the section types your foundation offers and exactly the knobs each one exposes, because **every `meta.js` is registered as the foundation's schema** when you publish. Your `meta.js` is the app's UI (see *meta.js* in Part 4).
|
|
1949
1950
|
|
|
1951
|
+
> ⚠️ **The FIRST push or publish of a site asks who owns it — and refuses if it cannot ask.**
|
|
1952
|
+
> That create decides which organization owns the site (and whose storage its assets are billed
|
|
1953
|
+
> to), it is the only moment the choice is made, and there is no CLI command to change it
|
|
1954
|
+
> afterwards. At a terminal you get a picker. **Without a terminal — CI, a script, or you, an
|
|
1955
|
+
> agent — the command exits non-zero instead of choosing**, and `--yes` refuses too, because
|
|
1956
|
+
> guessing an owner is not an answer. Name it and it never asks again:
|
|
1957
|
+
>
|
|
1958
|
+
> ```bash
|
|
1959
|
+
> uniweb publish --org @acme # an organization
|
|
1960
|
+
> uniweb publish --personal # your personal account, deliberately
|
|
1961
|
+
> ```
|
|
1962
|
+
>
|
|
1963
|
+
> The answer is recorded in `site.yml::$org` and committed, so it is a one-time choice per site,
|
|
1964
|
+
> not per machine. **Ask the human which one to use** rather than picking for them — a site in
|
|
1965
|
+
> the wrong org cannot be moved from here. Sites that already exist are unaffected: their
|
|
1966
|
+
> ownership is settled, so nothing is asked.
|
|
1967
|
+
>
|
|
1968
|
+
> Relatedly: these commands now **reject flags they do not recognize** instead of ignoring them.
|
|
1969
|
+
> If you get `Unknown flag`, read the suggestion — it is usually a near miss.
|
|
1970
|
+
|
|
1950
1971
|
**Sync is developer-only and one-sided by design.** `uniweb push` and `uniweb pull` are your commands, not theirs:
|
|
1951
1972
|
|
|
1952
1973
|
- **The app is the live source of truth for content** — where authors work and where your push lands live.
|
package/src/backend/site-sync.js
CHANGED
|
@@ -101,6 +101,15 @@ export function extractFinalized(payload) {
|
|
|
101
101
|
// resubmit returns the value we already hold, and anything else is theirs
|
|
102
102
|
// to report, not ours to derive.
|
|
103
103
|
version: typeof d?.version === 'string' ? d.version : null,
|
|
104
|
+
// Per-item post-write tokens, `{ recordUuid: <opaque> }` (backend `d7e46335`).
|
|
105
|
+
// Same map shape and same key name the PULL manifest entry stamps, deliberately
|
|
106
|
+
// — one kind of value, cached from whichever lane last handed it over, echoed
|
|
107
|
+
// as `item_base_versions`. Absent on an older backend ⇒ null ⇒ the cached
|
|
108
|
+
// tokens are left alone and gating degrades to the entity grain.
|
|
109
|
+
itemVersions:
|
|
110
|
+
d?.item_versions && typeof d.item_versions === 'object'
|
|
111
|
+
? d.item_versions
|
|
112
|
+
: null,
|
|
104
113
|
document: d?.document ?? null
|
|
105
114
|
}))
|
|
106
115
|
.filter((e) => Number.isInteger(e.index) && e.uuid)
|
|
@@ -1060,9 +1069,37 @@ export async function pushSyncPackages({
|
|
|
1060
1069
|
// otherwise the next attempt would re-send lane 1 with a base the backend has
|
|
1061
1070
|
// already moved past, and refuse a push the user just made.
|
|
1062
1071
|
const newVersions = {}
|
|
1072
|
+
// Per-item tokens from the SAME response. Keyed by record `$uuid` and flat
|
|
1073
|
+
// across entities (the cache is a single map), unlike `newVersions`, which is
|
|
1074
|
+
// keyed by entity uuid.
|
|
1075
|
+
//
|
|
1076
|
+
// ⛔ Both grains must be re-armed from the push, for one reason: a push writes,
|
|
1077
|
+
// so every token this clone holds for a record it just changed is now stale. Read
|
|
1078
|
+
// on pull only, push 2 classifies that record `pkg != base, host != base` and the
|
|
1079
|
+
// backend refuses it — a conflict naming records nobody else touched, on a second
|
|
1080
|
+
// push with no edit in between. And it is unrecoverable locally, because the
|
|
1081
|
+
// tokens are opaque by contract: a pull would be the only other source, and a
|
|
1082
|
+
// pull rewrites the working tree. That is precisely the `--force` habit the gate
|
|
1083
|
+
// exists to prevent. Returning the entity `version` on push was added to fix this
|
|
1084
|
+
// exact shape one grain up (see delivery-lane.md "Both feed directions are
|
|
1085
|
+
// load-bearing"); the item grain had the same hole until backend `d7e46335`
|
|
1086
|
+
// started echoing `item_versions` here.
|
|
1087
|
+
const newItemVersions = {}
|
|
1063
1088
|
const harvest = (finalized) => {
|
|
1064
|
-
for (const f of finalized || [])
|
|
1089
|
+
for (const f of finalized || []) {
|
|
1065
1090
|
if (f.uuid && f.version) newVersions[f.uuid] = f.version
|
|
1091
|
+
// Unconditionally, and NOT gated on `changed` — same rule as the entity
|
|
1092
|
+
// token: the backend pins "zero-write ⇒ version unmoved", so a no-op
|
|
1093
|
+
// resubmit hands back the value we already hold. An older backend omits the
|
|
1094
|
+
// field entirely, which leaves the cached tokens alone and degrades to the
|
|
1095
|
+
// entity grain, exactly as before.
|
|
1096
|
+
if (f.itemVersions) Object.assign(newItemVersions, f.itemVersions)
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
// Both grains land together, at every point the old code banked the entity one.
|
|
1100
|
+
const mergeHarvested = () => {
|
|
1101
|
+
mergeBaseVersions(siteDir, newVersions)
|
|
1102
|
+
mergeItemBaseVersions(siteDir, newItemVersions)
|
|
1066
1103
|
}
|
|
1067
1104
|
// The backend's post-write copy of the site-content document, kept for the
|
|
1068
1105
|
// remote-side unit base (see writeUnitBases).
|
|
@@ -1085,7 +1122,7 @@ export async function pushSyncPackages({
|
|
|
1085
1122
|
{ boundUuid: siteContentUuid }
|
|
1086
1123
|
)
|
|
1087
1124
|
if (!finalized) {
|
|
1088
|
-
|
|
1125
|
+
mergeHarvested()
|
|
1089
1126
|
return { exitCode: 1, finalizedTotal, wrote }
|
|
1090
1127
|
}
|
|
1091
1128
|
harvest(finalized)
|
|
@@ -1147,7 +1184,7 @@ export async function pushSyncPackages({
|
|
|
1147
1184
|
{ boundUuid: boundSiteUuid }
|
|
1148
1185
|
)
|
|
1149
1186
|
if (!finalized) {
|
|
1150
|
-
|
|
1187
|
+
mergeHarvested()
|
|
1151
1188
|
return { exitCode: 1, finalizedTotal, wrote }
|
|
1152
1189
|
}
|
|
1153
1190
|
harvest(finalized)
|
|
@@ -1164,7 +1201,7 @@ export async function pushSyncPackages({
|
|
|
1164
1201
|
// Entities absent from finalized[] (skipped, or not editable) keep their cached
|
|
1165
1202
|
// value — absence is not invalidation.
|
|
1166
1203
|
writeSyncCache(siteDir, hashes)
|
|
1167
|
-
|
|
1204
|
+
mergeHarvested()
|
|
1168
1205
|
// Re-base the page attribution: our emitted document and the backend's post-write
|
|
1169
1206
|
// copy of it are the two sides' new agreed state. Only when the site-content lane
|
|
1170
1207
|
// actually shipped — a push that skipped it left that state where it was.
|
package/src/commands/pull.js
CHANGED
|
@@ -76,13 +76,13 @@ import {
|
|
|
76
76
|
computeUnitHashes,
|
|
77
77
|
collectUnitUuids
|
|
78
78
|
} from '@uniweb/build/uwx'
|
|
79
|
-
import { makeModelResolver } from './push.js'
|
|
80
79
|
import {
|
|
81
80
|
readWritten,
|
|
82
81
|
recordWritten,
|
|
83
82
|
isPullOutput
|
|
84
83
|
} from '../utils/pull-written.js'
|
|
85
84
|
import {
|
|
85
|
+
makeModelResolver,
|
|
86
86
|
mergeBaseVersions,
|
|
87
87
|
mergeItemBaseVersions,
|
|
88
88
|
writeUnitBases,
|
package/src/framework-index.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-08-
|
|
3
|
+
"generatedAt": "2026-08-12T23:11:50.036Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/build": {
|
|
6
6
|
"version": "0.18.5",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
]
|
|
74
74
|
},
|
|
75
75
|
"@uniweb/runtime": {
|
|
76
|
-
"version": "0.11.
|
|
76
|
+
"version": "0.11.7",
|
|
77
77
|
"path": "framework/runtime",
|
|
78
78
|
"deps": [
|
|
79
79
|
"@uniweb/core",
|
package/src/utils/flag-guard.js
CHANGED
|
@@ -30,37 +30,81 @@ import { findUnknownFlags, didYouMean } from './args.js'
|
|
|
30
30
|
/** Accepted by every command, wherever they are actually consumed. */
|
|
31
31
|
const GLOBAL = ['--non-interactive', '--help', '-h']
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Login-method flags. Any verb that can hit an unauthenticated backend may fall
|
|
35
|
+
* into `ensureRegistryAuth`, which hands `args` to the login picker — so these are
|
|
36
|
+
* genuinely reachable from all of them, not just from `uniweb login`.
|
|
37
|
+
*/
|
|
38
|
+
const AUTH = ['--browser', '--password', '--token-paste']
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Flags a verb inherits by importing `commands/deploy.js` for `resolveSiteDir` /
|
|
42
|
+
* `resolveSiteBackend`. Inert on these verbs, and listed rather than filtered:
|
|
43
|
+
* accepting a flag that does nothing is exactly the pre-guard behaviour, while
|
|
44
|
+
* rejecting one that works is a broken command. The guard's job is catching
|
|
45
|
+
* `--backed`, not policing inert-but-valid spellings.
|
|
46
|
+
*/
|
|
47
|
+
const VIA_DEPLOY = ['--target', '--host', '--no-save']
|
|
48
|
+
|
|
33
49
|
/**
|
|
34
50
|
* Per-verb flag sets. Derived by scanning each command for dash-literals AND the
|
|
35
51
|
* helpers it calls — not from the help text, which has drifted from the parser in
|
|
36
52
|
* both directions (`--as-org` was implemented and undocumented; `--yes` is
|
|
37
53
|
* documented on `publish` and consumed two files away).
|
|
38
54
|
*/
|
|
39
|
-
|
|
55
|
+
const VERBS = {
|
|
40
56
|
push: [
|
|
41
57
|
'--all', '--as-org', '--org', '--backend', '--dry-run', '--force',
|
|
42
58
|
'--foundation', '--output', '-o', '--personal', '--registry', '--token',
|
|
43
|
-
|
|
59
|
+
// read in utils/conformance.js and backend/site-sync.js respectively —
|
|
60
|
+
// neither appears in push.js
|
|
61
|
+
'--no-validate', '--yes', ...VIA_DEPLOY
|
|
44
62
|
],
|
|
45
63
|
publish: [
|
|
46
64
|
'--as-org', '--org', '--backend', '--dry-run', '--force', '--foundation',
|
|
47
|
-
'--
|
|
65
|
+
'--personal', '--registry', '--token',
|
|
66
|
+
// read in utils/conformance.js, backend/site-sync.js, and
|
|
67
|
+
// backend/foundation-bring-along.js — none appear in publish.js
|
|
68
|
+
'--no-validate', '--yes', '--no-verify', ...VIA_DEPLOY
|
|
48
69
|
],
|
|
49
70
|
pull: [
|
|
50
71
|
'--backend', '--content-only', '--dry-run', '--force', '--merge',
|
|
51
|
-
'--no-collections', '--no-delete', '--no-prune', '--registry', '--token'
|
|
72
|
+
'--no-collections', '--no-delete', '--no-prune', '--registry', '--token',
|
|
73
|
+
// via backend/site-sync.js (the owner resolver) and utils/conformance.js
|
|
74
|
+
'--yes', '--org', '--as-org', '--no-validate', ...VIA_DEPLOY
|
|
52
75
|
],
|
|
53
76
|
clone: [
|
|
54
77
|
'--backend', '--content-only', '--no-collections', '--path', '--project',
|
|
55
|
-
'--registry', '--token'
|
|
78
|
+
'--registry', '--token', '--org', '--as-org'
|
|
56
79
|
],
|
|
57
80
|
register: [
|
|
58
81
|
'--backend', '--dry-run', '--json', '--output', '-o', '--registry',
|
|
59
|
-
'--schema-only', '--scope', '--token'
|
|
82
|
+
'--schema-only', '--scope', '--token', '--org', '--as-org'
|
|
60
83
|
],
|
|
61
|
-
status: [
|
|
84
|
+
status: [
|
|
85
|
+
'--backend', '--json', '--registry', '--remote', '--token', '--dry-run',
|
|
86
|
+
'--force', '--no-verify', '--no-validate', '--yes', '--org', '--as-org',
|
|
87
|
+
...VIA_DEPLOY
|
|
88
|
+
]
|
|
62
89
|
}
|
|
63
90
|
|
|
91
|
+
/**
|
|
92
|
+
* The accepted set per verb: its own flags, plus the login-method flags every
|
|
93
|
+
* backend verb can reach, plus the globals.
|
|
94
|
+
*
|
|
95
|
+
* Derived from each verb's IMPORT GRAPH, not from its own source and not from the
|
|
96
|
+
* help text — `test/flag-guard-coverage.test.js` walks that graph and fails if a
|
|
97
|
+
* verb can honour a flag this list omits. Deliberately an over-approximation: a
|
|
98
|
+
* flag accepted here but inert costs nothing (it was ignored before the guard
|
|
99
|
+
* existed), while one rejected here breaks a working command.
|
|
100
|
+
*/
|
|
101
|
+
export const VERB_FLAGS = Object.fromEntries(
|
|
102
|
+
Object.entries(VERBS).map(([verb, flags]) => [
|
|
103
|
+
verb,
|
|
104
|
+
[...new Set([...flags, ...AUTH])]
|
|
105
|
+
])
|
|
106
|
+
)
|
|
107
|
+
|
|
64
108
|
/**
|
|
65
109
|
* Check `args` against the verb's accepted set. Returns null when everything is
|
|
66
110
|
* recognized, or a ready-to-print message naming the first offender (plus a
|