uniweb 0.85.0 → 0.86.0
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 +7 -7
- package/partials/agents.md +44 -28
- package/src/backend/client.js +59 -6
- package/src/backend/foundation-bring-along.js +5 -3
- package/src/backend/service-request.js +6 -2
- package/src/backend/site-preview.js +1 -1
- package/src/backend/site-sync.js +7 -7
- package/src/commands/build.js +6 -2
- package/src/commands/clone.js +1 -1
- package/src/commands/deploy.js +3 -3
- package/src/commands/forget.js +7 -7
- package/src/commands/publish.js +14 -7
- package/src/commands/pull.js +9 -9
- package/src/commands/push.js +13 -7
- package/src/commands/refresh.js +2 -1
- package/src/commands/register.js +6 -6
- package/src/commands/rename.js +6 -6
- package/src/commands/site.js +358 -0
- package/src/commands/snapshot.js +14 -5
- package/src/commands/status.js +1 -1
- package/src/framework-index.json +11 -11
- package/src/index.js +41 -11
- package/src/utils/config.js +13 -13
- package/src/utils/flag-guard.js +30 -16
- package/src/utils/site-identity.js +3 -3
- package/src/utils/yaml-edit.js +0 -115
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.86.0",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -42,15 +42,15 @@
|
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
44
|
"@uniweb/content-writer": "^0.3.4",
|
|
45
|
-
"@uniweb/
|
|
46
|
-
"@uniweb/
|
|
47
|
-
"@uniweb/
|
|
48
|
-
"@uniweb/
|
|
49
|
-
"@uniweb/
|
|
45
|
+
"@uniweb/core": "^0.38.0",
|
|
46
|
+
"@uniweb/runtime": "^0.29.3",
|
|
47
|
+
"@uniweb/semantic-parser": "^1.5.1",
|
|
48
|
+
"@uniweb/schemas": "^0.13.0",
|
|
49
|
+
"@uniweb/kit": "^0.20.0"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"@uniweb/content-reader": "^1.2.8",
|
|
53
|
-
"@uniweb/build": "^0.
|
|
53
|
+
"@uniweb/build": "^0.79.0",
|
|
54
54
|
"@uniweb/semantic-parser": "^1.5.1"
|
|
55
55
|
},
|
|
56
56
|
"peerDependenciesMeta": {
|
package/partials/agents.md
CHANGED
|
@@ -899,7 +899,7 @@ brief:
|
|
|
899
899
|
The body — the value of the schema's content field, `body.content`.
|
|
900
900
|
```
|
|
901
901
|
|
|
902
|
-
**A component receives it in one of two shapes, and its `meta.js` says which**, per `data:` key — the same on a static site and a hosted one. `data: { articles: '@std/article' }` gets **briefs**: the brief's fields at the top (`article.title`), no other section. `data: { articles: '@std/article
|
|
902
|
+
**A component receives it in one of two shapes, and its `meta.js` says which**, per `data:` key — the same on a static site and a hosted one. `data: { articles: '@std/article' }` gets **briefs**: the brief's fields at the top (`article.title`), no other section. `data: { articles: { schema: '@std/article', whole: true } }` gets each record **whole**, as stored: every section under its own name, the brief's included (`article.brief.title`, `article.body.content`). Both carry `$name`, the handle. Lists carry briefs; a section that shows a record in full declares `whole: true`. A `fields:` schema's brief is the whole record, so its components need no `whole: true`. Full rules: `reference/entity-content.md`.
|
|
903
903
|
|
|
904
904
|
**A reference names the record it points at.** A `{ ref: '@/speaker' }` field is written as that record's name — `speaker: ada` for `records/speaker/ada.yml` (its file's name) — and a `many` one as a list of names. A component receives `{ entity, brief }`: the record reduced to its brief, so `talk.speaker.brief.name`. `uniweb validate` and a push report a name that matches no record; a push sends each reference to the record it names (in the same push when that record is new too), and a pull writes the name back.
|
|
905
905
|
|
|
@@ -962,15 +962,15 @@ pages/blog/
|
|
|
962
962
|
|
|
963
963
|
**Link a card with `item.$route` — never compose the URL.** Every record a query delivers carries `$route`, the URL of the parametric page whose route query is that query (`/blog/design-tips`), wherever the list appears — the query's own page, the homepage, a sidebar. No page for the query, or no value for the field its URL is built from, means no `$route`, never a broken one. `detailPage: page:<id>` on a fetch links the records to another page instead. `$` marks a field the framework fills, so a record's own `route` field is left alone. ⛔ `route:` on a query is retired and stops the build.
|
|
964
964
|
|
|
965
|
-
**Which query the URL narrows — the page's route query:** the `[slug]` page's own `query:`, else its parent page's (the usual shape, above), else `site.yml`'s (for a top-level `pages/[slug]/` only — the site's reaches no deeper page); if none declares one, the query its sections all declare. The first query of that level wins. Every section the route query reaches gets the one record; a section declaring a *different* query of its own gets that query as declared. The folder name says what the URL segment matches: `[slug]` the record's name (`$name`, a compiled record's file name) — or the field its route query binds `:slug` to (`where: { slug: :slug }`, how an external API's records, which have no `$name`, get pages), `[uuid]` its `$uuid`, any other `[name]` the record's own field of that name — and when that field holds several values, **any member** matches (the record's own link is its first value). Routing by a field that is not unique picks one record and which one is not guaranteed; the build warns. A folder inside `[slug]/` (`[slug]/cv/` → `/blog/:slug/cv`) is a parametric page too, about the same record: its route query is the `[slug]` page's, wherever that is declared, and a query `cv/` declares itself arrives under its own key. **A section chooses how it uses the page's record with `current:`** on its own `fetch:` — `only` (the default: the record
|
|
965
|
+
**Which query the URL narrows — the page's route query:** the `[slug]` page's own `query:`, else its parent page's (the usual shape, above), else `site.yml`'s (for a top-level `pages/[slug]/` only — the site's reaches no deeper page); if none declares one, the query its sections all declare. The first query of that level wins. Every section the route query reaches gets the one record; a section declaring a *different* query of its own gets that query as declared. The folder name says what the URL segment matches: `[slug]` the record's name (`$name`, a compiled record's file name) — or the field its route query binds `:slug` to (`where: { slug: :slug }`, how an external API's records, which have no `$name`, get pages), `[uuid]` its `$uuid`, any other `[name]` the record's own field of that name — and when that field holds several values, **any member** matches (the record's own link is its first value). Routing by a field that is not unique picks one record and which one is not guaranteed; the build warns. A folder inside `[slug]/` (`[slug]/cv/` → `/blog/:slug/cv`) is a parametric page too, about the same record: its route query is the `[slug]` page's, wherever that is declared, and a query `cv/` declares itself arrives under its own key. **A section chooses how it uses the page's record with `current:`** on its own `fetch:` — `only` (the default: the record), `exclude` (the others: "related"), `include` (all of them: a pager) — e.g. `fetch: { query: articles, as: related, current: exclude, limit: 3 }`, where `limit` counts the others, all from the query's set. **`current:` follows the query, not the key:** a fetch of the route query gets the record unless it says otherwise, whatever its `as`; a fetch of another query gets that query's records and reads `current:` only when written (`exclude` drops the page's record, `only` keeps just it). `refine: true` / `detail: false` are retired and stop the build. `[dir]` and `[path]` are refused as folder names, and so is any folder inside `[...path]/`.
|
|
966
966
|
|
|
967
|
-
> **The record arrives
|
|
967
|
+
> **The record arrives under the key the component declares, as that key says** — one record for a key declared `single: true` (`content.data.article` for `article: { schema: '@std/article', single: true }` over an `@std/article` query; `null` when the URL names none), a single-element array for any other key (`content.data.recent[0]`). It is the record's brief, or the record whole where the key declares `whole: true` — a section that shows an article's body declares `whole: true`. See *Data* in Part 4.
|
|
968
968
|
|
|
969
|
-
**Records with URLs of their own shape — `[...path]/`.** A folder named exactly `[...path]` (one fixed spelling) captures the rest of the URL: `/blog/my-post` and `/blog/rust/2025/my-post` both reach it. The capture yields three standard variables — `:path` (the whole capture), `:dir` (everything before the last segment), `:slug` (the last segment, the record's handle) — and the record is still delivered by its handle, so the section reads
|
|
969
|
+
**Records with URLs of their own shape — `[...path]/`.** A folder named exactly `[...path]` (one fixed spelling) captures the rest of the URL: `/blog/my-post` and `/blog/rust/2025/my-post` both reach it. The capture yields three standard variables — `:path` (the whole capture), `:dir` (everything before the last segment), `:slug` (the last segment, the record's handle) — and the record is still delivered by its handle, so the section reads it as before. The same three variables exist under every parametric page: under `[slug]`, `:slug` and `:path` are the segment and `:dir` is empty. A record's own URL (`$route`, and the page a static build writes) is its name, as under `[slug]`: `/blog/my-post` wherever `records/folder.yml` places it — a record does not carry its folder. A query may bind a part — `scope: :dir` exposes the folder branch, `where: { tag: :dir }` keeps it private — and an unbound or empty variable drops its clause, so one saved query serves the query's page and its parametric page, on a static site and a hosted one alike. Without `scope: :dir` the directory is decoration: the record is found by its handle wherever it sits. Reference: `reference/dynamic-routes.md`.
|
|
970
970
|
|
|
971
971
|
**Two options for bigger sets:**
|
|
972
972
|
|
|
973
|
-
A list carries briefs, so a schema that keeps its heavy parts out of the brief — `@std/article`'s body is its own section — keeps lists light with no configuration, while a section declaring
|
|
973
|
+
A list carries briefs, so a schema that keeps its heavy parts out of the brief — `@std/article`'s body is its own section — keeps lists light with no configuration, while a section declaring `whole: true` still receives the whole record on a `[slug]` page, and other components fetch it on demand with `useWholeRecord`. File-based records are written one file per record, whole, at `/data/<name>/<name of the record>.json`; an external query names its one-record request with `record:` instead (*Fetching from other sources* in Part 4). (`deferred:` is removed: a schema's brief decides what a list holds.)
|
|
974
974
|
|
|
975
975
|
`queryable:` declares which fields a reader may filter on, with enough metadata for the foundation to render controls:
|
|
976
976
|
|
|
@@ -1873,21 +1873,34 @@ Content-less containers appear as group nodes (`hasContent: false`) — use `nav
|
|
|
1873
1873
|
|
|
1874
1874
|
**A section receives the keys its component declares in `meta.js` `data:` — and nothing else** (plus any its foundation declares in `main.js` `data:`). A component that reads `content.data.articles` declares `articles`. Each declared key is filled by, in order: a tagged data block in the section; else the fetch that fills it, level by level from the section's own to the site's — a fetch whose `as` (its query's name by default) is the key, or else, **automatic `as`**, the first fetch under an undeclared key whose query's schema is the key's (`@/x` matches any scope's `x`); else `null`. So `data: { related: '@std/article' }` receives an `articles` query under `related`. `as:` on a fetch picks the key when two keys or two fetches share a schema. A fetch that fills none of a section's keys is not requested for it.
|
|
1875
1875
|
|
|
1876
|
-
**A
|
|
1876
|
+
**A key says how many records it holds, and how much of each — two flags, independent.** A ref alone (`'@std/article'`) is a **list of briefs**: on the query's own page every record the fetch takes, on a parametric page (`[slug]/`) a single-element array, `[]` when nothing matches. Write the key long to change either:
|
|
1877
1877
|
|
|
1878
|
-
|
|
1878
|
+
```js
|
|
1879
|
+
data: {
|
|
1880
|
+
articles: '@std/article', // a list of briefs
|
|
1881
|
+
article: { schema: '@std/article', single: true, whole: true }, // one record, whole
|
|
1882
|
+
card: { schema: '@std/article', single: true }, // one record's brief
|
|
1883
|
+
archive: { schema: '@std/article', whole: true }, // a list of whole records
|
|
1884
|
+
}
|
|
1885
|
+
```
|
|
1886
|
+
|
|
1887
|
+
- **`single: true`** — the key holds **one record**: the first its binding selects (on a `[slug]` page, the record the URL names), or `null` when there is none. The runtime asks for just that one. A binding over many gives the first: sort the query, or give the binding `limit: 1`.
|
|
1888
|
+
- **`whole: true`** — each record **whole**, as stored: every section under its own name, the brief's included — `article.brief.title`, `article.body.content`. Without it, each record is its **brief**: the brief section's fields at the top, the section not named. The runtime asks for what the key declares; a `whole` key its source cannot answer whole is `null`. A `fields:` schema's brief is the whole record, so `whole` is only needed for a schema with more than one section.
|
|
1889
|
+
- The long form takes `schema`, `single` and `whole`, nothing else — the build stops on any other property, and on `single` or `whole` over a schema whose root is a list (`@std/nav`, `@std/form`: the key holds that list). ⛔ `'@std/article/*'` is retired and stops the build: write `whole: true`.
|
|
1890
|
+
|
|
1891
|
+
When nothing fills a key it is `null`. An external query's records have no schema and arrive as its source answers them. Reference: `reference/component-metadata.md` § *One record, or a list* and § *Briefs or whole records*.
|
|
1879
1892
|
|
|
1880
1893
|
```jsx
|
|
1881
|
-
function Article({ content, block }) {
|
|
1894
|
+
function Article({ content, block }) { // data: { article: { schema: '@std/article', single: true } }
|
|
1882
1895
|
if (block.dataLoading) return <DataPlaceholder />
|
|
1883
|
-
if (block.dataError?.
|
|
1884
|
-
const article = content.data.
|
|
1896
|
+
if (block.dataError?.article) return <LoadFailed /> // the request failed — not "no records"
|
|
1897
|
+
const article = content.data.article // the record the [slug] page's URL names, or null
|
|
1885
1898
|
if (!article) return <NotFound />
|
|
1886
1899
|
return <ArticleView article={article} />
|
|
1887
1900
|
}
|
|
1888
1901
|
```
|
|
1889
1902
|
|
|
1890
|
-
|
|
1903
|
+
Any other reshaping is the foundation's job — once, with a `handlers.data` hook.
|
|
1891
1904
|
|
|
1892
1905
|
**Declaring keys and schemas.** `meta.js` declares each `content.data` key the component receives, with its schema, in a single `data:` field — there is no separate `schemas:` key. Each value is a **named ref**, an **inline field map**, an **inline rich-form** (`{ fields: [...] }`, an editor form), or `{}` for a key with no schema (an external API's records). Refs resolve on disk at build time, never fetched: `@/name` (this foundation's `schemas/`), `@std/name` (shared standards, from `@uniweb/schemas`), `@org/name` (an org's own `@org/schemas` package). A schema drives the editor, checks your data (`uniweb validate`), and lets a fetch of another name fill the key — it fills no field: **a record reaches the component as it is**, so a field it lacks is absent, and what that renders as is your component's choice (`record.status ?? 'published'`). **A named schema declares no `default:`** — the build refuses one; a `default:` belongs only in an inline field map or form, where an editor pre-fills from it. ⛔ **Delivery was default-on until this change** — every fetched key reached every component, and `data:` was a hint; a component that reads a key it does not declare now receives nothing under it. `data: false` declares nothing, like no `data:`. Keys a foundation's handlers (or a shared hook) read go in `main.js` `data:`, in the same form, and every section receives them.
|
|
1893
1906
|
|
|
@@ -1915,7 +1928,7 @@ fetch:
|
|
|
1915
1928
|
limit: 3
|
|
1916
1929
|
```
|
|
1917
1930
|
|
|
1918
|
-
**Lean lists.** A list carries each record's brief, so heavy parts belong in a section of their own — `@std/article`'s body is `body` — and never ride a list. A section that shows them declares
|
|
1931
|
+
**Lean lists.** A list carries each record's brief, so heavy parts belong in a section of their own — `@std/article`'s body is `body` — and never ride a list. A section that shows them declares its key `whole: true`; elsewhere components fetch the whole record on demand with `useWholeRecord(record, { query })`, which returns it as stored — `full.body.content`. Every file-based record gets its own file, `/data/<name>/<name of the record>.json`. An external query's whole record comes from its `record:` request, below. (`deferred:` is removed — the build stops on it.) The hook is safe to call on any query: when the query has no separate source for one record it returns the record you passed in.
|
|
1919
1932
|
|
|
1920
1933
|
**Component-side fetching.** When a component genuinely needs to fetch on its own (a search box, "load more", a lazy popover), use the kit hooks — `useFetched`, `useCacheEntry`, `useWholeRecord`. They share the framework's cache and dispatcher with declarative fetches; same-key requests dedupe automatically.
|
|
1921
1934
|
|
|
@@ -2026,7 +2039,7 @@ resolves by the same rule — the host's offer, then the site's own address, the
|
|
|
2026
2039
|
neither.
|
|
2027
2040
|
|
|
2028
2041
|
⭐ **Before you render UI for a service, ask whether the site has it** — one predicate per service,
|
|
2029
|
-
no arguments: `isSearchEnabled()`, `isSubmitEnabled()`, `
|
|
2042
|
+
no arguments: `isSearchEnabled()`, `isSubmitEnabled()`, `isBackendEnabled()`, `isAssistantEnabled()`,
|
|
2030
2043
|
`isTrackingEnabled()`.
|
|
2031
2044
|
|
|
2032
2045
|
```jsx
|
|
@@ -2457,7 +2470,7 @@ For cases the factory doesn't cover, write handlers directly using `Loom`, `inst
|
|
|
2457
2470
|
## Part 4b — When the site is also an app
|
|
2458
2471
|
|
|
2459
2472
|
Everything above is a site: content the author writes, built into pages. Some sites
|
|
2460
|
-
also have **
|
|
2473
|
+
also have **their own backend, the `backend` service** — accounts, per-visitor data, records their members
|
|
2461
2474
|
create and edit. `@uniweb/api` is its client.
|
|
2462
2475
|
|
|
2463
2476
|
⛔ **Only reach for this when the site actually has one.** A site without one is
|
|
@@ -2471,13 +2484,13 @@ npm install @uniweb/api # in the FOUNDATION, beside @uniweb/kit
|
|
|
2471
2484
|
### Ask before you draw
|
|
2472
2485
|
|
|
2473
2486
|
```jsx
|
|
2474
|
-
import {
|
|
2487
|
+
import { isBackendEnabled } from '@uniweb/kit'
|
|
2475
2488
|
import { useSession, SignedIn, SignedOut } from '@uniweb/api'
|
|
2476
2489
|
|
|
2477
|
-
if (!
|
|
2490
|
+
if (!isBackendEnabled()) return <StaticVersion /> // synchronous — nothing to await
|
|
2478
2491
|
```
|
|
2479
2492
|
|
|
2480
|
-
⛔ **When the site has no `
|
|
2493
|
+
⛔ **When the site has no `backend` service, draw nothing** — not a disabled control, and not an
|
|
2481
2494
|
explanation. Same rule as `services` in Part 4: which capabilities a site's operator
|
|
2482
2495
|
set up is none of a visitor's business, and "sign-in unavailable" reads as breakage
|
|
2483
2496
|
when it is simply a feature this site does not have. Render the version of your
|
|
@@ -2499,7 +2512,7 @@ remove it. `scope: 'mine'` lists only the viewer's own records. Without it, the
|
|
|
2499
2512
|
also gets everyone else's.
|
|
2500
2513
|
|
|
2501
2514
|
⭐ **`absent` and an empty `ready` are different answers, and confusing them is the
|
|
2502
|
-
mistake to avoid.** `absent` = there is no live source (no `
|
|
2515
|
+
mistake to avoid.** `absent` = there is no live source (no `backend` service, or nobody signed
|
|
2503
2516
|
in) → render the site's authored content. `ready` with `records: []` = the service
|
|
2504
2517
|
answered and there is nothing there → render your empty state. Showing "nothing yet"
|
|
2505
2518
|
for the first tells a visitor their content is gone when it was never requested.
|
|
@@ -2556,8 +2569,8 @@ You do not need a live backend to build against one. In `site.yml`:
|
|
|
2556
2569
|
|
|
2557
2570
|
```yaml
|
|
2558
2571
|
services:
|
|
2559
|
-
|
|
2560
|
-
$
|
|
2572
|
+
backend: true # ask your host for it when you publish
|
|
2573
|
+
$devBackend: ./mock/api.js # what answers it in `uniweb dev`; `$` keys are never published
|
|
2561
2574
|
```
|
|
2562
2575
|
|
|
2563
2576
|
```js
|
|
@@ -2566,7 +2579,7 @@ import { createMockBackend } from '@uniweb/api/mock'
|
|
|
2566
2579
|
export default createMockBackend({ seed }).fetch
|
|
2567
2580
|
```
|
|
2568
2581
|
|
|
2569
|
-
`uniweb dev` answers your site's `
|
|
2582
|
+
`uniweb dev` answers your site's `backend` service with it, at an address the dev server
|
|
2570
2583
|
supplies on its own origin, so cookies behave exactly as they will in production. Write
|
|
2571
2584
|
no address of your own for it: under `services:` an address means a backend you run, and
|
|
2572
2585
|
asks your host to leave its own off. It **enforces** who
|
|
@@ -2596,7 +2609,7 @@ uniweb push / pull / clone / status # Git-style content sync with the Uniweb b
|
|
|
2596
2609
|
uniweb refresh / sync # Catch up (git + backend, never pushes) / catch up, then push
|
|
2597
2610
|
uniweb login --org @acme # The workspace you work in — new sites are created there (see below)
|
|
2598
2611
|
uniweb register [--scope @scope] # Register a foundation + its data schemas to the registry
|
|
2599
|
-
uniweb login / logout # One backend, one workspace at a time: log in (--
|
|
2612
|
+
uniweb login / logout # One backend, one workspace at a time: log in (--server, --org) or out
|
|
2600
2613
|
uniweb org list / create <handle> # Your personal scope, and the orgs you belong to
|
|
2601
2614
|
uniweb content export [dir] # Package a site (or a built foundation's schema) as .uwx
|
|
2602
2615
|
|
|
@@ -2673,7 +2686,7 @@ Foundations have their own free path too: `uniweb add ci --target foundation` pu
|
|
|
2673
2686
|
|
|
2674
2687
|
**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. `uniweb pull` overwrites rather than merges, so it refuses while you have uncommitted changes. The recovery for both is `uniweb pull --merge` (or `uniweb refresh`, which runs `git pull` first): changes to different parts of a file combine silently, and a genuine overlap leaves conflict markers and a non-zero exit — so `uniweb pull --merge && uniweb push` never ships markers. `--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.
|
|
2675
2688
|
|
|
2676
|
-
**`sync.json` says which site this is, on each backend.** The first push to a backend records what that backend assigned — the site's id and owner, the ids of its records and uploaded files — in `sync.json` beside `site.yml`. Commit it; never edit it. **To make a new site from a copy of a project, run `uniweb forget --all` in the copy before its first push** — the copy carries the original's `sync.json`, so otherwise its push updates the original's site. When two projects in one workspace hold the same site, a push or publish from either is refused until that is done. `uniweb forget --
|
|
2689
|
+
**`sync.json` says which site this is, on each backend.** The first push to a backend records what that backend assigned — the site's id and owner, the ids of its records and uploaded files — in `sync.json` beside `site.yml`. Commit it; never edit it. **To make a new site from a copy of a project, run `uniweb forget --all` in the copy before its first push** — the copy carries the original's `sync.json`, so otherwise its push updates the original's site. When two projects in one workspace hold the same site, a push or publish from either is refused until that is done. `uniweb forget --server <url>` removes just one backend's records, such as a scratch server's. A record file's `$uuid` is its own id and stays in both cases. **`push`, `pull` and `publish` go to the backend you are logged in to** — the last `uniweb login --server <url>` — and never to one you are not logged in to. A bare `uniweb login` logs in to https://uniweb.app; for any other backend, name it — logging in is how you switch, and the backend commands take no `--server` of their own. When the project has no site on that backend but has one elsewhere, a push says so before creating a new one.
|
|
2677
2690
|
|
|
2678
2691
|
**`services:` in `site.yml` is where a site says which services it uses** — site search, form
|
|
2679
2692
|
handling, analytics, an assistant, accounts — one entry per service:
|
|
@@ -2683,12 +2696,13 @@ services:
|
|
|
2683
2696
|
search: true # on — your host's, or the built-in index
|
|
2684
2697
|
submit: false # off
|
|
2685
2698
|
tracking: https://collector.example.com/events # a provider you bring
|
|
2686
|
-
|
|
2699
|
+
backend: # on, with the service's own settings
|
|
2687
2700
|
grade: pro
|
|
2688
2701
|
```
|
|
2689
2702
|
|
|
2690
2703
|
An entry is `true`, `false`, an address of your own (a string, or `endpoint:` in a map), or a map
|
|
2691
|
-
of options
|
|
2704
|
+
of options; anything else stops the build and the push. ⚠️ `yes`, `no`, `on` and `off` are text to
|
|
2705
|
+
YAML, not switches — write `true` or `false`. On a site you push or publish it is also **what you ask your host for**: `true` asks for
|
|
2692
2706
|
its service, `false` asks it to turn its service off, and an address asks it to leave its own off so
|
|
2693
2707
|
yours answers. **On a site your host publishes, a service is off unless `services:` asks for it** —
|
|
2694
2708
|
list the ones the site uses (the templates list theirs). `records: true` asks for your records to be
|
|
@@ -2701,15 +2715,17 @@ your last pull) is left as it is, and one changed both in the file and on the si
|
|
|
2701
2715
|
pull stops the push, naming it — `uniweb pull --merge`, then push. `uniweb pull` writes what the site
|
|
2702
2716
|
has into `services:` (an off service with no settings only where the file already names it) — pull
|
|
2703
2717
|
before you edit. A change that needs payment is settled in the app: `publish` opens it. **Everything in an entry but a
|
|
2704
|
-
credential is public** — it is built into the site, except `
|
|
2718
|
+
credential is public** — it is built into the site, except `backend`'s settings, which only your host
|
|
2705
2719
|
reads; a key or token is set in the app. ⛔ The top-level `search:` / `submit:` / `assistant:` /
|
|
2706
|
-
`tracking:` / `api:` keys are retired: the build stops on them and says where each one moves.
|
|
2720
|
+
`tracking:` / `api:` keys are retired: the build stops on them and says where each one moves. So
|
|
2721
|
+
does `api` under `services:` — the site's own backend is the `backend` service — and `$devApi:`,
|
|
2722
|
+
which is `$devBackend:`.
|
|
2707
2723
|
|
|
2708
2724
|
**The Cloud also provides a real backend for structured data:** a database for every registered data schema, and a CMS that edits both static page content and dynamic data entities typed by those schemas. That's the piece that makes it viable for teams and client work — the client manages records, not markdown files.
|
|
2709
2725
|
|
|
2710
2726
|
Either side can publish. Nothing about this changes how you build: the same foundation and the same site run under `uniweb dev`, `uniweb export`, or a CI deploy with no account at all.
|
|
2711
2727
|
|
|
2712
|
-
**Publishing vs registering.** Foundations on Uniweb Cloud live in the catalog as `@org/name@version` — `name` from the foundation's `main.js`. When a foundation powers a single site, **don't run `uniweb register` yourself** — `uniweb publish` from the site directory releases the local foundation to the catalog (when its code changed) and goes live in one step. A registered version is immutable, so when the foundation's code changed under a version already registered, `push` and `publish` release the change under the next version and write it into the foundation's `package.json` — commit that file (`--no-release` sends the content without the code). The version it picks is always the next patch: for a change that is not backwards compatible, set a higher minor or major version in the foundation's `package.json` yourself before you push. They stop when the registry holds a version newer than yours, released from another copy: pull that change first, or pass `--bump` to release yours above it. Register deliberately only when the foundation is a product meant for multiple sites; consuming sites then pin `foundation: '@org/name@1.2.3'`. **The catalog is private and access-segregated, not a public package registry** — people see only the foundations licensed to sites they own or edit. The *site* carries the license, and it rides along with site ownership when a developer hands a site to a client. Don't describe publishing as making a foundation publicly discoverable. Schemas can also be registered on their own from a schemas-only package (`@uniweb/schemas`, any `@org/schemas`, or a bare folder of `schemas/*.{yml,json,js}`) — that's how `@std` schemas are published. Auth via `uniweb login` (`uniweb login --
|
|
2728
|
+
**Publishing vs registering.** Foundations on Uniweb Cloud live in the catalog as `@org/name@version` — `name` from the foundation's `main.js`. When a foundation powers a single site, **don't run `uniweb register` yourself** — `uniweb publish` from the site directory releases the local foundation to the catalog (when its code changed) and goes live in one step. A registered version is immutable, so when the foundation's code changed under a version already registered, `push` and `publish` release the change under the next version and write it into the foundation's `package.json` — commit that file (`--no-release` sends the content without the code). The version it picks is always the next patch: for a change that is not backwards compatible, set a higher minor or major version in the foundation's `package.json` yourself before you push. They stop when the registry holds a version newer than yours, released from another copy: pull that change first, or pass `--bump` to release yours above it. Register deliberately only when the foundation is a product meant for multiple sites; consuming sites then pin `foundation: '@org/name@1.2.3'`. **The catalog is private and access-segregated, not a public package registry** — people see only the foundations licensed to sites they own or edit. The *site* carries the license, and it rides along with site ownership when a developer hands a site to a client. Don't describe publishing as making a foundation publicly discoverable. Schemas can also be registered on their own from a schemas-only package (`@uniweb/schemas`, any `@org/schemas`, or a bare folder of `schemas/*.{yml,json,js}`) — that's how `@std` schemas are published. Auth via `uniweb login` (`uniweb login --server <url> --token <bearer>` without a terminal) or `UNIWEB_TOKEN`; preview with `--dry-run`.
|
|
2713
2729
|
|
|
2714
2730
|
### Staying current
|
|
2715
2731
|
|
package/src/backend/client.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* `fetch(… Authorization: Bearer …)` against a per-command default origin. It
|
|
8
8
|
* owns the three things that were previously scattered across a dozen files:
|
|
9
9
|
*
|
|
10
|
-
* 1. ORIGIN — where the backend is. resolveBackendOrigin():
|
|
10
|
+
* 1. ORIGIN — where the backend is. resolveBackendOrigin(): UNIWEB_SERVER >
|
|
11
11
|
* the backend you are logged in to > the default backend (below).
|
|
12
12
|
* Each is reduced to its origin, http(s) only. *(This named a
|
|
13
13
|
* `--backend` flag as the first tier until 2026-10-05; the verbs
|
|
@@ -46,7 +46,7 @@ import { uploadSiteAssets } from '../utils/asset-upload.js'
|
|
|
46
46
|
/**
|
|
47
47
|
* Resolve the backend a command talks to:
|
|
48
48
|
*
|
|
49
|
-
* 1.
|
|
49
|
+
* 1. UNIWEB_SERVER env — the override for automation (CI, scripts), one process
|
|
50
50
|
* 2. ⭐ the backend the user is LOGGED IN TO — the one session there is
|
|
51
51
|
* 3. the default backend — ~/.uniweb/config.json `registryApiUrl`, else uniweb.app —
|
|
52
52
|
* where the command's first request then asks the user to log in
|
|
@@ -60,9 +60,10 @@ import { uploadSiteAssets } from '../utils/asset-upload.js'
|
|
|
60
60
|
* ⛔ **No `--backend` tier, and no project tier.** The backend verbs had a per-command
|
|
61
61
|
* `--backend` until 2026-09-21 — it predates per-backend sessions, when one session
|
|
62
62
|
* slot made "aim this one command elsewhere" a flag's job; switching is a login now, and
|
|
63
|
-
* a script aims with
|
|
63
|
+
* a script aims with UNIWEB_SERVER without touching the machine's login. A
|
|
64
64
|
* project's sync.json and deploy.yml routed commands too, until the same day.
|
|
65
|
-
*
|
|
65
|
+
* A backend flag survives only where it SELECTS rather than routes — `--server`, `--backend`
|
|
66
|
+
* until 2026-10-07: `login` (where to log
|
|
66
67
|
* in) and `forget` (which backend's records to remove). `logout` needs none: there is
|
|
67
68
|
* one session.
|
|
68
69
|
*
|
|
@@ -227,6 +228,27 @@ export class WorkspaceMismatchError extends Error {
|
|
|
227
228
|
}
|
|
228
229
|
}
|
|
229
230
|
|
|
231
|
+
/**
|
|
232
|
+
* The backend could not be reached at all — the request never got an answer.
|
|
233
|
+
*
|
|
234
|
+
* ⛔ Not an answer, so it must not be read as one. A lookup that folds this into "not
|
|
235
|
+
* found" sends its caller down the not-found path: `readFoundationLatest` did, and a
|
|
236
|
+
* publish with the backend down announced "Releasing the foundation (not yet
|
|
237
|
+
* registered)…", failed inside `register`, and ended on "Fix the foundation".
|
|
238
|
+
*/
|
|
239
|
+
export class BackendUnreachableError extends Error {
|
|
240
|
+
/**
|
|
241
|
+
* @param {string} origin
|
|
242
|
+
* @param {unknown} cause - the transport error `fetch` threw
|
|
243
|
+
*/
|
|
244
|
+
constructor(origin, cause) {
|
|
245
|
+
super(`Could not reach the backend at ${origin}: ${cause?.message ?? cause}`)
|
|
246
|
+
this.name = 'BackendUnreachableError'
|
|
247
|
+
this.origin = origin
|
|
248
|
+
this.cause = cause
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
230
252
|
/**
|
|
231
253
|
* The line to print when a request THREW rather than answered.
|
|
232
254
|
*
|
|
@@ -562,7 +584,9 @@ export class BackendClient {
|
|
|
562
584
|
|
|
563
585
|
/**
|
|
564
586
|
* GET /dev/registry/{scope}/{name} → the latest registered foundation version
|
|
565
|
-
* + its content digest, or null
|
|
587
|
+
* + its content digest, or null when the backend answers with anything else (callers
|
|
588
|
+
* degrade). ⛔ A backend that cannot be reached is not an answer: that throws
|
|
589
|
+
* `BackendUnreachableError` rather than reading as "not registered".
|
|
566
590
|
*
|
|
567
591
|
* The bare `{scope}/{name}` path resolves the latest foundation version (the
|
|
568
592
|
* data-schema sibling is `/dev/registry/data-schemas/{scope}/{name}`). The
|
|
@@ -589,7 +613,11 @@ export class BackendClient {
|
|
|
589
613
|
...body,
|
|
590
614
|
latest_version: body.latest_version ?? body.version ?? null
|
|
591
615
|
}
|
|
592
|
-
} catch {
|
|
616
|
+
} catch (err) {
|
|
617
|
+
// Node's `fetch` throws `TypeError: fetch failed` when nothing answered.
|
|
618
|
+
if (err instanceof TypeError && /fetch failed/i.test(err.message)) {
|
|
619
|
+
throw new BackendUnreachableError(this.origin, err)
|
|
620
|
+
}
|
|
593
621
|
return null
|
|
594
622
|
}
|
|
595
623
|
}
|
|
@@ -785,6 +813,31 @@ export class BackendClient {
|
|
|
785
813
|
})
|
|
786
814
|
}
|
|
787
815
|
|
|
816
|
+
/**
|
|
817
|
+
* GET /dev/site — one page of the sites in the workspace the client names
|
|
818
|
+
* (`setWorkspace`; none ⇒ personal): `{ sites: [{ uuid, name?, updated_at,
|
|
819
|
+
* deployment?: { status, published_url, … } }] }`. A page shorter than `limit` is
|
|
820
|
+
* the last. `deployment` is absent for a site never published.
|
|
821
|
+
*
|
|
822
|
+
* @param {{ limit?: number, offset?: number }} [opts]
|
|
823
|
+
* @returns {Promise<Response>}
|
|
824
|
+
*/
|
|
825
|
+
async listSites({ limit = 100, offset = 0 } = {}) {
|
|
826
|
+
return this.request('/dev/site', { query: { limit, offset } })
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* DELETE /dev/site/{uuid} — delete a site, finally: no trash, no restore. Only an
|
|
831
|
+
* admin of the site may. Refused `409` with `blockers: [{ resource, detail }]` while
|
|
832
|
+
* anything is still active — `published` among them, which `unpublishSite` clears.
|
|
833
|
+
*
|
|
834
|
+
* @param {string} uuid - the site-content uuid
|
|
835
|
+
* @returns {Promise<Response>}
|
|
836
|
+
*/
|
|
837
|
+
async deleteSite(uuid) {
|
|
838
|
+
return this.request(`/dev/site/${encodeURIComponent(uuid)}`, { method: 'DELETE' })
|
|
839
|
+
}
|
|
840
|
+
|
|
788
841
|
/**
|
|
789
842
|
* GET /dev/site/status/{uuid} → the site's publish lifecycle (Contract 3,
|
|
790
843
|
* shipped backend-side — collab backend↔framework):
|
|
@@ -197,7 +197,7 @@ function writePkgVersion(dir, version) {
|
|
|
197
197
|
// and the WORKSPACE this command names (`--org` / `--personal`). The BACKEND travels in
|
|
198
198
|
// the child's environment (releaseFoundation), and the SESSION is the shared session
|
|
199
199
|
// file — or UNIWEB_TOKEN, which the child inherits. The backend commands take no
|
|
200
|
-
// `--
|
|
200
|
+
// backend flag (`--server` is the login's) and no `--token`.
|
|
201
201
|
//
|
|
202
202
|
// ⭐ The workspace travels because a bare name registers under the workspace the command
|
|
203
203
|
// works in *[Diego, 2026-10-06]*, and a workspace named on this command is this command's
|
|
@@ -577,7 +577,9 @@ function buildFoundation(local, cliBin) {
|
|
|
577
577
|
execFileSync('node', [cliBin, 'build', '--target', 'foundation'], {
|
|
578
578
|
cwd: local.dir,
|
|
579
579
|
stdio: 'inherit',
|
|
580
|
-
|
|
580
|
+
// A step of this command, so the build skips the next-step hints meant for
|
|
581
|
+
// someone who ran `uniweb build` themselves.
|
|
582
|
+
env: { ...process.env, UNIWEB_BUILD_STEP: '1' }
|
|
581
583
|
})
|
|
582
584
|
}
|
|
583
585
|
|
|
@@ -592,7 +594,7 @@ function releaseFoundation(local, args, cliBin, say, origin) {
|
|
|
592
594
|
execFileSync('node', [cliBin, 'register', ...forwardedFlags(args)], {
|
|
593
595
|
cwd: local.dir,
|
|
594
596
|
stdio: 'inherit',
|
|
595
|
-
env: origin ? { ...process.env,
|
|
597
|
+
env: origin ? { ...process.env, UNIWEB_SERVER: origin } : process.env
|
|
596
598
|
})
|
|
597
599
|
console.log('')
|
|
598
600
|
return true
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
import { createHash } from 'node:crypto'
|
|
29
29
|
import { existsSync, readFileSync } from 'node:fs'
|
|
30
30
|
import { join } from 'node:path'
|
|
31
|
-
import { readServicesRequest } from '@uniweb/build/uwx'
|
|
31
|
+
import { readServicesRequest, unreadableServices } from '@uniweb/build/uwx'
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* A stable fingerprint of one declared value, or `null` when the key is absent.
|
|
@@ -148,7 +148,7 @@ const names = (list) => list.map((n) => `\`${n}\``).join(', ')
|
|
|
148
148
|
|
|
149
149
|
/**
|
|
150
150
|
* Say what `site.yml::services` asks that will not be sent as written — a credential,
|
|
151
|
-
* an entry that is not one,
|
|
151
|
+
* an entry that is not one, a `backend` address — and where it and the foundation disagree,
|
|
152
152
|
* before a push or publish sends the rest. What a push sends is the producer's
|
|
153
153
|
* (`statedServices`), from the same file.
|
|
154
154
|
*
|
|
@@ -165,6 +165,10 @@ const names = (list) => list.map((n) => `\`${n}\``).join(', ')
|
|
|
165
165
|
* @param {string[]|null} [p.supports] - from `foundationSupports`; null = unknown
|
|
166
166
|
*/
|
|
167
167
|
export function announceServices({ siteYml, say, supports = null }) {
|
|
168
|
+
// An entry the push cannot read stops the package build next, which says what to write
|
|
169
|
+
// (`refuseUnreadableServices`). Nothing to add before it — and what this would say, read
|
|
170
|
+
// past that entry, is wrong: `search: yes` was "site.yml turns on `search`" (F14).
|
|
171
|
+
if (unreadableServices(siteYml?.services).length) return
|
|
168
172
|
const asks = readServicesRequest(siteYml?.services, { warn: (m) => say.warn(m) }) || []
|
|
169
173
|
if (!Array.isArray(supports)) return
|
|
170
174
|
const ignored = new Set(['tracking', 'records'])
|
|
@@ -46,7 +46,7 @@ export async function readSitePreview({ siteDir, args = [], client = null }) {
|
|
|
46
46
|
refused: [
|
|
47
47
|
`This site's foundation is ${ref}, and the backend the site is on says where that version is served.`,
|
|
48
48
|
known.length
|
|
49
|
-
? `The site is on ${known.join(', ')} — not on ${client.origin}, the backend you are logged in to. To preview it: uniweb login --
|
|
49
|
+
? `The site is on ${known.join(', ')} — not on ${client.origin}, the backend you are logged in to. To preview it: uniweb login --server <url>`
|
|
50
50
|
: `The site is on no backend yet: \`uniweb push\` puts it on ${client.origin}.`
|
|
51
51
|
]
|
|
52
52
|
}
|
package/src/backend/site-sync.js
CHANGED
|
@@ -44,7 +44,7 @@ import {
|
|
|
44
44
|
updateBackendMap,
|
|
45
45
|
clearBackendSections,
|
|
46
46
|
normalizeBackendOrigin,
|
|
47
|
-
|
|
47
|
+
writeSiteConfig,
|
|
48
48
|
harvestRecordItems,
|
|
49
49
|
storedRecordItems,
|
|
50
50
|
reprintRecordItems,
|
|
@@ -571,7 +571,7 @@ const readMap = (siteDir, backend, key) => {
|
|
|
571
571
|
* not change** — a partial site, published successfully, with nothing to
|
|
572
572
|
* indicate it.
|
|
573
573
|
*
|
|
574
|
-
* The guidance is `uniweb forget --
|
|
574
|
+
* The guidance is `uniweb forget --server <url>` now, which removes the uuid and the
|
|
575
575
|
* maps together, so following it no longer leads here.
|
|
576
576
|
*
|
|
577
577
|
* Call this BEFORE `ensureSiteExists`, which mints a uuid and would otherwise make
|
|
@@ -718,7 +718,7 @@ export function dropSiteBoundValues(siteDir, backend) {
|
|
|
718
718
|
if (
|
|
719
719
|
y.preview !== undefined &&
|
|
720
720
|
!isAuthoredPreview(y.preview) &&
|
|
721
|
-
|
|
721
|
+
writeSiteConfig(siteDir, { preview: null }) === 'updated'
|
|
722
722
|
) {
|
|
723
723
|
dropped.push('preview')
|
|
724
724
|
}
|
|
@@ -1902,7 +1902,7 @@ export async function pushSyncPackages({
|
|
|
1902
1902
|
} catch (err) {
|
|
1903
1903
|
error(describeRequestError(err, client.origin))
|
|
1904
1904
|
if (!(err instanceof WorkspaceMismatchError))
|
|
1905
|
-
note('Is that the backend you meant? Switch with: uniweb login --
|
|
1905
|
+
note('Is that the backend you meant? Switch with: uniweb login --server <url>')
|
|
1906
1906
|
return null
|
|
1907
1907
|
}
|
|
1908
1908
|
if (!res.ok) {
|
|
@@ -2067,7 +2067,7 @@ export async function pushSyncPackages({
|
|
|
2067
2067
|
error(`${label} push rejected: HTTP ${res.status} ${res.statusText}`)
|
|
2068
2068
|
if (res.status === 401 || res.status === 403) {
|
|
2069
2069
|
note(
|
|
2070
|
-
"Credentials weren't accepted — log in again (`uniweb login --
|
|
2070
|
+
"Credentials weren't accepted — log in again (`uniweb login --server <url>`), or check UNIWEB_TOKEN."
|
|
2071
2071
|
)
|
|
2072
2072
|
} else if (res.status === 404 && boundUuid) {
|
|
2073
2073
|
// The clone is bound to a site the backend does not have. There is no CLI
|
|
@@ -2097,10 +2097,10 @@ export async function pushSyncPackages({
|
|
|
2097
2097
|
'Two causes. Check the cheap one first: is this the backend the site lives on?'
|
|
2098
2098
|
)
|
|
2099
2099
|
note(
|
|
2100
|
-
` wrong backend → uniweb login --
|
|
2100
|
+
` wrong backend → uniweb login --server <the right one> (nothing is lost)`
|
|
2101
2101
|
)
|
|
2102
2102
|
note(
|
|
2103
|
-
` deleted there → uniweb forget --
|
|
2103
|
+
` deleted there → uniweb forget --server ${client.origin}, then push again: it creates a NEW site`
|
|
2104
2104
|
)
|
|
2105
2105
|
note('Deleting this folder removes only your local copy, either way.')
|
|
2106
2106
|
} else if (problem?.reason === 'template_records_not_copyable' && Array.isArray(problem.records)) {
|
package/src/commands/build.js
CHANGED
|
@@ -138,10 +138,11 @@ function detectProjectType(projectDir) {
|
|
|
138
138
|
*/
|
|
139
139
|
function runCommand(command, args, cwd) {
|
|
140
140
|
return new Promise((resolve, reject) => {
|
|
141
|
+
// No shell: the command is node and a script path, which needs none — and a shell
|
|
142
|
+
// splits a path with a space in it. (Node warns DEP0190 on args passed with one.)
|
|
141
143
|
const proc = spawn(command, args, {
|
|
142
144
|
cwd,
|
|
143
|
-
stdio: 'inherit'
|
|
144
|
-
shell: true
|
|
145
|
+
stdio: 'inherit'
|
|
145
146
|
})
|
|
146
147
|
|
|
147
148
|
proc.on('close', (code) => {
|
|
@@ -246,6 +247,9 @@ async function buildFoundation(projectDir, options = {}) {
|
|
|
246
247
|
log('')
|
|
247
248
|
log(`${colors.green}${colors.bright}Build complete!${colors.reset}`)
|
|
248
249
|
|
|
250
|
+
// The next step is for someone who ran `uniweb build` — not for a push or publish
|
|
251
|
+
// that builds the foundation as one of its own steps (UNIWEB_BUILD_STEP).
|
|
252
|
+
if (process.env.UNIWEB_BUILD_STEP) return
|
|
249
253
|
log('')
|
|
250
254
|
log(`${colors.bright}Share with clients:${colors.reset}`)
|
|
251
255
|
log(
|
package/src/commands/clone.js
CHANGED
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
* another workspace stops it.
|
|
45
45
|
*
|
|
46
46
|
* Backend: via BackendClient (the site-content pull lane). Origin from
|
|
47
|
-
*
|
|
47
|
+
* UNIWEB_SERVER > the local default (internal dev overrides;
|
|
48
48
|
* not the user-facing path — `uniweb login` determines the origin).
|
|
49
49
|
* Auth: UNIWEB_TOKEN > the stored session > `uniweb login`. No `--token` (retired
|
|
50
50
|
* from the backend commands 2026-09-21).
|
package/src/commands/deploy.js
CHANGED
|
@@ -195,8 +195,8 @@ export async function deploy(args = []) {
|
|
|
195
195
|
// uniweb target's `backend:` records where that target's publishes went; it routes
|
|
196
196
|
// nothing (publish files its record under the target naming its backend).
|
|
197
197
|
const goingTo = getRegistryApiBaseUrl()
|
|
198
|
-
const aimedBy = process.env.
|
|
199
|
-
? '
|
|
198
|
+
const aimedBy = process.env.UNIWEB_SERVER
|
|
199
|
+
? 'UNIWEB_SERVER'
|
|
200
200
|
: loggedInOrigin()
|
|
201
201
|
? 'the backend you are logged in to'
|
|
202
202
|
: 'the default backend — you are not logged in'
|
|
@@ -215,7 +215,7 @@ export async function deploy(args = []) {
|
|
|
215
215
|
say.err(
|
|
216
216
|
`Target '${resolved.targetName}' is on ${targetBackend}, but this would publish to ${goingTo} (${aimedBy}).`
|
|
217
217
|
)
|
|
218
|
-
say.dim(`To publish there, log in to it first: uniweb login --
|
|
218
|
+
say.dim(`To publish there, log in to it first: uniweb login --server ${targetBackend}`)
|
|
219
219
|
process.exit(1)
|
|
220
220
|
}
|
|
221
221
|
say.dim(
|
package/src/commands/forget.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* `uniweb forget` — remove what this project recorded about where it has synced and
|
|
3
3
|
* deployed. Local files only; nothing on any backend changes.
|
|
4
4
|
*
|
|
5
|
-
* uniweb forget --
|
|
5
|
+
* uniweb forget --server <url> one backend
|
|
6
6
|
* uniweb forget --all everything — for a COPY that is to become a new project
|
|
7
7
|
*
|
|
8
|
-
* ## `--
|
|
8
|
+
* ## `--server <url>`
|
|
9
9
|
*
|
|
10
10
|
* - that backend's section of `sync.json` — the site uuid, the record map, asset
|
|
11
11
|
* ids, provisioned services and the rest of what it minted;
|
|
@@ -43,12 +43,12 @@
|
|
|
43
43
|
* ## Why it exists
|
|
44
44
|
*
|
|
45
45
|
* Two jobs. A script pushes a template site to a short-lived dev server and then
|
|
46
|
-
* removes the traces of it without discarding the rest of the project (`--
|
|
46
|
+
* removes the traces of it without discarding the rest of the project (`--server`)
|
|
47
47
|
* — which is also why traceless publish was dropped: push and pull leave traces too.
|
|
48
48
|
* And someone duplicates a project to start a new site from it (`--all`).
|
|
49
49
|
*
|
|
50
50
|
* ⚠️ **No default target.** Forgetting a backend you still use means its next push
|
|
51
|
-
* creates a second site there, so the verb makes you name it — `--
|
|
51
|
+
* creates a second site there, so the verb makes you name it — `--server` is
|
|
52
52
|
* required even when the project has synced with only one.
|
|
53
53
|
*/
|
|
54
54
|
|
|
@@ -96,9 +96,9 @@ export async function forget(args = []) {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
const all = args.includes('--all')
|
|
99
|
-
const flag = readFlagValue(args, '--
|
|
99
|
+
const flag = readFlagValue(args, '--server')
|
|
100
100
|
if (all && flag) {
|
|
101
|
-
say.err('Use --
|
|
101
|
+
say.err('Use --server <url> or --all, not both.')
|
|
102
102
|
return { exitCode: 2 }
|
|
103
103
|
}
|
|
104
104
|
|
|
@@ -108,7 +108,7 @@ export async function forget(args = []) {
|
|
|
108
108
|
if (all) return forgetAll(siteDir)
|
|
109
109
|
|
|
110
110
|
if (!flag) {
|
|
111
|
-
say.err('Name what to forget: uniweb forget --
|
|
111
|
+
say.err('Name what to forget: uniweb forget --server <url>')
|
|
112
112
|
if (known.length) {
|
|
113
113
|
say.dim(`This project has synced with: ${known.join(', ')}`)
|
|
114
114
|
} else {
|