@sleekcms/sync 4.0.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 |
@@ -414,7 +418,7 @@ Content lives in `src/content/` as plain JSON. Edit any content file and save
414
418
 
415
419
  ## Site setup (`src/setup.json`)
416
420
 
417
- Site-wide decisions made once — the default page title, whether Tailwind is compiled, whether a sitemap is published, the cookie consent switch, and analytics tag ids — live in `src/setup.json`, mirrored from **Settings → Website** in the CMS. The file is JSON with a comment above each key explaining it:
421
+ Site-wide decisions made once — the default page title, whether Tailwind is compiled, whether a sitemap is published, the cookie consent switch, the chat bot switch, and analytics tag ids — live in `src/setup.json`, mirrored from **Settings → Website** in the CMS. The file is JSON with a comment above each key explaining it:
418
422
 
419
423
  ```json5
420
424
  {
@@ -422,6 +426,7 @@ Site-wide decisions made once — the default page title, whether Tailwind is co
422
426
  tw_enabled: true, // compile src/public/css/tailwind.css and inject it into every page
423
427
  sitemap_enabled: false, // publish sitemap.xml + robots.txt on every build
424
428
  cookie_consent: false, // consent banner; holds analytics + data-sleekcms-consent scripts until accepted
429
+ chat_enabled: false, // chat bot on every page, answering from the site's published pages
425
430
  analytics: { // tag ids injected into every page; include only the ones you use
426
431
  ga4: "G-XXXXXXXXXX",
427
432
  },
@@ -473,6 +478,23 @@ For images used in more than one place — logos, recurring icons, shared hero a
473
478
 
474
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')`.
475
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
+
476
498
  ### Images in markdown
477
499
 
478
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,14 +116,15 @@ 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). |
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. |
121
128
  | `analytics` | object | Marketing/analytics tag ids injected into every published page. |
122
129
 
123
130
  `analytics` is a flat map — include only the keys the site uses:
@@ -141,7 +148,7 @@ analytics: {
141
148
 
142
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).
143
150
 
144
- 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.
145
152
 
146
153
  ## 5. Common Tasks
147
154
 
@@ -172,6 +179,34 @@ Edit the matching file under `content/` — **do not** hard-code editable copy i
172
179
 
173
180
  All fields are optional — a missing value simply renders as `undefined`, so guard with `<% if (item.field) { %>` where it matters.
174
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
+
175
210
  ## 6. Models
176
211
 
177
212
  ### `.model` file format
@@ -202,8 +237,9 @@ Like JSON but with **no quotes**; each value is a field type name:
202
237
  | Model type | Content JSON value (what templates receive) |
203
238
  |---|---|
204
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 |
205
241
  | `richtext` | String of **HTML** — output with `<%- %>` |
206
- | `markdown` | String of **raw markdown** — convert with `<%- remark(item.x) %>` (see Helpers — rendering) |
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) |
207
243
  | `number` | Number |
208
244
  | `boolean` | `true` / `false` |
209
245
  | `emoji` | One emoji character string, e.g. `"😀"` (not free text) |
@@ -213,7 +249,7 @@ Like JSON but with **no quotes**; each value is a field type name:
213
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 |
214
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 |
215
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 |
216
- | `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` |
217
253
  | `json` | Any JSON |
218
254
  | `sheet` | Array of arrays |
219
255
  | `location` | `{ "markers": [{ "lat", "lng" }], "img": "<google static map url>" }` — `img` supports Google Static Maps params; markers are already part of the URL |
@@ -304,6 +340,10 @@ Give **every** image field a resolvable shortcut from the sources above — incl
304
340
 
305
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.)
306
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
+
307
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.
308
348
 
309
349
  ### Videos
@@ -320,6 +360,25 @@ Render in templates: `<%- embed(getVideo('hero-loop'), { size: '1280x720' }) %>`
320
360
 
321
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.
322
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
+
323
382
  ### Markdown content files (`.md`)
324
383
 
325
384
  A collection-page record is stored as Markdown with frontmatter instead of JSON when its model meets **all** of:
@@ -384,6 +443,8 @@ There is exactly **one** layout: `layouts/common.ejs`. It wraps every page autom
384
443
  </html>
385
444
  ```
386
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
+
387
448
  ### Helpers — content access
388
449
 
389
450
  | Function | Returns | Notes |
@@ -393,7 +454,8 @@ There is exactly **one** layout: `layouts/common.ejs`. It wraps every page autom
393
454
  | `getEntry(handle)` | Object \| Array | Entry by key. The `+` is optional — `getEntry('authors')` and `getEntry('authors+')` are equivalent. Single → object, collection → array |
394
455
  | `getSlugs(path)` | string[] | Slugs under a collection path |
395
456
  | `getImage(handle)` | Object \| undefined | Resolved image from `images.json` (bare handle, no `cms:` prefix) |
396
- | `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 |
397
459
  | `getContent(query?)` | Any | Full content payload, optionally filtered with JMESPath |
398
460
  | `path(page)` | String | URL path of a page object |
399
461
  | `url(pathOrPage?)` | String | Site origin; pass a path or page object for a full absolute URL |
@@ -405,11 +467,11 @@ There is exactly **one** layout: `layouts/common.ejs`. It wraps every page autom
405
467
  | `render(val, separator?)` | Render a block, entry, or array of them through its `blocks/<key>.ejs` / `entries/<key>.ejs` view. An `entries/<key>.ejs` view is only needed if you call `render()` on that entry — otherwise just use its data directly |
