@sleekcms/sync 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,13 +2,13 @@
2
2
 
3
3
  **Build complete websites with AI. Edit them locally. Deploy instantly.**
4
4
 
5
- The SleekCMS CLI brings your CMS to your code editor — with full AI-agent support baked in. Spin up an entirely new site by describing it in plain English, or drop into any existing site and edit templates, content, and styles the way you'd expect: locally, with real files, in your editor of choice.
5
+ SleekCMS Sync brings your CMS to your code editor — with full AI-agent support baked in. Spin up an entirely new site by describing it in plain English, or drop into any existing site and edit templates, content, and styles the way you'd expect: locally, with real files, in your editor of choice.
6
6
 
7
7
  Every save auto-syncs. Every change goes live. No build steps. No Git hooks. No infrastructure to manage.
8
8
 
9
9
  ---
10
10
 
11
- ## Why developers love it
11
+ ## How does it work
12
12
 
13
13
  - **AI generates your entire site from a description** — models, templates, layouts, styles, and content, all wired up and ready to go.
14
14
  - **AI-generated sites are plain files** — EJS templates, JSON content, CSS, JS. You own every line. Open in VS Code, Cursor, or any editor.
@@ -24,7 +24,7 @@ Every save auto-syncs. Every change goes live. No build steps. No Git hooks. No
24
24
  npx @sleekcms/sync --token <YOUR_AUTH_TOKEN>
25
25
  ```
26
26
 
27
- That's it. The CLI fetches your site, opens an editor prompt, and starts watching for changes. Grab a token from your [SleekCMS dashboard](https://app.sleekcms.com).
27
+ That's it. The CLI fetches your site, opens an editor prompt, and starts watching for changes. Grab a token from your [SleekCMS > Builder](https://app.sleekcms.com).
28
28
 
29
29
  ---
30
30
 
@@ -47,24 +47,10 @@ sleekcms --token <YOUR_AUTH_TOKEN>
47
47
 
48
48
  ## Usage
49
49
 
50
- ```bash
51
- npx @sleekcms/sync [OPTIONS]
52
- ```
53
-
54
- | Option | Alias | Description | Default |
55
- |--------------------|-------|--------------------------------------------------------|---------------|
56
- | `--token <token>` | `-t` | Your SleekCMS CLI auth token (required) | — |
57
- | `--path <path>` | `-p` | Parent directory for the local workspace | `~/.sleekcms` |
58
- | `--help` | `-h` | Show help | — |
59
-
60
- > The actual workspace folder is created at `<path>/<site-name>-<site-id>/`.
61
-
62
50
  ```bash
63
51
  # Basic
64
52
  npx @sleekcms/sync --token abc123-xxxx
65
53
 
