@sleekcms/sync 4.1.0 → 4.2.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
@@ -195,7 +195,9 @@ No manual deploy. No waiting. SleekCMS rebuilds on every sync and the preview re
195
195
  │ ├── pages/<key>/<slug>.json # Collection page content (one file per item)
196
196
  │ ├── entries/<key>.json # Single entry content
197
197
  │ ├── entries/<key>[].json # Collection entry content (array)
198
- │ └── images.json # Reusable site-level images (handle → shortcut)
198
+ │ ├── images.json # Reusable site-level images (handle → shortcut)
199
+ │ ├── videos.json # Site-level videos (handle → stock ref or upload)
200
+ │ └── files.json # The site's files, read-only (handle → file, name, url)
199
201
  │
200
202
  ├── public/
201
203
  │ ├── css/<name>.css # Stylesheets
@@ -310,6 +312,8 @@ Models describe the shape of your content. They're JSON-like files with unquoted
310
312
  | `getPages(path, opts?)` | Pages where path starts with prefix; pass `{ collection: true }` for collection pages only |
311
313
  | `getEntry(handle)` | Entry by handle |
312
314
  | `getImage(handle)` | Site-level image by handle (from `images.json`) |
315
+ | `getVideo(handle)` | Site-level video by handle (from `videos.json`); render it with `embed(video, attr?)` |
316
+ | `getFile(handle)` | Site file by handle (from `files.json`): `{ name, url, size, type, ext }`, like a `file` field |
313
317
  | `getSlugs(path)` | Slugs under a collection path |
314
318
  | `path(page)` | Relative URL path of a page (e.g. `/blog/my-post`) |
315
319
  | `url(pathOrPage?)` | Site origin, or a full absolute URL when given a path string or page object |
@@ -474,6 +478,23 @@ For images used in more than one place — logos, recurring icons, shared hero a
474
478
 
475
479
  Then reference any of them from any `image` field with `"cms:<handle>"` (e.g. `"cms:logo"`). Templates can also fetch the resolved object directly via `getImage('logo')`.
476
480
 
481
+ ### Files (`src/content/files.json`)
482
+
483
+ The site's files (Media > Files) are listed read-only as `handle → { file, name, url }`. Every sync refreshes the list from the server, and local edits to it are never pushed. Upload new files in the CMS.
484
+
485
+ ```json
486
+ {
487
+ "lunch-menu": { "file": "lunch-menu.pdf", "name": "Lunch Menu", "url": "https://files.sleekcms.com/…/lunch-menu.pdf" }
488
+ }
489
+ ```
490
+
491
+ A `file` field takes a listed handle or file name (`"lunch-menu"` or `"lunch-menu.pdf"`). Templates can fetch any file with `getFile('lunch-menu')`:
492
+
493
+ ```ejs
494
+ <% const menu = getFile('lunch-menu') %>
495
+ <% if (menu) { %><a href="<%= menu.url %>" download><%= menu.name %></a><% } %>
496
+ ```
497
+
477
498
  ### Images in markdown
478
499
 
479
500
  Inside `markdown` fields, the same shortcuts work in standard image syntax. Append `|<alt>` for alt text and a `<width>x<height>` token to set the rendered dimensions (defaults to `600x400`):
package/dist/AGENTS.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # SleekCMS Site Builder — Agent Guide
2
2
 
3
+ > **The user's request states intent; this guide states the platform.** A request (or a site the user points you at) may describe a feature loosely or in its own words. Build the *intent* with the documented CMS convention every time: a form for scheduling a time is a booking form (§11), a store follows §13, a blog gets the `rss.xml` feed (§12), and images go through the image pipeline (§7). The request guides content, copy and design: follow it by default, and improve on it where there's a clear opportunity. This guide decides how features are wired. The goal is always a fully working site built on CMS features.
4
+
3
5
  ## 1. Mental Model
4
6
 
5
7
  SleekCMS is a headless CMS plus static site generator. There are no servers, no git repos, nothing to compile locally. You build a site by editing three kinds of files, which sync automatically to the platform:
