diff --git a/.changeset/README.md b/.changeset/README.md index ccc0875..ef305df 100644 --- a/.changeset/README.md +++ b/.changeset/README.md @@ -36,4 +36,4 @@ npm view @clearcms/spec --json | jq '.dist.attestations' The `predicate.buildDefinition.externalParameters.workflow` field points at the exact `release.yml` invocation that produced the tarball. -Per [ADR-0014](../docs/adr/0014-two-physical-repos-end-mirroring.md). +Per [ADR-0014](../docs/adr/README.md#adr-0014--two-physical-repos--end-mirroring-public-is-source-of-truth). diff --git a/.changeset/admin-backfill-schema-versions-stub.md b/.changeset/admin-backfill-schema-versions-stub.md new file mode 100644 index 0000000..dfb88ba --- /dev/null +++ b/.changeset/admin-backfill-schema-versions-stub.md @@ -0,0 +1,36 @@ +--- +'@clearcms/admin': minor +--- + +Add `clear-admin backfill-schema-versions` CLI surface (STUB). + +Per ADR-0022 §backfill, this command walks the bucket and stamps every +record missing a `schemaVersion` field. The full bucket walk + stamp +implementation lands in Phase 6 closeout; today's commit ships the +CLI surface so adopters and tests can target the command name now. + +Argv shape (locked): + + clear-admin backfill-schema-versions [--dry-run] [--collection ] + + --dry-run scan + count only; no writes. + --collection scope to a single collection (default: every + record type — items, pages, page-data, block + instances, globals, theme tokens). + -h, --help show help. + +Exits 0 on the not-yet-implemented happy path (with a clear message +naming ADR-0022 §backfill); exits 2 on argv error. Pattern matches +`migrate-records` (Stream-Migrate, PR #76). + +The full implementation will use `@clearcms/storage`'s +`writeStampedJson` primitive (PR #72 commit 1) so the stamp behaviour +matches every other named writer. + +11 new unit tests cover the argv parser (defaults, `--dry-run`, +`--collection` both forms, missing values, unknown flags, help +sentinel) and the stub's exit code surface. + +Eng B Phase 5/6 storage track, commit 6 of N on PR #72 (final commit +on this PR — closes the named-writer + path-helper + backfill-CLI +sweep). Independent of PR #57; lands when storage track ships. diff --git a/.changeset/admin-doctor-checks-v1.md b/.changeset/admin-doctor-checks-v1.md new file mode 100644 index 0000000..b666cf3 --- /dev/null +++ b/.changeset/admin-doctor-checks-v1.md @@ -0,0 +1,35 @@ +--- +'@clearcms/admin': minor +--- + +Add 4 new `clear-admin doctor` checks for the v1 site-mode + ADR-0022 +schema-version contract: + +- **`checkModeConsistency`** (ADR-0021) — `clear.config.json`'s `mode` + vs the bucket's site-mode artifacts. Headless + `theme/blocks/` files + present → warning ("site-mode artifacts in a headless project; flip + the mode or run `clean-mode-artifacts`"). Site + no `theme/blocks/` + → info. +- **`checkDanglingBlockTypes`** (ADR-0021) — page `data.json` block + manifests reference unknown block types (no + `theme/blocks/.schema.json`). Each hit → warning. +- **`checkStaleSchemaVersions`** (ADR-0022) — record `schemaVersion` + vs schema's current `version`, per-collection counts. Suggests + `clear-admin migrate-records` for each stale collection. +- **`checkDanglingTokenRefs`** (ADR-0025) — block-instance fields with + `widget: "select" + optionsFromTokens: ` reference token + names not declared in `theme/tokens.json[]`. Each hit → + warning. + +Each check is a pure async function returning `DoctorIssue[]`; all four +return zero issues on a healthy v0/v1 bucket. Fixtures are fs-only +(`mkdtemp` + JSON file writes) — none of the checks require the SQLite +DB harness, so the Windows EBUSY pre-existing failure mode (issue #56) +doesn't apply. + +Includes a tiny local `assumeSchemaVersion` shim in +`doctor-stale-schema-versions.ts` until PR #57 (Phase 3 spec) merges +and the helper becomes consumable from `@clearcms/spec` directly. + +Eng B Phase 6 doctor track. 18 new tests; existing doctor tests +unchanged. diff --git a/.changeset/admin-migrate-records-stub.md b/.changeset/admin-migrate-records-stub.md new file mode 100644 index 0000000..e04f373 --- /dev/null +++ b/.changeset/admin-migrate-records-stub.md @@ -0,0 +1,24 @@ +--- +'@clearcms/admin': minor +--- + +`clear-admin migrate-records` — bulk record migration runner (ADR-0022, Phase 5 stub). + +Adds the CLI surface that adopters will run to apply pending migrations +across a bucket. Argv shape: + +``` +clear-admin migrate-records --collection= [--dry-run] +clear-admin migrate-records --all [--dry-run] +clear-admin migrate-records --help +``` + +Stub-only: argv parsing + help + scope validation are wired today so +adopter scripts can gate on `--help`. The actual bucket walk lands in +Phase 6 closeout once the storage-side `schemaVersion` write-stamp from +the v1 storage track has propagated. Imports the runtime from +`@clearcms/spec/migrate` (this release's spec bump) so the package edge +is locked from day one. + +The bin help block lists the new subcommand so `clear-admin help` shows +the path to migration tooling without further ceremony. diff --git a/.changeset/adr-sweep-docs-polish.md b/.changeset/adr-sweep-docs-polish.md new file mode 100644 index 0000000..c3d6a52 --- /dev/null +++ b/.changeset/adr-sweep-docs-polish.md @@ -0,0 +1,17 @@ +--- +--- + +docs(adr,protocol): v1 status sweep + block-library author guide + +Sweeps ADR statuses for v1.0: +- 0007 reframed by 0021 (page block storage shifted to v1 dialect) +- 0008 superseded by 0024 (Workers renderer dropped for self-hosted Astro) +- 0021–0028 accepted with implementation status notes + +Adds new author guide at docs/guides/block-library-authoring.md covering +the block-library publishing rhythm (ADR-0028) and widget dialect (ADR-0027). + +CONTENT-PROTOCOL v1 polish: bucket-layout examples now match the +template-site-marketing scaffold output; cross-links to the author guide. + +Closes T-7.1. diff --git a/.changeset/blocks-marketing-init.md b/.changeset/blocks-marketing-init.md new file mode 100644 index 0000000..4a3354d --- /dev/null +++ b/.changeset/blocks-marketing-init.md @@ -0,0 +1,46 @@ +--- +'@clearcms/blocks-marketing': minor +--- + +Initial scaffold of `@clearcms/blocks-marketing` — the v1 site-mode +marketing block library per ADRs 0024 / 0025 / 0028. + +This commit ships: + +- Package skeleton (`package.json`, `tsconfig.json`, + `tsconfig.build.json`) mirroring `@clearcms/astro`'s shape — main / + types at `dist/`, files=["blocks","examples","migrations","src","dist"], + `astro` declared as peer (`^6.0.0`) and devDep, `@clearcms/spec` + workspace dep for block-schema validation. +- `src/index.ts` — the block-name registry (`MARKETING_BLOCK_NAMES`, + starting with `Hero`) plus `marketingBlockSchemas` (per-block JSON + re-exports) and `getMarketingBlockSchema(name)`. Astro components + are NOT re-exported via `src/index.ts`; adopters import them + directly from `@clearcms/blocks-marketing/blocks/.astro`. +- First block — **Hero**: + - `blocks/Hero.astro` — typed Astro component. Tokens-driven via + ADR-0025 CSS custom properties (`--clear-color-`, + `--clear-space-`); fallback values keep markup styled + when a token isn't declared. Class names follow + `clearcms-block-hero__` BEM-ish convention. + - `blocks/Hero.schema.json` — block-schema at version `1.0.0` + (`{ name, version, fields }` per ADR-0028). 6 fields: + `headline`, `subheadline`, `background` (optionsFromTokens + color), `padding` (optionsFromTokens space), `ctaLabel`, + `ctaHref`. + - `examples/Hero.json` — example block-instance fixture + (BlockInstanceSchema-shaped) for tests + scaffolder seed. +- `src/blocks-marketing.test.ts` — 5 tests. Validates every name in + the registry has a schema, the resolver returns the right schema / + undefined for unknowns, every block-schema parses against + `BlockSchemaSchema`, every example parses against + `BlockInstanceSchema`. + +The remaining 9 blocks (Features / CTA / FAQ / Footer / Testimonials +/ Pricing / Logos / Contact / RichText) ship in subsequent commits on +this branch — same shape as Hero (component + schema + example + +registry entry + test row added by the `describe.each` already in +place). + +Eng B Phase 5, commit 1 of N. New package; not yet published — +queues for the next publish train. diff --git a/.changeset/doctor-mode-consistency-resolver.md b/.changeset/doctor-mode-consistency-resolver.md new file mode 100644 index 0000000..46c6f98 --- /dev/null +++ b/.changeset/doctor-mode-consistency-resolver.md @@ -0,0 +1,12 @@ +--- +'@clearcms/admin': patch +--- + +`clear-admin doctor`'s mode-consistency check now consumes +`loadProjectConfig().mode` (T-5A.1) instead of re-parsing the config +file. New WARN: when the raw `mode` value in env (`CLEAR_MODE`) or +`clear.config.json` is not in the closed enum, the resolver silently +falls back to `headless` — doctor surfaces the typo'd value loudly so +the operator sees it instead of debugging a silently-headless install. + +Closes #128. diff --git a/.changeset/migrate-bucket-to-r2.md b/.changeset/migrate-bucket-to-r2.md new file mode 100644 index 0000000..9d2c852 --- /dev/null +++ b/.changeset/migrate-bucket-to-r2.md @@ -0,0 +1,27 @@ +--- +'@clearcms/admin': minor +--- + +feat(admin): `clear-admin migrate-bucket-to-r2 ` — one-shot fs → R2 +data migration for an existing project. Reads the platform R2 token from +`R2_*` env vars (source `~/.r2-platform.env`), creates the bucket +(idempotent, default name `clear-`, override via +`--bucket-name=`), uploads every file under the local `bucket/` to +R2 at the same key, atomically rewrites `~/clear/registry.json` with +`bucketProvider: 'r2'` + the new `r2` block, and renames the local +bucket dir to `bucket.archived-/` so the operator can verify +parity before deleting. + +v1 happy-path only. Implements ADR-0017 steps 1, 2, 3, 5, 6, 7, 8. +Deferred (in-progress follow-ups): + +- Step 4 / per-project scoped R2 token mint per ADR-0016. Today the + command inherits the platform token at runtime; a future PR adds the + `R2 → CreateToken` call and writes the per-project token into the + registry entry. +- Resume-on-failure via `.migration-progress.json` (ADR-0017 "Risks" + section). v1 fails fast on partial uploads; rerun end-to-end after + fixing the underlying issue. +- Streaming PUTs for large media. v1 reads each file fully into memory + before uploading — fine for text-shaped sites, fails on multi-GB + media libraries. diff --git a/.changeset/migrate-runner-admin.md b/.changeset/migrate-runner-admin.md new file mode 100644 index 0000000..ec4db44 --- /dev/null +++ b/.changeset/migrate-runner-admin.md @@ -0,0 +1,59 @@ +--- +'@clearcms/admin': minor +--- + +`clear-admin migrate-records` + `clear-admin backfill-schema-versions` +— full implementations replacing the Phase 5 stubs (T-6.1 / ADR-0022 / +ADR-0028 / ADR-0015). + +`clear-admin migrate-records`: + +``` +clear-admin migrate-records --collection= [--dry-run] [--type=] [--bucket=] [--verbose] +clear-admin migrate-records --all [--dry-run] [--type=] [--bucket=] [--verbose] +``` + +The Phase 5 stub's argv contract (`--collection=` / `--all` / +`--dry-run` / `--help`) is preserved; `--bucket=`, +`--type=`, and `--verbose` are additive. + +Behaviour, end-to-end: + +1. Resolves the bucket root from `--bucket` > env `CLEAR_STORAGE_ROOT` + > `clear.config.json#bucket` > default `./data/storage`. +2. Discovers migrations via `discoverMigrations` (per-collection + adopter migrations under `/schemas//migrations/`), + `discoverPerBucketMigrations` (per-bucket cross-cutting adopter + migrations under `/migrations//`, ADR-0015), and + `loadBlockMigrations` (every installed `@clearcms/blocks-*` + library, ADR-0028). +3. Walks items (published + drafts), pages, page-data, and + per-instance block files in scope. +4. For each record below the schema's current version, builds the + chain via `migrate-bridge.migrateRecord` and writes the migrated + record back via the existing `writeX` storage helpers (which stamp + `schemaVersion` on save). `--dry-run` skips writes. +5. Per-record reporter prints `OK`, `MIGRATED v→v`, `SKIPPED `, + `FAILED `. Exit 0 on success or any `--dry-run`; exit 1 on + any record-level failure. + +`clear-admin backfill-schema-versions`: + +``` +clear-admin backfill-schema-versions [--dry-run] [--collection ] [--bucket ] [--verbose] +``` + +The Phase 5 stub's argv contract (`--dry-run`, `--collection`, `--help`) +is preserved; `--bucket` and `--verbose` are additive. + +Walks every record kind (items, pages, page-data, block instances, +identity, nav, theme tokens). For records missing or carrying a +malformed `schemaVersion`, stamps the record with the matching schema +file's `version` (or `DEFAULT_SCHEMA_VERSION` `"1.0.0"` when the +schema's version is also absent, per `assumeSchemaVersion`). Writes go +through the existing named writers (`writeItem`, `writePage`, +`writePageData`, `writeBlock`, `writeIdentity`, `writeNav`, +`writeTokens`) so the on-disk encoding policy stays uniform. + +Both CLIs are wired into the existing `bin/clear-admin.js` dispatch — +no new bin wrappers needed. diff --git a/.changeset/migrate-runner-blocks-marketing.md b/.changeset/migrate-runner-blocks-marketing.md new file mode 100644 index 0000000..2a256ab --- /dev/null +++ b/.changeset/migrate-runner-blocks-marketing.md @@ -0,0 +1,37 @@ +--- +'@clearcms/blocks-marketing': major +--- + +Hero v1 → v2 — schema bump + migration (T-6.1 / ADR-0028 demo). + +The first sample per-block migration shipped from a `@clearcms/blocks-*` +library. Demonstrates the end-to-end greedy-at-read + lazy-on-save +loop adopters get for free once `clear-admin migrate-records` lands: + +- `blocks/Hero.schema.json` — `version` bumps `1.0.0` → `2.0.0`. + Renames the `subheadline` field to `subhead` and adds two optional + fields, `secondaryCtaLabel` and `secondaryCtaHref`, for hero layouts + with a primary + secondary action pair. +- `migrations/Hero/1.0.0-to-2.0.0.json` — declarative migration that + renames the persisted `props.subheadline` field to `props.subhead` + on existing block instances. Discovered by + `loadBlockMigrations({ projectRoot })` from + `@clearcms/spec/migrate/runtime` per ADR-0028's library convention + (`/migrations//-to-.{json|ts}`). +- `blocks/Hero.astro` — props interface updated to the v2 shape; + template renders the optional secondary CTA when both label + href + are present. Subhead class renamed in CSS to match the new field + name. +- `examples/Hero.json` — fixture bumped to `schemaVersion: "2.0.0"` to + exercise the v2 shape end-to-end. + +**Breaking** for adopters who pinned `@clearcms/blocks-marketing@^0.1`: +the `Hero` Astro component's `subheadline` prop no longer exists. Any +adopter `` call site needs to flip to +`subhead`. Bucket records for `Hero` block instances are migrated +automatically by `clear-admin migrate-records` (or transparently at +read time via the storage `migrate-bridge`); no manual JSON-rewriting +required. + +Other nine v1 blocks stay at `version: "1.0.0"` until they need +their own schema bump. diff --git a/.changeset/migrate-runner-spec.md b/.changeset/migrate-runner-spec.md new file mode 100644 index 0000000..0f2a703 --- /dev/null +++ b/.changeset/migrate-runner-spec.md @@ -0,0 +1,52 @@ +--- +'@clearcms/spec': minor +--- + +Migration runner — Phase 6 closeout for T-6.1 (ADR-0022 + ADR-0028 + ADR-0015). + +Extends `@clearcms/spec/migrate` (pure runner) and +`@clearcms/spec/migrate/runtime` (Node-only fs discovery) with the +exports the bulk runner / storage bridge / per-block libraries need to +chain end-to-end: + +`./migrate` (pure, browser-safe — additive): + +- `applyChain(record, chain)` — apply a pre-resolved migration chain to + a record. Skips the `buildMigrationChain` BFS when the caller already + has the chain (e.g. cached per-blockType in a request-scope read + helper). Same return contract as `migrate`: stamps the chain's tail + `schemaVersion` and appends `_migration_warnings` for cross-format + ops. Empty chain is a no-op identity returning a shallow clone. +- `inferTargetVersion(record, blockType, registry)` — returns the + registered current version for a block type, or `null` when the type + is unknown. Used by greedy-at-read paths that need to know what + version to migrate _to_ before they call `migrate`. +- `BlockTypeRegistryEntry` type — the `{ type, currentVersion }` shape + `inferTargetVersion`'s registry takes. + +`./migrate/runtime` (Node-only — additive): + +- `discoverPerBucketMigrations({ bucketRoot, scope? })` — walks + `/migrations//-to-.{json|ts}` for + cross-cutting adopter-defined migrations (ADR-0015 adapter-aware). + When `scope` is omitted, every immediate subdirectory of + `/migrations/` is treated as a scope and walked. Returns + `DiscoveredPerBucketMigration[]` — same `Migration` payload as + `discoverMigrations` but with a `'per-bucket'` source kind. +- `loadBlockMigrations({ projectRoot? })` — walks every installed + `@clearcms/blocks-*` package for per-block migrations across every + block type the package ships. Mirrors + `discoverMigrations({ blockType })` but iterates every block type + found under each package's `migrations/` root in one pass — used by + the bulk command to bootstrap its per-blockType chain cache without N + round-trips. + +No existing exports change shape. The `@clearcms/spec/migrate` barrel +stays free of `node:*` imports — discovery exports remain gated behind +the `node`-conditional `./migrate/runtime` sub-path so admin's Astro +client bundle stays clean (the bug PR #106 fixed; this changeset +preserves it). + +Sample migration shipped in `@clearcms/blocks-marketing` (separate +changeset) — Hero v1→v2 demonstrates a one-step rename + additive +fields end-to-end. diff --git a/.changeset/migrate-runner-storage.md b/.changeset/migrate-runner-storage.md new file mode 100644 index 0000000..f035fd9 --- /dev/null +++ b/.changeset/migrate-runner-storage.md @@ -0,0 +1,47 @@ +--- +'@clearcms/storage': minor +--- + +`@clearcms/storage/migrate-bridge` — greedy-at-read + lazy-on-save +bridge to `@clearcms/spec/migrate` (T-6.1 / ADR-0022). + +New `./migrate-bridge` sub-path (Node-conditional in `package.json`'s +`exports` so browser bundles that respect export conditions skip the +file). Composes the existing pure read helpers with the migration +runner so the read path can transparently upgrade older records: + +- `migrateRecord(record, opts)` — pure, in-memory. Reads + `record.schemaVersion` via `assumeSchemaVersion`, builds a chain to + `opts.currentVersion`, and applies it via `applyChain`. Returns the + raw record verbatim when already at `currentVersion` (fast path). + Throws when no chain exists. The optional `onWarnings` sink fires + once per record that produced `_migration_warnings` so callers can + surface cross-format conversion notes (tiptap↔markdown). +- `readMigratedItem(adapter, key, opts)` — wraps `readItem` + + `migrateRecord`. +- `readMigratedPage(adapter, slug, opts)` — wraps `readPage` + + `migrateRecord`. +- `readMigratedPageData(adapter, slug, opts)` — wraps `readPageData` + + `migrateRecord`. Non-object payloads pass through unchanged + (back-compat with the v0 array / primitive shape). +- `readMigratedBlock(adapter, input, opts)` — wraps `readBlock` + + `migrateRecord`. + +Lazy-on-save uses the existing `writeX` helpers — they already accept +a `schemaVersion` argument and stamp it via `stampSchemaVersion`. +Callers that mutate a migrated record before saving simply pass the +chain's tail version as `schemaVersion`; no new write helpers are +introduced. + +Why a sub-path rather than threading `MigrationOptions` through every +read helper: the migration runner imports `node:fs/promises` for fs +discovery (`@clearcms/spec/migrate/runtime`). The pure read helpers +stay browser-bundleable because they don't import the runtime +sub-path. Pulling discovery into items.ts / pages.ts would re-poison +Astro's client bundle — the bug PR #106 fixed. This bridge file is the +only Node-only file in `@clearcms/storage`'s public surface, isolated +behind the explicit `./migrate-bridge` opt-in. + +Used by `clear-admin migrate-records` (admin track) for the bulk +chain-and-write pass. The editor read path can opt-in record-by-record +without paying the discovery cost on every read. diff --git a/.changeset/renderer-scaffold.md b/.changeset/renderer-scaffold.md new file mode 100644 index 0000000..bbe6d4e --- /dev/null +++ b/.changeset/renderer-scaffold.md @@ -0,0 +1,5 @@ +--- +'@clearcms/renderer': minor +--- + +New package: `@clearcms/renderer` library scaffold per ADR-0024. Framework-neutral block dispatch (`renderBlocks`), `UnknownBlock` fallback (ADR-0021), and `usePreview()` helper for ADR-0026 T1 click-to-focus. Types + dispatch only — no real blocks yet. diff --git a/.changeset/slice-0021-bug-3.md b/.changeset/slice-0021-bug-3.md new file mode 100644 index 0000000..09a1116 --- /dev/null +++ b/.changeset/slice-0021-bug-3.md @@ -0,0 +1,9 @@ +--- +'@clearcms/admin': patch +--- + +Fix `PagePageEditor.handlePublish` publishing pages whose draft validation just rejected (SLICE-0021 BUG-3). + +`handlePublish` ran `await handleSave()` then guarded on `dataErrors.length > 0 || errMsg !== null`. Those state writes inside `handleSave` had not flushed to the closure yet, so the guard always saw the pre-call (empty) values and proceeded to publish a page whose `pageData.save` was just rejected. The operator saw a "Published" pill on a page whose draft data was rejected. + +`handleSave` now returns `{ ok: boolean }` and `handlePublish` checks the return value rather than React state. diff --git a/.changeset/slice-0021-bug-4.md b/.changeset/slice-0021-bug-4.md new file mode 100644 index 0000000..6ca949f --- /dev/null +++ b/.changeset/slice-0021-bug-4.md @@ -0,0 +1,9 @@ +--- +'@clearcms/admin': patch +--- + +Make `settings.setLocales` atomic so a mid-write failure cannot leave a half-saved locale config (SLICE-0021 BUG-4). + +The action wrote `default_locale`, `enabled_locales`, and `fallback_strategy` as three separate `settings` upserts in a non-transactional loop. If the second write failed, the first was already committed and the third never ran — projects ended up with, e.g., a new `default_locale` that wasn't in the still-old `enabled_locales` array, with no recovery affordance. + +The three upserts now run inside a single Drizzle transaction (`db.transaction(...)`). The new helper `lib/settings-locales.ts` holds the logic; the action is a thin wrapper. diff --git a/.changeset/slice-0021-bug-5.md b/.changeset/slice-0021-bug-5.md new file mode 100644 index 0000000..29d8097 --- /dev/null +++ b/.changeset/slice-0021-bug-5.md @@ -0,0 +1,9 @@ +--- +'@clearcms/admin': patch +--- + +Stop swallowing FTS index errors at item-write call sites (SLICE-0021 BUG-5). + +Eight call sites in `lib/items.ts` and one in `lib/scheduler.ts` used the `void indexItem(...)` / `void unindexItem(...)` pattern to keep the FTS5 write off the request path. When the FTS write threw — locked DB, schema drift, libsql connection-reset — the rejection was silently dropped. Items saved successfully but search missed them with no signal until an operator ran `clear-admin reindex`. + +New helpers `indexItemAsync` / `unindexItemAsync` keep the non-blocking shape but route rejections through a logger (defaults to `console.error`). Operators now see `[search] indexItem failed for :` lines on FTS-write failures. diff --git a/.changeset/slice-0021-fields-cache-parity.md b/.changeset/slice-0021-fields-cache-parity.md new file mode 100644 index 0000000..3ad1a0f --- /dev/null +++ b/.changeset/slice-0021-fields-cache-parity.md @@ -0,0 +1,9 @@ +--- +'@clearcms/admin': patch +--- + +Invalidate the collection-schema cache after `fields.*` mutations (SLICE-0021 follow-on). + +`actions.collections.setEditorMode` calls `invalidateCollectionSchema(slug)` after writing; the four `fields.*` handlers (`create`, `update`, `delete`, `reorder`) did the bucket dual-write but skipped the invalidation. Result: `loadCollectionSchema(slug)` could serve a compiled validator without the new field until the next bucket-watch tick, making the editor's validation lag behind the actual schema. + +Each `fields.*` handler now invalidates the cache after `syncCollectionToBucket`, matching the `setEditorMode` pattern. diff --git a/.changeset/spec-block-schema-dialect.md b/.changeset/spec-block-schema-dialect.md new file mode 100644 index 0000000..036c96a --- /dev/null +++ b/.changeset/spec-block-schema-dialect.md @@ -0,0 +1,42 @@ +--- +'@clearcms/spec': minor +--- + +Add the block-schema dialect, the block-instance shape, and the +collection-schema `blockStorage` strategy hint per ADR-0021. + +`@clearcms/spec/blocks` (new sub-path): +- `BlockSchemaSchema` — the on-disk shape at + `theme/blocks/.schema.json`. Carries `name`, semver `version`, + open-shaped `fields` map, optional `title` / `description`, and the + `deprecated: boolean | string` field for ADR-0028's two-phase removal + flow. +- `BlockInstanceSchema` — the `{id, type, schemaVersion?, props?}` + record persisted either inline in `data.json`'s `blocks: [...]` + (`blockStorage: "inline"`) or as a per-instance file + (`blockStorage: "files"`). UUID-identified per ADR-0021's + manifest-of-record design (no numeric-prefix filenames). +- `BlockInstanceManifestEntrySchema` — the `{id, type}` parent-record + manifest entry referenced from `data.json` when storage is `"files"`. + +`ClearSchema` (collection + layout schemas) gains +`blockStorage?: "inline" | "files"`. The meta-schema validates the +field; the runtime default (`"files"` for `pages`, `"inline"` elsewhere) +is admin-side. Switching strategies on an existing collection is a +one-way migration handled by `clear-admin migrate-block-storage`. + +New bucket-path helpers: +- `blockSchemaPath({ name }) → "theme/blocks/.schema.json"` +- `blockInstancePath({ slug, id, type }) → "content/pages//blocks/-.json"` +- `parseBlockInstancePath(key)` — the inverse, tolerant of nested slugs. +- `THEME_BLOCKS_PREFIX`, `PAGE_BLOCKS_SUBDIR` constants exported from + `bucket/layout.ts`. + +Generates `dist/schemas/{block-schema,block-instance, +block-instance-manifest-entry}.json` for non-TS consumers. + +Eng B Phase 3 commit 7 of 8. Additive — existing v0.x buckets continue +to validate; site-mode artifacts (`theme/blocks/`, +`content/pages//blocks/`) only appear when the operator opts in. +The categorised theme-tokens shape (ADR-0025 reshape) ships with the +migration runner in commit 8. diff --git a/.changeset/spec-bucket-tokens-and-migrations.md b/.changeset/spec-bucket-tokens-and-migrations.md new file mode 100644 index 0000000..e199659 --- /dev/null +++ b/.changeset/spec-bucket-tokens-and-migrations.md @@ -0,0 +1,45 @@ +--- +'@clearcms/spec': minor +--- + +Add the per-bucket theme-tokens shape (ADR-0025) and the Migration type +(ADR-0022). + +`@clearcms/spec/tokens` (new sub-path): +- `BucketTokensSchema` — the on-disk shape at `theme/tokens.json`. + Categorised: `color` / `space` / `type` / `radius` / `shadow` / + `motion` (closed set per `THEME_TOKEN_CATEGORIES`). Each category is + optional; entries are project-defined `: ` strings. The + renderer maps name → `var(--clear--)`. Strict — + unknown top-level categories reject. Distinct from the v0 + `ThemeTokensSchema` in `globals/theme.ts`, which describes the flat + identity-theme block exposed via `/api/v1/identity`; the two coexist + while admin migrates. +- `getToken(tokens, category, name)` and `listTokens(tokens)` helpers + for doctor checks and validators (e.g. dangling + `optionsFromTokens` references). +- `DEFAULT_BUCKET_TOKENS` — fresh-bucket starter at `schemaVersion: + "1.0.0"` with no entries declared. + +`@clearcms/spec/migrate` (new sub-path) — the TYPES only; the runner +that chains migrations at read time lives in a future commit (block 5b +of the v1 plan): +- `MigrationOpSchema` — discriminated union of declarative ops + (`rename`, `default`, `remove`, `tiptap-to-markdown`, + `markdown-to-tiptap`). The two cross-format ops are best-effort and + the runner annotates `_migration_warnings` per ADR-0027. +- `DeclarativeMigrationSchema` — the `/migrations/-to-.json` + file shape: `{ from, to, ops, description? }`. +- `ImperativeMigration` — TS-only interface for `.ts` migration modules + that default-export a `{ from, to, apply, description? }` object. +- `Migration` union of both flavours; `isDeclarativeMigration` runtime + discriminator. +- `migrationFilename` / `parseMigrationFilename` — the + `-to-.{json|ts}` filename contract. + +Generates `dist/schemas/{bucket-tokens, declarative-migration, +migration-op}.json` for non-TS consumers. + +Eng B Phase 3 commit 8 of 8 (final). Additive — existing v0.x buckets +continue to validate. Phase 3 spec foundation complete; PR ready for +review. diff --git a/.changeset/spec-editor-modes-sections-canvas.md b/.changeset/spec-editor-modes-sections-canvas.md new file mode 100644 index 0000000..5f14815 --- /dev/null +++ b/.changeset/spec-editor-modes-sections-canvas.md @@ -0,0 +1,26 @@ +--- +'@clearcms/spec': minor +--- + +Extend `CLEAR_EDITOR_MODES` with `sections` and `canvas` per ADR-0021 + +ADR-0023. + +The closed set goes from `['form', 'article']` to +`['form', 'article', 'sections', 'canvas']`. `form` and `article` work in +either project mode; `sections` and `canvas` are site-mode-only surfaces +that drive block composition (`sections` ships with v1, `canvas` is a +later release). + +Adds `SITE_MODE_ONLY_EDITORS` (the `['sections', 'canvas']` subset) and a +`requiresSiteMode(editor)` predicate so the future `clear-admin doctor` +check (`editor: "sections"|"canvas"` declared in headless mode → config +error) and admin route guards can share one rule. + +`editorModeFor()` now resolves all four modes; unknown values still fall +back to `'form'` for the defensive narrowing path. The meta-schema's +`editor` enum is derived from `CLEAR_EDITOR_MODES` and picks up the new +values automatically. `validateArticleSchema()` is unchanged — sections / +canvas validation lives with the block-schema dialect (commit 6). + +Eng B Phase 3 commit 3 of 8. Additive — existing schemas declaring +`editor: "form"` or `editor: "article"` continue to validate. diff --git a/.changeset/spec-field-extensions-allowlist.md b/.changeset/spec-field-extensions-allowlist.md new file mode 100644 index 0000000..d495554 --- /dev/null +++ b/.changeset/spec-field-extensions-allowlist.md @@ -0,0 +1,28 @@ +--- +'@clearcms/spec': minor +--- + +Add field-level `extensions: string[]` allowlist on richtext + markdown +widgets per ADR-0021 Q3.5 / ADR-0027. + +`ClearSchemaProperty` gains an optional `extensions` array. The +meta-schema validates it as a uniqueItems array of non-empty strings; +semantic meaning of each name is the admin / renderer's territory (the +spec doesn't enumerate the closed set — adopters opt into TipTap +extensions, GFM features, third-party extensions by name). + +New helper `extensionsFor(prop)` returns the array when present + valid, +returns `null` when absent (so callers can apply their own +widget-specific lean default), and silently drops malformed entries +(non-string / empty / duplicates) when the array is partly bad. + +Lean defaults stay admin-side, not in the spec — they depend on context +(inline richtext vs article body vs markdown), and embedding them here +would duplicate the dispatcher. Documented in the type's JSDoc: +- richtext, inline: `["bold", "italic", "link", "lists"]` +- richtext, article body: `+ heading, image, blockquote, code` +- markdown: `["gfm"]` (CommonMark + GitHub-Flavored) + +Eng B Phase 3 commit 5 of 8. Additive — existing schemas continue to +validate. Field-level `optionsFromTokens` for select widgets follows in +commit 6. diff --git a/.changeset/spec-markdown-widget.md b/.changeset/spec-markdown-widget.md new file mode 100644 index 0000000..c58bb39 --- /dev/null +++ b/.changeset/spec-markdown-widget.md @@ -0,0 +1,30 @@ +--- +'@clearcms/spec': minor +--- + +Add `markdown` as a first-class widget alongside `richtext` per ADR-0027. + +`CLEAR_WIDGETS` extends from 10 → 11 entries; the meta-schema widget enum +picks up the new value automatically. The two prose widgets are +deliberately distinct — different storage formats, different render +paths, no round-tripping: + +- `widget: "richtext"` stores TipTap JSON. WYSIWYG editor (the existing + surface). +- `widget: "markdown"` stores a markdown STRING. Monospace textarea, soft + wrap, optional preview pane. No slash commands; markdown shortcuts are + literal because the source IS markdown. + +`widgetFor` honours an explicit `widget: "markdown"` (and the legacy +`clear:widget` spelling). Markdown is never inferred from `type` / +`format` — it's opt-in only, since a string field is neither rich nor +markdown by default. `widthFor` defaults markdown to `"full"` (same as +richtext / textarea — needs the room). + +Switching a field from `richtext` to `markdown` (or vice-versa) is a +schema migration; the cross-format ops (`tiptap-to-markdown`, +`markdown-to-tiptap`) ship with the migration runner in commit 8. + +Eng B Phase 3 commit 4 of 8. Additive — existing schemas continue to +validate. Field-level `extensions` allowlist (per-field TipTap / markdown +extension list) lands in commit 5. diff --git a/.changeset/spec-migrate-runner.md b/.changeset/spec-migrate-runner.md new file mode 100644 index 0000000..7005ad8 --- /dev/null +++ b/.changeset/spec-migrate-runner.md @@ -0,0 +1,31 @@ +--- +'@clearcms/spec': minor +--- + +Migration runner per ADR-0022 + library discovery per ADR-0028. + +`@clearcms/spec/migrate` (new export sub-path) gains the runtime that +chains record migrations between `schemaVersion` values: + +- `migrate(record, fromVersion, toVersion, migrations)` — pure. Walks the + shortest chain end-to-end, applying declarative ops or imperative + `apply` functions in order. Stamps the resulting `schemaVersion` and + appends best-effort warnings to `_migration_warnings: string[]` for + cross-format conversions (tiptap↔markdown, ADR-0027). Throws when no + chain exists. +- `buildMigrationChain(fromVersion, toVersion, migrations)` — pure BFS + for the shortest reachable path. Returns `Migration[]` or `null`. +- `applyOp(record, op)` — pure, single-op walker. JSON-Pointer-ish paths + with array-index support; tilde escaping per RFC 6901. +- `discoverMigrations(opts)` — async fs walk. Reads adopter migrations + under `/migrations/` and library migrations under + `node_modules/@clearcms/blocks-*/migrations//` (ADR-0028). + Loads `.json` via `JSON.parse` + `DeclarativeMigrationSchema.parse` + and `.ts` via dynamic `import()`. +- `tiptapToMarkdown` / `markdownToTiptap` — tiny in-package serializers + used by the cross-format ops. No `marked` / `remark` dep — zod stays + the only runtime dep on `@clearcms/spec`. + +Phase 5 commit. Additive — the existing `Migration` types from PR #57 +are unchanged. The bulk `clear-admin migrate-records` driver lands as a +stub alongside; the editor read path consumes the runner in Phase 6. diff --git a/.changeset/spec-migrate-runtime-split.md b/.changeset/spec-migrate-runtime-split.md new file mode 100644 index 0000000..baf9178 --- /dev/null +++ b/.changeset/spec-migrate-runtime-split.md @@ -0,0 +1,85 @@ +--- +'@clearcms/spec': minor +'@clearcms/core': minor +'@clearcms/storage': minor +'@clearcms/design': minor +'@clearcms/admin': patch +--- + +Publish-hygiene unblocker: ship compiled `dist/` from four +shape-broken packages, plus split `@clearcms/spec/migrate` into a +browser-safe pure runner + a Node-only filesystem discovery sub-export +(closes T-3.8.5, closes #105, closes #107). + +## `@clearcms/spec/migrate` split (#105 / T-3.8.5) + +PR #76's migration runner placed `discoverMigrations` + its +`node:fs/promises` / `node:url` / `node:path` imports at the top of +`packages/spec/src/migrate/run.ts`. The barrel re-exported it. Any +admin import of `@clearcms/spec` transitively pulled the node imports +into Vite's browser pass, where the `__vite-browser-external` shim +doesn't satisfy `join` / `resolve` named bindings — admin's build +failed with `"join" is not exported by "__vite-browser-external"`. + +Split: + +- `./migrate/run.ts` keeps the **pure** runner: `migrate`, + `buildMigrationChain`, `applyOp`, plus the JSON-Pointer-ish path + walker helpers. No node imports. Safe for any bundle. +- `./migrate/runtime.ts` (new) holds **Node-only** filesystem + discovery: `discoverMigrations`, plus the `MigrationSource` / + `DiscoveredMigration` / `DiscoverOptions` types and the + `safeReaddir` / `tryLoadMigration` / `pickImperativeExport` / + `defaultListBlockPackages` / `derivePackageName` helpers. +- `./migrate/index.ts` re-exports the pure runner only. **No + discovery exports.** +- `packages/spec/package.json` gains a Node-conditional sub-path + `./migrate/runtime` (`{types, node}` — no `default`). +- `apps/admin/src/cli/migrate-records.ts` imports + `discoverMigrations` from `@clearcms/spec/migrate/runtime` (was + `@clearcms/spec/migrate`). + +## Raw-TS → compiled `dist/` for `core`, `storage`, `design` (#107) + +Lead's post-v0.4.2 audit caught three more packages shipping raw `.ts` +sources with `main`/`types`/`exports` pointing at `./src/*.ts`. Latent +bug — Vite/Astro inline-transforms them at SSR today, but fragile +against any non-Vite consumer (raw node, bare astro build, future +config change). Same fix as `@clearcms/db@0.1.2` (#99): + +- New `tsconfig.build.json` per package — `noEmit: false`, + `declaration`, `declarationMap`, `sourceMap`, + `rewriteRelativeImportExtensions`. Excludes `*.test.ts`. +- `package.json` `main` / `types` → `./dist/index.js` / + `./dist/index.d.ts`. Each `exports` sub-path becomes a `{types, + default}` conditional pointing at the compiled file. +- `scripts.build` — `tsc -p tsconfig.build.json`. Picked up by + `pnpm -r build`. +- `files` — replaces `src` with `dist`. Raw `.ts` no longer ships in + the published tarball. + +`@clearcms/design` keeps its non-TS exports (`./tokens.css`, +`./fonts.css`, `./clear-glyph.svg`, `./tailwind-preset` cjs) at the +package root (unchanged). The TS exports (`./links`, `./glyph`, +`./logo`) compile to `dist/` via `tsconfig.build.json` with `rootDir: +"."` and `include: ["links.ts", "ClearGlyph.tsx", "ClearLogo.tsx"]`. +`files` keeps the static asset list, replaces the per-`.ts` entries +with `dist`. + +`@clearcms/spec` `files` also drops `src` (was `["src", "dist", ...]`) +— spec already shipped `dist/` correctly via `main`/`types`/`exports`, +but ALSO shipped its source for no benefit. + +## Verified locally + +- `pnpm --filter @clearcms/spec build && test` — clean (249/249). +- `pnpm -r build` — clean across all 13 workspace projects, including + admin's Astro server bundle (the original `__vite-browser-external` + error is gone). +- `pnpm pack` content for each package contains only `dist/` (and the + static assets for `@clearcms/design`); zero `src/` files; zero + `*.test.*` files. + +Eng B Phase 3 wrap-up — T-3.8.5 unblocker for the rest of the v1 +train. Closes both `#105` (Astro build break) and `#107` (raw-TS +audit). diff --git a/.changeset/spec-mode-flag.md b/.changeset/spec-mode-flag.md new file mode 100644 index 0000000..4e108f7 --- /dev/null +++ b/.changeset/spec-mode-flag.md @@ -0,0 +1,15 @@ +--- +'@clearcms/spec': minor +--- + +Add `clear.config.json` Zod contract with `mode: "headless" | "site"` field +(default `"headless"`) per ADR-0021. Exports `ClearConfigFileSchema`, +`ProjectModeSchema`, `PROJECT_MODES`, `DEFAULT_PROJECT_MODE`, +`CLEAR_CONFIG_VERSION`, plus the `BucketProviderSchema`, +`ProjectConfigLocalesSchema`, and `ProjectConfigR2Schema` sub-shapes that +mirror what admin already persists. Existing `clear.config.json` files +without `mode` continue to validate (default applies); composer is +explicitly rejected as a project-mode value (it remains a SaaS-tier word +per ADR-0009). + +Additive — no existing field shape changes. diff --git a/.changeset/spec-options-from-tokens.md b/.changeset/spec-options-from-tokens.md new file mode 100644 index 0000000..00184af --- /dev/null +++ b/.changeset/spec-options-from-tokens.md @@ -0,0 +1,29 @@ +--- +'@clearcms/spec': minor +--- + +Add field-level `optionsFromTokens` keyword on `widget: "select"` fields +per ADR-0025. + +When set, the admin's select widget enumerates the named tokens from +`theme/tokens.json` under the named category instead of the field's +inline `enum`. The stored value is the token NAME (not the resolved +value) — the renderer maps name → `var(--clear--)` so +token edits ripple to every block instance with no content migration. + +`THEME_TOKEN_CATEGORIES` (closed set: color / space / type / radius / +shadow / motion) and the matching `ThemeTokenCategorySchema` / +`ThemeTokenCategory` exports land in `@clearcms/spec/globals` for shared +use by the renderer + admin. Adding a category is a spec change. + +`ClearSchemaProperty.optionsFromTokens?: ThemeTokenCategory` plus the new +`optionsFromTokensFor(prop)` resolver. The meta-schema validates the +field as an enum of the six categories; ignored by widgets other than +`select`. + +The concrete `theme/tokens.json` shape (per-category named-entries map +with `schemaVersion`) lands in commit 7 alongside the block-schema +dialect. + +Eng B Phase 3 commit 6 of 8. Additive — existing schemas continue to +validate. diff --git a/.changeset/spec-schema-version.md b/.changeset/spec-schema-version.md new file mode 100644 index 0000000..fac055c --- /dev/null +++ b/.changeset/spec-schema-version.md @@ -0,0 +1,24 @@ +--- +'@clearcms/spec': minor +--- + +Add `schemaVersion` (records) + `version` (schema files) per ADR-0022. + +Records — `ItemFile`, `Item`, `RestItem`, `PageDocument`, `RestPage`, +`Identity`, `Nav`, `ThemeTokens` — gain an optional top-level +`schemaVersion` semver string. Schema files — the `ClearSchema` shape used +by `schemas/.json` and `theme/layouts/.schema.json` — gain +an optional top-level `version` semver string; the meta-schema validates +the field with a semver pattern. + +New exports: `SemverSchema`, `SchemaVersionSchema`, `DEFAULT_SCHEMA_VERSION`, +`assumeSchemaVersion(record, options?)`. The helper extracts the field +when valid, falls back to `"1.0.0"` when absent or malformed, and fires +an `onMissing` callback so admin / doctor surfaces can emit the +deprecation warning ADR-0022 requires. Fields stay `.optional()` (NOT +`.default()`) so the doctor's "untracked" check can distinguish stamped +records from unstamped ones; the helper applies the default at the call +site. + +Eng B Phase 3 commit 2 of 8. Additive — existing buckets continue to +validate. diff --git a/.changeset/storage-named-globals.md b/.changeset/storage-named-globals.md new file mode 100644 index 0000000..574456d --- /dev/null +++ b/.changeset/storage-named-globals.md @@ -0,0 +1,34 @@ +--- +'@clearcms/storage': minor +--- + +Add named-global write helpers — `writeIdentity`, `writeNav`, +`writeTokens` — plus generic `writeGlobal(name, value)` dispatch and +matching read counterparts. Per ADR-0022, every global record on disk +now carries a `schemaVersion` stamp without callers having to wire it. + +`@clearcms/storage` exposes (`./named-globals` sub-path): +- `writeIdentity(adapter, identity, schemaVersion?)` → + `content/globals/identity.json` +- `writeNav(adapter, nav, schemaVersion?)` → + `content/globals/nav.json` +- `writeTokens(adapter, tokens, schemaVersion?)` → + `theme/tokens.json` (accepts both v0 identity-theme and v1 + categorised shapes; the migration runner converts between them) +- `writeGlobal(adapter, name, value, schemaVersion?)` — generic + dispatch on `GlobalName = "identity" | "nav" | "tokens"`. TS-level + exhaustiveness so adding a new global is a compile error elsewhere. +- `readIdentity` / `readNav` / `readTokens` / `readGlobal` mirror the + writers — same key, same null-on-miss semantics. + +All writers route through the existing `writeStampedJson` primitive +from this package, inheriting the explicit-arg → record-existing → +`DEFAULT_SCHEMA_VERSION` precedence chain. Path constants come from +`@clearcms/spec`'s `identityPath()` / `navPath()` / `themeTokensPath()` +— single source of truth across admin / SDK / migration tools. + +15 new tests cover round-trips, idempotency, schemaVersion precedence, +the GLOBAL_NAMES closed set, and the dispatch routing. + +Eng B Phase 5/6 storage track, commit 2 of N. Additive — existing +`writeJson` callers and the `writePageData` family are untouched. diff --git a/.changeset/storage-paths-helpers.md b/.changeset/storage-paths-helpers.md new file mode 100644 index 0000000..ab2663a --- /dev/null +++ b/.changeset/storage-paths-helpers.md @@ -0,0 +1,24 @@ +--- +'@clearcms/storage': minor +--- + +Add storage-side bucket-path helpers per ADR-0021 site-mode layout. + +`@clearcms/storage` exposes (`./paths` sub-path): +- `themeBlocksDir()` → `"theme/blocks"` +- `themeTokensPath()` — re-export of `@clearcms/spec`'s helper +- `pageBlocksDir(slug)` → `"content/pages//blocks"` +- `pageBlockPath(slug, id, type)` → + `"content/pages//blocks/-.json"`. Positional-args + shorthand for `@clearcms/spec`'s + `blockInstancePath({slug, id, type})`; delegates to that helper so + the path layout stays in lockstep across spec / storage / SDK. + +These are conveniences for callers that already pulled in `@clearcms/storage` +and want the per-purpose key without the spec sub-path import. Same +strings either way; one place to update if the layout ever moves. + +7 new tests cover all four helpers (flat slugs, nested slugs, +`pageBlockPath` ↔ `pageBlocksDir` consistency). + +Eng B Phase 5/6 storage track, commit 5 on PR #72. diff --git a/.changeset/storage-stamp-schema-version.md b/.changeset/storage-stamp-schema-version.md new file mode 100644 index 0000000..4d130cf --- /dev/null +++ b/.changeset/storage-stamp-schema-version.md @@ -0,0 +1,24 @@ +--- +'@clearcms/storage': minor +--- + +Add `stampSchemaVersion` + `writeStampedJson` — the load-bearing +schema-version write primitives per ADR-0022. + +`@clearcms/storage` now depends on `@clearcms/spec` (workspace) for +`DEFAULT_SCHEMA_VERSION` and `assumeSchemaVersion`. Re-exports +`DEFAULT_SCHEMA_VERSION` for adopters that already pulled in storage. + +- `stampSchemaVersion(record, schemaVersion?)` — pure. Returns a new + record with `schemaVersion` set. Precedence: explicit arg → record's + existing valid semver → `DEFAULT_SCHEMA_VERSION` (`"1.0.0"`). Treats a + malformed existing value as missing per `assumeSchemaVersion`'s + validation rules. Never mutates the input. +- `writeStampedJson(adapter, key, record, schemaVersion?)` — wraps + `writeJson` (same temp+rename atomicity, same UTF-8 pretty-printed + JSON + trailing newline). Stamps then writes. + +Eng B Phase 5/6 storage-track commit 1 of N. Additive — existing +`writeJson` callers are unaffected; the named higher-level helpers +(`writeItem`, `writePage`, `writeGlobal`, `writeTokens`, `writeBlock`) +land in subsequent commits and build on top of this primitive. diff --git a/.changeset/storage-write-block-strategy.md b/.changeset/storage-write-block-strategy.md new file mode 100644 index 0000000..ea92fe6 --- /dev/null +++ b/.changeset/storage-write-block-strategy.md @@ -0,0 +1,47 @@ +--- +'@clearcms/storage': minor +--- + +Add `writeBlock` + `resolveBlockStorage` and augment `writePageData` to +stamp `schemaVersion` per ADR-0022. + +`@clearcms/storage` exposes (`./blocks` sub-path): +- `writeBlock(adapter, slug, instance, schemaVersion?)` → + `content/pages//blocks/-.json`. Used when + `blockStorage: "files"` is in effect for the parent collection. Path + built via `@clearcms/spec`'s `blockInstancePath()`. Handles nested + page slugs (`about/team`). +- `readBlock(adapter, {slug, id, type})` — symmetric read; null on miss. +- `deleteBlock(adapter, {slug, id, type})` — idempotent; 404-on-delete + treated as success. +- `resolveBlockStorage(collectionSchema, collectionSlug?)` — per + ADR-0021. Precedence: explicit `blockStorage` field → default + `"files"` for the conventional `pages` collection → default + `"inline"` for everything else. Defensive against null / unknown + values. + +`writePageData` augment (existing helper, additive change): +- Adds optional `schemaVersion?: string` parameter. +- When `data` is an object, stamps `schemaVersion` via + `stampSchemaVersion` (same precedence chain as every other named + writer in this package). +- Non-object payloads (legacy edge cases) write through unchanged for + back-compat. +- Documents the ADR-0021 site-mode manifest semantics in the docstring + — `data.json`'s `blocks: [...]` array carries `{id, type}` manifest + entries when `blockStorage: "files"` (per-instance files via + `writeBlock`), or full `{id, type, schemaVersion, props}` inline + entries when `blockStorage: "inline"`. Storage layer is shape-agnostic; + the strategy choice belongs to the caller. + +15 new tests cover the file-strategy round-trip, deleteBlock +idempotency, the strategy resolver's precedence chain, and defensive +handling of malformed schema input. Combined with commits 1–3, the +storage-track PR now ships every named writer the lead asked for — +`writeItem`, `writePage`, `writePageData` (augmented), `writeBlock`, +`writeTokens`, `writeGlobal` — all routing through one `writeStampedJson` +primitive. + +Eng B Phase 5/6 storage track, commit 4 of N (final named-writer +commit on PR #72; the `clear-admin backfill-schema-versions` CLI lands +separately on a follow-up branch). diff --git a/.changeset/storage-write-item-page.md b/.changeset/storage-write-item-page.md new file mode 100644 index 0000000..4828db2 --- /dev/null +++ b/.changeset/storage-write-item-page.md @@ -0,0 +1,36 @@ +--- +'@clearcms/storage': minor +--- + +Add `writeItem` + `writePage` (and read counterparts) — status-routed +record writers that stamp `schemaVersion` per ADR-0022. + +`@clearcms/storage` exposes: +- `writeItem(adapter, item, schemaVersion?)` (`./items` sub-path) — + routes by `item.status` via `@clearcms/spec`'s `itemPath()`: + - `published` → `collections///.json` + - `draft` / `scheduled` → `drafts///.json` + Records need only the four routing keys (`collection`, `slug`, + `locale`, `status`); fully-typed `ItemFile` from `@clearcms/spec` + satisfies this trivially. +- `readItem(adapter, key)` — symmetric read; null on miss or corrupt. +- `writePage(adapter, page, schemaVersion?)` (`./pages` sub-path) → + `content/pages//index.json`. Handles nested slugs + (`about/team`) automatically. +- `readPage(adapter, slug)` — symmetric read. + +Both routes delegate to `writeStampedJson` so the precedence chain +(explicit arg → record's existing valid semver → +`DEFAULT_SCHEMA_VERSION`) is identical across every named writer in +this package. + +Trash + revision flows are NOT in scope here — those are status +transitions composed by admin's items service. `writeItem` is the +canonical "write the on-disk record file" helper; admin layers +status-transition operations on top. + +13 new tests cover status routing for all three statuses, round-trips, +schemaVersion precedence, nested page slugs, and idempotency. + +Eng B Phase 5/6 storage track, commit 3 of N. Additive — existing +`writePageData` is untouched (its block-manifest augment is commit 4). diff --git a/.changeset/template-site-marketing.md b/.changeset/template-site-marketing.md new file mode 100644 index 0000000..0e17558 --- /dev/null +++ b/.changeset/template-site-marketing.md @@ -0,0 +1,14 @@ +--- +"@clearcms/create": minor +--- + +feat(create): add template-site-marketing scaffold + --theme=marketing flag + +New site-mode scaffold pre-wired with @clearcms/blocks-marketing v1 (10 blocks) +and a sample home page (Hero + Features + CTA). Set mode: "site" in +clear.config.json. Default theme tokens placed at theme/tokens.json and +block schemas at theme/blocks/.schema.json. + +Usage: npx @clearcms/create my-site --theme=marketing + +Closes T-5F.1. diff --git a/.changeset/v1-block-designer.md b/.changeset/v1-block-designer.md new file mode 100644 index 0000000..4cd8cd2 --- /dev/null +++ b/.changeset/v1-block-designer.md @@ -0,0 +1,29 @@ +--- +'@clearcms/admin': minor +--- + +Block-schema designer extension (T-5C.1, ADR-0024). The visual SchemaDesigner +now reads + writes `theme/blocks/.schema.json` files in addition to +the existing collection + layout schemas. Site-mode-only. + +- `apps/admin/src/lib/schemas.ts` — adds `'block'` to `SchemaKind`, + `b:` key encoding, `theme/blocks/*.schema.json` listing, + and BlockSchema ↔ ClearSchema dialect conversion (BlockSchema's + `fields` becomes ClearSchema's `properties`; block-level meta — + `name`/`version`/`title`/`description`/`deprecated` — round-trips + via a `clear:block` meta field the designer ignores). +- `apps/admin/src/components/schema-designer/SchemaList.tsx` — third + section under "Block schemas," shown only when at least one block + schema exists in the bucket. +- `apps/admin/src/pages/settings/blocks/index.astro` — site-mode-only + list (302s to /settings in headless, /schemas in site). +- `apps/admin/src/pages/settings/blocks/[name].astro` — site-mode-only + designer (302s to /schemas/b:). +- `apps/admin/src/components/settingsSections.ts` — Settings sub-nav + adds "Schemas" + "Block schemas" entries (gated by schemaEditor). + +Tests: 18 cases (was 10) — block-key encoding edge cases (PascalCase +required, path traversal rejected, empty rejected) + dialect conversion +round-trip (fields/properties + meta + deprecated marker). + +Closes T-5C.1. diff --git a/.changeset/v1-editor-surface-registry.md b/.changeset/v1-editor-surface-registry.md new file mode 100644 index 0000000..ef3cb1e --- /dev/null +++ b/.changeset/v1-editor-surface-registry.md @@ -0,0 +1,29 @@ +--- +'@clearcms/admin': minor +--- + +Editor surface registry (Phase 4, ADR-0023, closes #52). The schema's +`editor:` keyword now resolves through a closed-core registry of four +surfaces — `form`, `article`, `sections`, `canvas` — each implementing +a stable `EditorSurface` interface (`validate()` + `Component`). + +- `apps/admin/src/surfaces/{form,article,sections,canvas}/index.tsx` — one + module per surface. `form` and `article` delegate to the existing + `SchemaForm` for behavior parity; `sections` and `canvas` render + not-implemented placeholders gated by validators (sections requires + site-mode + block schemas; canvas is reserved for post-v1). +- `apps/admin/src/surfaces/types.ts` — `EditorSurface`, `SurfaceProps`, + `SurfaceContext`, `SurfaceValidationResult`, `SiteContext`, plus + `ok()` / `failed()` validator helpers. +- `apps/admin/src/surfaces/index.ts` — `surfaceFor(schema)` dispatcher + + the closed registry object. +- `PageItemEditor` and `PagePageEditor` now dispatch via `surfaceFor()` + instead of branching on a narrowed `'form' | 'article'` value. When a + surface validator refuses to mount, both surfaces render a typed + `SurfaceValidationFailed` block inline. + +The registry is internal-only per ADR-0023 — no public registration API +in v1. Opening it to npm-shipped third-party surfaces is a future ADR +keyed off concrete adopter demand. + +Tests: 10 cases covering dispatch + per-surface validators. diff --git a/.changeset/v1-mode-gating.md b/.changeset/v1-mode-gating.md new file mode 100644 index 0000000..db36ef6 --- /dev/null +++ b/.changeset/v1-mode-gating.md @@ -0,0 +1,22 @@ +--- +'@clearcms/admin': patch +--- + +Project mode plumbing for ADR-0021 (T-5A.1). Admin now resolves `mode` +from `clear.config.json` (or env `CLEAR_MODE`) at boot and exposes a +cached `getProjectMode()` / `isSiteMode()` helper. Defaults to +`headless` — existing v0.x installs are unchanged. + +Site-mode-only surfaces (theme tokens UI, sections editor, designer +extension, blocks library) consume this in subsequent T-5x PRs to gate +their sidebar entries + route guards. + +- `apps/admin/src/lib/project-config.ts` — adds `mode: ProjectMode` to + `ResolvedProjectConfig`; resolves env > file > default with + `sources.mode` provenance tracking. +- `apps/admin/src/lib/project-mode.ts` — cached `getProjectMode()` / + `isSiteMode()` helpers for runtime use (Shell, middleware, + surface dispatcher). + +Tests: 6 cases on the cached helper + 4 cases on the resolver +(default, file, env override, unknown-value fallback). diff --git a/.changeset/v1-sections-surface.md b/.changeset/v1-sections-surface.md new file mode 100644 index 0000000..87cd85c --- /dev/null +++ b/.changeset/v1-sections-surface.md @@ -0,0 +1,35 @@ +--- +'@clearcms/admin': minor +--- + +Sections surface MVP (Phase 5D, ADR-0021/ADR-0024). The `sections` +editor surface now mounts a real composition UX in site-mode admins +with at least one block schema. + +- `apps/admin/src/surfaces/sections/SectionsSurface.tsx` — vertical + stack of `SectionCard`s rendering each block instance from the + `value.sections` manifest, plus an `AddSectionPicker` to append + new sections by type. +- `apps/admin/src/surfaces/sections/SectionCard.tsx` — header (block + type label + reorder buttons + delete) + body (per-block fields via + SchemaForm with the block-schema converted to ClearSchema shape). + Unknown block types render an inline error pointing at /settings/blocks. +- `apps/admin/src/surfaces/sections/AddSectionPicker.tsx` — minimal + v1 picker (select + add button); empty-state message when no block + types exist. +- `apps/admin/src/surfaces/sections/types.ts` — `SectionInstance` + + `SectionsManifest` shape, `toManifest` defensive coercion, and a + crypto-random `newSectionId` helper. +- `apps/admin/src/surfaces/sections/index.tsx` — replaces the Phase 4 + placeholder; validator returns `ok()` when site-mode + block-schemas + are present. + +Storage strategy: v1 ships with the inline-in-pageData strategy. Per- +instance block file storage (`writeBlock` / `blockInstancePath`) +lands as a v1.x follow-up (#142+). Drag-and-drop reorder via dnd-kit, +visual block-type picker, and click-to-focus from the live preview +are also v1.x scope. + +Tests: 11 surface-registry cases (was 10) + 7 manifest helper cases. + +Closes T-5D.1, T-5D.2, T-5D.3 (basic), T-5D.4. Defers T-5D.5/6/7/8 to v1.x. diff --git a/.changeset/v1-tokens-ui.md b/.changeset/v1-tokens-ui.md new file mode 100644 index 0000000..89f4daf --- /dev/null +++ b/.changeset/v1-tokens-ui.md @@ -0,0 +1,22 @@ +--- +'@clearcms/admin': minor +--- + +Theme tokens UI (T-5B.1, ADR-0024). Site-mode-only route at +`/settings/site/tokens` lets operators edit `theme/tokens.json` from +the admin instead of by hand. Three groups per the PLAN spec: colors, +typography, and "space, radius, shadow, motion." + +- `apps/admin/src/components/tokens/{ColorPicker,LivePreview,TokensPanel,types}` — + the editor + a sandboxed live preview (button + heading + card) that + reflects every change in real time without recolouring the admin chrome. +- `apps/admin/src/components/PageTokens.tsx` — orchestrator, save flow, + dirty tracking. +- `apps/admin/src/pages/settings/site/tokens.astro` — route + isSiteMode + guard (redirects headless installs to /settings). +- `apps/admin/src/actions/index.ts` — `adminTokens.save` action gated by + isSiteMode + writes via `@clearcms/storage`'s writeTokens. Permissive + zod schema so adopters can add custom token groups without a code + change; unknown groups round-trip verbatim. + +Closes T-5B.1. diff --git a/.changeset/windows-sqlite-teardown.md b/.changeset/windows-sqlite-teardown.md new file mode 100644 index 0000000..c91483f --- /dev/null +++ b/.changeset/windows-sqlite-teardown.md @@ -0,0 +1,31 @@ +--- +'@clearcms/admin': patch +--- + +Fix Windows SQLite teardown EBUSY in admin tests (#56). + +Test suites that exercised real libSQL (`pages-reindex`, schemas `[key]` API, +preview `[token]` API) and the backup/restore CLI tests failed on Windows +fresh clones with `EBUSY: resource busy or locked, unlink ...\clear.db`. Two +compounding causes: + +1. The cached libSQL singleton in `apps/admin/src/lib/db.ts` is opened lazily + when a route is imported. Tests closed their own setup-time client but + never the route-side cached client, so the file handle persisted past + `afterAll`. +2. POSIX silently allows unlink-while-open. Windows doesn't, and the OS + sometimes needs a brief retry window even after `close()` returns. + +This change adds: + +- A new `closeDb()` export from `apps/admin/src/lib/db.ts` that closes the + cached client and clears the singleton. Tests call it in `afterAll` before + removing tempdirs. +- A new `safeRm(path)` test helper in `apps/admin/src/test/teardown.ts` that + retries `rm` with backoff on EBUSY (5 retries, linear 50ms backoff). + +Wired into the affected suites: `pages-reindex.test.ts`, `schemas/[key].test.ts`, +`preview/[token].test.ts`, `backup.test.ts`, `restore.test.ts`. + +Linux/macOS unaffected — `safeRm` falls through to a single `rm` call when +no EBUSY occurs. diff --git a/AGENTS.md b/AGENTS.md index 0e850b8..3402f64 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,21 +1,38 @@ -# clear — agent handoff +# AGENTS.md -> **Before reading anything else, read `STATE.md` at the repo root.** It's the only authoritative "what's actually shipped right now" surface — memory and ADRs go stale. +Onboarding for AI coding agents (Claude Code, Cursor, Devin, etc.) working in this repo. Same instructions apply to humans contributing for the first time. ## Read in this order -1. **[`STATE.md`](./STATE.md)** — what's shipped vs proposed vs in-flight, dated. Always start here. -2. **[`ROADMAP.md`](./ROADMAP.md)** — the public-safe view of recently-shipped + what's next. -3. **[`CONTEXT.md`](./CONTEXT.md)** — domain language. Canonical for terminology. -4. **[`docs/adr/README.md`](./docs/adr/README.md)** — ADR index with status. Drill into the individual ADRs for decisions. -5. **[`README.md`](./README.md)** — public-facing entrypoint and quickstart. -6. **[`docs/CONTENT-PROTOCOL.md`](./docs/CONTENT-PROTOCOL.md)** — bucket layout + REST contract as wired. -7. **[`ARCHITECTURE.md`](./ARCHITECTURE.md)** — concise architectural overview. +1. **[`README.md`](./README.md)** — entrypoint. Current status, the two modes (headless / site), quickstart, mental model, repository map, what's next, what's deferred. +2. **[`ARCHITECTURE.md`](./ARCHITECTURE.md)** — system shape. How packages compose; bucket / DB / frontend split. +3. **[`docs/glossary.md`](./docs/glossary.md)** — domain vocabulary. Canonical names for the things this project talks about. Use these terms in code, commits, and PRs. +4. **[`docs/adr/README.md`](./docs/adr/README.md)** — decision index. One-paragraph rationale per ADR. Read this before proposing structural changes. +5. **[`docs/CONTENT-PROTOCOL.md`](./docs/CONTENT-PROTOCOL.md)** — bucket layout + REST contract as wired today. -If memory contradicts STATE.md, STATE.md wins. If STATE.md contradicts an ADR, the ADR wins for the *decision* and STATE.md wins for the *what's-shipped* state. +The four files above are sufficient context for almost any task. If you're touching a specific package, read its `packages//README.md` and `packages//CHANGELOG.md` next. + +## Where work lives + +- **Issues** ([github.com/clearcms/clear/issues](https://github.com/clearcms/clear/issues)) — every work unit. Pick one. The `agent-ready` label flags issues that are well-scoped and safe for an agent to take end-to-end without judgement calls. The `human-only` label flags the opposite. +- **Discussions** ([github.com/clearcms/clear/discussions](https://github.com/clearcms/clear/discussions)) — RFC-style threads for design before there's an issue. +- **Project board** — open issues + PRs grouped by readiness (Triage / Ready / In progress / Review / Done). +- **Releases page** — what shipped, when. The single timeline. +- **`.changeset/`** — queued package version bumps. Every user-facing PR adds one. + +There is no `STATE.md` / `ROADMAP.md` / `PLAN.md` in this repo. If you're looking for "what's true right now" it's the Releases page + Project board. If you're looking for "what's coming" it's `README.md`'s "What's next" section + open issues. + +## Repo conventions + +- **Commits** — short imperative subject, lowercase prefix (`fix(admin):`, `feat(spec):`, `docs(adr):`, `chore:`). Reference the issue (`closes #N`) in the body, not the subject. +- **PRs** — one issue per PR where possible. Body: a short summary + a test plan as a bulleted checklist. +- **Tests** — every behavioural change ships with a test that fails before the change and passes after. +- **Changesets** — every PR with a user-facing change in `apps/admin` or `packages/*` adds a `.changeset/.md` with the bump kind and a changelog line. Run `pnpm changeset` to scaffold one. +- **No new ADRs in this repo** — the public decision index ([`docs/adr/README.md`](./docs/adr/README.md)) is hand-curated. If you think a decision needs to be recorded, file a Discussion first; ADR drafts live in the private planning repo and only the rationale paragraph lands here. +- **Build before claiming done** — `pnpm install && pnpm -r build && pnpm typecheck && pnpm test` from the repo root passes cleanly. ## Companion repo -`clearcms/cloud` (private, hosted-mode console) is the operator side. Its `docs/AGENTS.md` points back here for session-start reading; this repo holds the protocol and the ADRs. +`clearcms/cloud` (private) is the operator-console code for the hosted SaaS tier. Its own `AGENTS.md` points back here for protocol-level reading. - +`clearcms/plan` (private) holds working notes — pre-PR design drafts, ADR drafts in flight, archive of historical planning artefacts. Agents shouldn't normally need to read it; humans on the core team do. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 8d650a6..14cea3a 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,13 +1,13 @@ # clear — architecture overview -> Concise architectural summary. **For decisions, [`docs/adr/`](./docs/adr/) is canonical.** For terminology, [`CONTEXT.md`](./CONTEXT.md) is canonical. This document is for someone who wants the shape of the system at a glance. +> Concise architectural summary. **For decisions, [`docs/adr/README.md`](./docs/adr/README.md) is canonical.** For terminology, [`docs/glossary.md`](./docs/glossary.md) is canonical. This document is for someone who wants the shape of the system at a glance. > See [`docs/CONTENT-PROTOCOL.md`](./docs/CONTENT-PROTOCOL.md) for what's wired today (bucket layout + REST contract). ## What clear is A portable, files-first headless CMS. **Per-instance bucket; the admin app is single-instance per install.** Astro 6 + React islands. -Multi-instance orchestration is the **console** app's job — closed-source, part of the hosted product, not part of the OSS distribution. The OSS admin never knows about more than one instance; it points at one bucket via env vars. See [`docs/adr/0001-oss-single-instance-saas-multi-tenant.md`](./docs/adr/0001-oss-single-instance-saas-multi-tenant.md). +Multi-instance orchestration is the **console** app's job — closed-source, part of the hosted product, not part of the OSS distribution. The OSS admin never knows about more than one instance; it points at one bucket via env vars. See [ADR-0001 (OSS / SaaS split)](./docs/adr/README.md#adr-0001-twin--oss-covers-single-instance-saas-covers-multi-tenant). ## The four pieces @@ -15,7 +15,7 @@ The system is four decoupled pieces glued by the bucket protocol: 1. **Bucket** — per-instance content store (R2/S3/fs). Files = source of truth. OSS. 2. **Admin** (default port 3001 single-instance; one port per project under the cloud console) — the editor. Edits one bucket. OSS, MIT. Lives in `apps/admin/` in this repo. -3. **Console** (port 2000) — hosted operator panel: project registry, launch buttons, identity bridging. **Closed-source**. Lives in the sister `clearcms/cloud` repo (per [ADR-0014](./docs/adr/0014-two-physical-repos-end-mirroring.md)). Consumes `@clearcms/admin` from npm. +3. **Console** (port 2000) — hosted operator panel: project registry, launch buttons, identity bridging. **Closed-source**. Lives in the sister `clearcms/cloud` repo (per [ADR-0014](./docs/adr/README.md#adr-0014--two-physical-repos--end-mirroring-public-is-source-of-truth)). Consumes `@clearcms/admin` from npm. 4. **Renderer** — public site, read-only against the bucket. Today: consumer FEs render via REST or SDK; turnkey `@clearcms/renderer` is planned. ## Stack @@ -73,7 +73,7 @@ For exact on-disk JSON shape, REST surface, and edge cases see `docs/CONTENT-PRO ## Schema model — bucket-files first, schema-driven editor - Schemas live in the bucket as JSON Schema (Draft 2020-12) files: `schemas/.json` for collections and `theme/layouts/.schema.json` for page layouts. The DB carries a query-fast projection of the same data. -- Editor surface is generated from the schema, not hardcoded ([ADR-0020](./docs/adr/0020-schema-first-editor-inversion.md)). Two modes: +- Editor surface is generated from the schema, not hardcoded ([ADR-0020](./docs/adr/README.md#adr-0020--schema-first-editor-inversion-form-by-default-article-opt-in)). Two modes: - `editor: "form"` (default) — 2-column form with one input per top-level property, one widget per field (`text`, `textarea`, `richtext`, `number`, `url`, `date`, `image`, `select`, `array`, `object`). - `editor: "article"` — full-canvas TipTap for the schema's single `widget: "richtext"` field; other fields collapse into a sidebar. Schema MUST declare exactly one richtext field (validated at save time). - Field width on the 12-column grid is a `width: "full" | "half" | "third"` keyword, with widget-keyed defaults (`text/number/url/date/select` → half; `textarea/richtext/array/object/image` → full). @@ -96,7 +96,7 @@ For exact on-disk JSON shape, REST surface, and edge cases see `docs/CONTENT-PRO ## Editor -- Schema-first ([ADR-0020](./docs/adr/0020-schema-first-editor-inversion.md)). The schema is the canvas; TipTap is one widget, not the whole editor surface. +- Schema-first ([ADR-0020](./docs/adr/README.md#adr-0020--schema-first-editor-inversion-form-by-default-article-opt-in)). The schema is the canvas; TipTap is one widget, not the whole editor surface. - TipTap 3 powers the `richtext` widget and article-mode canvas. ProseMirror JSON is canonical storage. - Inline-edit (preview): iframe + signed preview JWT + postMessage live updates. Single editor v1. - Yjs / Hocuspocus collab → v2. @@ -137,7 +137,7 @@ Yjs collab · libSQL vector / AI / TOON / MCP · Canvas + content sections · Se ## CLI -The only CLI is **`clear-admin`** — the bin shipped by `@clearcms/admin` ([ADR-0005](./docs/adr/0005-collapse-public-cli-into-clear-admin.md), [ADR-0018](./docs/adr/0018-single-cli-surface.md)). Subcommands: `start`, `bootstrap`, `migrate`, `import`, `scaffold`, `reindex`, `sync-pages`, `doctor`, `config`, `reset-password`, `sync-media`, `find-unused-media`, `env`. Operates on one bucket + one DB at a time, driven by env vars (`CLEAR_STORAGE_ROOT`, `CLEAR_DB_URL`, …) and the per-project `clear.config.json`. Adopters install via `npx @clearcms/create` or `npm i -D @clearcms/admin`. +The only CLI is **`clear-admin`** — the bin shipped by `@clearcms/admin` ([ADR-0005](./docs/adr/README.md#adr-0005--collapse-the-public-cli-into-clear-admin), [ADR-0018](./docs/adr/README.md#adr-0018--single-cli-surface--clear-admin-kill-clearctl)). Subcommands: `start`, `bootstrap`, `migrate`, `import`, `scaffold`, `reindex`, `sync-pages`, `doctor`, `config`, `reset-password`, `sync-media`, `find-unused-media`, `env`. Operates on one bucket + one DB at a time, driven by env vars (`CLEAR_STORAGE_ROOT`, `CLEAR_DB_URL`, …) and the per-project `clear.config.json`. Adopters install via `npx @clearcms/create` or `npm i -D @clearcms/admin`. Multi-project orchestration (allocating ports, spawning admin processes per project, registry writes) belongs to the cloud console — it's the registry-aware orchestrator that spawns `clear-admin start` per project. Project registry: `~/clear/registry.json`, shape `{projects: [{slug, adminPort, sitePort, bucket, db, domain?}, ...]}`. Project data lives at `~/clear//{bucket/, clear.db}`. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d3f6677 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,30 @@ +# CLAUDE.md + +This file exists so Claude Code automatically picks up project conventions when you open this repo. + +The canonical onboarding doc is [`AGENTS.md`](./AGENTS.md) — start there. It applies equally to Claude Code, Cursor, and other AI coding agents, plus humans contributing for the first time. + +## Quick orientation + +- Read [`README.md`](./README.md) first — current status, two modes (headless / site), quickstart, repo map. +- Work units live in GitHub Issues; the `agent-ready` label flags issues safe to take end-to-end. +- Decisions live in [`docs/adr/README.md`](./docs/adr/README.md) — one-paragraph rationale per ADR. +- Per-package detail in `packages//README.md` + `packages//CHANGELOG.md`. + +## Canonical structure (locked 2026-05-08) — don't regress + +This repo's doc surface was deliberately converged to the largest-used OSS conventions on 2026-05-08, sampled across Payload / Strapi / Sanity / Directus / TinaCMS / KeystoneJS / Astro / Next.js. Future sessions **must not** undo it. Specifically: + +- **Do not add** `STATE.md`, `ROADMAP.md`, `CONTEXT.md`, `PLAN.md`, or `MASTER_PLAN.md` at the repo root. Zero of eight sampled OSS projects have them; they're working-notes filenames, not OSS-standard surfaces. "What's true now" lives on the [GitHub Releases page](https://github.com/clearcms/clear/releases) and `README.md`'s "Current status" section. "What's coming" lives in `README.md`'s "What's next" section + open Issues. Domain vocabulary lives in `docs/glossary.md`. +- **Do not add individual ADR files** back to `docs/adr/`. The single `docs/adr/README.md` index — one paragraph of *why* per decision — is the canonical public surface. Long-form ADR drafts live in the private `clearcms/plan` repo under `decisions/`. +- **Do not recreate** `slices/`, `MASTER_PLAN.md`, `handoffs/`, or `proposals/` anywhere. They were retired in favour of [GitHub Issues](https://github.com/clearcms/clear/issues) + Projects + Discussions. If you find yourself wanting to write a `slices/SLICE-XXXX/` folder or a `plan-for-X.md`, the right move is to open an Issue with the right labels (`agent-ready` / `human-only` + `area:*`). +- **The fixed root surface** is: `README.md`, `ARCHITECTURE.md`, `AGENTS.md`, `CLAUDE.md`, `CONTRIBUTING.md`, `LICENSE`, `SECURITY.md`, `CODE_OF_CONDUCT.md`. Deeper docs go under `docs/`. Per-package detail goes under `packages//README.md` + `packages//CHANGELOG.md`. + +If a session insists on regressing one of the above, pause and confirm with the user before doing it. The cleanup that produced this shape was deliberate; reverting it should be a deliberate decision, not an accident. + +## Project-specific Claude rules + +- This repo's owner runs Claude Code in **caveman mode** for chat (drop articles + filler) but expects full sentences in artifacts (commits, PRs, ADR rationale, docs). +- Every behavioural change in `apps/admin` or `packages/*` ships with a test + a `.changeset/.md` describing the bump kind. The build train relies on changesets to drive npm publishes. +- For private working-notes (in-flight design, ADR drafts), use the private `clearcms/plan` repo, not this one. +- For work units, use GitHub Issues — never invent a parallel markdown planning surface. diff --git a/CONTEXT.md b/CONTEXT.md deleted file mode 100644 index c0ce98b..0000000 --- a/CONTEXT.md +++ /dev/null @@ -1,106 +0,0 @@ -# Clear - -A content-protocol-in-buckets system. Files in a bucket are the source of truth; the database is a derived index; the editor is one of several possible clients of the bucket. - -The product ships in two flavours: a single-instance OSS distribution that anyone can self-host, and a multi-tenant hosted service that the company sells. - -## Language - -### Distribution - -**Single instance**: -One site, one machine, fully self-contained — bucket + admin + frontend in one install. No project registry, no multi-project switching. Created by `npx @clearcms/create my-site`. -_Avoid_: "single-tenant" (overloaded with multi-user-but-one-org meaning), "self-hosted" (used as a flag, not a noun) - -**Hosted**: -The multi-tenant SaaS offering — one operator runs N sites for N customers, identity bridged across them, billing and orchestration included. Closed source. -_Avoid_: "cloud", "managed" (used freely as adjectives but not as the canonical term) - -### Apps - -**Admin**: -The editor app. One process per site. Reads and writes a single bucket and its derived database. OSS, MIT. -_Avoid_: "CMS", "dashboard", "panel" - -**Console**: -The operator panel for the hosted service. Manages many sites, launches admins, tracks projects, owns identity across them. Closed source. -_Avoid_: "dashboard", "control plane" - -**Frontend**: -The public-facing website that consumes the bucket via SDK or REST. User code, not platform code. Lives in the user's own repo. -_Avoid_: "renderer" (reserved for a future read-only public-domain app per the brief), "site" (used informally but not canonical) - -### Storage - -**Bucket**: -The per-site content store. S3-shaped (R2, S3, MinIO, local fs). The layout itself is the protocol — see `docs/CONTENT-PROTOCOL.md`. -_Avoid_: "storage", "store" - -**Index**: -The derived database (`clear.db`, libSQL). Rebuilt from the bucket on demand. Holds platform state (sessions, tokens) plus a query-fast projection of the bucket. -_Avoid_: "DB" when precision matters - -### Identity - -**Owner**: -The first user seeded by bootstrap into a single-instance admin. One per instance in OSS. Has full rights. -_Avoid_: "admin user", "root" - -**Member**: -A non-owner user with a role (editor, author, developer). Schema exists but member invites are not exposed in OSS — that surface is part of the hosted service. - -### Editor model - -**Schema** (clear schema): -A JSON Schema (Draft 2020-12) file that drives the editor surface. Two homes: -- `schemas/.json` — collection schema (one per collection) -- `theme/layouts/.schema.json` — page layout schema (one per declared page layout) - -The schema is the truth ([ADR-0020](./docs/adr/0020-schema-first-editor-inversion.md)). Both the form and the validator are generated from it. - -**Editor mode**: -The schema's top-level `editor` keyword. One of: -- `form` (default) — 2-column grid; one input per top-level property; renders inline `richtext` widgets at 240–400px. -- `article` — full-canvas TipTap for the schema's single `widget: "richtext"` field. Other fields collapse to a sidebar. Schema MUST declare exactly one richtext field. -_Avoid_: "form mode" / "article mode" without the `editor:` prefix when in code. - -**Widget**: -Field-level UI hint (`widget: ""` keyword). One of `text`, `textarea`, `richtext`, `number`, `url`, `date`, `image`, `select`, `array`, `object`. The legacy `clear:widget` spelling is still recognised; new code uses the unprefixed `widget`. - -**Width**: -Field-level grid hint (`width: "full" | "half" | "third"`). Maps to a 12-column grid (12/6/4 spans). Defaults are widget-keyed: `text/number/url/date/select` → `half`; `textarea/richtext/array/object/image` → `full`. Legacy spelling: `clear:width`. - -**Page-scoped data** (`data.json`): -Per-page structured data file at `content/pages//data.json`, validated against the page's resolved layout schema ([ADR-0010](./docs/adr/0010-singletons-as-page-scoped-data.md), v1 abridged). Sibling to `index.json` (the page document). Read across all four SDK backends as `cms.pages.get(slug).data`. - -## Relationships - -- A **single instance** has exactly one **bucket**, one **index**, and one **admin** process -- An **admin** writes to its **bucket**; the **bucket** is the source of truth; the **index** is rebuildable from it -- An **owner** belongs to exactly one **single instance** (no cross-instance identity in OSS) -- The **hosted** service runs many **single instances** and adds a **console** that bridges identity across them -- A **frontend** reads from a **bucket** via the SDK or REST; it never writes, and it never reads the **index** (the SDK's `fs`, `r2`, and `s3` backends bypass the admin entirely; only the `rest` backend round-trips through the admin process) - -## Example dialogue - -> **Adopter:** "Can I add a teammate as an editor on my self-hosted instance?" -> **Maintainer:** "The schema supports it but the OSS admin doesn't expose member invites — that's part of the **hosted** service. For OSS the assumption is one **owner** per **single instance**." - -> **Adopter:** "If I want to run five sites, do I clone the platform?" -> **Maintainer:** "No — the platform isn't open source. Run `npx @clearcms/create` five times in five folders, or use the **hosted** service if you want them managed in one place." - -## Repos - -**`clearcms/clear`** (public, MIT): the OSS distribution — protocol, SDK, Astro adapter, scaffolder, admin (which ships the `clear-admin` CLI bin). This repo. - -**`clearcms/cloud`** (private): the hosted service — console, multi-project orchestration, identity bridging, billing. - -The boundary between them is the npm registry. The cloud repo consumes public packages by version; nothing closed lives in `clearcms/clear`. See [ADR-0014](./docs/adr/0014-two-physical-repos-end-mirroring.md). - -## Flagged ambiguities - -- "platform" — historically referred to the entire monorepo; now reserved for the closed-source multi-tenant codebase (`clearcms/cloud`). The OSS bits are not "the platform." -- "account" — used loosely to mean both **owner** and the **hosted**-side customer record. The owner is per-instance; hosted accounts span instances. Different tables, different concepts. -- "page" vs. "item" — both are documents in the bucket. **Pages** live at `content/pages//index.json` and own their URL (`/`). **Items** live at `collections///.json` and don't own a URL — the consuming frontend chooses how to route them. - - diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7bd8b9c..a38bf6e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,9 +6,9 @@ Thanks for considering a contribution. clear is small and pre-1.0 — the easies 1. **Open an issue first** for anything bigger than a typo or one-line bug. clear is opinionated and a lot of decisions live in `docs/adr/` — it's faster to discuss approach before code than to redo a PR. 2. **Read the canonical docs.** Three files cover most "why does it work this way" questions: - - [`CONTEXT.md`](./CONTEXT.md) — the glossary. Single, Hosted, Bucket, Index, Owner, Member each have precise meanings. Use them. + - [`docs/glossary.md`](./docs/glossary.md) — the glossary. Single, Hosted, Bucket, Index, Owner, Member each have precise meanings. Use them. - [`docs/CONTENT-PROTOCOL.md`](./docs/CONTENT-PROTOCOL.md) — the bucket layout + REST contract. The protocol is the product; please don't break it without an ADR. - - [`docs/adr/`](./docs/adr/) — the architecture decisions, numbered + immutable. Read 0001–0007 if you're touching anything structural. + - [`docs/adr/README.md`](./docs/adr/README.md) — the architecture decision index, one paragraph of *why* per ADR. Read at least the v1 cluster (0021–0028) before touching anything structural. 3. **Run `pnpm install` from the repo root.** clear is a pnpm workspace; package-by-package commands are dispatched by `--filter`. ## Local development @@ -42,7 +42,7 @@ Per-package commands are dispatched via `pnpm -r` or `pnpm --filter`. There is n ## What's NOT in scope here -clear is intentionally **single-instance per install** — one bucket, one admin, one site per install. PRs that add multi-instance orchestration, project registries, or shared identity across installs will be redirected. That's a different shape of product. See [`docs/adr/0001-oss-single-instance.md`](./docs/adr/0001-oss-single-instance.md). +clear is intentionally **single-instance per install** — one bucket, one admin, one site per install. PRs that add multi-instance orchestration, project registries, or shared identity across installs will be redirected. That's a different shape of product. See [ADR-0001](./docs/adr/README.md#adr-0001--single-instance-per-install). ## Code style diff --git a/README.md b/README.md index 043e32c..8614aef 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,15 @@ # clear -A portable, files-first headless CMS. Content lives as JSON files in a bucket (S3, R2, or local filesystem) and is the source of truth. A database next to the admin holds a query-fast projection of the bucket (rebuildable from the bucket) plus operational state (sessions, tokens, jobs — not rebuildable). Frontends read the bucket directly; the database is never on the read path. The bucket layout is the protocol. +A portable, files-first CMS. Content lives as JSON files in a bucket (S3, R2, or local filesystem) and is the source of truth. A database next to the admin holds a query-fast projection of the bucket (rebuildable from the bucket) plus operational state (sessions, tokens, jobs — not rebuildable). Frontends read the bucket directly; the database is never on the read path. The bucket layout is the protocol. + +## Two modes (v1, [ADR-0021](./docs/adr/README.md#adr-0021--headless--site-mode-split)) + +clear ships in two modes — operators pick at scaffold time. + +- **Headless mode** (`mode: "headless"`, the default in v1). The admin authors content; your frontend (Astro, Next.js, etc.) renders it however you want. No assumptions about the rendering stack. +- **Site mode** (`mode: "site"`, new in v1). The admin authors content **and** composes pages from a typed block library (`theme/blocks/.schema.json`). Operators get a visual sections editor + theme tokens UI; the renderer ships as a library you import into any Astro/React/etc. site. `npx @clearcms/create my-site --theme=marketing` scaffolds a site-mode project pre-wired with `@clearcms/blocks-marketing`'s 10 marketing blocks (Hero, Features, CTA, etc.). + +Both modes use the same bucket protocol — site mode adds `theme/blocks/` and `theme/tokens.json`, that's the entire surface delta. Existing v0.x installs upgrade to v1 unchanged; site mode is opt-in via `clear.config.json`. See [`docs/migration-v0.2-to-v1.0.md`](./docs/migration-v0.2-to-v1.0.md) for the upgrade walkthrough. ## Get in touch @@ -13,7 +22,7 @@ clear ships out of two physical repos: - **`clearcms/clear`** — this repo. The OSS distribution (MIT). The admin editor, the protocol spec, the consumer SDK, the Astro adapter, the scaffolder, and the single-instance CLI. Published to npm under `@clearcms/*`. - **`clearcms/cloud`** — the closed-source hosted-service code. The operator console (multi-project orchestration, identity bridging across instances). Consumes `@clearcms/admin` from npm; nothing closed lives here. -The split was operationalised in [ADR-0014](./docs/adr/0014-two-physical-repos-end-mirroring.md). You don't need the cloud repo to self-host. +The split was operationalised in [ADR-0014](./docs/adr/README.md#adr-0014--two-physical-repos--end-mirroring-public-is-source-of-truth). You don't need the cloud repo to self-host. ## Quick start — new project @@ -60,7 +69,7 @@ const cms = createClient({ backend: 'rest', adminUrl: 'http://localhost:3001' }) const posts = await cms.collection('posts').list(); ``` -`clear-admin` is the only OSS CLI ([ADR-0018](./docs/adr/0018-single-cli-surface.md)). Its subcommands — `start`, `bootstrap`, `migrate`, `import`, `scaffold`, `reindex`, `sync-pages`, `doctor`, `config`, `env`, `reset-password`, `sync-media`, `find-unused-media` — operate on the bucket and DB directly with no framework dependency. +`clear-admin` is the only OSS CLI ([ADR-0018](./docs/adr/README.md#adr-0018--single-cli-surface--clear-admin-kill-clearctl)). Its subcommands — `start`, `bootstrap`, `migrate`, `import`, `scaffold`, `reindex`, `sync-pages`, `doctor`, `config`, `env`, `reset-password`, `sync-media`, `find-unused-media` — operate on the bucket and DB directly with no framework dependency. ## Mental model @@ -93,25 +102,30 @@ packages/ ├── storage/ @clearcms/storage — bucket adapters (fs, R2, S3) ├── design/ @clearcms/design — shared brand tokens (placeholder) ├── mcp/ @clearcms/mcp — MCP server (experimental) +├── renderer/ @clearcms/renderer — site-mode renderer library +├── blocks-marketing/ @clearcms/blocks-marketing — 10 marketing blocks └── create-clear/ @clearcms/create — scaffolder (npx target) ``` ## Editor model — schema-first -The admin renders the editor surface from a JSON schema, not from a hardcoded layout ([ADR-0020](./docs/adr/0020-schema-first-editor-inversion.md)). Two modes: +The admin renders the editor surface from a JSON schema, not from a hardcoded layout ([ADR-0020](./docs/adr/README.md#adr-0020--schema-first-editor-inversion-form-by-default-article-opt-in)). Four surfaces in v1 — closed core ([ADR-0023](./docs/adr/README.md#adr-0023--editor-surface-registry--closed-core-internal-contract)): -- **`form`** (default) — 2-column grid generated from the schema. Use it for structured content (contact pages, recipes, team members). TipTap is one widget (`widget: "richtext"`) — not the canvas. +- **`form`** — 2-column grid generated from the schema. Default. Use it for structured content (contact pages, recipes, team members). TipTap is one widget (`widget: "richtext"`) — not the canvas. - **`article`** — full-canvas TipTap, opt-in via `editor: "article"` on the schema. Use it for long-form prose like blog posts. +- **`sections`** (site mode) — visual block composer. Drag-add typed blocks from the project's `theme/blocks/` library; reorder with up/down chevrons; per-block fields render as form widgets. +- **`canvas`** (site mode, reserved) — placeholder for the post-v1 free-form canvas surface. Schemas live in the bucket alongside content: ``` data/storage/ ├── schemas/.json ← collection schema (items) -└── theme/layouts/.schema.json ← page layout schema (pages) +├── theme/layouts/.schema.json ← page layout schema (pages) +└── theme/blocks/.schema.json ← block schema (site mode only) ``` -Per-page structured data lives next to the page document at `content/pages//data.json` ([ADR-0010](./docs/adr/0010-singletons-as-page-scoped-data.md), v1 abridged). The SDK reads it across all four backends: +Per-page structured data lives next to the page document at `content/pages//data.json` ([ADR-0010](./docs/adr/README.md#adr-0010--singletons-as-page-scoped-data-not-collections)). The SDK reads it across all four backends: ```ts const home = await cms.pages.get('home'); @@ -124,24 +138,50 @@ post?.data.body; // ProseMirror JSON See [`docs/integrations/astro.md`](./docs/integrations/astro.md) for the full Astro wiring. -## Reading order for contributors +## What's next (themes, not dates) + +- **CLI parity** — every action that the admin UI can do, `clear-admin` can do too. Operators can manage settings, users, invites, content from a terminal at 3am without opening a browser. +- **Form-layout polish** — explicit row containers, top-level tabs, collapsible field groups for richer schemas. +- **Drag-and-drop sections reorder** — site-mode sections surface ships in v1 with up/down chevrons; `@dnd-kit` integration is queued for v1.x. +- **Per-instance block storage** — collections can declare `blockStorage: "files"` to spill block content to per-block files alongside the page; v1 ships inline-in-pageData only. +- **Backups** — `clear-admin backup` / `restore` for one-shot tarballs of bucket + db with manifest. +- **fs → R2 migration** ([ADR-0017](./docs/adr/README.md#adr-0017--local-fs--r2-bucket-migration-command)) — adopters who started on local filesystem can migrate to R2 without re-bootstrapping. +- **Hosting topology** ([ADR-0013](./docs/adr/README.md#adr-0013--hosting-topology)) — Fly.io + Cloudflare Workers paths for the SaaS deployment. + +For deferred-past-v1 items see the [`What's deferred`](#whats-deferred) section below. + +## What's deferred -1. [`STATE.md`](./STATE.md) — live "what's true right now" snapshot. **Read first** — memory and old ADRs go stale. -2. [`ROADMAP.md`](./ROADMAP.md) — what's recently shipped + what's coming next. -3. [`CONTEXT.md`](./CONTEXT.md) — domain language. Canonical for terminology. -4. [`docs/adr/`](./docs/adr/) — architecture decisions in order. Read before proposing structural change. -5. [`docs/CONTENT-PROTOCOL.md`](./docs/CONTENT-PROTOCOL.md) — bucket layout + REST contract as wired today. -6. [`AGENTS.md`](./AGENTS.md) — agent-targeted reading guide. -7. [`ARCHITECTURE.md`](./ARCHITECTURE.md) — concise architectural overview. +- **GitHub-as-bucket adapter** ([ADR-0011](./docs/adr/README.md#adr-0011--github-as-a-public-read-only-bucket-backend)) — read-only bucket served from a public GitHub repo via `raw.githubusercontent.com`; targets a post-v1 minor. +- **Locale-aware schemas** — single schema per collection / layout for now; per-locale schema variants are post-v1. +- **JWT identity bridge** ([ADR-0004](./docs/adr/README.md#adr-0004--defer-jwt-identity-bridge-until-first-paying-customer)) — until first paying SaaS customer. +- **Multi-tenant operator console** — single-operator scope through v1; the multi-tenant console lives in the closed-source `clearcms/cloud` repo when it ships. -## Status +## Current status -Pre-1.0. All `@clearcms/*` packages on npm at 0.2.x. v0.3 is in flight (schema-first editor — see the queued changeset in `.changeset/`). v1.0 cuts when CLI parity + docs rewrite ship. +v1.0 is in the final stamp pass on `release/v1.0`. All `@clearcms/*` packages are queued for `1.0.0` via the Changesets train. Adopters checking which versions are live should run `npm view @clearcms/admin version`; the [GitHub Releases page](https://github.com/clearcms/clear/releases) is the canonical timeline of what shipped when. Per-package detail lives in each package's `CHANGELOG.md` (managed by Changesets). -For per-package versions and what's shipped vs. proposed vs. in-flight, [`STATE.md`](./STATE.md) is the single source of truth. For per-package release history, look at each package's `CHANGELOG.md` (managed by Changesets). +## How releases happen + +`@clearcms/*` packages release via [Changesets](https://github.com/changesets/changesets) on merges to `main`. Every PR with a user-facing change includes a `.changeset/*.md` describing the bump. Releases ship to npm with OIDC provenance. + +## How to contribute + +- File issues at https://github.com/clearcms/clear/issues +- Discussions: https://github.com/clearcms/clear/discussions +- ADRs are summarised in [`docs/adr/README.md`](./docs/adr/README.md). Read existing ones before proposing protocol changes; new ADR proposals start as a Discussion. + +## Reading order + +1. **README.md** — you are here. Quickstart + current status. +2. [`ARCHITECTURE.md`](./ARCHITECTURE.md) — concise architectural overview. +3. [`docs/glossary.md`](./docs/glossary.md) — domain language. Canonical for terminology. +4. [`docs/adr/README.md`](./docs/adr/README.md) — public decision index with one-paragraph rationale per ADR. +5. [`docs/CONTENT-PROTOCOL.md`](./docs/CONTENT-PROTOCOL.md) — bucket layout + REST contract as wired. +6. [`docs/migration-v0.2-to-v1.0.md`](./docs/migration-v0.2-to-v1.0.md) — upgrade walkthrough from v0.x. + +For agent / AI-coder onboarding, see [`AGENTS.md`](./AGENTS.md). ## License OSS distribution under MIT. Hosted-service code (in `clearcms/cloud`) is closed and not licensed for redistribution. - - diff --git a/ROADMAP.md b/ROADMAP.md deleted file mode 100644 index 6318dc1..0000000 --- a/ROADMAP.md +++ /dev/null @@ -1,44 +0,0 @@ -# clear — roadmap - -What's recently shipped and what's coming. For per-package details, see each package's `CHANGELOG.md`. For protocol-level decisions, see `docs/adr/`. For "what's true right now," see `STATE.md`. - -## Recently shipped (v0.2.x) - -- **Schema-first editor** (ADR-0020) — Pages and items are edited via a form generated from a JSON schema. TipTap is one widget (`widget: "richtext"`), not the canvas. Two modes: `form` (default) and `article` (full-canvas TipTap, opt-in for long-form prose). -- **Page-scoped data** (ADR-0010 v1) — `content/pages//data.json` sibling stores per-page structured fields. `cms.pages.get(slug).data` reads it across all 4 SDK backends. -- **Pages preview** — admin's preview button works on pages, not just collection items. -- **Single CLI** (ADR-0018) — `clear-admin` is the one binary. The old `clearctl` is removed. Per-project ops live in `clear-admin` (`bootstrap`, `migrate`, `import`, `reindex`, `sync-pages`, `scaffold`, `doctor`, `config`, `start`). -- **Doctor + per-project config** — `clear-admin doctor` runs 10+ health checks. `clear.config.json` (env > file > defaults) lives next to each project's bucket+db. -- **Adopter pain killed** — every issue filed against v0.1.x is fixed: `CLEAR_SKIP_SAMPLE_SEEDS`, full env-var documentation in `--help`, `--port`/`--host` flags, fs `watch` in `@clearcms/astro`, `renderTipTap()` helper, scaffolder ships a working `/preview` route, navigation editor default-on, schema editor default-on. - -See package `CHANGELOG.md` files for the full list. - -## What's next (themes, not dates) - -- **CLI parity** — every action that the admin UI can do, `clear-admin` can do too. Operators can manage settings, users, invites, content from a terminal at 3am without opening a browser. -- **Form-layout polish** — explicit row containers, top-level tabs, collapsible field groups for richer schemas. -- **Nav groups** — `kind: "group"` items in `content/globals/nav.json` for mega-menu / dropdown navs. -- **Backups** — `clear-admin backup` / `restore` for one-shot tarballs of bucket + db with manifest. -- **fs → R2 migration** (ADR-0017) — adopters who started on local filesystem can migrate to R2 without re-bootstrapping. -- **Hosting topology** (ADR-0013) — Fly.io + Cloudflare Workers paths for the SaaS deployment. - -## What's deferred - -- **Block-as-files** (ADR-0007) — pages-as-folders + block instances. Targeting v1.0 protocol revision; v0.x stays flat. -- **Locale-aware schemas** — single schema per collection / layout for now. -- **JWT identity bridge** (ADR-0004) — until first paying customer. -- **Multi-tenant / public hosting** of the operator console — single-operator scope through v1. - -## How releases happen - -`@clearcms/*` packages release via [Changesets](https://github.com/changesets/changesets) on merges to `main`. Every PR with a user-facing change includes a `.changeset/*.md` describing the bump. Releases ship to npm with OIDC provenance ([ADR-0014](docs/adr/0014-two-physical-repos-end-mirroring.md)). - -## How to contribute - -- File issues at https://github.com/clearcms/clear/issues -- Discussions: https://github.com/clearcms/clear/discussions -- ADRs in `docs/adr/` — read existing ones before proposing protocol changes; new ADRs get filed as PRs - -## Operator-internal (not for adopters) - -The operator's day-to-day planning, in-flight prompts, and cross-repo coordination live in a private repo (`clearcms/planning`). This roadmap is the public-safe surface; full internal scheduling is out of scope here. diff --git a/STATE.md b/STATE.md deleted file mode 100644 index b059ce9..0000000 --- a/STATE.md +++ /dev/null @@ -1,83 +0,0 @@ -# clear — current state - -Last updated: 2026-05-05 (Prompt 16 / ADR-0020 in flight on `feat/schema-first-editor`) - -## Session protocol - -**Always read this file first.** Memory entries and old ADR text go stale faster than this file does. If a memory entry contradicts STATE.md, trust STATE.md and update the memory. - -## Package versions (HEAD on `main`) - -These are the versions in each package's `package.json` at HEAD on `main`. The operator publishes from these tags; treat the working set as live unless a changeset is queued (none currently). - -- `@clearcms/admin` 0.2.0 -- `@clearcms/astro` 0.2.0 -- `@clearcms/sdk` 0.2.0 -- `@clearcms/core` 0.2.0 -- `@clearcms/spec` 0.2.0 -- `@clearcms/storage` 0.2.0 -- `@clearcms/index` 0.2.0 -- `@clearcms/create` 0.2.1 -- `@clearcms/db` 0.1.1 (no recent changes) -- `@clearcms/design` 0.1.0 -- `@clearcms/mcp` 0.1.0 (experimental — not battle-tested) - -## CLI - -Single CLI binary: `clear-admin` (in `@clearcms/admin`, bin at `apps/admin/bin/clear-admin.js`). The old `clearctl` is **DELETED** (ADR-0018, merged 2026-05-04 in `feat/kill-clearctl`). Per-project adopter ops live in `clear-admin`; multi-project orchestration (registry, `new`, `list`, `admin `) lives in the cloud console (separate repo). - -Per-project verbs available on `clear-admin`: `bootstrap`, `migrate`, `import `, `reindex`, `sync-pages`, `scaffold --from=`, `doctor`, `config`, `env`, `start`. - -## Admin runtime - -- Per-project model today: each project gets its own port; the cloud console spawns one `clear-admin` process per project. -- One-slot-per-host model: not in any current ADR or branch. (User-side proposal labelled "ADR-0019 / Prompt 14" exists outside the repo; nothing to read in-tree yet.) -- All admin subsystems default-ON as of 2026-05-04 (`apps/admin/src/lib/features.ts` `DEFAULT_SUBSYSTEMS`). `schemaEditor` was the last holdout (was off-by-default; flipped on in `feat/schema-default-on`, merged 2026-05-05 00:01 local). `navigation` was flipped earlier in `feat/nav-subsystem-default-on`. The audit on the schema-default-on merge confirmed every other subsystem was already on. - -## Bucket protocol - -- v0 layout (descriptive, not stable): `docs/CONTENT-PROTOCOL.md` is current. -- Pages are flat: `content/pages//index.json` only. Per-page structured data (`data.json` sibling) is **in flight** on `feat/page-scoped-data` (4 commits ahead of main, ADR-0010 v1 abridged). Branches add a Data tab to `PagePageEditor`, a `pageData.save` action, REST extension at `/api/v1/pages/.data`, and storage helpers. -- Blocks-as-files (ADR-0007) is **deferred to v0.3+ / v1.0**. The ADR's own status line says "vision — not for 0.1.x; targets v0.2 or v1.0 protocol revision". - -## Working features (shipped on main) - -- Pages preview: wired end-to-end. PreviewPanel in `PagePageEditor`, page-token branch in `/api/v1/preview/[token]`. Merged 2026-05-05 00:11 local in `feat/pages-preview` (referenced as "Prompt 12" in commit body). -- Pages reindex: `clear-admin reindex` rebuilds `pages_index` from bucket; `clear-admin sync-pages` for pages-only refresh (Issues #1, #2). -- TipTap renderer: `renderTipTap` exported from `@clearcms/astro` (`packages/astro/src/render-tiptap.ts`). Scaffolder template wires it into the post page (Issue #7). -- fs watch: `clearLoader({ watch: true })` and `pagesLoader({ watch: true })` in `@clearcms/astro` (dev only). Wraps the chokidar instance Astro provides via the integration; falls back to a self-managed chokidar if Astro's watcher isn't there. `packages/astro/src/watch.ts`. (Issue #4) -- Doctor: `clear-admin doctor` runs 10 health checks — config file, bucket path, bucket layout, db file, db schema, db migrations, bucket migrations, ports, versions, bucket identity (`apps/admin/src/cli/doctor.ts`). -- Config file: per-project `clear.config.json`. Precedence: env > file > defaults. `clear-admin config` prints the resolved config with a `sources` block tagging each field's origin. -- Sample-seed skip: `CLEAR_SKIP_SAMPLE_SEEDS=1` in bootstrap (`apps/admin/src/lib/admin-env.ts`, Issue #3). -- Network access: admin defaults to `127.0.0.1` (`apps/admin/bin/clear-admin.js:94`); set `HOST=0.0.0.0` env or pass `--host` to expose. Astro 6 strict same-origin POST guard is disabled (`security: { checkOrigin: false }` in `apps/admin/astro.config.ts:30`) — admin auth is cookie-based and the guard breaks Astro Actions calls. - -## Known footguns - -- `apps/admin/dist/` is force-tracked (165 files in git despite `.gitignore` listing `dist/`). Same for `packages/index/dist/` (16 files tracked). Repo wart from an early publish-day rehearsal; cleanup is its own task. Knock-on: edits to admin source can land out of sync with the tracked compiled output unless the dist files are regenerated and re-staged. -- Astro 6 silent port-fallback: if a configured port is busy, Astro picks the next free one and the registry lies. Doctor's `checkPorts` warns; manual stop+restart fixes. -- After a fresh clone of clear, the dist files are present (because tracked) but may be stale relative to source. `pnpm -r build` before any dev/test/admin run. - -## In flight - -- `feat/schema-first-editor` (8 commits ahead of main) — Prompt 16 / ADR-0020. Schema is now the truth; SchemaForm replaces the body+data tabs in both page and item editors. Article mode (full-canvas TipTap) opt-in via `editor: "article"` on the schema. Bucket migration 0004 writes default schemas + lifts page body content into data.json (with backup). -- `feat/page-scoped-data` (4 commits ahead of main) — already merged on this branch's parent (07954ca). ADR-0010 v1 abridged. Labelled "Prompt 13" externally. -- Nav groups (labelled "Prompt 15" externally): no branch yet. `packages/spec/src/globals/nav.ts` has no `kind` discriminator and no `group` variant; design lives outside the repo for now. Treat as not-started. - -## Open ADRs (highest-impact first) - -- ADR-0017 fs → R2 migration: not implemented. -- ADR-0010 page-scoped data: in flight on `feat/page-scoped-data`. -- ADR-0007 pages as folders, blocks as files: deferred (v0.2 or v1.0 protocol revision). -- ADR-0011 GitHub as bucket backend: proposed, not implemented. -- ADR-0012 `@clearcms/design` package: proposed; `@clearcms/design` 0.1.0 placeholder ships, no published tokens yet. -- ADR-0013 hosting topology (Fly.io console + per-customer admin, CF Workers renderer): proposed, not implemented. -- ADR-0014 two physical repos, end mirroring: operationalised — `clearcms/clear` and `clearcms/cloud` are split. Cloud spawns `clear-admin` from `@clearcms/admin` directly (ADR-0018). -- ADR-0015 bucket migrations through adapter: proposed (R2 blocker). -- ADR-0016 per-project R2 keypair isolation: proposed (security follow-up to ADR-0009). - -## Repo / branch state - -- Default branch: `main`. -- This file lives on branch `feat/state-md-and-adr-index`. -- Active feature branch with code: `feat/page-scoped-data` (4 commits ahead). All other `feat/*` branches are merged. -- Two-repo split is real: `clearcms/clear` (this repo, OSS, MIT) and `clearcms/cloud` (private, hosted-mode console). Cross-repo coupling is via npm; cloud consumes `@clearcms/admin` and resolves the bin via `require.resolve('@clearcms/admin/bin/clear-admin.js')`. diff --git a/apps/admin/CHANGELOG.md b/apps/admin/CHANGELOG.md index 7e32450..46e97cf 100644 --- a/apps/admin/CHANGELOG.md +++ b/apps/admin/CHANGELOG.md @@ -1,5 +1,26 @@ # @clearcms/admin +## 0.5.0 + +### Minor Changes + +- **Markdown widget** for the form surface (SLICE-0011). Schemas can now opt + fields into `widget: "markdown"` to get a raw-markdown textarea with a + live preview pane and a small toolbar (bold, italic, link, heading, list). + Cmd/Ctrl-B, Cmd/Ctrl-I, and Cmd/Ctrl-K shortcuts wrap the selection or + insert a link skeleton at the caret. The widget stores raw markdown text + on save; the consuming renderer parses at read time. Defaults to full + width on the SchemaForm grid, mirroring textarea/richtext. + + Existing schemas using `widget: "textarea"` or `widget: "richtext"` are + unaffected — markdown is purely additive. + + Internal: routes through the existing `FieldRouter` switch in + `apps/admin/src/components/widgets/`. Preview rendering uses `marked` + (already a dependency) with a small inline strip for `'; + const h = await mount(evil); + const preview = getPreview(h.container); + // The textarea is allowed to keep the raw source (escaped HTML, + // never executed); the preview is what gets injected via + // dangerouslySetInnerHTML, so it's the only place the strip matters. + expect(preview.innerHTML).not.toContain(' { + const evil = ''; + const h = await mount(evil); + const preview = getPreview(h.container); + expect(preview.innerHTML).not.toContain('onerror'); + h.unmount(); + }); +}); diff --git a/apps/admin/src/components/widgets/Markdown.tsx b/apps/admin/src/components/widgets/Markdown.tsx new file mode 100644 index 0000000..14d8561 --- /dev/null +++ b/apps/admin/src/components/widgets/Markdown.tsx @@ -0,0 +1,239 @@ +// Markdown widget — raw-markdown textarea + live preview pane. Slots into +// the form surface for long-form prose where the full TipTap richtext +// surface is too heavyweight (FAQ answers, blog posts authored as md, +// descriptions). Stores raw markdown text; the consuming renderer parses +// at read time. +// +// Shape mirrors RichText.tsx (inline card, min/max height) so the form +// grid stays consistent across the three prose widgets (textarea / markdown +// / richtext). +// +// We intentionally avoid pulling in @codemirror/lang-markdown — the bundle +// cost is ~200KB and a plain textarea + monospaced font covers the v1 +// editing surface. If syntax highlighting becomes a real ask, swap the +// textarea for a CodeMirror instance behind the same WidgetProps contract. + +import { useId, useRef, useState, useEffect } from 'react'; +import { marked } from 'marked'; +import type { WidgetProps } from './types'; + +interface ToolbarShortcut { + label: string; + title: string; + apply: (selection: SelectionState) => InsertResult; +} + +interface SelectionState { + value: string; + start: number; + end: number; +} + +interface InsertResult { + next: string; + cursorStart: number; + cursorEnd: number; +} + +function wrap(prefix: string, suffix: string, fallback: string) { + return ({ value, start, end }: SelectionState): InsertResult => { + const selected = value.slice(start, end); + const inner = selected.length > 0 ? selected : fallback; + const next = `${value.slice(0, start)}${prefix}${inner}${suffix}${value.slice(end)}`; + const cursorStart = start + prefix.length; + const cursorEnd = cursorStart + inner.length; + return { next, cursorStart, cursorEnd }; + }; +} + +function linePrefix(prefix: string) { + return ({ value, start, end }: SelectionState): InsertResult => { + const lineStart = value.lastIndexOf('\n', start - 1) + 1; + const next = `${value.slice(0, lineStart)}${prefix}${value.slice(lineStart)}`; + const cursor = end + prefix.length; + return { next, cursorStart: cursor, cursorEnd: cursor }; + }; +} + +function linkInsert({ value, start, end }: SelectionState): InsertResult { + const selected = value.slice(start, end); + const text = selected.length > 0 ? selected : 'link text'; + const insert = `[${text}](https://)`; + const next = `${value.slice(0, start)}${insert}${value.slice(end)}`; + const urlStart = start + text.length + 3; + return { next, cursorStart: urlStart, cursorEnd: urlStart + 8 }; +} + +const SHORTCUTS: Record = { + bold: { label: 'B', title: 'Bold (Cmd/Ctrl+B)', apply: wrap('**', '**', 'bold') }, + italic: { label: 'I', title: 'Italic (Cmd/Ctrl+I)', apply: wrap('_', '_', 'italic') }, + link: { label: 'Link', title: 'Insert link (Cmd/Ctrl+K)', apply: linkInsert }, + heading: { label: 'H', title: 'Heading', apply: linePrefix('## ') }, + list: { label: '•', title: 'Bullet list', apply: linePrefix('- ') }, +}; + +// Strip the obvious XSS shapes from marked output. Defense in depth: the +// admin is single-tenant and the operator is trusted, but member roles +// (Editor/Author) can also reach this widget, and the live preview runs +// in the operator's session. We block