66
- # Custom workspace folder
67
- npx @sleekcms/sync -t abc123-xxxx -p ~/Sites
68
54
  ```
69
55
 
70
56
  Once running, press `r` to re-fetch all files or `x` / `Ctrl+C` to exit.
package/dist/AGENT.md CHANGED
@@ -52,6 +52,7 @@ Examples:
52
52
 
53
53
  /content/pages/<key>.json Content for a single (non-list) page
54
54
  /content/pages/<key>/<slug>.json Content for one item of a collection page (<key> ends with +)
55
+ /content/pages/<key>/<slug>.md Same as above, but as Markdown with YAML frontmatter — used when the model qualifies (see "Markdown content files" below)
55
56
  /content/entries/<key>.json Content for a single entry (object)
56
57
  /content/entries/<key+>.json Content for a collection entry (array of objects; <key>+ matches the model filename)
57
58
 
@@ -267,7 +268,8 @@ Model fields declare both the editor type and the shape expected in content JSON
267
268
  | `datetime` | ISO 8601 string |
268
269
  | `time` | `"HH:mm"` string |
269
270
  | `image` | Either a resolved `{ "url": "...", "alt": "..." }` object, **or** a shortcut string `"<source>:<search>"` (e.g., `"pexels:doctor"`, `"url:https://picsum.photos/200.jpg"`, `"cms:logo"`). Supported sources: `unsplash`, `pexels`, `pixabay`, `iconify`, `url`, `cms` (reuses an image declared in `/images.json` by handle). Append `\|<alt text>` to set the image's alt, e.g. `"pexels:doctor\|Smiling doctor with stethoscope"`. On save, the sync engine resolves the shortcut/link to a full image object automatically. |
270
- | `video` | `{ "url": "...", "embed": "..." }` |
271
+ | `video` | Object of shape `{ "source": "BUNNY", "video_id": "...", "embed_url": "...", "title": "..." }`. **Do not write or invent this value.** Video uploads are user-driven in the CMS UI — the user uploads to Bunny.net and the sync engine stores the resolved object. In templates, always render videos with the `embed()` helper (e.g. `<%- embed(item.intro_video, { size: '1280x720' }) %>`), never by hand-rolling an iframe. |
272
+ | `file` | Object of shape `{ "name": "...", "url": "...", "size": 0, "type": "...", "ext": "..." }` describing a downloadable document (PDF, CSV, DOC/DOCX, XLS/XLSX, PPT/PPTX, TSV, TXT — max 10 MB). **Do not write or invent this value.** File uploads are user-driven in the CMS UI — the user attaches the file via the editor and the sync engine stores the resolved object. In templates, render a link with `item.file.url` (download URL on `files.sleekcms.com`) and `item.file.name` (display name), e.g. `<a href="<%= item.brochure.url %>" download><%= item.brochure.name %></a>`. |
271
273
  | `json` | Object or array |
272
274
  | `sheet` | Array of arrays |
273
275
  | `location` | `{ "markers": [{ "lat": n, "lng": n }], "img": "..." }`; `img` is a Google Maps Static Image URL that already includes the markers and can include formatting parameters such as `size`, `zoom`, `scale`, `maptype`, and styling |
@@ -290,10 +292,11 @@ Content files are JSON records under `/content/` that hold the actual values for
290
292
 
291
293
  ### File layout
292
294
 
293
- | Model shape | File path | JSON top-level |
295
+ | Model shape | File path | Top-level shape |
294
296
  |---|---|---|
295
297
  | Single page (e.g., `about`) | `content/pages/about.json` | Object |
296
298
  | Collection page (e.g., `blog+`) | `content/pages/blog+/<slug>.json` (the `+` is part of the key, not an extra suffix) | Object; one file per slug |
299
+ | Collection page with one markdown body (see below) | `content/pages/blog+/<slug>.md` | YAML frontmatter + markdown body |
297
300
  | Single entry (e.g., `header`) | `content/entries/header.json` | Object |
298
301
  | Collection entry (e.g., `authors`) | `content/entries/authors+.json` (the `+` is part of the key — same as the model filename) | Array of objects |
299
302
 
@@ -324,6 +327,69 @@ The content file at `content/pages/about.json`:
324
327
 
325
328
  Here `hero` is embedded block data stored directly in the page content file. `image` and `hero.background` use the shortcut form — on save, the sync engine replaces each shortcut with a real image object (`{ "url": "...", "alt": "..." }`). Write the object form directly when you have a specific asset URL.
326
329
 
330
+ ### Markdown content files
331
+
332
+ For a **collection page** whose model is shaped like "metadata + one long-form body", the sync engine stores each record as a **Markdown file with JSON frontmatter** (`.md`) instead of `.json`. The body is plain Markdown; the metadata is a JSON object between `---` markers.
333
+
334
+ A model qualifies when **all** of the following hold:
335
+
336
+ 1. It is a **page** model (lives in `models/pages/`).
337
+ 2. It is a **collection** (key ends with `+`, so each record is its own file).
338
+ 3. It has **exactly one** top-level field of type `markdown`.
339
+ 4. It has **no** `richtext` or `stack` fields anywhere (top-level or nested).
340
+
341
+ When the model qualifies, content lives at `content/pages/<key+>/<slug>.md`. All non-markdown fields go into the JSON frontmatter; the single `markdown` field's value becomes the body.
342
+
343
+ Example — given the model `models/pages/blog+.model`:
344
+
345
+ ```
346
+ {
347
+ title: text,
348
+ date: date,
349
+ summary: paragraph,
350
+ body: markdown
351
+ }
352
+ ```
353
+
354
+ The record `content/pages/blog+/welcome-post.md` looks like:
355
+
356
+ ```markdown
357
+ ---
358
+ {
359
+ "title": "Welcome to the blog",
360
+ "date": "2026-05-19",
361
+ "summary": "A short post about how the new blog works."
362
+ }
363
+ ---
364
+ # Hello
365
+
366
+ This is the **first** post.
367
+
368
+ - one
369
+ - two
370
+ ```
371
+
372
+ Notes:
373
+ - The body field's handle (`body` above) is whatever the single `markdown` field is called in the model — its key never appears in the frontmatter; its value is the markdown content.
374
+ - Image shortcut conventions still apply inside the markdown body (`![alt](pexels:doctor)`) and inside frontmatter image fields (`"pexels:doctor"`).
375
+ - If the model doesn't qualify (e.g., it has two markdown fields, or a `stack`), the content stays JSON at `content/pages/<key+>/<slug>.json` — no change.
376
+ - The server still accepts `.json` for qualifying models — sending `.md` is preferred but not required. The next pull will re-emit the canonical `.md` form.
377
+
378
+ **YAML frontmatter is also accepted as input.** The server detects whichever shape you write between the `---` markers and parses it correctly. The canonical form emitted by the server is JSON — on the next pull, YAML frontmatter is rewritten to JSON.
379
+
380
+ ```markdown
381
+ ---
382
+ title: Welcome to the blog
383
+ date: '2026-05-19'
384
+ summary: A short post.
385
+ ---
386
+ # Hello
387
+
388
+ This is the **first** post.
389
+ ```
390
+
391
+ JSON5 features (trailing commas, `//` or `/* */` comments, unquoted keys) are accepted in JSON frontmatter. Choose JSON for programmatic generation (and to match canonical output); YAML is fine for hand-edits.
392
+
327
393
  ### Reusable images (`/images.json`)