@@ -51,6 +53,8 @@ Pages and entries are either **single** (one record) or a **collection** (many r
51
53
 
52
54
  Never guess at why something isn't rendering — read `sync-errors.log` first, then run `node build.mjs` and read its output.
53
55
 
56
+ A passing build proves the site compiles, not that its interactions work. For anything interactive (menus, tabs, carousels, toggles, forms), check the markup, handlers, responsive variants, active states and keyboard behaviour against the request. In your summary to the user, keep what you reviewed in code apart from what you actually tested in a browser.
57
+
54
58
  ## 3. File Naming Rules (applies to models, views, and content)
55
59
 
56
60
  1. Keys are **lowercase**, words separated by `-` (dash). Example: `getting-started`.
@@ -69,10 +73,11 @@ Never guess at why something isn't rendering — read `sync-errors.log` first, t
69
73
  | `docs_getting-started` | `/docs/getting-started` |
70
74
  | `docs_guides+` | `/docs/guides/<slug>` |
71
75
  | `rss.xml` | `/rss.xml` (no layout) |
76
+ | `redirect.txt` | `/redirect.txt` (no layout) — redirect rules, see §5 |
72
77
 
73
78
  For collection pages, the `<slug>` is the content file's name: `content/pages/blog+/my-post.json` → `/blog/my-post`. Renaming the file renames the URL.
74
79
 
75
- **Renaming.** Rename a page, entry, or block by `mv`-ing only its model file — a collection too (`blog+.model`, not its folder). The sync renames it on the server and moves its view, content, and records to match: `models/pages/about.model` → `about-us.model` moves `/about` to `/about-us`. A collection record is the exception: its content file name is its slug, so to change one record's URL, rename that file within its folder — `content/pages/blog+/old-post.json` → `new-post.json` moves just that record from `/blog/old-post` to `/blog/new-post`. Keep slugs lowercase and dash-separated, and keep the file in its collection's folder. The kind and `+` can't change. Update hard-coded links and `getEntry()` calls that use the old name.
80
+ **Renaming.** Rename a page, entry, or block by `mv`-ing only its model file — a collection too (`blog+.model`, not its folder). The sync renames it on the server and moves its view, content, and records to match: `models/pages/about.model` → `about-us.model` moves `/about` to `/about-us`. A collection record is the exception: its content file name is its slug, so to change one record's URL, rename that file within its folder — `content/pages/blog+/old-post.json` → `new-post.json` moves just that record from `/blog/old-post` to `/blog/new-post`. Keep slugs lowercase and dash-separated, and keep the file in its collection's folder. The kind and `+` can't change. Update hard-coded links and `getEntry()` calls that use the old name, and add a redirect for a page URL that visitors or search engines may already know (§5).
76
81
 
77
82
  ## 4. Folder Structure (under `src/`)
78
83
 
@@ -97,6 +102,7 @@ content/entries/<key>.json Content for a single entry (object)
97
102
  content/entries/<key>+.json Collection entry — ONE file holding a JSON array of all items
98
103
  content/images.json Site-level reusable images: handle → shortcut (§7)
99
104
  content/videos.json Site-level videos: handle → pexels:/pixabay: ref (user uploads show as embed_url) (§7)
105
+ content/files.json The site's files (Media > Files), read-only: handle → { file, name, url } (§7)
100
106
 
101
107
  setup.json Site setup (JSON, comments allowed): default page title, Tailwind on/off, sitemap on/off, cookie consent on/off, analytics tag ids — edit values in place, never delete
102
108
  ```
@@ -110,13 +116,13 @@ setup.json Site setup (JSON, comments allowed): default pag
110
116
  - **Public URL** (what `link()`/`script()` and HTML `href`/`src` take): `/css/styles.css`, `/js/app.js`.
111
117
  - **Page route** (what visitors see): `/about`, `/blog/<slug>`. Computed from the page key.
112
118
 
113
- **Site setup (`setup.json`)** — the set-once site decisions, mirrored from Settings > Website in the CMS. Each key is explained by the comment above it in the file:
119
+ **Site setup (`setup.json`)** — the set-once site decisions, the same values as Manage Site in the CMS. Each key is explained by the comment above it in the file:
114
120
 
115
121
  | Key | Value | Effect |
116
122
  |---|---|---|
117
- | `title` | string or `null` | Default `<title>` for pages that don't set their own (via `title()` or the SEO block). `null` = the site name. |
123
+ | `title` | string or `null` | Default `<title>` for pages that don't set their own (via `title()` or the SEO block). `null` = the site name. Max 255 characters. |
118
124
  | `tw_enabled` | boolean | `true` compiles `css/tailwind.css` and injects it into every page — required for any site styled with Tailwind utilities. |
119
- | `sitemap_enabled` | boolean | `true` publishes `sitemap.xml` + `robots.txt` on every build. Turn it on for public sites and SEO work. |
125
+ | `sitemap_enabled` | boolean | `true` publishes `sitemap.xml` + `robots.txt` on every build. Turn it on for public sites and SEO work. Which pages are *excluded* is a CMS UI setting (Manage Site > Search Visibility), never this file. |
120
126
  | `cookie_consent` | boolean | `true` injects a consent banner into every page and holds the analytics tags, plus any script or iframe tagged `data-sleekcms-consent`, until the visitor accepts its category. A toggle only — banner text, position and styling are authored in HTML (§14). |
121
127
  | `chat_enabled` | boolean | `true` adds the chat bot to every published page; it answers visitor questions from the site's published pages (re-read on every publish). A toggle only — the welcome message, knowledge files and instructions are managed in the CMS. `false` removes it from the next publish and stops open chats right away. |
122
128
  | `analytics` | object | Marketing/analytics tag ids injected into every published page. |
@@ -142,7 +148,7 @@ analytics: {
142
148
 
143
149
  Omit a key (or the whole `analytics` object) to leave that integration alone; write `analytics: {}` to clear every one. Unrecognized keys and unusable values are dropped with a warning, and a malformed id is skipped at build time rather than breaking the build. Put tag ids here rather than pasting vendor snippets into a layout — the build injects the right script for each id, and with `cookie_consent: true` it gates them behind the banner automatically (§14).
144
150
 
145
- Edit values in place and keep the keys. Keys you omit keep their stored values; unknown keys are ignored. Which pages are excluded from the sitemap is a CMS UI setting, not a key in this file. Never delete the file.
151
+ Edit values in place and keep the keys. Keys you omit keep their stored values. An unknown key is dropped with a warning rather than failing the save, but warnings don't reach `sync-errors.log`, so match key names to the comments in the file. Never delete the file.
146
152
 
147
153
  ## 5. Common Tasks
148
154
 
@@ -173,6 +179,34 @@ Edit the matching file under `content/` — **do not** hard-code editable copy i
173
179
 
174
180
  All fields are optional — a missing value simply renders as `undefined`, so guard with `<% if (item.field) { %>` where it matters.
175
181
 
182
+ ### Redirect old URLs
183
+
184
+ When a page's URL changes (a rename, a restructure, a rebuild that retires old paths), keep inbound links working with a `redirect.txt` page. Like `rss.xml`, its key has an extension, so it is emitted raw with no layout (§3). Each line is one rule, `from to [status]` (`/old-about /about 301`). `from` must match the path exactly, and rules apply before a 404.
185
+
186
+ **`models/pages/redirect.txt.model`**
187
+ ```
188
+ {
189
+ redirects: [
190
+ {
191
+ from: text,
192
+ to: link,
193
+ status: text
194
+ }
195
+ ]
196
+ }
197
+ ```
198
+
199
+ **`content/pages/redirect.txt.json`**
200
+ ```json
201
+ { "redirects": [{ "from": "/old-about", "to": "/about", "status": "301" }] }
202
+ ```
203
+
204
+ **`pages/redirect.txt.ejs`** — one rule per line:
205
+ ```ejs
206
+ <% (item.redirects || []).forEach(function (r) { %><%= r.from %> <%= r.to %> <%= r.status || 301 %>
207
+ <% }) %>
208
+ ```
209
+
176
210
  ## 6. Models
177
211
 
178
212
  ### `.model` file format
@@ -203,6 +237,7 @@ Like JSON but with **no quotes**; each value is a field type name:
203
237
  | Model type | Content JSON value (what templates receive) |
204
238
  |---|---|
205
239
  | `text`, `paragraph`, `code`, `color`, `link` | String |
240
+ | `guid` | String — a unique id (e.g. order numbers, external ids). The CMS editor pre-fills a new GUID for each record created in the UI. When you write content, give each record its own unique string, e.g. a UUID |
206
241
  | `richtext` | String of **HTML** — output with `<%- %>` |
207
242
  | `markdown` | String of **raw markdown** — convert with `<%- remark(item.x) %>`, which renders GFM (tables, task lists, footnotes), syntax-highlighted code fences, math (`$…$`, `$$…$$`) and ```` ```mermaid ```` diagrams as SVG (see Helpers — rendering) |
208
243
  | `number` | Number |
@@ -214,7 +249,7 @@ Like JSON but with **no quotes**; each value is a field type name:
214
249
  | `image` | Shortcut string (preferred when writing) or resolved `{ "url", "alt" }` object — see §7. Shortcuts only resolve in `image`/`icon` fields — in a `text` field the string stays literal text |
215
250
  | `icon` | Svg icon. Use for icons the user is expected to edit/swap in the CMS (e.g. features/services). Fill with placeholder `"iconify:<prefix:name or search>"` (or `"cms:<handle>"` pointing at an svg); render with `svg(item.icon)`. Fixed UI icons don't need fields — call `svg('iconify:...')` directly in templates |
216
251
  | `video` | Ref string (preferred when writing) or the resolved object you read back. Write `"cms:<handle>"` to reuse a video from `content/videos.json` (or a user upload by its handle), or `"pexels:<search>\|<title>"` / `"pixabay:<search>\|<title>"` for a one-off — resolved server-side (see §7). **Never invent** `video_id`/`embed_url` objects; render with `embed()` only |
217
- | `file` | Write a plain file name, e.g. `"brochure.pdf"` (pdf, docx, xlsx, pptx, csv, tsv, txt, md — no path, no URL, never an object). On save the CMS attaches the site file with that name, or creates a small placeholder file the user replaces in the CMS UI; reads back as the file name. Render a link with `item.x.url` / `item.x.name` |
252
+ | `file` | Write a plain file name, e.g. `"brochure.pdf"` (pdf, docx, xlsx, pptx, csv, tsv, txt, md — no path, no URL, never an object). A listed file's handle from `content/files.json` works too, and wins over a file with that name. On save the CMS attaches that file, or (for a file name nothing matches) creates a small placeholder file the user replaces in the CMS UI; reads back as the file name. Render a link with `item.x.url` / `item.x.name` |
218
253
  | `json` | Any JSON |
219
254
  | `sheet` | Array of arrays |
220
255
  | `location` | `{ "markers": [{ "lat", "lng" }], "img": "<google static map url>" }` — `img` supports Google Static Maps params; markers are already part of the URL |
@@ -305,6 +340,10 @@ Give **every** image field a resolvable shortcut from the sources above — incl
305
340
 
306
341
  then reference with `"cms:logo"` in any `image`/`icon` field, or render directly in templates — `svg('cms:logo', { class: 'h-8 w-auto' })` for SVGs/icons, `img('cms:hero', '1920x600')` for photos. (`getImage('logo')` returns the resolved image object for a bare handle — rarely needed; the helpers take the value or `cms:` ref directly.)
307
342
 
343
+ Each handle's value is an image source from the list above, never another handle: `"logo": "cms:new-mark"` is ignored. To change what a handle shows, change its own value; every `cms:<handle>` on the site follows.
344
+
345
+ **The user's own images:** you can't upload a file from the workspace. A local path is not an image source, and `url:` needs a public URL. When the user wants their own photo or logo on the site, use `url:<public url>` if they give you one. Otherwise ask them to upload it in the CMS under a handle. After the workspace re-fetches from the server, the upload appears in `content/images.json` as `"<handle>": "url:<path>"`. Leave that value as it is and reference the image as `cms:<handle>`. Until then, ship a stock placeholder in the slot as usual.
346
+
308
347
  **Images inside markdown:** use the same shortcuts in standard syntax: `![alt](pexels:doctor)`. Append `|<alt>` for alt text and an optional `<W>x<H>` token to set rendered size (default `600x400`): `![doctor](pexels:doctor|Friendly doctor 800x600)`. Refs are rewritten to CDN URLs on save.
309
348
 
310
349
  ### Videos
@@ -321,6 +360,25 @@ Render in templates: `<%- embed(getVideo('hero-loop'), { size: '1280x720' }) %>`
321
360
 
322
361
  Content `video` fields accept the same refs: `"cms:<handle>"` reuses a declared (or user-uploaded) video by handle — preferred when the same video appears in more than one place — while a direct `"pexels:<search>|<title>"` / `"pixabay:<search>|<title>"` creates a record-local video. Stock videos read back as refs; user-uploaded videos read back as objects — echo those untouched.
323
362
 
363
+ ### Files
364
+
365
+ `content/files.json` lists the site's files (Media > Files) as `handle → { file, name, url }`: `file` is the file name, `name` its display name, `url` its public download link.
366
+
367
+ ```json
368
+ { "lunch-menu": { "file": "lunch-menu.pdf", "name": "Lunch Menu", "url": "https://files.sleekcms.com/…/lunch-menu.pdf" } }
369
+ ```
370
+
371
+ It's read-only: edits to it are never saved, and every sync replaces it with the site's current list. Check it before writing a new file name.
372
+
373
+ A `file` field takes a listed handle (`"lunch-menu"`) or file name (`"lunch-menu.pdf"`). Render a file no field points at with `getFile(handle)`, which returns the same `{ name, url, size, type, ext }` as a `file` field (or `undefined` for an unknown handle):
374
+
375
+ ```ejs
376
+ <% const menu = getFile('lunch-menu') %>
377
+ <% if (menu) { %><a href="<%= menu.url %>" download><%= menu.name %></a><% } %>
378
+ ```
379
+
380
+ **The user's own documents:** you can't upload a file from the workspace. Ask the user to upload it in the CMS (Media > Files); it appears in `content/files.json` after the next sync. Until then, a `file` field with a new file name gets a placeholder the user replaces in the CMS.
381
+
324
382
  ### Markdown content files (`.md`)
325
383
 
326
384
  A collection-page record is stored as Markdown with frontmatter instead of JSON when its model meets **all** of:
@@ -385,6 +443,8 @@ There is exactly **one** layout: `layouts/common.ejs`. It wraps every page autom
385
443
  </html>
386
444
  ```
387
445
 
446
+ **Views render standalone.** Every view file is compiled and rendered on its own: one view can't include another file. Only two things carry one template's output into another. `render(val)` renders blocks and entries, each through its own view. `main` is the current page, already rendered, and is usable only in the layout. Neither pulls in another page view. Write each page view fully self-contained. To reuse markup across pages, move the shared piece into an entry (`render(getEntry('cta'))`) or a block, or simply repeat the markup.
447
+
388
448
  ### Helpers — content access
389
449
 
390
450
  | Function | Returns | Notes |
@@ -394,7 +454,8 @@ There is exactly **one** layout: `layouts/common.ejs`. It wraps every page autom
394
454
  | `getEntry(handle)` | Object \| Array | Entry by key. The `+` is optional — `getEntry('authors')` and `getEntry('authors+')` are equivalent. Single → object, collection → array |
395
455
  | `getSlugs(path)` | string[] | Slugs under a collection path |
396
456
  | `getImage(handle)` | Object \| undefined | Resolved image from `images.json` (bare handle, no `cms:` prefix) |
397
- | `getVideo(handle)` | Object \| undefined | Video from `videos.json` |
457
+ | `getVideo(handle)` | Object \| undefined | Video from `videos.json` as `{ title, source, video_id, embed_url }` — render with `embed()` |
458
+ | `getFile(handle)` | Object \| undefined | File from `files.json` as `{ name, url, size, type, ext }`, like a `file` field |
398
459
  | `getContent(query?)` | Any | Full content payload, optionally filtered with JMESPath |
399
460
  | `path(page)` | String | URL path of a page object |
400
461
  | `url(pathOrPage?)` | String | Site origin; pass a path or page object for a full absolute URL |
@@ -432,7 +493,9 @@ Render `icon` fields or direct icon shortcuts with `svg()` — it inlines the sv
432
493
 
433
494
  > **Direct UI Icons vs. CMS-Editable Icons:**
434
495
  > - **Static UI icons** (chevrons, arrows, badges, buttons, decorative bullet checks): render directly in `.ejs` with `svg('iconify:<family>:<name>', { class: '...' })` — no model field or `images.json` entry required.
435
- > - **User-editable icons** (site logo, service/feature icons): when the user is expected to customize/swap the icon in the CMS, expose it via `content/images.json` (for site handles like `"logo"`) or an `icon: icon` model field, providing a placeholder `"iconify:..."` in content so the site ships visually complete. Render with `svg('cms:logo')` or `svg(item.icon)`.
496
+ > - **User-editable icons** (site logo, service/feature icons): when the user is expected to customize/swap the icon in the CMS, expose it via `content/images.json` (for site handles like `"logo"`) or an `icon: icon` model field, providing a placeholder `"iconify:..."` in content so the site ships visually complete. Render with `svg('cms:logo')` or `svg(item.icon)`. The inline SVG takes the CSS text color, including after the user swaps it in the CMS.
497
+ > - **Logo and favicon:** default to a business-appropriate Iconify icon (`"logo": "iconify:…"` in `content/images.json`) and keep it CMS-editable so the user can replace it later.
498
+ > - **User-uploaded images, logos included,** may be raster images or SVGs. Render them with `img()` and give them `image` model fields. Reserve `svg()` and `icon` fields for assets known to be SVG.
436
499
 
437
500
  ### Helpers — video
438
501
 
@@ -462,7 +525,9 @@ Animation needs no `script()` call at all — `data-motion` attributes are built
462
525
  </section>
463
526
  ```
464
527
 
465
- **CSS/JS rules:** include assets only via `link()` / `script()` — never raw `<link>`/`<script>` tags in views. Exception: creating `css/tailwind.css` enables Tailwind v4 — the file is the Tailwind *input CSS*, compiled against the rendered HTML and injected automatically; never `link()` it. Whenever you write it, keep `@import "tailwindcss";` as its first statement (plus the two `@plugin` lines: `@plugin "@tailwindcss/typography";` and `@plugin "@tailwindcss/forms";`) — dropping the import silently removes all Tailwind styling. Define the site's fonts/colors/radii as `@theme` tokens there (giving utilities like `font-display` / `bg-brand-600`) with a `/* DESIGN */` header comment recording the design direction. v4, not v3: custom utilities go in `@utility name { … }`, never `@layer utilities { .name { … } }` (a v3 pattern that yields "Cannot apply unknown utility class"); config is CSS-first — there is no `tailwind.config.js`. Default to a polished Tailwind design with a deliberate design direction.
528
+ **CSS/JS rules:** include assets only via `link()` / `script()` — never raw `<link>`/`<script>` tags in views. Exception: creating `css/tailwind.css` enables Tailwind v4 — the file is the Tailwind *input CSS*, compiled against the rendered HTML and injected automatically; never `link()` it. Whenever you write it, keep `@import "tailwindcss";` as its first statement (plus the two `@plugin` lines: `@plugin "@tailwindcss/typography";` and `@plugin "@tailwindcss/forms";`) — dropping the import silently removes all Tailwind styling. Define the site's fonts/colors/radii as `@theme` tokens there (giving utilities like `font-display` / `bg-brand-600`) with a `/* DESIGN */` header comment recording the design direction. v4, not v3: custom utilities go in `@utility name { … }`, never `@layer utilities { .name { … } }` (a v3 pattern that yields "Cannot apply unknown utility class"). `@utility` makes them composable with variants and available to `@apply`. Config is CSS-first: there is no `tailwind.config.js`.
529
+
530
+ **Style every page with Tailwind v4 utility classes in the view's `class` attribute**, built from those `@theme` tokens. Layout, spacing, color, type, borders, shadows, gradients, hover/focus states and responsive behaviour are all utilities, so `tailwind.css` stays the site's one stylesheet and the whole design re-themes from it. Keyframes, decorative pseudo-element layers and repeated multi-property effects live in `tailwind.css` too (`@theme --animate-*`, `@layer base`, `@utility`). Default to a polished Tailwind design with a deliberate design direction.
466
531
 
467
532
  ## 9. SEO
468
533
 
@@ -486,7 +551,7 @@ Make every site strongly SEO- and sharing-friendly. The standard pattern is a re
486
551
 
487
552
  Add `seo: block(seo)` to page models and render it **once from the layout head**: `<% if (item.seo) { %><%- render(item.seo) %><% } %>` in `layouts/common.ejs` (as in the layout example above) — not per page view.
488
553
 
489
- Three site-level SEO settings live in `setup.json` (§4), not in views: `sitemap_enabled: true` publishes `sitemap.xml` + `robots.txt` on every build; `title` is the site's default `<title>` for pages whose SEO block leaves the title empty (`null` = the site name); and a Google Search Console verification token goes in `analytics.gsc`. Sitemap page exclusions are chosen in the CMS UI — there is no file setting for them.
554
+ Three site-level SEO settings live in `setup.json` (§4), not in views: `sitemap_enabled: true` publishes `sitemap.xml` + `robots.txt` on every build; `title` is the site's default `<title>` for pages whose SEO block leaves the title empty (`null` = the site name); and a Google Search Console verification token goes in `analytics.gsc`. Sitemap page exclusions are chosen in the CMS UI (Manage Site > Search Visibility) — there is no file setting for them.
490
555
 
491
556
  ## 10. Forms
492
557
 
@@ -601,7 +666,7 @@ Snipcart powers stores. Use it when the business **sells products** and the user
601
666
 
602
667
  Use this when the user wants a **cookie banner** — GDPR / ePrivacy consent for tracking scripts and third-party embeds. One switch turns it on; everything else is HTML. Tag each script or iframe that needs consent with `data-sleekcms-consent="<category>"`, and at build time SleekCMS ships it inert, injects a consent banner and runtime into every page, and activates it only after the visitor accepts that category. Choices are stored in a first-party cookie and honoured on every return visit. **Never add a banner speculatively** — only when the user asks, or the site clearly needs one.
603
668
 
604
- **Turn it on** — `cookie_consent: true` in `setup.json` (§4), the same switch as Settings > Website > Analytics in the CMS. Off (the default): nothing is injected and tagged scripts run as normal.
669
+ **Turn it on** — `cookie_consent: true` in `setup.json` (§4), the same switch as Manage Site > Tracking & Analytics in the CMS. Off (the default): nothing is injected and tagged scripts run as normal.
605
670
 
606
671
  **Tag scripts via `script()`** — views never contain raw `<script>` tags, so the category goes in the attributes object:
607
672
 
@@ -863,7 +928,7 @@ if (!matchMedia('(prefers-reduced-motion: reduce)').matches) {
863
928
  4. `file` field values are plain file names ending in a supported extension (`"brochure.pdf"`) — the CMS attaches or placeholder-creates the file server-side; never write objects or URLs.
864
929
  5. Never invent `video_id`/`embed_url` objects — render videos with `embed()` only. Videos are set via refs: `content/videos.json` handles for shared videos, and `cms:<handle>` / provider refs in `video` fields (§7); values that read back as embed URLs or objects are user uploads — leave them untouched.
865
930
  6. `richtext` → `<%- item.x %>`. `markdown` → `<%- remark(item.x) %>`. Never load highlight.js, KaTeX or Mermaid yourself.
866
- 7. CSS/JS via `link()`/`script()` only; `css/tailwind.css` is auto-injected — never `link()` it. When writing tailwind.css, `@import "tailwindcss";` must stay as its first statement — dropping it silently removes all Tailwind styling.
931
+ 7. CSS/JS via `link()`/`script()` only; `css/tailwind.css` is auto-injected — never `link()` it. When writing tailwind.css, `@import "tailwindcss";` must stay as its first statement — dropping it silently removes all Tailwind styling. Style views with Tailwind v4 utilities in `class`, built from the `@theme` tokens; `tailwind.css` is the site's one stylesheet and also holds keyframes, `@layer base` and `@utility` effects.
867
932
  8. Editable copy belongs in `content/`, not hard-coded in `.ejs`. Fixed micro-labels may stay in views.
868
933
  9. In page keys, `-` separates words and `_` means `/`. `_index` is the home page only; a section index page uses the plain section key.
869
934
  10. After edits, check `sync-errors.log` in the workspace root. New line = failure for that file; line removed = fixed.
@@ -871,6 +936,8 @@ if (!matchMedia('(prefers-reduced-motion: reduce)').matches) {
871
936
  12. Image shortcuts (`"iconify:..."`, `"pexels:..."`, etc.) resolve **only** in `image`/`icon`-typed fields. CMS-editable icons are declared `icon: icon`, never `icon: text` — render with `svg(item.icon)`; fixed UI icons need no field: `svg('iconify:...')` directly in the view.
872
937
  13. Use entries (`entry(key+)`) for shared content (authors, testimonials, nav, footer) — not blocks. Block models cannot contain `block(...)` or `stack` fields; use blocks only when a group of fields also benefits from a reusable view.
873
938
  14. Always create an RSS feed for blogs (`rss.xml` key) and make every site strongly SEO- and sharing-friendly (reusable `seo` block rendered once from the layout head).
874
- 15. Site-wide setup — default page title, Tailwind on/off, sitemap on/off, cookie consent on/off, analytics tag ids — lives in `setup.json` (§4). Change values there rather than hard-coding a site title into the layout, and never delete the file. Sitemap page exclusions are UI-only.
939
+ 15. Site-wide setup — default page title, Tailwind on/off, sitemap on/off, cookie consent on/off, analytics tag ids — lives in `setup.json` (§4). Change values there rather than hard-coding a site title into the layout, and never delete the file. Sitemap page exclusions are UI-only (Manage Site > Search Visibility).
875
940
  16. Cookie consent (§14) is a `setup.json` switch plus `data-sleekcms-consent` attributes — tag third-party scripts through `script({ …, 'data-sleekcms-consent': 'category' })`, never a raw tag, and add a footer "Cookie settings" link. Only when the user asks for a banner.
876
941
  17. Motion (§15): `data-motion` attributes are built in (no `script()` call) — reveals, stagger, split text, counters, parallax, scroll effects, marquees; `script('motion')` only for your own Motion code; never two motion libraries on a page; reduced motion is handled for you.
942
+ 18. Redirects (§5): when a page URL changes or a rebuild retires old paths, add `from to [status]` rules to the `redirect.txt` page so inbound links keep working.
943
+ 19. Views render standalone — there's no including one view in another. Reuse markup through `render()` on a block or entry, or repeat it.
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import{readFileSync as Jt}from"node:fs";import{fileURLToPath as Ar}from"node:url";import Ge from"node:path";import{render as _r}from"ink";import{execSync as nt,spawn as B}from"child_process";import{program as Ze,Option as Qe}from"commander";function rt(){return Ze.name("cms-sync").description("SleekCMS CLI tool to sync and edit CMS templates locally. Add each site you want to sync by approving it in your browser \u2014 changes are watched and pushed automatically.").addOption(new Qe("-v, --version","output the version number").hideHelp()).addOption(new Qe("-e, --env <env>","Environment (localhost, development, production)").default("production").hideHelp()).option("-p, --path <path>","Directory path for files (default: ~/SleekCMS)").addHelpText("after",`
2
+ import{readFileSync as qt}from"node:fs";import{fileURLToPath as Or}from"node:url";import Ue from"node:path";import{render as Ir}from"ink";import{execSync as rt,spawn as B}from"child_process";import{program as Qe,Option as et}from"commander";function st(){return Qe.name("cms-sync").description("SleekCMS CLI tool to sync and edit CMS templates locally. Add each site you want to sync by approving it in your browser \u2014 changes are watched and pushed automatically.").addOption(new et("-v, --version","output the version number").hideHelp()).addOption(new et("-e, --env <env>","Environment (localhost, development, production)").default("production").hideHelp()).option("-p, --path <path>","Directory path for files (default: ~/SleekCMS)").addHelpText("after",`
3
3
  Examples:
4
4
  $ sleekcms # add sites in your browser, pick which to sync
5
5
  $ sleekcms -e development # use a non-production backend
6
- `).parse(process.argv),Ze.opts()}function R(e){let t=process.platform==="win32"?`where ${e}`:`which ${e}`;try{return nt(t,{stdio:"ignore"}),!0}catch{return!1}}function st(e){if(process.platform!=="darwin")return!1;try{return nt(`osascript -e 'id of application "${e}"'`,{stdio:"ignore"}),!0}catch{return!1}}function an(e,t){let n={detached:!0,stdio:"ignore",windowsHide:!0};process.platform==="win32"?B("cmd.exe",["/c",e,...t],n).unref():B(e,t,n).unref()}var et=[{name:"VS Code",cmd:"code",args:e=>["-n",e]},{name:"Cursor",cmd:"cursor",args:e=>["-n",e]}];function tt(e,t){let r=(t?et.filter(s=>s.cmd===t):et).find(s=>R(s.cmd));return r?(an(r.cmd,r.args(e)),r.name):null}function cn(e){if(process.platform==="darwin")return B("open",["-a","Terminal",e],{detached:!0,stdio:"ignore"}).unref(),!0;if(process.platform==="win32"){if(R("wt"))B("wt.exe",["-d",e],{detached:!0,stdio:"ignore"}).unref();else{let t=R("powershell")?"powershell.exe":"cmd.exe";B("cmd.exe",["/c","start","",t],{cwd:e,detached:!0,stdio:"ignore"}).unref()}return!0}return!1}function ln(e){return st("iTerm")?(B("open",["-a","iTerm",e],{detached:!0,stdio:"ignore"}).unref(),!0):!1}function un(e){return`'${e.replace(/'/g,"'\\''")}'`}var dn=[{cmd:"x-terminal-emulator",args:e=>["-e","bash","-c",e]},{cmd:"gnome-terminal",args:e=>["--","bash","-c",e]},{cmd:"konsole",args:e=>["-e","bash","-c",e]},{cmd:"xfce4-terminal",args:e=>["-x","bash","-c",e]},{cmd:"kitty",args:e=>["bash","-c",e]},{cmd:"alacritty",args:e=>["-e","bash","-c",e]},{cmd:"xterm",args:e=>["-e","bash","-c",e]}];function it(e,t){if(!R(t))return!1;if(process.platform==="darwin"){let r=`tell application "Terminal" to do script ("cd " & quoted form of "${e}" & " && ${t}")`;return B("osascript",["-e",r,"-e",'tell application "Terminal" to activate'],{detached:!0,stdio:"ignore"}).unref(),!0}if(process.platform==="win32")return R("wt")?B("wt.exe",["-d",e,"cmd","/k",t],{detached:!0,stdio:"ignore"}).unref():B("cmd.exe",["/c","start","","cmd","/k",t],{cwd:e,detached:!0,stdio:"ignore"}).unref(),!0;let n=`cd ${un(e)} && ${t}; exec bash`;for(let r of dn)if(R(r.cmd))try{return B(r.cmd,r.args(n),{cwd:e,detached:!0,stdio:"ignore"}).unref(),!0}catch{}return!1}function gn(e){return it(e,"claude")}function pn(e){return it(e,"pi")}function ue(e){if(!e)return!1;try{let t=process.platform==="darwin"?B("open",[e],{detached:!0,stdio:"ignore"}):process.platform==="win32"?B("cmd.exe",["/c","start","",e],{detached:!0,stdio:"ignore",windowsHide:!0}):B("xdg-open",[e],{detached:!0,stdio:"ignore"});return t.on("error",()=>{}),t.unref(),!0}catch{return!1}}function ot(){let e=process.platform==="darwin",t=process.platform==="win32",n=[];return(e||t)&&n.push({key:"terminal",label:"terminal",run:cn}),st("iTerm")&&n.push({key:"iterm",label:"iterm",run:ln}),R("code")&&n.push({key:"code",label:"vscode",run:r=>tt(r,"code")!==null}),R("cursor")&&n.push({key:"cursor",label:"cursor",run:r=>tt(r,"cursor")!==null}),R("claude")&&n.push({key:"claude",label:"claude",run:gn}),R("pi")&&n.push({key:"pi",label:"pi",run:pn}),n}import{EventEmitter as Zn}from"node:events";import{spawn as Qn}from"node:child_process";import te from"fs-extra";import Ae from"path";import er from"chokidar";import{createHash as hn}from"crypto";import h from"fs-extra";import mn from"os";import y from"path";var at={localhost:"http://app.sleekcms.test/api/mcp",development:"https://app.sleekcms.dev/api/mcp",production:"https://app.sleekcms.com/api/mcp"},wn=["src/views/blocks","src/views/entries","src/views/pages","src/views/layouts","src/models/blocks","src/models/entries","src/models/pages","src/content/pages","src/content/entries","src/public/js","src/public/css"],H=class extends Error{status;body;constructor(t,n,r){super(t),this.status=n,this.body=r}};async function de(e,t,n,r,s){let i=await fetch(e+r,{method:n,headers:{Authorization:`Bearer ${t}`,...s!==void 0?{"Content-Type":"application/json"}:{}},body:s!==void 0?JSON.stringify(s):void 0});if(!i.ok){let d=await i.text();throw new H(`${n} ${r} \u2192 ${i.status}: ${d}`,i.status,d)}return i.json()}var yn=["AGENT.md","CLAUDE.md",y.join(".vscode","copilot-instructions.md")],bn="mrmlnc.vscode-json5";async function vn(e,t){t&&(await h.outputFile(y.join(e,"AGENTS.md"),t),await Promise.all(yn.map(n=>h.remove(y.join(e,n))))),await h.outputFile(y.join(e,".vscode","settings.json"),JSON.stringify({"files.associations":{"*.model":"javascript","**/src/**/*.json":"json5"},"[javascript]":{"editor.defaultFormatter":"esbenp.prettier-vscode"},"js/ts.validate.enabled":!1},null,2)),await h.outputFile(y.join(e,".vscode","extensions.json"),JSON.stringify({recommendations:[bn]},null,2))}async function Sn(e,t){if(!t)return;let n=y.join(e,"build.mjs"),r=t.match(/build\.mjs v\d+/)?.[0];if(await h.pathExists(n)){let s=(await h.readFile(n,"utf-8")).match(/build\.mjs v\d+/)?.[0];if(s&&s===r)return}await h.outputFile(n,t)}async function xn(e,t,n,r){let s=y.join(e,".cache","site.json"),i=await h.readJson(s).catch(()=>null);i&&(i.env!==n.env||i.slug!==n.slug)&&(r?.("\u267B\uFE0F Workspace was tied to a different site \u2014 recreating it."),await h.emptyDir(e)),await h.outputJson(s,n),await h.outputFile(y.join(e,".cache","token"),t)}function Tn(e,t){let n=`${t.name.substr(0,20)} ${t.id}`.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,""),r=e||y.join(mn.homedir(),"SleekCMS");return y.resolve(r,n)}async function Ee(e){let t=(e.token||"").trim();if(!t)throw new Error("syncSite: token is required");let n=e.log??(b=>console.log(b)),r=e.onProgress,s=(e.env||"production").toLowerCase(),i=at[s]||at.production,d=await de(i,t,"GET","/get_site"),l=e.viewsDir?y.resolve(e.viewsDir):Tn(e.path,d);await h.ensureDir(l),await xn(l,t,{env:s,slug:String(d.id)},n),await Promise.all(wn.map(b=>h.ensureDir(y.join(l,b)))),await Sn(l,e.buildScript);let w=y.join(l,".cache","state.json");e.flush&&await h.remove(w);let S=!await h.pathExists(w),u=S?{}:(await h.readJson(w)).fileMap||{},p=0,x=0,m=0;if(S)({fileMap:u,pulled:x}=await In(l,i,t,n,r)),await vn(l,e.agentMd);else{let b=await _n(l,u,i,t,n);m=b.renamed,p=await En(l,u,i,t,n,r,b.held)}return await h.outputJson(w,{fileMap:u},{spaces:2}),{viewsDir:l,site:d,isFirstRun:S,pushed:p,pulled:x,renamed:m}}var Z=e=>hn("sha1").update(e).digest("hex"),kn=new Set([".DS_Store","Thumbs.db","desktop.ini"]);async function Be(e){let t=y.join(e,"src");if(!await h.pathExists(t))return[];let n=[];async function r(s){let i=await h.readdir(s,{withFileTypes:!0});for(let d of i){if(kn.has(d.name))continue;let l=y.join(s,d.name);d.isDirectory()?await r(l):d.isFile()&&n.push(y.relative(e,l).replace(/\\/g,"/"))}}return await r(t),n}function ct(e){let t=e.split(y.sep).join("/");return t==="src/setup.json"?0:t==="src/content/images.json"?1:t==="src/content/videos.json"?2:t.startsWith("src/models/")?3:t.startsWith("src/content/")?4:5}var lt=10;function $n(e){let t=[];for(let n=0;n<e.length;n+=lt)t.push(e.slice(n,n+lt));return t}async function En(e,t,n,r,s,i,d=new Set){let l=[];for(let p of await Be(e)){if(d.has(p))continue;let x=y.join(e,p),m=t[p],b=await h.stat(x);if(m&&m.mtimeMs===b.mtimeMs){m.hash||(m.hash=Z(await h.readFile(x,"utf-8")));continue}let k=await h.readFile(x,"utf-8");k.trim()&&l.push({rel:p,full:x,stat:b,content:k,prior:m})}if(l.length===0)return 0;l.sort((p,x)=>ct(p.rel)-ct(x.rel)),s(`\u2B06 pushing ${l.length} file(s)`),i?.(0,l.length);let w=await pt(e),S=0,u=0;for(let p of $n(l)){let x;try{x=await de(n,r,"POST","/save_files",p.map(b=>({path:b.rel,content:b.content})))}catch(b){s(`\u274C Error saving files: ${An(b)}`),s(`\u23F8 ${l.length-u} file(s) not pushed \u2014 saving any file retries them.`);break}let m=Bn(x);for(let b=0;b<p.length;b++){let k=p[b],_=Pn(k,b,x,m);if(!_){let N="No save response returned";w[k.rel]=N,s(`\u274C Error saving ${k.rel}: ${N}`);continue}if(_.error){w[k.rel]=_.error,s(`\u274C Error saving ${k.rel}: ${_.error}`);continue}delete w[k.rel],t[k.rel]={mtimeMs:k.stat.mtimeMs,hash:Z(k.content)},s(`\u2705 ${k.prior?"Updated":"Created"} ${k.rel}`),S++}u+=p.length,i?.(u,l.length)}return u===0?0:(await ft(e,w),S)}function Bn(e){let t=new Map;for(let n of e)n.path&&t.set(n.path,n);return t}function Pn(e,t,n,r){let s=r.get(e.rel);if(s)return s;let i=n[t];if(!i?.path||i.path===e.rel)return i}var ut=/^src\/(?:(?:models|views)\/(?:pages|entries|blocks)|content\/(?:pages|entries))\//,Cn={PAGE:"pages",ENTRY:"entries",BLOCK:"blocks"},Mn=e=>e.split("/").slice(0,3).join("/");function Rn(e,t,n){let r=new Map,s=(i,d)=>{let l=`${Mn(i)}|${d}`;return r.has(l)||r.set(l,{from:[],to:[],hash:d}),r.get(l)};for(let i of e)s(i,n[i].hash).from.push(i);for(let[i,d]of t)s(i,d).to.push(i);return[...r.values()].filter(i=>i.from.length===1&&i.to.length===1).map(i=>({from:i.from[0],to:i.to[0],hash:i.hash}))}function Ln(e,t){let n=new Map,r=(s,i,d,l,w)=>{let S=n.get(s);S?(S.pairs.push(w),i<S.rank&&Object.assign(S,{from:d,to:l,rank:i})):n.set(s,{from:d,to:l,rank:i,pairs:[w]})};for(let s of e){let[,i,d,...l]=s.from.split("/"),w=s.to.split("/").slice(3);if(i==="content"&&d==="pages"&&l.length>1){let[u,...p]=l,[x,...m]=w;if(m.length===0)continue;x===u?r(`record:${s.from}`,0,s.from,s.to,s):p.join("/")===m.join("/")&&![...t].some(b=>b.startsWith(`src/content/pages/${u}/`))&&r(`pages:${u}`,3,`src/content/pages/${u}`,`src/content/pages/${x}`,s);continue}let S=l.join("/").replace(/\.[^./]+$/,"");r(`${d}:${S}`,i==="models"?0:i==="views"?1:2,s.from,s.to,s)}return[...n.values()].map(({from:s,to:i,pairs:d})=>({from:s,to:i,pairs:d}))}function dt(e,t){if(e.slug!==void 0&&e.new_slug!==void 0){let d=`src/content/pages/${e.key}/`,l=t.startsWith(d)?t.slice(d.length).match(/^(.+)\.(json|md)$/):null;return l&&l[1].toLowerCase()===e.slug.toLowerCase()?`${d}${e.new_slug}.${l[2]}`:null}let n=Cn[e.type],{key:r,new_key:s}=e;if(!n||!s)return null;if(t===`src/models/${n}/${r}.model`)return`src/models/${n}/${s}.model`;if(t===`src/views/${n}/${r}.ejs`)return`src/views/${n}/${s}.ejs`;if(n!=="blocks"&&(t===`src/content/${n}/${r}.json`||t===`src/content/${n}/${r}.md`))return`src/content/${n}/${s}${y.extname(t)}`;let i=`src/content/pages/${r}/`;return n==="pages"&&r.endsWith("+")&&t.startsWith(i)?`src/content/pages/${s}/${t.slice(i.length)}`:null}async function jn(e,t){let n=e;for(;n.startsWith(t+y.sep);){let r=await h.readdir(n).catch(()=>null);if(!r||r.length>0)return;await h.remove(n),n=y.dirname(n)}}async function Dn(e,t,n,r){let s=u=>y.join(e,u),i=y.join(e,"src"),d=u=>y.resolve(e,u).startsWith(i+y.sep),l=async(u,p)=>{t[u]={mtimeMs:(await h.stat(s(u))).mtimeMs,hash:p}},w=u=>jn(y.dirname(s(u)),y.join(e,...u.split("/").slice(0,3))),S=[];for(let u of r){let p=dt(n.rename,u.from)??u.to;d(p)&&(p!==u.to&&(await h.move(s(u.to),s(p),{overwrite:!0}),await w(u.to),S.push(u.to)),delete t[u.from],S.push(u.from),await l(p,u.hash))}for(let u of new Set([...await Be(e),...Object.keys(t)])){let p=dt(n.rename,u);if(!p||!d(p))continue;let x=await h.pathExists(s(u));if(x&&await h.pathExists(s(p)))continue;let m=t[u];if(delete t[u],S.push(u),!x)continue;let b=!!m?.hash&&m.mtimeMs===(await h.stat(s(u))).mtimeMs;await h.move(s(u),s(p)),b&&await l(p,m.hash),await w(u)}for(let u of n.files||[]){let p=t[u.path];if(!p||!d(u.path))continue;let x=await h.stat(s(u.path)).catch(()=>null);!x||x.mtimeMs!==p.mtimeMs||await h.readFile(s(u.path),"utf-8")!==u.content&&(await h.outputFile(s(u.path),u.content),await l(u.path,Z(u.content)))}return S}function gt(e){try{let t=JSON.parse(e.body).message;if(t)return t}catch{}return(e.body||e.message||String(e)).slice(0,300)}function An(e){return e instanceof H?e.body.match(/<title>([^<]*)<\/title>/i)?.[1].trim()||gt(e).replace(/\s+/g," "):e instanceof Error?e.message:String(e)}async function _n(e,t,n,r,s){let i=new Set,d=await Be(e),l=new Set(d),w=Object.keys(t).filter(m=>!l.has(m)&&t[m].hash&&ut.test(m));if(w.length===0)return{renamed:0,held:i};let S=new Map;for(let m of d){if(t[m]||!ut.test(m))continue;let b=await h.readFile(y.join(e,m),"utf-8");b.trim()&&S.set(m,Z(b))}let u=Ln(Rn(w,S,t),l);if(u.length===0)return{renamed:0,held:i};let p=await pt(e),x=0;for(let m of u){let b;try{b=await de(n,r,"POST","/rename",{from:m.from,to:m.to})}catch(C){let M=C,Y=gt(M);if(M.status>=400&&M.status<500)s(`\u26A0\uFE0F Couldn't rename ${m.from} \u2192 ${m.to}: ${Y} Saving it as a new file.`);else{for(let j of m.pairs)i.add(j.to),p[j.to]=`rename from ${j.from} failed: ${Y}`;s(`\u274C Error renaming ${m.from} \u2192 ${m.to}: ${Y}`)}continue}let k=await Dn(e,t,b,m.pairs);for(let C of[...k,...m.pairs.map(M=>M.to)])delete p[C];let[_,...N]=b.message.split(`
7
- `);s(`\u{1F500} ${_}`);for(let C of N)s(` ${C}`);x++}return await ft(e,p),{renamed:x,held:i}}var Pe="sync-errors.log",$e=new Map;async function pt(e){let t=y.resolve(e),n=$e.get(t);if(n)return n;let r=await On(t);return $e.set(t,r),r}async function On(e){let t=y.join(e,Pe);if(!await h.pathExists(t))return{};let n=await h.readFile(t,"utf-8"),r={};for(let s of n.split(`
8
- `)){let i=s.indexOf(": ");i>0&&(r[s.slice(0,i)]=s.slice(i+2))}return r}async function ft(e,t){let n=y.join(e,Pe),r=Object.entries(t);if(r.length===0){await h.remove(n);return}await h.outputFile(n,r.map(([s,i])=>`${s}: ${i}`).join(`
6
+ `).parse(process.argv),Qe.opts()}function R(e){let t=process.platform==="win32"?`where ${e}`:`which ${e}`;try{return rt(t,{stdio:"ignore"}),!0}catch{return!1}}function it(e){if(process.platform!=="darwin")return!1;try{return rt(`osascript -e 'id of application "${e}"'`,{stdio:"ignore"}),!0}catch{return!1}}function ln(e,t){let n={detached:!0,stdio:"ignore",windowsHide:!0};process.platform==="win32"?B("cmd.exe",["/c",e,...t],n).unref():B(e,t,n).unref()}var tt=[{name:"VS Code",cmd:"code",args:e=>["-n",e]},{name:"Cursor",cmd:"cursor",args:e=>["-n",e]}];function nt(e,t){let r=(t?tt.filter(s=>s.cmd===t):tt).find(s=>R(s.cmd));return r?(ln(r.cmd,r.args(e)),r.name):null}function un(e){if(process.platform==="darwin")return B("open",["-a","Terminal",e],{detached:!0,stdio:"ignore"}).unref(),!0;if(process.platform==="win32"){if(R("wt"))B("wt.exe",["-d",e],{detached:!0,stdio:"ignore"}).unref();else{let t=R("powershell")?"powershell.exe":"cmd.exe";B("cmd.exe",["/c","start","",t],{cwd:e,detached:!0,stdio:"ignore"}).unref()}return!0}return!1}function dn(e){return it("iTerm")?(B("open",["-a","iTerm",e],{detached:!0,stdio:"ignore"}).unref(),!0):!1}function gn(e){return`'${e.replace(/'/g,"'\\''")}'`}var fn=[{cmd:"x-terminal-emulator",args:e=>["-e","bash","-c",e]},{cmd:"gnome-terminal",args:e=>["--","bash","-c",e]},{cmd:"konsole",args:e=>["-e","bash","-c",e]},{cmd:"xfce4-terminal",args:e=>["-x","bash","-c",e]},{cmd:"kitty",args:e=>["bash","-c",e]},{cmd:"alacritty",args:e=>["-e","bash","-c",e]},{cmd:"xterm",args:e=>["-e","bash","-c",e]}];function ot(e,t){if(!R(t))return!1;if(process.platform==="darwin"){let r=`tell application "Terminal" to do script ("cd " & quoted form of "${e}" & " && ${t}")`;return B("osascript",["-e",r,"-e",'tell application "Terminal" to activate'],{detached:!0,stdio:"ignore"}).unref(),!0}if(process.platform==="win32")return R("wt")?B("wt.exe",["-d",e,"cmd","/k",t],{detached:!0,stdio:"ignore"}).unref():B("cmd.exe",["/c","start","","cmd","/k",t],{cwd:e,detached:!0,stdio:"ignore"}).unref(),!0;let n=`cd ${gn(e)} && ${t}; exec bash`;for(let r of fn)if(R(r.cmd))try{return B(r.cmd,r.args(n),{cwd:e,detached:!0,stdio:"ignore"}).unref(),!0}catch{}return!1}function pn(e){return ot(e,"claude")}function hn(e){return ot(e,"pi")}function ge(e){if(!e)return!1;try{let t=process.platform==="darwin"?B("open",[e],{detached:!0,stdio:"ignore"}):process.platform==="win32"?B("cmd.exe",["/c","start","",e],{detached:!0,stdio:"ignore",windowsHide:!0}):B("xdg-open",[e],{detached:!0,stdio:"ignore"});return t.on("error",()=>{}),t.unref(),!0}catch{return!1}}function at(){let e=process.platform==="darwin",t=process.platform==="win32",n=[];return(e||t)&&n.push({key:"terminal",label:"terminal",run:un}),it("iTerm")&&n.push({key:"iterm",label:"iterm",run:dn}),R("code")&&n.push({key:"code",label:"vscode",run:r=>nt(r,"code")!==null}),R("cursor")&&n.push({key:"cursor",label:"cursor",run:r=>nt(r,"cursor")!==null}),R("claude")&&n.push({key:"claude",label:"claude",run:pn}),R("pi")&&n.push({key:"pi",label:"pi",run:hn}),n}import{EventEmitter as er}from"node:events";import{spawn as tr}from"node:child_process";import re from"fs-extra";import Ae from"path";import nr from"chokidar";import{createHash as wn}from"crypto";import h from"fs-extra";import yn from"os";import b from"path";var ct={localhost:"http://app.sleekcms.test/api/mcp",development:"https://app.sleekcms.dev/api/mcp",production:"https://app.sleekcms.com/api/mcp"},bn=["src/views/blocks","src/views/entries","src/views/pages","src/views/layouts","src/models/blocks","src/models/entries","src/models/pages","src/content/pages","src/content/entries","src/public/js","src/public/css"],N=class extends Error{status;body;constructor(t,n,r){super(t),this.status=n,this.body=r}};async function ee(e,t,n,r,s){let i=await fetch(e+r,{method:n,headers:{Authorization:`Bearer ${t}`,...s!==void 0?{"Content-Type":"application/json"}:{}},body:s!==void 0?JSON.stringify(s):void 0});if(!i.ok){let d=await i.text();throw new N(`${n} ${r} \u2192 ${i.status}: ${d}`,i.status,d)}return i.json()}var vn=["AGENT.md","CLAUDE.md",b.join(".vscode","copilot-instructions.md")],Sn="mrmlnc.vscode-json5";async function xn(e,t){t&&(await h.outputFile(b.join(e,"AGENTS.md"),t),await Promise.all(vn.map(n=>h.remove(b.join(e,n))))),await h.outputFile(b.join(e,".vscode","settings.json"),JSON.stringify({"files.associations":{"*.model":"javascript","**/src/**/*.json":"json5"},"[javascript]":{"editor.defaultFormatter":"esbenp.prettier-vscode"},"js/ts.validate.enabled":!1},null,2)),await h.outputFile(b.join(e,".vscode","extensions.json"),JSON.stringify({recommendations:[Sn]},null,2))}async function Tn(e,t){if(!t)return;let n=b.join(e,"build.mjs"),r=t.match(/build\.mjs v\d+/)?.[0];if(await h.pathExists(n)){let s=(await h.readFile(n,"utf-8")).match(/build\.mjs v\d+/)?.[0];if(s&&s===r)return}await h.outputFile(n,t)}async function kn(e,t,n,r){let s=b.join(e,".cache","site.json"),i=await h.readJson(s).catch(()=>null);i&&(i.env!==n.env||i.slug!==n.slug)&&(r?.("\u267B\uFE0F Workspace was tied to a different site \u2014 recreating it."),await h.emptyDir(e)),await h.outputJson(s,n),await h.outputFile(b.join(e,".cache","token"),t)}function $n(e,t){let n=`${t.name.substr(0,20)} ${t.id}`.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,""),r=e||b.join(yn.homedir(),"SleekCMS");return b.resolve(r,n)}async function Be(e){let t=(e.token||"").trim();if(!t)throw new Error("syncSite: token is required");let n=e.log??(v=>console.log(v)),r=e.onProgress,s=(e.env||"production").toLowerCase(),i=ct[s]||ct.production,d=await ee(i,t,"GET","/get_site"),a=e.viewsDir?b.resolve(e.viewsDir):$n(e.path,d);await h.ensureDir(a),await kn(a,t,{env:s,slug:String(d.id)},n),await Promise.all(bn.map(v=>h.ensureDir(b.join(a,v)))),await Tn(a,e.buildScript);let y=b.join(a,".cache","state.json");e.flush&&await h.remove(y);let w=!await h.pathExists(y),u=w?{}:(await h.readJson(y)).fileMap||{},f=0,x=0,m=0;if(w)({fileMap:u,pulled:x}=await Nn(a,i,t,n,r)),await xn(a,e.agentMd);else{let v=await In(a,u,i,t,n);m=v.renamed,f=await Cn(a,u,i,t,n,r,v.held),await Bn(a,u,i,t,n)}return await h.outputJson(y,{fileMap:u},{spaces:2}),{viewsDir:a,site:d,isFirstRun:w,pushed:f,pulled:x,renamed:m}}var z=e=>wn("sha1").update(e).digest("hex"),En=new Set([".DS_Store","Thumbs.db","desktop.ini"]);async function Pe(e){let t=b.join(e,"src");if(!await h.pathExists(t))return[];let n=[];async function r(s){let i=await h.readdir(s,{withFileTypes:!0});for(let d of i){if(En.has(d.name))continue;let a=b.join(s,d.name);d.isDirectory()?await r(a):d.isFile()&&n.push(b.relative(e,a).replace(/\\/g,"/"))}}return await r(t),n}var J="src/content/files.json";async function Bn(e,t,n,r,s){let i;try{i=await ee(n,r,"GET","/get_files?kind=files")}catch(w){if(w instanceof N&&w.status>=400&&w.status<500)return;s(`\u26A0\uFE0F Couldn't refresh ${J}: ${pt(w)}`);return}let d=i.find(w=>w.path===J);if(!d)return;let a=b.join(e,J);(await h.pathExists(a)?await h.readFile(a,"utf-8"):null)!==d.content&&(await h.outputFile(a,d.content),t[J]={mtimeMs:(await h.stat(a)).mtimeMs,hash:z(d.content)},s(`\u{1F4C4} Refreshed ${J}`))}function lt(e){let t=e.split(b.sep).join("/");return t==="src/setup.json"?0:t==="src/content/images.json"?1:t==="src/content/videos.json"?2:t.startsWith("src/models/")?3:t.startsWith("src/content/")?4:5}var ut=10;function Pn(e){let t=[];for(let n=0;n<e.length;n+=ut)t.push(e.slice(n,n+ut));return t}async function Cn(e,t,n,r,s,i,d=new Set){let a=[];for(let f of await Pe(e)){if(d.has(f)||f===J)continue;let x=b.join(e,f),m=t[f],v=await h.stat(x);if(m&&m.mtimeMs===v.mtimeMs){m.hash||(m.hash=z(await h.readFile(x,"utf-8")));continue}let k=await h.readFile(x,"utf-8");k.trim()&&a.push({rel:f,full:x,stat:v,content:k,prior:m})}if(a.length===0)return 0;a.sort((f,x)=>lt(f.rel)-lt(x.rel)),s(`\u2B06 pushing ${a.length} file(s)`),i?.(0,a.length);let y=await ht(e),w=0,u=0;for(let f of Pn(a)){let x;try{x=await ee(n,r,"POST","/save_files",f.map(v=>({path:v.rel,content:v.content})))}catch(v){s(`\u274C Error saving files: ${pt(v)}`),s(`\u23F8 ${a.length-u} file(s) not pushed \u2014 saving any file retries them.`);break}let m=Mn(x);for(let v=0;v<f.length;v++){let k=f[v],A=Rn(k,v,x,m);if(!A){let W="No save response returned";y[k.rel]=W,s(`\u274C Error saving ${k.rel}: ${W}`);continue}if(A.error){y[k.rel]=A.error,s(`\u274C Error saving ${k.rel}: ${A.error}`);continue}delete y[k.rel],t[k.rel]={mtimeMs:k.stat.mtimeMs,hash:z(k.content)},s(`\u2705 ${k.prior?"Updated":"Created"} ${k.rel}`),w++}u+=f.length,i?.(u,a.length)}return u===0?0:(await mt(e,y),w)}function Mn(e){let t=new Map;for(let n of e)n.path&&t.set(n.path,n);return t}function Rn(e,t,n,r){let s=r.get(e.rel);if(s)return s;let i=n[t];if(!i?.path||i.path===e.rel)return i}var dt=/^src\/(?:(?:models|views)\/(?:pages|entries|blocks)|content\/(?:pages|entries))\//,Ln={PAGE:"pages",ENTRY:"entries",BLOCK:"blocks"},jn=e=>e.split("/").slice(0,3).join("/");function Dn(e,t,n){let r=new Map,s=(i,d)=>{let a=`${jn(i)}|${d}`;return r.has(a)||r.set(a,{from:[],to:[],hash:d}),r.get(a)};for(let i of e)s(i,n[i].hash).from.push(i);for(let[i,d]of t)s(i,d).to.push(i);return[...r.values()].filter(i=>i.from.length===1&&i.to.length===1).map(i=>({from:i.from[0],to:i.to[0],hash:i.hash}))}function _n(e,t){let n=new Map,r=(s,i,d,a,y)=>{let w=n.get(s);w?(w.pairs.push(y),i<w.rank&&Object.assign(w,{from:d,to:a,rank:i})):n.set(s,{from:d,to:a,rank:i,pairs:[y]})};for(let s of e){let[,i,d,...a]=s.from.split("/"),y=s.to.split("/").slice(3);if(i==="content"&&d==="pages"&&a.length>1){let[u,...f]=a,[x,...m]=y;if(m.length===0)continue;x===u?r(`record:${s.from}`,0,s.from,s.to,s):f.join("/")===m.join("/")&&![...t].some(v=>v.startsWith(`src/content/pages/${u}/`))&&r(`pages:${u}`,3,`src/content/pages/${u}`,`src/content/pages/${x}`,s);continue}let w=a.join("/").replace(/\.[^./]+$/,"");r(`${d}:${w}`,i==="models"?0:i==="views"?1:2,s.from,s.to,s)}return[...n.values()].map(({from:s,to:i,pairs:d})=>({from:s,to:i,pairs:d}))}function gt(e,t){if(e.slug!==void 0&&e.new_slug!==void 0){let d=`src/content/pages/${e.key}/`,a=t.startsWith(d)?t.slice(d.length).match(/^(.+)\.(json|md)$/):null;return a&&a[1].toLowerCase()===e.slug.toLowerCase()?`${d}${e.new_slug}.${a[2]}`:null}let n=Ln[e.type],{key:r,new_key:s}=e;if(!n||!s)return null;if(t===`src/models/${n}/${r}.model`)return`src/models/${n}/${s}.model`;if(t===`src/views/${n}/${r}.ejs`)return`src/views/${n}/${s}.ejs`;if(n!=="blocks"&&(t===`src/content/${n}/${r}.json`||t===`src/content/${n}/${r}.md`))return`src/content/${n}/${s}${b.extname(t)}`;let i=`src/content/pages/${r}/`;return n==="pages"&&r.endsWith("+")&&t.startsWith(i)?`src/content/pages/${s}/${t.slice(i.length)}`:null}async function An(e,t){let n=e;for(;n.startsWith(t+b.sep);){let r=await h.readdir(n).catch(()=>null);if(!r||r.length>0)return;await h.remove(n),n=b.dirname(n)}}async function On(e,t,n,r){let s=u=>b.join(e,u),i=b.join(e,"src"),d=u=>b.resolve(e,u).startsWith(i+b.sep),a=async(u,f)=>{t[u]={mtimeMs:(await h.stat(s(u))).mtimeMs,hash:f}},y=u=>An(b.dirname(s(u)),b.join(e,...u.split("/").slice(0,3))),w=[];for(let u of r){let f=gt(n.rename,u.from)??u.to;d(f)&&(f!==u.to&&(await h.move(s(u.to),s(f),{overwrite:!0}),await y(u.to),w.push(u.to)),delete t[u.from],w.push(u.from),await a(f,u.hash))}for(let u of new Set([...await Pe(e),...Object.keys(t)])){let f=gt(n.rename,u);if(!f||!d(f))continue;let x=await h.pathExists(s(u));if(x&&await h.pathExists(s(f)))continue;let m=t[u];if(delete t[u],w.push(u),!x)continue;let v=!!m?.hash&&m.mtimeMs===(await h.stat(s(u))).mtimeMs;await h.move(s(u),s(f)),v&&await a(f,m.hash),await y(u)}for(let u of n.files||[]){let f=t[u.path];if(!f||!d(u.path))continue;let x=await h.stat(s(u.path)).catch(()=>null);!x||x.mtimeMs!==f.mtimeMs||await h.readFile(s(u.path),"utf-8")!==u.content&&(await h.outputFile(s(u.path),u.content),await a(u.path,z(u.content)))}return w}function ft(e){try{let t=JSON.parse(e.body).message;if(t)return t}catch{}return(e.body||e.message||String(e)).slice(0,300)}function pt(e){return e instanceof N?e.body.match(/<title>([^<]*)<\/title>/i)?.[1].trim()||ft(e).replace(/\s+/g," "):e instanceof Error?e.message:String(e)}async function In(e,t,n,r,s){let i=new Set,d=await Pe(e),a=new Set(d),y=Object.keys(t).filter(m=>!a.has(m)&&t[m].hash&&dt.test(m));if(y.length===0)return{renamed:0,held:i};let w=new Map;for(let m of d){if(t[m]||!dt.test(m))continue;let v=await h.readFile(b.join(e,m),"utf-8");v.trim()&&w.set(m,z(v))}let u=_n(Dn(y,w,t),a);if(u.length===0)return{renamed:0,held:i};let f=await ht(e),x=0;for(let m of u){let v;try{v=await ee(n,r,"POST","/rename",{from:m.from,to:m.to})}catch(C){let M=C,K=ft(M);if(M.status>=400&&M.status<500)s(`\u26A0\uFE0F Couldn't rename ${m.from} \u2192 ${m.to}: ${K} Saving it as a new file.`);else{for(let j of m.pairs)i.add(j.to),f[j.to]=`rename from ${j.from} failed: ${K}`;s(`\u274C Error renaming ${m.from} \u2192 ${m.to}: ${K}`)}continue}let k=await On(e,t,v,m.pairs);for(let C of[...k,...m.pairs.map(M=>M.to)])delete f[C];let[A,...W]=v.message.split(`
7
+ `);s(`\u{1F500} ${A}`);for(let C of W)s(` ${C}`);x++}return await mt(e,f),{renamed:x,held:i}}var Ce="sync-errors.log",Ee=new Map;async function ht(e){let t=b.resolve(e),n=Ee.get(t);if(n)return n;let r=await Fn(t);return Ee.set(t,r),r}async function Fn(e){let t=b.join(e,Ce);if(!await h.pathExists(t))return{};let n=await h.readFile(t,"utf-8"),r={};for(let s of n.split(`
8
+ `)){let i=s.indexOf(": ");i>0&&(r[s.slice(0,i)]=s.slice(i+2))}return r}async function mt(e,t){let n=b.join(e,Ce),r=Object.entries(t);if(r.length===0){await h.remove(n);return}await h.outputFile(n,r.map(([s,i])=>`${s}: ${i}`).join(`
9
9
  `)+`
10
- `)}async function In(e,t,n,r,s){$e.delete(y.resolve(e)),await h.remove(y.join(e,Pe)),r("\u{1F4E5} Fetching files...");let i=await de(t,n,"GET","/get_files"),d={},l=0;for(let w of i){let S=y.join(e,w.path);await h.outputFile(S,w.content);let u=(await h.stat(S)).mtimeMs;l++,s?.(l,i.length),d[w.path]={mtimeMs:u,hash:Z(w.content)}}return r(`\u2714\uFE0F Synced ${i.length} file(s).`),{fileMap:d,pulled:l}}import Fn from"path";import Nn from"chokidar";var Wn=5e3,Gn=/(^|[/\\])(\.DS_Store|Thumbs\.db|desktop\.ini)$/,Q=class{viewsDir;onSync;debounceMs;watcher=null;debounceTimer=null;isShuttingDown=!1;dirty=!1;syncInFlight=!1;constructor(t){this.viewsDir=t.viewsDir,this.onSync=t.onSync,this.debounceMs=t.debounceMs??Wn}start(){if(this.watcher)return;let t=Fn.join(this.viewsDir,"src");this.watcher=Nn.watch([t],{persistent:!0,ignoreInitial:!0,ignored:Gn}).on("change",()=>this.scheduleSync()).on("add",()=>this.scheduleSync()).on("unlink",()=>this.scheduleSync()).on("addDir",()=>this.scheduleSync()).on("unlinkDir",()=>this.scheduleSync())}setShuttingDown(t){this.isShuttingDown=t}async stop(){this.debounceTimer&&(clearTimeout(this.debounceTimer),this.debounceTimer=null),this.watcher&&(await this.watcher.close(),this.watcher=null)}scheduleSync(){this.isShuttingDown||(this.dirty=!0,this.debounceTimer&&clearTimeout(this.debounceTimer),this.debounceTimer=setTimeout(()=>this.flush(),this.debounceMs))}async flush(){if(this.debounceTimer=null,!(!this.dirty||this.isShuttingDown)){if(this.syncInFlight){this.debounceTimer=setTimeout(()=>this.flush(),this.debounceMs);return}this.dirty=!1,this.syncInFlight=!0;try{await this.onSync()}catch(t){let n=t;console.error("\u274C Sync failed:",n.body||n.message)}finally{this.syncInFlight=!1}}}};import Ce from"fs-extra";import Un from"os";import mt from"path";var ht={localhost:"http://app.sleekcms.test/api",development:"https://app.sleekcms.dev/api",production:"https://app.sleekcms.com/api"};function ee(e){return ht[e]||ht.production}function Re(e){return ee(e).replace(/\/api$/,"")}var Hn=mt.join(Un.homedir(),"SleekCMS"),J=mt.join(Hn,"auth.json"),Me=class extends Error{status;body;constructor(t,n,r=""){super(t),this.name="ApiError",this.status=n,this.body=r}};async function ge(e=J){let t=null;try{t=await Ce.readJson(e)}catch{return{sites:[],hadLegacyLogin:!1}}return{sites:Array.isArray(t?.sites)?t.sites.filter(r=>r&&typeof r.token=="string"&&typeof r.siteId=="number"):[],hadLegacyLogin:typeof t?.userToken=="string"}}async function wt(e,t=J){await Ce.outputJson(t,{sites:e},{spaces:2});try{await Ce.chmod(t,384)}catch{}}async function Le(e,t=J){let{sites:n}=await ge(t),r=[...n.filter(s=>!(s.env===e.env&&s.siteId===e.siteId)),e];return await wt(r,t),r}async function je(e,t,n=J){let{sites:r}=await ge(n),s=r.filter(i=>!(i.env===e&&i.siteId===t));return await wt(s,n),s}async function De(e,t,n,r){let s=await fetch(ee(e)+r,{method:n,headers:{Authorization:`Bearer ${t}`}});if(!s.ok){let i=await s.text().catch(()=>"");throw new Me(`${n} ${r} \u2192 ${s.status}: ${i}`,s.status,i)}return s.json()}function Jn(e){if(e==null)return"(unknown error)";if(typeof e=="string")return e;if(typeof e!="object")return String(e);let t=e,n=typeof t.file_path=="string"?t.file_path:"",r=typeof t.path=="string"?t.path:typeof t.filename=="string"?t.filename:"",s=n||r,i=t.line!=null?`:${t.line}`:"",d=t.column!=null?`:${t.column}`:"",l=typeof t.message=="string"&&t.message?t.message:typeof t.name=="string"&&t.name?t.name:"",w=n&&r&&r!==n?` (on page ${r})`:"";if(s)return`${s}${i}${d}${w}: ${l||"(no message)"}`;if(l)return l;try{return JSON.stringify(e)}catch{return"(unknown error)"}}async function yt(e,t){let n;try{n=await De(e,t,"GET","/site_build")}catch{return null}if(!Array.isArray(n)||n.length===0)return null;let r=n[0],s=n.find(d=>typeof d.url=="string"&&d.url.length>0),i=Array.isArray(r.errors)?r.errors.map(Jn):[];return i.length>0?{status:"error",mode:"preview",errors:i,url:s?.url||void 0,at:r.updated_at?Date.parse(r.updated_at):void 0}:s?{status:"success",mode:"preview",url:s.url||void 0,at:s.updated_at?Date.parse(s.updated_at):void 0}:null}import{createHash as zn,randomBytes as qn}from"crypto";var Yn="urn:sleekcms:grant-type:cli_poll",Kn="SleekCMS Sync",bt=e=>e.toString("base64").replace(/=+$/,"").replace(/\+/g,"-").replace(/\//g,"_");function vt(){let e=bt(qn(32)),t=bt(zn("sha256").update(e).digest());return{verifier:e,challenge:t}}function St(e){let t=e.replace(/[^A-Za-z0-9]/g,"").slice(0,8).toUpperCase();return`${t.slice(0,4)}-${t.slice(4)}`}async function xt(e,t=fetch){let n={origin:Re(e),authorizeEndpoint:`${ee(e)}/oauth/authorize`};try{let r=await t(`${Re(e)}/.well-known/oauth-authorization-server`);if(!r.ok)return n;let s=await r.json();if(typeof s.issuer=="string"&&typeof s.authorization_endpoint=="string")return{origin:s.issuer.replace(/\/+$/,""),authorizeEndpoint:s.authorization_endpoint}}catch{}return n}function Tt(e,t){let n=new URLSearchParams({response_type:"code",client_name:Kn,redirect_uri:`${e.origin}/oauth/cli-done`,code_challenge:t,code_challenge_method:"S256",resource:`${e.origin}/mcp/`});return`${e.authorizeEndpoint}?${n.toString()}`}var z=class extends Error{constructor(t){super(t),this.name="SignInError"}},Xn=e=>new Promise(t=>setTimeout(t,e));async function kt(e,t,n={}){let{timeoutMs:r=10*6e4,signal:s,fetchImpl:i=fetch,sleep:d=Xn}=n,l=n.intervalMs??2e3,w=Date.now()+r,S=`${ee(e)}/oauth/token`;for(;;){if(s?.aborted)throw new z("Cancelled.");let u=null;try{u=await i(S,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({grant_type:Yn,code_verifier:t})})}catch{u=null}if(u){let p=await u.json().catch(()=>({}));if(u.ok&&p.access_token)return p.access_token;if(u.status===429)l=Math.min(l*2,3e4);else if(p.error!=="authorization_pending")throw new z(p.error==="invalid_grant"?"That approval can no longer be used. Add the site again.":`Sign-in failed: ${p.error||`HTTP ${u.status}`}`)}if(Date.now()+l>w)throw new z("Timed out waiting for approval in the browser.");await d(l)}}function Vn(e){let t=e.split(".")[1];if(!t)throw new z("The server returned a token this version can't read.");return JSON.parse(Buffer.from(t,"base64url").toString("utf8"))}async function $t(e,t){let n=Vn(t),r=await De(e,t,"GET","/mcp/get_site");return{env:e,siteId:n.sid,slug:n.slug,name:r.name,org:{name:r.org},role:r.role||void 0,token:t,addedAt:Date.now()}}var Et=500;function tr(){return new Date().toTimeString().slice(0,8)}function nr(e){let t=e;if(t.body){try{let n=JSON.parse(t.body);if(n&&typeof n.message=="string")return n.message;if(n&&typeof n.error=="string")return n.error}catch{}return t.body}return t.message}function Bt(e){return e instanceof H&&e.status===401}var rr=e=>({id:e.siteId,name:e.name,slug:e.slug});async function Pt(e){e&&(await te.remove(Ae.join(e,"src")),await te.remove(Ae.join(e,".cache","state.json")))}var pe=class extends Zn{env="production";saved=[];synced=new Map;connecting=null;message;messageIsError=!1;agentMd;buildScript;configPath;isShuttingDown=!1;basePath;pollOptions={};openBrowser=ue;constructor(t,n,r=J){super(),this.agentMd=t,this.buildScript=n,this.configPath=r}changed(){this.emit("change")}flash(t,n=!1){this.message=t,this.messageIsError=n,this.changed()}appendLog(t,n){let r=this.synced.get(t);r&&(r.logs.push({time:tr(),text:n}),r.logs.length>Et&&r.logs.splice(0,r.logs.length-Et),this.changed())}siteCount(){return this.synced.size}syncingCount(){let t=0;for(let n of this.synced.values())n.status==="syncing"&&t++;return t}watchingCount(){let t=0;for(let n of this.synced.values())(n.status==="watching"||n.status==="syncing")&&t++;return t}async init(t){this.env=t;let{sites:n,hadLegacyLogin:r}=await ge(this.configPath);this.saved=n.filter(s=>s.env===t),r&&this.flash("SleekCMS Sync now connects one site at a time \u2014 press a to add a site in your browser."),this.changed()}isSynced(t){return this.synced.has(t)}async connectSite(){if(this.connecting)return;let{verifier:t,challenge:n}=vt(),r=new AbortController;this.connecting={code:St(n),url:"",abort:r},this.changed();let s=Tt(await xt(this.env),n);if(r.signal.aborted){this.connecting=null,this.changed();return}this.connecting.url=s,this.message=void 0,this.changed(),this.openBrowser(s)||this.flash("Couldn't open a browser \u2014 open the address shown to continue.",!0);let i;try{let l=await kt(this.env,t,{...this.pollOptions,signal:r.signal});i=await $t(this.env,l)}catch(l){this.connecting=null,r.signal.aborted?this.changed():this.flash(nr(l),!0);return}this.connecting=null;let d=await Le(i,this.configPath);this.saved=d.filter(l=>l.env===this.env),this.flash(`Added "${i.name}".`),this.synced.has(i.siteId)||await this.startSite(i)}cancelConnect(){this.connecting?.abort.abort()}async startSite(t){let n=rr(t);if(this.synced.has(n.id))return;let r={org:t.org,site:n,siteToken:t.token,role:t.role,viewsDir:"",status:"syncing",logs:[]};this.synced.set(n.id,r),this.appendLog(n.id,"\u2795 added \u2014 pulling files"),this.changed();try{let s=await Ee({token:t.token,env:this.env,path:this.basePath,agentMd:this.agentMd,buildScript:this.buildScript,log:i=>this.appendLog(n.id,i),onProgress:(i,d)=>{r.progress={done:i,total:d},this.changed()}});this.noteRole(n.id,s.site.role),r.viewsDir=s.viewsDir,r.progress=void 0,r.status="watching",r.lastSyncedAt=Date.now(),r.lastMsg=`pulled ${s.pulled} file(s)`,r.watcher=new Q({viewsDir:s.viewsDir,onSync:()=>this.runSync(n.id)}),r.watcher.start(),this.watchBuildStatus(n.id),this.seedLastBuild(n.id),this.appendLog(n.id,"\u{1F440} watching for changes")}catch(s){if(Bt(s))return void await this.revoke(n.id);let i=s;r.status="error",r.progress=void 0,r.lastMsg=i.body||i.message,this.appendLog(n.id,`\u274C ${i.body||i.message}`)}this.changed()}noteRole(t,n){let r=this.synced.get(t);if(!n||!r||r.role===n)return;r.role=n;let s=this.saved.find(i=>i.siteId===t);s&&(s.role=n,Le({...s},this.configPath).catch(()=>{}))}async revoke(t){let n=this.synced.get(t),r=n?.site.name??this.saved.find(d=>d.siteId===t)?.name??"this site";n&&(n.watcher?.setShuttingDown(!0),await n.watcher?.stop(),await n.buildWatcher?.close(),this.synced.delete(t));let s=await je(this.env,t,this.configPath);this.saved=s.filter(d=>d.env===this.env);let i=n?.viewsDir?` Local files kept in ${n.viewsDir}.`:"";this.flash(`Access to "${r}" was revoked \u2014 press a to add it again.${i}`,!0)}async forgetSite(t){if(this.synced.has(t)){this.flash("Disconnect it first (d), then press x to forget it.",!0);return}let n=this.saved.find(s=>s.siteId===t)?.name??"the site",r=await je(this.env,t,this.configPath);this.saved=r.filter(s=>s.env===this.env),this.flash(`Forgot "${n}". To revoke its access everywhere, log out of all sessions in SleekCMS.`)}async removeSite(t){let n=this.synced.get(t);n&&(n.watcher?.setShuttingDown(!0),await n.watcher?.stop(),await n.buildWatcher?.close(),await Pt(n.viewsDir),this.synced.delete(t),this.changed())}buildSite(t,n=!1){let r=this.synced.get(t);if(!r||!r.viewsDir)return;if(r.role&&r.role!=="ADMIN"){this.flash(`Building and publishing are for site ADMINs \u2014 you're ${r.role} on "${r.site.name}".`,!0);return}let s=n?"live":"preview";this.writeBuildFile(r.viewsDir,{status:"building",mode:s,stage:"queued",url:r.lastBuild?.url,at:Date.now()}),Qn("node",n?["build.mjs","--publish"]:["build.mjs"],{cwd:r.viewsDir,stdio:"ignore"}).on("error",l=>{this.writeBuildFile(r.viewsDir,{status:"error",mode:s,errors:[l.message],url:r.lastBuild?.url,at:Date.now()}),this.appendLog(t,`\u274C build: ${l.message}`)}),this.appendLog(t,n?"\u{1F680} publishing\u2026":"\u{1F528} building\u2026")}buildFile(t){return Ae.join(t,".cache","build.json")}async readBuildFile(t){try{let n=await te.readJson(this.buildFile(t));return n&&n.status?n:null}catch{return null}}async writeBuildFile(t,n){let r=this.buildFile(t),s=`${r}.tmp`;try{await te.outputJson(s,n),await te.rename(s,r)}catch{}}watchBuildStatus(t){let n=this.synced.get(t);!n||!n.viewsDir||(n.buildWatcher=er.watch(this.buildFile(n.viewsDir),{ignoreInitial:!1}).on("add",()=>{this.readBuildStatus(t)}).on("change",()=>{this.readBuildStatus(t)}))}async readBuildStatus(t){let n=this.synced.get(t);if(!n)return;let r=await this.readBuildFile(n.viewsDir);if(!r)return;let s=n.lastBuild?.url;n.lastBuild={status:r.status,mode:r.mode??"preview",url:r.url??(r.status==="success"?void 0:s),errors:r.errors,stage:r.stage,at:r.at},this.changed()}async seedLastBuild(t){let n=this.synced.get(t);if(!n)return;let r=await yt(this.env,n.siteToken);if(!r)return;let s=this.synced.get(t);if(!s)return;let i=await this.readBuildFile(s.viewsDir);i?.at!=null&&(r.at==null||i.at>=r.at)||await this.writeBuildFile(s.viewsDir,r)}async refetch(t){await this.runSync(t,{flush:!0})}async runSync(t,n={}){let r=this.synced.get(t);if(!(!r||!r.viewsDir||r.status==="paused")){r.status="syncing",this.changed();try{let s=await Ee({token:r.siteToken,env:this.env,viewsDir:r.viewsDir,agentMd:this.agentMd,buildScript:this.buildScript,flush:n.flush,log:i=>this.appendLog(t,i),onProgress:(i,d)=>{r.progress={done:i,total:d},this.changed()}});if(this.noteRole(t,s.site.role),r.progress=void 0,r.status="watching",r.lastSyncedAt=Date.now(),n.flush)r.lastMsg=`re-fetched ${s.pulled} file(s)`;else{let i=[s.renamed>0?`renamed ${s.renamed}`:"",s.pushed>0?`pushed ${s.pushed} file(s)`:""].filter(Boolean);r.lastMsg=i.length>0?i.join(", "):"up to date"}}catch(s){if(Bt(s))return void await this.revoke(t);let i=s;r.status="error",r.progress=void 0,r.lastMsg=i.body||i.message,this.appendLog(t,`\u274C ${i.body||i.message}`)}this.changed()}}async togglePause(t){let n=this.synced.get(t);!n||!n.viewsDir||(n.status==="paused"?(n.watcher=new Q({viewsDir:n.viewsDir,onSync:()=>this.runSync(t)}),n.watcher.start(),n.status="watching",this.appendLog(t,"\u25B6 resumed \u2014 watching for changes")):(n.watcher?.setShuttingDown(!0),await n.watcher?.stop(),n.watcher=void 0,n.progress=void 0,n.status="paused",this.appendLog(t,"\u23F8 paused")),this.changed())}async cleanupAll(){this.cancelConnect();for(let t of this.synced.values())t.watcher?.setShuttingDown(!0),await t.watcher?.stop(),await t.buildWatcher?.close(),await Pt(t.viewsDir)}async exit(){this.isShuttingDown||(this.isShuttingDown=!0,await this.cleanupAll(),this.emit("exit"))}};import{useInput as vr}from"ink";import{Fragment as ar,useEffect as ne,useMemo as cr,useRef as lr,useState as L}from"react";import{Box as f,Text as c,useInput as ur,useStdout as dr}from"ink";import gr from"ink-text-input";import{createRequire as sr}from"node:module";var ir=sr(import.meta.url),fe=ir("../package.json").version;import or from"node:os";function _e(e){if(!e)return"\u2014";let t=or.homedir();return e.startsWith(t)?"~"+e.slice(t.length):e}function Oe(e){if(!e)return"\u2014";let t=Math.floor((Date.now()-e)/1e3);if(t<5)return"just now";if(t<60)return`${t}s ago`;let n=Math.floor(t/60);if(n<60)return`${n}m ago`;let r=Math.floor(n/60);return r<24?`${r}h ago`:`${Math.floor(r/24)}d ago`}function me(e){return!e||e.total<=0?null:Math.round(e.done/e.total*100)}var he=["\u280B","\u2819","\u2839","\u2838","\u283C","\u2834","\u2826","\u2827","\u2807","\u280F"];function Ie(e){return he[(e%he.length+he.length)%he.length]}function we(e){switch(e){case"syncing":return{dot:"\u25CF",color:"green",label:"syncing"};case"watching":return{dot:"\u25CB",color:"gray",label:"watching"};case"paused":return{dot:"\u2016",color:"yellow",label:"paused"};case"error":return{dot:"\u2717",color:"red",label:"error"}}}function Ct(e){if(!e)return{dot:"\xB7",color:"gray",label:"\u2014"};switch(e.status){case"building":return{dot:"\u2234",color:"yellow",label:"building"};case"error":return{dot:"\u2717",color:"red",label:"failed"};case"success":return{dot:"\u2713",color:"green",label:e.mode==="live"?"live":"preview"}}}import{Fragment as Fe,jsx as o,jsxs as g}from"react/jsx-runtime";var pr=(e,t)=>t<=0?0:Math.max(0,Math.min(e,t-1)),fr=16,hr=5,mr="greenBright",Mt="blueBright",Rt="magentaBright";function Lt({k:e,bg:t=mr}){return o(c,{color:"black",backgroundColor:t,children:` ${e} `})}function O({k:e,label:t,width:n=16,bg:r}){return g(f,{width:n,children:[o(Lt,{k:e,bg:r}),o(c,{color:"gray",children:` ${t}`})]})}function E({k:e,label:t,bg:n}){return g(f,{marginBottom:1,children:[o(f,{width:9,children:o(Lt,{k:e,bg:n})}),o(c,{color:"gray",children:t})]})}function wr({launchers:e}){return g(f,{flexDirection:"column",borderStyle:"round",borderColor:"green",paddingX:2,paddingY:1,width:66,children:[g(f,{justifyContent:"space-between",children:[o(c,{color:"green",bold:!0,children:"KEYBOARD SHORTCUTS"}),o(c,{color:"gray",children:"? or esc to close"})]}),g(f,{marginTop:1,children:[g(f,{flexDirection:"column",width:34,children:[o(c,{color:"cyan",bold:!0,children:"Navigate"}),o(E,{k:"\u2191\u2193",label:"move selection"}),o(E,{k:"tab",label:"focus logs \xB7 \u2191\u2193 scroll"}),o(E,{k:"esc",label:"clear filter / back"}),o(f,{marginTop:1,children:o(c,{color:"cyan",bold:!0,children:"Global"})}),o(E,{k:"/",label:"search sites"}),o(E,{k:"a",label:"add a site (browser)"}),o(E,{k:"?",label:"toggle this help",bg:Mt}),o(E,{k:"ctrl+c",label:"quit"})]}),g(f,{flexDirection:"column",flexGrow:1,children:[o(c,{color:"cyan",bold:!0,children:"Site actions"}),o(E,{k:"s",label:"start sync"}),o(E,{k:"d",label:"disconnect"}),o(E,{k:"x",label:"forget site"}),o(E,{k:"p",label:"pause / resume sync"}),o(E,{k:"b",label:"build preview"}),o(E,{k:"B",label:"publish live"}),o(E,{k:"o",label:"open preview url"}),e.map((t,n)=>o(E,{k:String(n+1),label:`open in ${t.label}`,bg:Rt},t.key))]})]})]})}function yr({percent:e,width:t=28}){let n=Math.round(Math.max(0,Math.min(100,e))/100*t);return g(c,{children:[o(c,{color:"green",children:"\u2588".repeat(n)}),o(c,{color:"gray",children:"\u2591".repeat(Math.max(0,t-n))})]})}function br({frame:e,width:t=24,seg:n=6}){let r=t+n,s=e%r-n,i="";for(let d=0;d<t;d++)i+=d>=s&&d<s+n?"\u25AE":"\u25AF";return o(c,{color:"yellow",children:i})}function jt({controller:e}){let{stdout:t}=dr(),n=cr(()=>ot(),[]),[,r]=L(0),[s,i]=L(()=>new Date),[d,l]=L(null),[w,S]=L(""),[u,p]=L(!1),[x,m]=L(0),[b,k]=L(!1),[_,N]=L(!1),[C,M]=L("list"),[Y,j]=L(0),He=()=>r(a=>a+1);ne(()=>(e.on("change",He),()=>{e.off("change",He)}),[e]),ne(()=>{let a,v=()=>{i(new Date),a=setTimeout(v,6e4-Date.now()%6e4)};return a=setTimeout(v,6e4-Date.now()%6e4),()=>clearTimeout(a)},[]);let K=e.saved.map(a=>({org:a.org,site:{id:a.siteId,name:a.name,slug:a.slug},saved:a,entry:e.synced.get(a.siteId)})).sort((a,v)=>(a.entry?0:1)-(v.entry?0:1)),W=e.connecting,I=w.trim().toLowerCase(),P=I?K.filter(a=>a.site.name.toLowerCase().includes(I)||(a.org.name||"").toLowerCase().includes(I)):K,G=P.findIndex(a=>a.site.id===d);G<0&&(G=0);let $=P[G],T=$?.entry,se=Math.max(3,(t?.rows??30)-26),F=T?.logs??[],Se=Math.max(0,F.length-se),Yt=Math.min(Y,Se),ie=F.length-Yt,Je=F.slice(Math.max(0,ie-se),ie),Kt=$?.site.id;ne(()=>j(0),[Kt]);let D=T?.lastBuild,xe=D?.status==="building",Te=Ct(D),oe=xe||!!W;if(!oe){for(let a of e.synced.values())if(a.status==="syncing"){oe=!0;break}}ne(()=>{if(!oe)return;let a=setInterval(()=>m(v=>v+1),120);return()=>clearInterval(a)},[oe]);let ze=P.findIndex(a=>!a.entry),U=Math.max(hr,(t?.rows??30)-fr),X=P.length,ae=X>U,Xt=Math.floor(U/2),V=ae?Math.max(0,Math.min(G-Xt,X-U)):0,ke=Math.min(X,V+U),Vt=P.slice(V,ke),qe=lr(!1);ne(()=>{qe.current||(qe.current=!0,e.saved.length===0&&!e.connecting&&e.connectSite())},[e]);let ce=a=>{if(P.length===0)return;let v=pr(G+a,P.length);l(P[v].site.id)},Zt=()=>{if($){if($.entry){e.flash("Already syncing \u2014 press d to disconnect.");return}e.startSite($.saved)}},Qt=()=>{$?.entry&&e.removeSite($.site.id)},en=()=>{$&&e.forgetSite($.site.id)},tn=a=>{if(!T){e.flash("Press s to start syncing this site first.",!0);return}N(!0);let v=a.run(T.viewsDir);e.flash(v?`Opened ${a.label} at ${_e(T.viewsDir)}`:`Couldn't open ${a.label}.`,!v)},nn=()=>{if(!T){e.flash("Press s to start syncing this site first.",!0);return}e.togglePause(T.site.id)},Ye=a=>{if(!T){e.flash("Press s to start syncing this site first.",!0);return}e.buildSite(T.site.id,a),e.flash(a?"Publishing \u2014 making this live\u2026":"Building preview\u2026")},rn=()=>{let a=T?.lastBuild?.url;if(!a){e.flash("No build URL yet \u2014 press b to build.",!0);return}e.flash(ue(a)?`Opening ${a}`:"Couldn't open the browser.",!1)};ur((a,v)=>{if(b){(a==="?"||v.escape)&&k(!1);return}if(W&&v.escape)return void e.cancelConnect();if(u){v.escape?(p(!1),S(""),l(null)):v.return?p(!1):v.upArrow?ce(-1):v.downArrow&&ce(1);return}if(v.tab){C==="logs"?M("list"):T&&M("logs"),j(0);return}if(C==="logs"){if(v.escape){M("list"),j(0);return}if(v.upArrow)return void j(A=>Math.min(A+1,Se));if(v.downArrow)return void j(A=>Math.max(0,A-1))}if(v.escape){S(""),l(null);return}if(a==="?")return void k(!0);if(a==="/")return M("list"),void p(!0);if(a==="a"||a==="n")return void e.connectSite();if(v.upArrow)return ce(-1);if(v.downArrow)return ce(1);if(a==="s")return Zt();if(a==="d")return Qt();if(a==="x")return en();if(a==="p")return nn();if(a==="b")return Ye(!1);if(a==="B")return Ye(!0);if(a==="o")return rn();if(a>="1"&&a<="9"){let A=n[parseInt(a,10)-1];if(A)return tn(A)}});let Ke=g(f,{justifyContent:"space-between",paddingX:1,children:[g(f,{children:[o(c,{color:"green",bold:!0,children:"\u25C6 SleekCMS"}),o(c,{color:"gray",children:` v${fe} \u2502 `}),o(c,{children:e.env==="production"?"Sync":`Sync \xB7 ${e.env}`})]}),g(f,{children:[g(c,{color:"gray",children:[I?`${P.length}/${K.length}`:K.length," sites \xB7 "]}),g(c,{color:"green",children:[e.watchingCount()," watching"]}),o(c,{color:"gray",children:` \xB7 ${s.toTimeString().slice(0,5)}`})]})]});return b?g(f,{flexDirection:"column",children:[Ke,o(f,{marginTop:1,justifyContent:"center",children:o(wr,{launchers:n})})]}):g(f,{flexDirection:"column",children:[Ke,g(f,{children:[g(f,{flexDirection:"column",borderStyle:"round",borderColor:"green",width:38,paddingX:1,children:[g(f,{justifyContent:"space-between",children:[o(c,{color:"green",bold:!0,children:"SITE WORKSPACES"}),K.length>U&&!u&&!I?o(c,{color:"gray",children:"/ filter"}):null]}),u?g(f,{children:[o(c,{color:"cyan",children:"/ "}),o(gr,{value:w,onChange:S,onSubmit:()=>p(!1),placeholder:"type to filter\u2026"})]}):I?o(c,{color:"gray",children:`/ ${w} \xB7 ${P.length} match${P.length===1?"":"es"}`}):null,o(f,{flexDirection:"column",marginTop:1,minHeight:ae?U+3:void 0,children:P.length===0?o(c,{color:"gray",children:I?`no matches for \u201C${w}\u201D`:"no sites yet \u2014 press a to add one"}):g(Fe,{children:[ae?o(c,{color:"gray",children:V>0?` \u2191 ${V} more`:" "}):null,Vt.map((a,v)=>{let A=V+v,Xe=a.entry?we(a.entry.status):{dot:"\xB7",color:"gray",label:"idle"},Ve=me(a.entry?.progress),sn=a.entry?a.entry.status==="syncing"&&Ve!=null?`${Ve}%`:a.entry.status==="paused"?"paused":"--":"",le=A===G,on=A===ze&&ze>0;return g(ar,{children:[on?o(c,{color:"gray",children:"\u2500".repeat(34)}):null,g(f,{justifyContent:"space-between",children:[g(f,{children:[o(c,{color:le?"green":"gray",children:le?"\u276F ":" "}),g(c,{color:a.entry?.status==="watching"||a.entry?.status==="syncing"?"green":Xe.color,children:[a.entry?.status==="syncing"?Ie(x):a.entry?.status==="watching"?"\u25CF":Xe.dot," "]}),o(c,{color:le?"white":a.entry?void 0:"gray",bold:le,children:a.site.name})]}),o(c,{color:a.entry?.status==="syncing"?"green":"gray",children:sn})]})]},a.site.id)}),ae?o(c,{color:"gray",children:ke<X?` \u2193 ${X-ke} more`:" "}):null]})}),o(f,{marginTop:1,children:o(c,{color:"gray",children:"+ add site \xB7 a"})})]}),g(f,{flexDirection:"column",flexGrow:1,marginLeft:1,children:[o(f,{flexDirection:"column",borderStyle:"round",borderColor:"gray",paddingX:1,children:$?g(Fe,{children:[g(c,{children:[$.org.name?g(c,{color:"gray",children:[$.org.name," / "]}):null,o(c,{color:"cyan",bold:!0,children:$.site.name})]}),g(f,{marginTop:1,children:[o(c,{color:"gray",children:"status "}),T?o(c,{color:we(T.status).color,children:`: ${we(T.status).label}`}):o(c,{color:"gray",children:": idle \u2014 not syncing (press s to start)"})]}),g(f,{children:[o(c,{color:"gray",children:"role "}),o(c,{children:T?.role??$.saved.role??"\u2014"}),(T?.role??$.saved.role??"ADMIN")!=="ADMIN"?o(c,{color:"gray",children:" \xB7 build & publish need ADMIN"}):null]}),g(f,{children:[o(c,{color:"gray",children:"path "}),o(c,{wrap:"truncate-end",children:T?.viewsDir?_e(T.viewsDir):"\u2014"})]}),g(f,{children:[o(c,{color:"gray",children:"synced "}),o(c,{children:Oe(T?.lastSyncedAt)}),o(c,{color:"gray",children:" progress "}),o(c,{color:T?.status==="syncing"?"green":"gray",children:T?.status==="syncing"?`${me(T.progress)??0}%`:"\u2014"})]}),o(f,{marginTop:1,children:o(yr,{percent:T?T.status==="syncing"?me(T.progress)??0:T.status==="watching"?100:0:0})}),T?g(Fe,{children:[g(f,{marginTop:1,children:[o(c,{color:"gray",children:"build "}),o(c,{color:Te.color,children:`${Te.dot} ${Te.label}`}),o(c,{color:"gray",children:" last "}),o(c,{children:xe?"in progress":Oe(D?.at)})]}),g(f,{children:[o(c,{color:"gray",children:"url "}),D?.url?o(c,{color:"cyan",wrap:"truncate-end",children:D.url}):o(c,{color:"gray",children:"\u2014"})]}),D?.status==="error"&&D.errors?.length?g(f,{children:[o(c,{color:"red",children:"\u258C "}),o(c,{color:"red",wrap:"truncate-end",children:`\u2717 ${D.errors[0]}`})]}):null,xe?g(f,{children:[o(br,{frame:x}),o(c,{color:"gray",children:` ${D?.stage??"building"}`})]}):null]}):null]}):o(c,{color:"gray",children:"No site selected."})}),W?g(f,{flexDirection:"column",borderStyle:"round",borderColor:"blue",paddingX:1,marginTop:1,flexGrow:1,children:[o(c,{color:"cyan",bold:!0,children:"ADD A SITE"}),o(c,{children:"Approve it in your browser: pick a site, or create one."}),g(f,{marginTop:1,children:[o(c,{color:"gray",children:"Check the page shows "}),o(c,{color:"cyan",bold:!0,children:W.code})]}),o(c,{color:"gray",children:"Only approve a page that shows this code."}),g(f,{marginTop:1,children:[g(c,{color:"yellow",children:[Ie(x)," "]}),o(c,{color:"gray",children:"waiting for approval \xB7 esc to cancel"})]})]}):g(f,{flexDirection:"column",borderStyle:"round",borderColor:C==="logs"?"green":"gray",paddingX:1,marginTop:1,height:se+4,flexShrink:0,overflowY:"hidden",children:[g(f,{justifyContent:"space-between",children:[g(c,{children:[o(c,{color:"cyan",bold:!0,children:"LOGS \xB7 "}),o(c,{color:"white",children:$?.site.name??"\u2014"})]}),T&&F.length>0?C==="logs"?o(c,{color:"green",children:`\u2191\u2193 scroll \xB7 esc back${Se>0?` \xB7 ${ie}/${F.length}`:""}`}):F.length>se?o(c,{color:"gray",children:"tab to scroll"}):null:null]}),o(f,{flexDirection:"column",marginTop:1,children:!T||F.length===0?o(c,{color:"gray",children:"no activity yet"}):Je.map((a,v)=>g(c,{wrap:"truncate-end",children:[g(c,{color:"gray",children:[a.time," "]}),a.text]},ie-Je.length+v))})]})]})]}),W?.url?g(f,{flexDirection:"column",paddingX:1,marginTop:1,children:[o(c,{color:"gray",children:"No browser? Open this address on any device:"}),o(c,{color:"cyan",children:W.url})]}):null,e.message?o(f,{paddingX:1,children:g(c,{color:e.messageIsError?"red":"gray",wrap:"truncate-end",children:[e.messageIsError?"\u2717 ":"",e.message]})}):null,g(f,{flexDirection:"column",marginTop:1,children:[g(f,{paddingX:1,justifyContent:"space-between",children:[g(f,{flexWrap:"wrap",children:[o(O,{k:"\u2191\u2193",label:"select"}),o(O,{k:"/",label:"search"}),o(O,{k:"s",label:"start sync"}),o(O,{k:"d",label:"disconnect"}),o(O,{k:"a",label:"add site"}),o(O,{k:"ctrl+c",label:"quit",width:14})]}),o(O,{k:"?",label:"all keys",width:12,bg:Mt})]}),!_&&n.length>0?o(f,{paddingX:1,marginTop:1,flexWrap:"wrap",children:n.map((a,v)=>o(O,{k:String(v+1),label:a.label,bg:Rt},a.key))}):null]})]})}import{jsx as Sr}from"react/jsx-runtime";function Dt({controller:e}){return vr((t,n)=>{n.ctrl&&t==="c"&&e.exit()}),Sr(jt,{controller:e})}import*as Nt from"os";import*as We from"util";var xr={production:"https://9Q849fBKdCJcK6j8M5SHpWjt@s2790579.us-west-2a.betterstackdata.com/2790579",development:"https://oj8Je2g5rZhXAv6NZ3CxYHFb@s2790583.us-west-2a.betterstackdata.com/2790583"},re,At,Tr=()=>At??=import("@sentry/node").then(e=>(e.init({dsn:re.dsn,environment:re.environment,serverName:re.service,initialScope:{tags:{service:re.service}},defaultIntegrations:!1,tracesSampleRate:0,maxBreadcrumbs:0,normalizeDepth:3}),e)).catch(e=>{console.warn("[observability] provider failed to load:",e?.message)}),_t=({service:e,environment:t})=>{let n=xr[t];return n&&(re={dsn:n,environment:t,service:e}),!!n},Ot=(e,t)=>{Tr().then(n=>n?.captureException(e,t&&{extra:t}))},It=async e=>{await(await At)?.flush(e)};var $r=10*6e4,Er=3,Br=30,Pr=1e3;var ve=!1,Ne=!1;var Wt=console.error.bind(console),q=new Map,Ft=new WeakSet,ye={start:0,sent:0},Cr=()=>process.env.ENV||process.env.AWS_LAMBDA_FUNCTION_NAME?.match(/-(production|development)$/)?.[1],Gt=({service:e,environment:t=Cr(),quiet:n=!1})=>{ve=!!t&&_t({service:e,environment:t}),n||console.log(`[observability] ${e}: ${ve?`reporting errors (${t})`:"off"}`)},Ut=(e,t)=>{if(!(!ve||Ne)){Ne=!0;try{let n=jr(e);if(Ft.has(n))return;Ft.add(n);let r=Mr(Rr(n));if(r===!1)return;Ot(Dr(n),r?{...t,suppressed:r}:t)}catch(n){Wt("[observability] capture failed:",n?.message)}finally{Ne=!1}}},Ht=async(e=2e3)=>{if(ve)try{await It(e)}catch(t){Wt("[observability] flush failed:",t?.message)}};var Mr=e=>{let t=Date.now();t-ye.start>=6e4&&(ye={start:t,sent:0});let n=q.get(e),r=0;return(!n||t-n.start>=$r)&&(r=n?.dropped??0,n={start:t,sent:0,dropped:0},q.delete(e),q.set(e,n),q.size>Pr&&q.delete(q.keys().next().value)),n.sent>=Er||ye.sent>=Br?(n.dropped++,!1):(n.sent++,ye.sent++,r)},Rr=e=>{let t=e.stack?.split(`
11
- `).find(n=>/^\s*at /.test(n)&&!/node_modules|node:|[\\/]observability[\\/]/.test(n));return`${e.name}|${Lr(e.message)}|${t?.trim()??""}`},Lr=e=>e.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi,"<id>").replace(/\b[0-9a-f]{16,}\b/gi,"<id>").replace(/\d+/g,"<n>").slice(0,300),jr=e=>{if(e instanceof Error)return e;let t=new Error(typeof e=="string"?e:We.inspect(e,{depth:3,breakLength:1/0}));return t.name="NonErrorThrown",t},be=(()=>{try{return Nt.homedir()}catch{return""}})(),Dr=e=>{if(be.length<2||!`${e.message}
12
- ${e.stack}`.includes(be))return e;let t=new Error(e.message.split(be).join("~"));return t.name=e.name,t.stack=e.stack?.split(be).join("~"),t};import{jsx as Nr}from"react/jsx-runtime";var Ue=rt();Ue.version&&(console.log(fe),process.exit(0));var zt=(Ue.env??"production").toLowerCase();Gt({service:"cms-sync",environment:process.env.DO_NOT_TRACK==="1"?null:zt,quiet:!0});var qt=Ge.dirname(Ar(import.meta.url)),Or=Jt(Ge.join(qt,"AGENTS.md"),"utf-8"),Ir=Jt(Ge.join(qt,"build.mjs"),"utf-8");async function Fr(){let e=new pe(Or,Ir);e.basePath=Ue.path,await e.init(zt),process.stdout.isTTY&&process.stdout.write("\x1B[2J\x1B[3J\x1B[H");let t=_r(Nr(Dt,{controller:e}),{exitOnCtrlC:!1}),n=r=>{t.unmount(),console.log(r),process.exit(0)};e.once("exit",()=>n("\u2713 Stopped. Local site files were cleaned up.")),process.on("SIGINT",()=>{e.exit()}),process.on("SIGTERM",()=>{e.exit()}),await t.waitUntilExit()}Fr().catch(async e=>{let t=e;console.error("\u274C",t.body||t.message),t.body||(Ut(e),await Ht()),process.exit(1)});
10
+ `)}async function Nn(e,t,n,r,s){Ee.delete(b.resolve(e)),await h.remove(b.join(e,Ce)),r("\u{1F4E5} Fetching files...");let i=await ee(t,n,"GET","/get_files"),d={},a=0;for(let y of i){let w=b.join(e,y.path);await h.outputFile(w,y.content);let u=(await h.stat(w)).mtimeMs;a++,s?.(a,i.length),d[y.path]={mtimeMs:u,hash:z(y.content)}}return r(`\u2714\uFE0F Synced ${i.length} file(s).`),{fileMap:d,pulled:a}}import Wn from"path";import Gn from"chokidar";var Un=5e3,Hn=/(^|[/\\])(\.DS_Store|Thumbs\.db|desktop\.ini)$/,te=class{viewsDir;onSync;debounceMs;watcher=null;debounceTimer=null;isShuttingDown=!1;dirty=!1;syncInFlight=!1;constructor(t){this.viewsDir=t.viewsDir,this.onSync=t.onSync,this.debounceMs=t.debounceMs??Un}start(){if(this.watcher)return;let t=Wn.join(this.viewsDir,"src");this.watcher=Gn.watch([t],{persistent:!0,ignoreInitial:!0,ignored:Hn}).on("change",()=>this.scheduleSync()).on("add",()=>this.scheduleSync()).on("unlink",()=>this.scheduleSync()).on("addDir",()=>this.scheduleSync()).on("unlinkDir",()=>this.scheduleSync())}setShuttingDown(t){this.isShuttingDown=t}async stop(){this.debounceTimer&&(clearTimeout(this.debounceTimer),this.debounceTimer=null),this.watcher&&(await this.watcher.close(),this.watcher=null)}scheduleSync(){this.isShuttingDown||(this.dirty=!0,this.debounceTimer&&clearTimeout(this.debounceTimer),this.debounceTimer=setTimeout(()=>this.flush(),this.debounceMs))}async flush(){if(this.debounceTimer=null,!(!this.dirty||this.isShuttingDown)){if(this.syncInFlight){this.debounceTimer=setTimeout(()=>this.flush(),this.debounceMs);return}this.dirty=!1,this.syncInFlight=!0;try{await this.onSync()}catch(t){let n=t;console.error("\u274C Sync failed:",n.body||n.message)}finally{this.syncInFlight=!1}}}};import Me from"fs-extra";import Jn from"os";import yt from"path";var wt={localhost:"http://app.sleekcms.test/api",development:"https://app.sleekcms.dev/api",production:"https://app.sleekcms.com/api"};function ne(e){return wt[e]||wt.production}function Le(e){return ne(e).replace(/\/api$/,"")}var zn=yt.join(Jn.homedir(),"SleekCMS"),q=yt.join(zn,"auth.json"),Re=class extends Error{status;body;constructor(t,n,r=""){super(t),this.name="ApiError",this.status=n,this.body=r}};async function fe(e=q){let t=null;try{t=await Me.readJson(e)}catch{return{sites:[],hadLegacyLogin:!1}}return{sites:Array.isArray(t?.sites)?t.sites.filter(r=>r&&typeof r.token=="string"&&typeof r.siteId=="number"):[],hadLegacyLogin:typeof t?.userToken=="string"}}async function bt(e,t=q){await Me.outputJson(t,{sites:e},{spaces:2});try{await Me.chmod(t,384)}catch{}}async function je(e,t=q){let{sites:n}=await fe(t),r=[...n.filter(s=>!(s.env===e.env&&s.siteId===e.siteId)),e];return await bt(r,t),r}async function De(e,t,n=q){let{sites:r}=await fe(n),s=r.filter(i=>!(i.env===e&&i.siteId===t));return await bt(s,n),s}async function _e(e,t,n,r){let s=await fetch(ne(e)+r,{method:n,headers:{Authorization:`Bearer ${t}`}});if(!s.ok){let i=await s.text().catch(()=>"");throw new Re(`${n} ${r} \u2192 ${s.status}: ${i}`,s.status,i)}return s.json()}function qn(e){if(e==null)return"(unknown error)";if(typeof e=="string")return e;if(typeof e!="object")return String(e);let t=e,n=typeof t.file_path=="string"?t.file_path:"",r=typeof t.path=="string"?t.path:typeof t.filename=="string"?t.filename:"",s=n||r,i=t.line!=null?`:${t.line}`:"",d=t.column!=null?`:${t.column}`:"",a=typeof t.message=="string"&&t.message?t.message:typeof t.name=="string"&&t.name?t.name:"",y=n&&r&&r!==n?` (on page ${r})`:"";if(s)return`${s}${i}${d}${y}: ${a||"(no message)"}`;if(a)return a;try{return JSON.stringify(e)}catch{return"(unknown error)"}}async function vt(e,t){let n;try{n=await _e(e,t,"GET","/site_build")}catch{return null}if(!Array.isArray(n)||n.length===0)return null;let r=n[0],s=n.find(d=>typeof d.url=="string"&&d.url.length>0),i=Array.isArray(r.errors)?r.errors.map(qn):[];return i.length>0?{status:"error",mode:"preview",errors:i,url:s?.url||void 0,at:r.updated_at?Date.parse(r.updated_at):void 0}:s?{status:"success",mode:"preview",url:s.url||void 0,at:s.updated_at?Date.parse(s.updated_at):void 0}:null}import{createHash as Xn,randomBytes as Yn}from"crypto";var Kn="urn:sleekcms:grant-type:cli_poll",Vn="SleekCMS Sync",St=e=>e.toString("base64").replace(/=+$/,"").replace(/\+/g,"-").replace(/\//g,"_");function xt(){let e=St(Yn(32)),t=St(Xn("sha256").update(e).digest());return{verifier:e,challenge:t}}function Tt(e){let t=e.replace(/[^A-Za-z0-9]/g,"").slice(0,8).toUpperCase();return`${t.slice(0,4)}-${t.slice(4)}`}async function kt(e,t=fetch){let n={origin:Le(e),authorizeEndpoint:`${ne(e)}/oauth/authorize`};try{let r=await t(`${Le(e)}/.well-known/oauth-authorization-server`);if(!r.ok)return n;let s=await r.json();if(typeof s.issuer=="string"&&typeof s.authorization_endpoint=="string")return{origin:s.issuer.replace(/\/+$/,""),authorizeEndpoint:s.authorization_endpoint}}catch{}return n}function $t(e,t){let n=new URLSearchParams({response_type:"code",client_name:Vn,redirect_uri:`${e.origin}/oauth/cli-done`,code_challenge:t,code_challenge_method:"S256",resource:`${e.origin}/mcp/`});return`${e.authorizeEndpoint}?${n.toString()}`}var X=class extends Error{constructor(t){super(t),this.name="SignInError"}},Zn=e=>new Promise(t=>setTimeout(t,e));async function Et(e,t,n={}){let{timeoutMs:r=10*6e4,signal:s,fetchImpl:i=fetch,sleep:d=Zn}=n,a=n.intervalMs??2e3,y=Date.now()+r,w=`${ne(e)}/oauth/token`;for(;;){if(s?.aborted)throw new X("Cancelled.");let u=null;try{u=await i(w,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({grant_type:Kn,code_verifier:t})})}catch{u=null}if(u){let f=await u.json().catch(()=>({}));if(u.ok&&f.access_token)return f.access_token;if(u.status===429)a=Math.min(a*2,3e4);else if(f.error!=="authorization_pending")throw new X(f.error==="invalid_grant"?"That approval can no longer be used. Add the site again.":`Sign-in failed: ${f.error||`HTTP ${u.status}`}`)}if(Date.now()+a>y)throw new X("Timed out waiting for approval in the browser.");await d(a)}}function Qn(e){let t=e.split(".")[1];if(!t)throw new X("The server returned a token this version can't read.");return JSON.parse(Buffer.from(t,"base64url").toString("utf8"))}async function Bt(e,t){let n=Qn(t),r=await _e(e,t,"GET","/mcp/get_site");return{env:e,siteId:n.sid,slug:n.slug,name:r.name,org:{name:r.org},role:r.role||void 0,token:t,addedAt:Date.now()}}var Pt=500;function rr(){return new Date().toTimeString().slice(0,8)}function sr(e){let t=e;if(t.body){try{let n=JSON.parse(t.body);if(n&&typeof n.message=="string")return n.message;if(n&&typeof n.error=="string")return n.error}catch{}return t.body}return t.message}function Ct(e){return e instanceof N&&e.status===401}var ir=e=>({id:e.siteId,name:e.name,slug:e.slug});async function Mt(e){e&&(await re.remove(Ae.join(e,"src")),await re.remove(Ae.join(e,".cache","state.json")))}var pe=class extends er{env="production";saved=[];synced=new Map;connecting=null;message;messageIsError=!1;agentMd;buildScript;configPath;isShuttingDown=!1;basePath;pollOptions={};openBrowser=ge;constructor(t,n,r=q){super(),this.agentMd=t,this.buildScript=n,this.configPath=r}changed(){this.emit("change")}flash(t,n=!1){this.message=t,this.messageIsError=n,this.changed()}appendLog(t,n){let r=this.synced.get(t);r&&(r.logs.push({time:rr(),text:n}),r.logs.length>Pt&&r.logs.splice(0,r.logs.length-Pt),this.changed())}siteCount(){return this.synced.size}syncingCount(){let t=0;for(let n of this.synced.values())n.status==="syncing"&&t++;return t}watchingCount(){let t=0;for(let n of this.synced.values())(n.status==="watching"||n.status==="syncing")&&t++;return t}async init(t){this.env=t;let{sites:n,hadLegacyLogin:r}=await fe(this.configPath);this.saved=n.filter(s=>s.env===t),r&&this.flash("SleekCMS Sync now connects one site at a time \u2014 press a to add a site in your browser."),this.changed()}isSynced(t){return this.synced.has(t)}async connectSite(){if(this.connecting)return;let{verifier:t,challenge:n}=xt(),r=new AbortController;this.connecting={code:Tt(n),url:"",abort:r},this.changed();let s=$t(await kt(this.env),n);if(r.signal.aborted){this.connecting=null,this.changed();return}this.connecting.url=s,this.message=void 0,this.changed(),this.openBrowser(s)||this.flash("Couldn't open a browser \u2014 open the address shown to continue.",!0);let i;try{let a=await Et(this.env,t,{...this.pollOptions,signal:r.signal});i=await Bt(this.env,a)}catch(a){this.connecting=null,r.signal.aborted?this.changed():this.flash(sr(a),!0);return}this.connecting=null;let d=await je(i,this.configPath);this.saved=d.filter(a=>a.env===this.env),this.flash(`Added "${i.name}".`),this.synced.has(i.siteId)||await this.startSite(i)}cancelConnect(){this.connecting?.abort.abort()}async startSite(t){let n=ir(t);if(this.synced.has(n.id))return;let r={org:t.org,site:n,siteToken:t.token,role:t.role,viewsDir:"",status:"syncing",logs:[]};this.synced.set(n.id,r),this.appendLog(n.id,"\u2795 added \u2014 pulling files"),this.changed();try{let s=await Be({token:t.token,env:this.env,path:this.basePath,agentMd:this.agentMd,buildScript:this.buildScript,log:i=>this.appendLog(n.id,i),onProgress:(i,d)=>{r.progress={done:i,total:d},this.changed()}});this.noteRole(n.id,s.site.role),r.viewsDir=s.viewsDir,r.progress=void 0,r.status="watching",r.lastSyncedAt=Date.now(),r.lastMsg=`pulled ${s.pulled} file(s)`,r.watcher=new te({viewsDir:s.viewsDir,onSync:()=>this.runSync(n.id)}),r.watcher.start(),this.watchBuildStatus(n.id),this.seedLastBuild(n.id),this.appendLog(n.id,"\u{1F440} watching for changes")}catch(s){if(Ct(s))return void await this.revoke(n.id);let i=s;r.status="error",r.progress=void 0,r.lastMsg=i.body||i.message,this.appendLog(n.id,`\u274C ${i.body||i.message}`)}this.changed()}noteRole(t,n){let r=this.synced.get(t);if(!n||!r||r.role===n)return;r.role=n;let s=this.saved.find(i=>i.siteId===t);s&&(s.role=n,je({...s},this.configPath).catch(()=>{}))}async revoke(t){let n=this.synced.get(t),r=n?.site.name??this.saved.find(d=>d.siteId===t)?.name??"this site";n&&(n.watcher?.setShuttingDown(!0),await n.watcher?.stop(),await n.buildWatcher?.close(),this.synced.delete(t));let s=await De(this.env,t,this.configPath);this.saved=s.filter(d=>d.env===this.env);let i=n?.viewsDir?` Local files kept in ${n.viewsDir}.`:"";this.flash(`Access to "${r}" was revoked \u2014 press a to add it again.${i}`,!0)}async forgetSite(t){if(this.synced.has(t)){this.flash("Disconnect it first (d), then press x to forget it.",!0);return}let n=this.saved.find(s=>s.siteId===t)?.name??"the site",r=await De(this.env,t,this.configPath);this.saved=r.filter(s=>s.env===this.env),this.flash(`Forgot "${n}". To revoke its access everywhere, log out of all sessions in SleekCMS.`)}async removeSite(t){let n=this.synced.get(t);n&&(n.watcher?.setShuttingDown(!0),await n.watcher?.stop(),await n.buildWatcher?.close(),await Mt(n.viewsDir),this.synced.delete(t),this.changed())}buildSite(t,n=!1){let r=this.synced.get(t);if(!r||!r.viewsDir)return;if(r.role&&r.role!=="ADMIN"){this.flash(`Building and publishing are for site ADMINs \u2014 you're ${r.role} on "${r.site.name}".`,!0);return}let s=n?"live":"preview";this.writeBuildFile(r.viewsDir,{status:"building",mode:s,stage:"queued",url:r.lastBuild?.url,at:Date.now()}),tr("node",n?["build.mjs","--publish"]:["build.mjs"],{cwd:r.viewsDir,stdio:"ignore"}).on("error",a=>{this.writeBuildFile(r.viewsDir,{status:"error",mode:s,errors:[a.message],url:r.lastBuild?.url,at:Date.now()}),this.appendLog(t,`\u274C build: ${a.message}`)}),this.appendLog(t,n?"\u{1F680} publishing\u2026":"\u{1F528} building\u2026")}buildFile(t){return Ae.join(t,".cache","build.json")}async readBuildFile(t){try{let n=await re.readJson(this.buildFile(t));return n&&n.status?n:null}catch{return null}}async writeBuildFile(t,n){let r=this.buildFile(t),s=`${r}.tmp`;try{await re.outputJson(s,n),await re.rename(s,r)}catch{}}watchBuildStatus(t){let n=this.synced.get(t);!n||!n.viewsDir||(n.buildWatcher=nr.watch(this.buildFile(n.viewsDir),{ignoreInitial:!1}).on("add",()=>{this.readBuildStatus(t)}).on("change",()=>{this.readBuildStatus(t)}))}async readBuildStatus(t){let n=this.synced.get(t);if(!n)return;let r=await this.readBuildFile(n.viewsDir);if(!r)return;let s=n.lastBuild?.url;n.lastBuild={status:r.status,mode:r.mode??"preview",url:r.url??(r.status==="success"?void 0:s),errors:r.errors,stage:r.stage,at:r.at},this.changed()}async seedLastBuild(t){let n=this.synced.get(t);if(!n)return;let r=await vt(this.env,n.siteToken);if(!r)return;let s=this.synced.get(t);if(!s)return;let i=await this.readBuildFile(s.viewsDir);i?.at!=null&&(r.at==null||i.at>=r.at)||await this.writeBuildFile(s.viewsDir,r)}async refetch(t){await this.runSync(t,{flush:!0})}async runSync(t,n={}){let r=this.synced.get(t);if(!(!r||!r.viewsDir||r.status==="paused")){r.status="syncing",this.changed();try{let s=await Be({token:r.siteToken,env:this.env,viewsDir:r.viewsDir,agentMd:this.agentMd,buildScript:this.buildScript,flush:n.flush,log:i=>this.appendLog(t,i),onProgress:(i,d)=>{r.progress={done:i,total:d},this.changed()}});if(this.noteRole(t,s.site.role),r.progress=void 0,r.status="watching",r.lastSyncedAt=Date.now(),n.flush)r.lastMsg=`re-fetched ${s.pulled} file(s)`;else{let i=[s.renamed>0?`renamed ${s.renamed}`:"",s.pushed>0?`pushed ${s.pushed} file(s)`:""].filter(Boolean);r.lastMsg=i.length>0?i.join(", "):"up to date"}}catch(s){if(Ct(s))return void await this.revoke(t);let i=s;r.status="error",r.progress=void 0,r.lastMsg=i.body||i.message,this.appendLog(t,`\u274C ${i.body||i.message}`)}this.changed()}}async togglePause(t){let n=this.synced.get(t);!n||!n.viewsDir||(n.status==="paused"?(n.watcher=new te({viewsDir:n.viewsDir,onSync:()=>this.runSync(t)}),n.watcher.start(),n.status="watching",this.appendLog(t,"\u25B6 resumed \u2014 watching for changes")):(n.watcher?.setShuttingDown(!0),await n.watcher?.stop(),n.watcher=void 0,n.progress=void 0,n.status="paused",this.appendLog(t,"\u23F8 paused")),this.changed())}async cleanupAll(){this.cancelConnect();for(let t of this.synced.values())t.watcher?.setShuttingDown(!0),await t.watcher?.stop(),await t.buildWatcher?.close(),await Mt(t.viewsDir)}async exit(){this.isShuttingDown||(this.isShuttingDown=!0,await this.cleanupAll(),this.emit("exit"))}};import{useInput as xr}from"ink";import{Fragment as lr,useEffect as se,useMemo as ur,useRef as dr,useState as L}from"react";import{Box as p,Text as l,useInput as gr,useStdout as fr}from"ink";import pr from"ink-text-input";import{createRequire as or}from"node:module";var ar=or(import.meta.url),he=ar("../package.json").version;import cr from"node:os";function Oe(e){if(!e)return"\u2014";let t=cr.homedir();return e.startsWith(t)?"~"+e.slice(t.length):e}function Ie(e){if(!e)return"\u2014";let t=Math.floor((Date.now()-e)/1e3);if(t<5)return"just now";if(t<60)return`${t}s ago`;let n=Math.floor(t/60);if(n<60)return`${n}m ago`;let r=Math.floor(n/60);return r<24?`${r}h ago`:`${Math.floor(r/24)}d ago`}function we(e){return!e||e.total<=0?null:Math.round(e.done/e.total*100)}var me=["\u280B","\u2819","\u2839","\u2838","\u283C","\u2834","\u2826","\u2827","\u2807","\u280F"];function Fe(e){return me[(e%me.length+me.length)%me.length]}function ye(e){switch(e){case"syncing":return{dot:"\u25CF",color:"green",label:"syncing"};case"watching":return{dot:"\u25CB",color:"gray",label:"watching"};case"paused":return{dot:"\u2016",color:"yellow",label:"paused"};case"error":return{dot:"\u2717",color:"red",label:"error"}}}function Rt(e){if(!e)return{dot:"\xB7",color:"gray",label:"\u2014"};switch(e.status){case"building":return{dot:"\u2234",color:"yellow",label:"building"};case"error":return{dot:"\u2717",color:"red",label:"failed"};case"success":return{dot:"\u2713",color:"green",label:e.mode==="live"?"live":"preview"}}}import{Fragment as Ne,jsx as o,jsxs as g}from"react/jsx-runtime";var hr=(e,t)=>t<=0?0:Math.max(0,Math.min(e,t-1)),mr=16,wr=5,yr="greenBright",Lt="blueBright",jt="magentaBright";function Dt({k:e,bg:t=yr}){return o(l,{color:"black",backgroundColor:t,children:` ${e} `})}function O({k:e,label:t,width:n=16,bg:r}){return g(p,{width:n,children:[o(Dt,{k:e,bg:r}),o(l,{color:"gray",children:` ${t}`})]})}function E({k:e,label:t,bg:n}){return g(p,{marginBottom:1,children:[o(p,{width:9,children:o(Dt,{k:e,bg:n})}),o(l,{color:"gray",children:t})]})}function br({launchers:e}){return g(p,{flexDirection:"column",borderStyle:"round",borderColor:"green",paddingX:2,paddingY:1,width:66,children:[g(p,{justifyContent:"space-between",children:[o(l,{color:"green",bold:!0,children:"KEYBOARD SHORTCUTS"}),o(l,{color:"gray",children:"? or esc to close"})]}),g(p,{marginTop:1,children:[g(p,{flexDirection:"column",width:34,children:[o(l,{color:"cyan",bold:!0,children:"Navigate"}),o(E,{k:"\u2191\u2193",label:"move selection"}),o(E,{k:"tab",label:"focus logs \xB7 \u2191\u2193 scroll"}),o(E,{k:"esc",label:"clear filter / back"}),o(p,{marginTop:1,children:o(l,{color:"cyan",bold:!0,children:"Global"})}),o(E,{k:"/",label:"search sites"}),o(E,{k:"a",label:"add a site (browser)"}),o(E,{k:"?",label:"toggle this help",bg:Lt}),o(E,{k:"ctrl+c",label:"quit"})]}),g(p,{flexDirection:"column",flexGrow:1,children:[o(l,{color:"cyan",bold:!0,children:"Site actions"}),o(E,{k:"s",label:"start sync"}),o(E,{k:"d",label:"disconnect"}),o(E,{k:"x",label:"forget site"}),o(E,{k:"p",label:"pause / resume sync"}),o(E,{k:"b",label:"build preview"}),o(E,{k:"B",label:"publish live"}),o(E,{k:"o",label:"open preview url"}),e.map((t,n)=>o(E,{k:String(n+1),label:`open in ${t.label}`,bg:jt},t.key))]})]})]})}function vr({percent:e,width:t=28}){let n=Math.round(Math.max(0,Math.min(100,e))/100*t);return g(l,{children:[o(l,{color:"green",children:"\u2588".repeat(n)}),o(l,{color:"gray",children:"\u2591".repeat(Math.max(0,t-n))})]})}function Sr({frame:e,width:t=24,seg:n=6}){let r=t+n,s=e%r-n,i="";for(let d=0;d<t;d++)i+=d>=s&&d<s+n?"\u25AE":"\u25AF";return o(l,{color:"yellow",children:i})}function _t({controller:e}){let{stdout:t}=fr(),n=ur(()=>at(),[]),[,r]=L(0),[s,i]=L(()=>new Date),[d,a]=L(null),[y,w]=L(""),[u,f]=L(!1),[x,m]=L(0),[v,k]=L(!1),[A,W]=L(!1),[C,M]=L("list"),[K,j]=L(0),Je=()=>r(c=>c+1);se(()=>(e.on("change",Je),()=>{e.off("change",Je)}),[e]),se(()=>{let c,S=()=>{i(new Date),c=setTimeout(S,6e4-Date.now()%6e4)};return c=setTimeout(S,6e4-Date.now()%6e4),()=>clearTimeout(c)},[]);let V=e.saved.map(c=>({org:c.org,site:{id:c.siteId,name:c.name,slug:c.slug},saved:c,entry:e.synced.get(c.siteId)})).sort((c,S)=>(c.entry?0:1)-(S.entry?0:1)),G=e.connecting,I=y.trim().toLowerCase(),P=I?V.filter(c=>c.site.name.toLowerCase().includes(I)||(c.org.name||"").toLowerCase().includes(I)):V,U=P.findIndex(c=>c.site.id===d);U<0&&(U=0);let $=P[U],T=$?.entry,oe=Math.max(3,(t?.rows??30)-26),F=T?.logs??[],xe=Math.max(0,F.length-oe),Kt=Math.min(K,xe),ae=F.length-Kt,ze=F.slice(Math.max(0,ae-oe),ae),Vt=$?.site.id;se(()=>j(0),[Vt]);let D=T?.lastBuild,Te=D?.status==="building",ke=Rt(D),ce=Te||!!G;if(!ce){for(let c of e.synced.values())if(c.status==="syncing"){ce=!0;break}}se(()=>{if(!ce)return;let c=setInterval(()=>m(S=>S+1),120);return()=>clearInterval(c)},[ce]);let qe=P.findIndex(c=>!c.entry),H=Math.max(wr,(t?.rows??30)-mr),Z=P.length,le=Z>H,Zt=Math.floor(H/2),Q=le?Math.max(0,Math.min(U-Zt,Z-H)):0,$e=Math.min(Z,Q+H),Qt=P.slice(Q,$e),Xe=dr(!1);se(()=>{Xe.current||(Xe.current=!0,e.saved.length===0&&!e.connecting&&e.connectSite())},[e]);let ue=c=>{if(P.length===0)return;let S=hr(U+c,P.length);a(P[S].site.id)},en=()=>{if($){if($.entry){e.flash("Already syncing \u2014 press d to disconnect.");return}e.startSite($.saved)}},tn=()=>{$?.entry&&e.removeSite($.site.id)},nn=()=>{$&&e.forgetSite($.site.id)},rn=c=>{if(!T){e.flash("Press s to start syncing this site first.",!0);return}W(!0);let S=c.run(T.viewsDir);e.flash(S?`Opened ${c.label} at ${Oe(T.viewsDir)}`:`Couldn't open ${c.label}.`,!S)},sn=()=>{if(!T){e.flash("Press s to start syncing this site first.",!0);return}e.togglePause(T.site.id)},Ye=c=>{if(!T){e.flash("Press s to start syncing this site first.",!0);return}e.buildSite(T.site.id,c),e.flash(c?"Publishing \u2014 making this live\u2026":"Building preview\u2026")},on=()=>{let c=T?.lastBuild?.url;if(!c){e.flash("No build URL yet \u2014 press b to build.",!0);return}e.flash(ge(c)?`Opening ${c}`:"Couldn't open the browser.",!1)};gr((c,S)=>{if(v){(c==="?"||S.escape)&&k(!1);return}if(G&&S.escape)return void e.cancelConnect();if(u){S.escape?(f(!1),w(""),a(null)):S.return?f(!1):S.upArrow?ue(-1):S.downArrow&&ue(1);return}if(S.tab){C==="logs"?M("list"):T&&M("logs"),j(0);return}if(C==="logs"){if(S.escape){M("list"),j(0);return}if(S.upArrow)return void j(_=>Math.min(_+1,xe));if(S.downArrow)return void j(_=>Math.max(0,_-1))}if(S.escape){w(""),a(null);return}if(c==="?")return void k(!0);if(c==="/")return M("list"),void f(!0);if(c==="a"||c==="n")return void e.connectSite();if(S.upArrow)return ue(-1);if(S.downArrow)return ue(1);if(c==="s")return en();if(c==="d")return tn();if(c==="x")return nn();if(c==="p")return sn();if(c==="b")return Ye(!1);if(c==="B")return Ye(!0);if(c==="o")return on();if(c>="1"&&c<="9"){let _=n[parseInt(c,10)-1];if(_)return rn(_)}});let Ke=g(p,{justifyContent:"space-between",paddingX:1,children:[g(p,{children:[o(l,{color:"green",bold:!0,children:"\u25C6 SleekCMS"}),o(l,{color:"gray",children:` v${he} \u2502 `}),o(l,{children:e.env==="production"?"Sync":`Sync \xB7 ${e.env}`})]}),g(p,{children:[g(l,{color:"gray",children:[I?`${P.length}/${V.length}`:V.length," sites \xB7 "]}),g(l,{color:"green",children:[e.watchingCount()," watching"]}),o(l,{color:"gray",children:` \xB7 ${s.toTimeString().slice(0,5)}`})]})]});return v?g(p,{flexDirection:"column",children:[Ke,o(p,{marginTop:1,justifyContent:"center",children:o(br,{launchers:n})})]}):g(p,{flexDirection:"column",children:[Ke,g(p,{children:[g(p,{flexDirection:"column",borderStyle:"round",borderColor:"green",width:38,paddingX:1,children:[g(p,{justifyContent:"space-between",children:[o(l,{color:"green",bold:!0,children:"SITE WORKSPACES"}),V.length>H&&!u&&!I?o(l,{color:"gray",children:"/ filter"}):null]}),u?g(p,{children:[o(l,{color:"cyan",children:"/ "}),o(pr,{value:y,onChange:w,onSubmit:()=>f(!1),placeholder:"type to filter\u2026"})]}):I?o(l,{color:"gray",children:`/ ${y} \xB7 ${P.length} match${P.length===1?"":"es"}`}):null,o(p,{flexDirection:"column",marginTop:1,minHeight:le?H+3:void 0,children:P.length===0?o(l,{color:"gray",children:I?`no matches for \u201C${y}\u201D`:"no sites yet \u2014 press a to add one"}):g(Ne,{children:[le?o(l,{color:"gray",children:Q>0?` \u2191 ${Q} more`:" "}):null,Qt.map((c,S)=>{let _=Q+S,Ve=c.entry?ye(c.entry.status):{dot:"\xB7",color:"gray",label:"idle"},Ze=we(c.entry?.progress),an=c.entry?c.entry.status==="syncing"&&Ze!=null?`${Ze}%`:c.entry.status==="paused"?"paused":"--":"",de=_===U,cn=_===qe&&qe>0;return g(lr,{children:[cn?o(l,{color:"gray",children:"\u2500".repeat(34)}):null,g(p,{justifyContent:"space-between",children:[g(p,{children:[o(l,{color:de?"green":"gray",children:de?"\u276F ":" "}),g(l,{color:c.entry?.status==="watching"||c.entry?.status==="syncing"?"green":Ve.color,children:[c.entry?.status==="syncing"?Fe(x):c.entry?.status==="watching"?"\u25CF":Ve.dot," "]}),o(l,{color:de?"white":c.entry?void 0:"gray",bold:de,children:c.site.name})]}),o(l,{color:c.entry?.status==="syncing"?"green":"gray",children:an})]})]},c.site.id)}),le?o(l,{color:"gray",children:$e<Z?` \u2193 ${Z-$e} more`:" "}):null]})}),o(p,{marginTop:1,children:o(l,{color:"gray",children:"+ add site \xB7 a"})})]}),g(p,{flexDirection:"column",flexGrow:1,marginLeft:1,children:[o(p,{flexDirection:"column",borderStyle:"round",borderColor:"gray",paddingX:1,children:$?g(Ne,{children:[g(l,{children:[$.org.name?g(l,{color:"gray",children:[$.org.name," / "]}):null,o(l,{color:"cyan",bold:!0,children:$.site.name})]}),g(p,{marginTop:1,children:[o(l,{color:"gray",children:"status "}),T?o(l,{color:ye(T.status).color,children:`: ${ye(T.status).label}`}):o(l,{color:"gray",children:": idle \u2014 not syncing (press s to start)"})]}),g(p,{children:[o(l,{color:"gray",children:"role "}),o(l,{children:T?.role??$.saved.role??"\u2014"}),(T?.role??$.saved.role??"ADMIN")!=="ADMIN"?o(l,{color:"gray",children:" \xB7 build & publish need ADMIN"}):null]}),g(p,{children:[o(l,{color:"gray",children:"path "}),o(l,{wrap:"truncate-end",children:T?.viewsDir?Oe(T.viewsDir):"\u2014"})]}),g(p,{children:[o(l,{color:"gray",children:"synced "}),o(l,{children:Ie(T?.lastSyncedAt)}),o(l,{color:"gray",children:" progress "}),o(l,{color:T?.status==="syncing"?"green":"gray",children:T?.status==="syncing"?`${we(T.progress)??0}%`:"\u2014"})]}),o(p,{marginTop:1,children:o(vr,{percent:T?T.status==="syncing"?we(T.progress)??0:T.status==="watching"?100:0:0})}),T?g(Ne,{children:[g(p,{marginTop:1,children:[o(l,{color:"gray",children:"build "}),o(l,{color:ke.color,children:`${ke.dot} ${ke.label}`}),o(l,{color:"gray",children:" last "}),o(l,{children:Te?"in progress":Ie(D?.at)})]}),g(p,{children:[o(l,{color:"gray",children:"url "}),D?.url?o(l,{color:"cyan",wrap:"truncate-end",children:D.url}):o(l,{color:"gray",children:"\u2014"})]}),D?.status==="error"&&D.errors?.length?g(p,{children:[o(l,{color:"red",children:"\u258C "}),o(l,{color:"red",wrap:"truncate-end",children:`\u2717 ${D.errors[0]}`})]}):null,Te?g(p,{children:[o(Sr,{frame:x}),o(l,{color:"gray",children:` ${D?.stage??"building"}`})]}):null]}):null]}):o(l,{color:"gray",children:"No site selected."})}),G?g(p,{flexDirection:"column",borderStyle:"round",borderColor:"blue",paddingX:1,marginTop:1,flexGrow:1,children:[o(l,{color:"cyan",bold:!0,children:"ADD A SITE"}),o(l,{children:"Approve it in your browser: pick a site, or create one."}),g(p,{marginTop:1,children:[o(l,{color:"gray",children:"Check the page shows "}),o(l,{color:"cyan",bold:!0,children:G.code})]}),o(l,{color:"gray",children:"Only approve a page that shows this code."}),g(p,{marginTop:1,children:[g(l,{color:"yellow",children:[Fe(x)," "]}),o(l,{color:"gray",children:"waiting for approval \xB7 esc to cancel"})]})]}):g(p,{flexDirection:"column",borderStyle:"round",borderColor:C==="logs"?"green":"gray",paddingX:1,marginTop:1,height:oe+4,flexShrink:0,overflowY:"hidden",children:[g(p,{justifyContent:"space-between",children:[g(l,{children:[o(l,{color:"cyan",bold:!0,children:"LOGS \xB7 "}),o(l,{color:"white",children:$?.site.name??"\u2014"})]}),T&&F.length>0?C==="logs"?o(l,{color:"green",children:`\u2191\u2193 scroll \xB7 esc back${xe>0?` \xB7 ${ae}/${F.length}`:""}`}):F.length>oe?o(l,{color:"gray",children:"tab to scroll"}):null:null]}),o(p,{flexDirection:"column",marginTop:1,children:!T||F.length===0?o(l,{color:"gray",children:"no activity yet"}):ze.map((c,S)=>g(l,{wrap:"truncate-end",children:[g(l,{color:"gray",children:[c.time," "]}),c.text]},ae-ze.length+S))})]})]})]}),G?.url?g(p,{flexDirection:"column",paddingX:1,marginTop:1,children:[o(l,{color:"gray",children:"No browser? Open this address on any device:"}),o(l,{color:"cyan",children:G.url})]}):null,e.message?o(p,{paddingX:1,children:g(l,{color:e.messageIsError?"red":"gray",wrap:"truncate-end",children:[e.messageIsError?"\u2717 ":"",e.message]})}):null,g(p,{flexDirection:"column",marginTop:1,children:[g(p,{paddingX:1,justifyContent:"space-between",children:[g(p,{flexWrap:"wrap",children:[o(O,{k:"\u2191\u2193",label:"select"}),o(O,{k:"/",label:"search"}),o(O,{k:"s",label:"start sync"}),o(O,{k:"d",label:"disconnect"}),o(O,{k:"a",label:"add site"}),o(O,{k:"ctrl+c",label:"quit",width:14})]}),o(O,{k:"?",label:"all keys",width:12,bg:Lt})]}),!A&&n.length>0?o(p,{paddingX:1,marginTop:1,flexWrap:"wrap",children:n.map((c,S)=>o(O,{k:String(S+1),label:c.label,bg:jt},c.key))}):null]})]})}import{jsx as Tr}from"react/jsx-runtime";function At({controller:e}){return xr((t,n)=>{n.ctrl&&t==="c"&&e.exit()}),Tr(_t,{controller:e})}import*as Gt from"os";import*as Ge from"util";var kr={production:"https://9Q849fBKdCJcK6j8M5SHpWjt@s2790579.us-west-2a.betterstackdata.com/2790579",development:"https://oj8Je2g5rZhXAv6NZ3CxYHFb@s2790583.us-west-2a.betterstackdata.com/2790583"},ie,Ot,$r=()=>Ot??=import("@sentry/node").then(e=>(e.init({dsn:ie.dsn,environment:ie.environment,serverName:ie.service,initialScope:{tags:{service:ie.service}},defaultIntegrations:!1,tracesSampleRate:0,maxBreadcrumbs:0,normalizeDepth:3}),e)).catch(e=>{console.warn("[observability] provider failed to load:",e?.message)}),It=({service:e,environment:t})=>{let n=kr[t];return n&&(ie={dsn:n,environment:t,service:e}),!!n},Ft=(e,t)=>{$r().then(n=>n?.captureException(e,t&&{extra:t}))},Nt=async e=>{await(await Ot)?.flush(e)};var Br=10*6e4,Pr=3,Cr=30,Mr=1e3;var Se=!1,We=!1;var Ut=console.error.bind(console),Y=new Map,Wt=new WeakSet,be={start:0,sent:0},Rr=()=>process.env.ENV||process.env.AWS_LAMBDA_FUNCTION_NAME?.match(/-(production|development)$/)?.[1],Ht=({service:e,environment:t=Rr(),quiet:n=!1})=>{Se=!!t&&It({service:e,environment:t}),n||console.log(`[observability] ${e}: ${Se?`reporting errors (${t})`:"off"}`)},Jt=(e,t)=>{if(!(!Se||We)){We=!0;try{let n=_r(e);if(Wt.has(n))return;Wt.add(n);let r=Lr(jr(n));if(r===!1)return;Ft(Ar(n),r?{...t,suppressed:r}:t)}catch(n){Ut("[observability] capture failed:",n?.message)}finally{We=!1}}},zt=async(e=2e3)=>{if(Se)try{await Nt(e)}catch(t){Ut("[observability] flush failed:",t?.message)}};var Lr=e=>{let t=Date.now();t-be.start>=6e4&&(be={start:t,sent:0});let n=Y.get(e),r=0;return(!n||t-n.start>=Br)&&(r=n?.dropped??0,n={start:t,sent:0,dropped:0},Y.delete(e),Y.set(e,n),Y.size>Mr&&Y.delete(Y.keys().next().value)),n.sent>=Pr||be.sent>=Cr?(n.dropped++,!1):(n.sent++,be.sent++,r)},jr=e=>{let t=e.stack?.split(`
11
+ `).find(n=>/^\s*at /.test(n)&&!/node_modules|node:|[\\/]observability[\\/]/.test(n));return`${e.name}|${Dr(e.message)}|${t?.trim()??""}`},Dr=e=>e.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi,"<id>").replace(/\b[0-9a-f]{16,}\b/gi,"<id>").replace(/\d+/g,"<n>").slice(0,300),_r=e=>{if(e instanceof Error)return e;let t=new Error(typeof e=="string"?e:Ge.inspect(e,{depth:3,breakLength:1/0}));return t.name="NonErrorThrown",t},ve=(()=>{try{return Gt.homedir()}catch{return""}})(),Ar=e=>{if(ve.length<2||!`${e.message}
12
+ ${e.stack}`.includes(ve))return e;let t=new Error(e.message.split(ve).join("~"));return t.name=e.name,t.stack=e.stack?.split(ve).join("~"),t};import{jsx as Gr}from"react/jsx-runtime";var He=st();He.version&&(console.log(he),process.exit(0));var Xt=(He.env??"production").toLowerCase();Ht({service:"cms-sync",environment:process.env.DO_NOT_TRACK==="1"?null:Xt,quiet:!0});var Yt=Ue.dirname(Or(import.meta.url)),Fr=qt(Ue.join(Yt,"AGENTS.md"),"utf-8"),Nr=qt(Ue.join(Yt,"build.mjs"),"utf-8");async function Wr(){let e=new pe(Fr,Nr);e.basePath=He.path,await e.init(Xt),process.stdout.isTTY&&process.stdout.write("\x1B[2J\x1B[3J\x1B[H");let t=Ir(Gr(At,{controller:e}),{exitOnCtrlC:!1}),n=r=>{t.unmount(),console.log(r),process.exit(0)};e.once("exit",()=>n("\u2713 Stopped. Local site files were cleaned up.")),process.on("SIGINT",()=>{e.exit()}),process.on("SIGTERM",()=>{e.exit()}),await t.waitUntilExit()}Wr().catch(async e=>{let t=e;console.error("\u274C",t.body||t.message),t.body||(Jt(e),await zt()),process.exit(1)});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleekcms/sync",
3
- "version": "4.1.0",
3
+ "version": "4.2.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",