406
468
  | `remark(md, opts?)` | Markdown string → HTML string, with build-time highlighted code blocks, math and Mermaid diagrams (below) |
407
469
 
408
- Render every `markdown` field with `remark()`, whatever it holds today — editors add code, math and diagrams later; plain prose comes out as ordinary HTML with nothing injected. On top of standard markdown (including GFM tables, task lists and footnotes) it renders fenced code blocks (```` ```js ````) with syntax highlighting at build time, math written as `$e^{i\pi} + 1 = 0$` inline, `$$ … $$` on its own lines or a ```` ```math ```` fence (KaTeX), and ```` ```mermaid ```` fences as diagrams drawn in the visitor's browser. It injects only the CSS/JS the rendered markdown needs (KaTeX CSS, the code theme, the Mermaid runtime). **Never rebuild any of it yourself:** no highlight.js, Prism, KaTeX or Mermaid via `link()`/`script()`, and no `public/js` that highlights `pre code`, converts `language-mermaid` blocks or renders math.
470
+ Render every `markdown` field with `remark()`, whatever it holds today — editors add code, math and diagrams later; plain prose comes out as ordinary HTML with nothing injected. On top of standard markdown (including GFM tables, task lists and footnotes) it renders fenced code blocks (```` ```js ````) with syntax highlighting at build time, math written as `$e^{i\pi} + 1 = 0$` inline, `$$ … $$` on its own lines or a ```` ```math ```` fence (KaTeX), and ```` ```mermaid ```` fences as diagrams, drawn as SVG at build time. It injects only the CSS/JS the rendered markdown needs (KaTeX CSS, the code theme, the diagram styles). **Never rebuild any of it yourself:** no highlight.js, Prism, KaTeX or Mermaid via `link()`/`script()`, and no `public/js` that highlights `pre code`, converts `language-mermaid` blocks or renders math.
409
471
 
410
472
  - `opts.codeTheme` — a highlight.js theme name: default `github-dark`, or e.g. `github`, `atom-one-dark`, `base16/dracula`. The theme sets the token colors and the code box's background together, so pick code colors with `codeTheme`, never CSS: don't restyle `.hljs` / `pre code` colors or backgrounds, and don't clear `pre`'s background (`prose-pre:bg-transparent`) — a dark theme's light text on a light page all but disappears. `github-dark` suits the dark `pre` that `prose` draws; use `github` only when the design wants light code blocks. One theme per page.
411
473
  - A single-`$` pair is math only when it hugs its content (`$x^2$`), so prices like "$5 to $10" stay text. Escape a dollar that must stay literal as `\$`.
412
- - Mermaid uses its light default theme. On a dark design, start each diagram with frontmatter: `---`, `config:`, ` theme: dark`, `---` on their own lines above the diagram.
474
+ - Diagrams use Mermaid syntax (flowchart, sequence, class, state, ER, Gantt, pie and the other built-in types) and match the page's light or dark design on their own: don't style or theme them (no `theme` config, no CSS). Labels are plain text — no HTML.
413
475
 
414
476
  ```ejs
415
477
  <div class="prose prose-lg max-w-none"><%- remark(item.body) %></div>
@@ -431,7 +493,9 @@ Render `icon` fields or direct icon shortcuts with `svg()` — it inlines the sv
431
493
 
432
494
  > **Direct UI Icons vs. CMS-Editable Icons:**
433
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.
434
- > - **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.
435
499
 
436
500
  ### Helpers — video
437
501
 
@@ -461,7 +525,9 @@ Animation needs no `script()` call at all — `data-motion` attributes are built
461
525
  </section>
462
526
  ```
463
527
 
464
- **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.
465
531
 
466
532
  ## 9. SEO
467
533
 
@@ -485,7 +551,7 @@ Make every site strongly SEO- and sharing-friendly. The standard pattern is a re
485
551
 
486
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.
487
553
 
488
- 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.
489
555
 
490
556
  ## 10. Forms
491
557
 
@@ -600,7 +666,7 @@ Snipcart powers stores. Use it when the business **sells products** and the user
600
666
 
601
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.
602
668
 
603
- **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.
604
670
 
605
671
  **Tag scripts via `script()`** — views never contain raw `<script>` tags, so the category goes in the attributes object:
606
672
 
@@ -862,7 +928,7 @@ if (!matchMedia('(prefers-reduced-motion: reduce)').matches) {
862
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.
863
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.
864
930
  6. `richtext` → `<%- item.x %>`. `markdown` → `<%- remark(item.x) %>`. Never load highlight.js, KaTeX or Mermaid yourself.
865
- 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.
866
932
  8. Editable copy belongs in `content/`, not hard-coded in `.ejs`. Fixed micro-labels may stay in views.
867
933
  9. In page keys, `-` separates words and `_` means `/`. `_index` is the home page only; a section index page uses the plain section key.
868
934
  10. After edits, check `sync-errors.log` in the workspace root. New line = failure for that file; line removed = fixed.
@@ -870,6 +936,8 @@ if (!matchMedia('(prefers-reduced-motion: reduce)').matches) {
870
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.
871
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.
872
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).
873
- 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).
874
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.
875
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.0.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",