328
394
 
329
395
  For images used in more than one place (logos, recurring icons, hero art), declare them once in `/images.json` as a flat map of `handle → shortcut` using the same shortcut convention as `image` fields:
@@ -414,6 +480,25 @@ Page records include: `item._path`, `item._slug` (collections), `item._meta.upda
414
480
 
415
481
  `attr` can be `"WxH"` or `{ w, h, size, zoom, maptype, scale, class, style }`.
416
482
 
483
+ ### Video
484
+
485
+ `video` field content is created by the user via the CMS UI (uploading to Bunny.net). **The agent must never write a value into a `video` field, build an `<iframe>` tag by hand, or guess a `video_id` / `embed_url`.** Always render a video by passing the field object to the `embed()` helper.
486
+
487
+ | Function | Returns | Description |
488
+ |---|---|---|
489
+ | `embed(video, attr?)` | HTML string | Provider-aware `<iframe>` for the video |
490
+
491
+ `attr` is `"WxH"` or `{ w, h, size, autoplay, muted, loop, preload, t, class, style }`.
492
+
493
+ ```ejs
494
+ <%- embed(item.intro_video) %>
495
+ <%- embed(item.intro_video, '1280x720') %>
496
+ <%- embed(item.intro_video, { autoplay: true, muted: true, loop: true }) %>
497
+ <%- embed(item.intro_video, { size: '800x450', class: 'video', style: 'border-radius:8px' }) %>
498
+ ```
499
+
500
+ Most browsers block autoplay unless `muted: true` is also set. `autoplay` is added to the iframe's `allow` attribute only when the caller opts in.
501
+
417
502
  ### Head Injection
418
503
 
419
504
  Call from **any template** (page, block, entry, or layout). Deduplicated automatically.
@@ -622,5 +707,6 @@ Template:
622
707
  13. Block models cannot contain `block(...)` fields. Use groups, collections, or `entry(key)` references instead.
623
708
  14. For `image` fields in content JSON, prefer the shortcut form `"<source>:<search>"` (sources: `unsplash`, `pexels`, `pixabay`, `iconify`) — e.g., `"pexels:doctor"`. Add alt text by appending `|<alt>` to the shortcut: `"pexels:doctor|Smiling doctor with stethoscope"`. The sync engine resolves it to a full `{ url, alt }` object on save. When the same image is reused across pages (logos, shared icons, recurring art), declare it once in `/images.json` and reference it via `"cms:<handle>"`.
624
709
  15. Inside `markdown` fields, embed images with `![alt](<source>:<search>)` — same sources as image fields. Append `|<alt>` to store alt on the image record (e.g. `![doctor](pexels:doctor|Friendly family doctor)`). Including a `<W>x<H>` token in the description (e.g. `|hero shot 1200x600`) sets the rendered URL's `w` and `h` query params; otherwise the default is `600x400`. On save, refs are rewritten to actual CDN URLs and the underlying image record is created automatically.
625
- 16. Always create RSS feed for blogs and link them in meta so it is discoverable. Use "rss.xml" as the key.
626
- 17. Make the sites extremely SEO friendly and sharing friendly.
710
+ 16. **Collection pages whose model qualifies as "markdown content" page model, collection (key ends with `+`), exactly one top-level `markdown` field, no `richtext` or `stack` anywhere — are stored as `.md` files with JSON frontmatter at `content/pages/<key+>/<slug>.md` (not `.json`).** All non-markdown fields go into the JSON frontmatter object between the `---` markers; the single `markdown` field's value is the body. When creating or editing such a record, prefer `.md`; the server also accepts `.json` for the same model. Inside the `.md` file, frontmatter may be written as JSON (canonical), JSON5 (trailing commas, `//` or `/* */` comments, unquoted keys), or YAML — all three are parsed correctly on save. Pulls always re-emit JSON as the canonical form.
711
+ 17. Always create RSS feed for blogs and link them in meta so it is discoverable. Use "rss.xml" as the key.
712
+ 18. Make the sites extremely SEO friendly and sharing friendly.
package/dist/watcher.js CHANGED
@@ -19,10 +19,15 @@ const path_1 = __importDefault(require("path"));
19
19
  const chokidar_1 = __importDefault(require("chokidar"));
