@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 +3 -17
- package/dist/AGENT.md +90 -4
- package/dist/watcher.js +20 -9
- package/package.json +1 -1
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
|
-
|
|
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
|
-
##
|
|
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
|
|
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` | `{ "
|
|
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 |
|
|
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 (``) 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 `` — same sources as image fields. Append `|<alt>` to store alt on the image record (e.g. ``). 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.
|
|
626
|
-
17.
|
|
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
|
|
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
|
-
|
|
38
|
-
clearTimeout(idleTimer);
|
|
42
|
+
lastActivityAt = Date.now();
|
|
39
43
|
if (isShuttingDown || !onIdle)
|
|
40
44
|
return;
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
},
|
|
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 (
|
|
104
|
-
|
|
105
|
-
|
|
114
|
+
if (idleCheckTimer) {
|
|
115
|
+
clearInterval(idleCheckTimer);
|
|
116
|
+
idleCheckTimer = null;
|
|
106
117
|
}
|
|
107
118
|
if (watcher) {
|
|
108
119
|
await watcher.close();
|