20
20
  const DEBOUNCE_DELAY = 5000;
21
21
  const IDLE_TIMEOUT_MS = 30 * 60 * 1000;
22
+ // Poll on a short interval and compare wall-clock time so the timeout still
23
+ // fires correctly after the system has been asleep (Node's setTimeout runs on
24
+ // a monotonic clock that pauses during sleep).
25
+ const IDLE_CHECK_INTERVAL_MS = 60 * 1000;
22
26
  let watcher = null;
23
27
  let isShuttingDown = false;
24
28
  let debounceTimer = null;
25
- let idleTimer = null;
29
+ let idleCheckTimer = null;
30
+ let lastActivityAt = 0;
26
31
  let dirty = false;
27
32
  let syncInFlight = false;
28
33
  let viewsDir = null;
@@ -34,17 +39,23 @@ function init(options) {
34
39
  onIdle = options.onIdle ?? null;
35
40
  }
36
41
  function resetIdleTimer() {
37
- if (idleTimer)
38
- clearTimeout(idleTimer);
42
+ lastActivityAt = Date.now();
39
43
  if (isShuttingDown || !onIdle)
40
44
  return;
41
- idleTimer = setTimeout(() => {
42
- idleTimer = null;
45
+ if (idleCheckTimer)
46
+ return;
47
+ idleCheckTimer = setInterval(() => {
43
48
  if (isShuttingDown)
44
49
  return;
50
+ if (Date.now() - lastActivityAt < IDLE_TIMEOUT_MS)
51
+ return;
52
+ if (idleCheckTimer) {
53
+ clearInterval(idleCheckTimer);
54
+ idleCheckTimer = null;
55
+ }
45
56
  console.log(`\n💤 No changes for ${IDLE_TIMEOUT_MS / 60000} minutes. Terminating.`);
46
57
  onIdle?.();
47
- }, IDLE_TIMEOUT_MS);
58
+ }, IDLE_CHECK_INTERVAL_MS);
48
59
  }
49
60
  function setShuttingDown(value) {
50
61
  isShuttingDown = value;
@@ -100,9 +111,9 @@ function monitorFiles() {
100
111
  resetIdleTimer();
101
112
  }
102
113
  async function stopWatching() {
103
- if (idleTimer) {
104
- clearTimeout(idleTimer);
105
- idleTimer = null;
114
+ if (idleCheckTimer) {
115
+ clearInterval(idleCheckTimer);
116
+ idleCheckTimer = null;
106
117
  }
107
118
  if (watcher) {
108
119
  await watcher.close();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleekcms/sync",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Edit SleekCMS sites locally — models, content, templates, images — with live two-way sync and AI agent support.",
5
5
  "keywords": [
6
6
  "sleekcms",