@sleekcms/sync 4.1.0 → 4.3.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
@@ -174,6 +174,8 @@ No manual deploy. No waiting. SleekCMS rebuilds on every sync and the preview re
174
174
  ```
175
175
  <site-name>/
176
176
  ├── AGENTS.md # AI agent reference (auto-generated)
177
+ ├── build.mjs # Build / publish the site: node build.mjs [--publish]
178
+ ├── upload.mjs # Upload images and documents: node upload.mjs <path>...
177
179
  ├── .vscode/
178
180
  │ ├── settings.json # Editor settings (site .json files open as JSON5)
179
181
  │ └── extensions.json # Recommends the JSON5 extension
@@ -195,7 +197,9 @@ No manual deploy. No waiting. SleekCMS rebuilds on every sync and the preview re
195
197
  │ ├── pages/<key>/<slug>.json # Collection page content (one file per item)
196
198
  │ ├── entries/<key>.json # Single entry content
197
199
  │ ├── entries/<key>[].json # Collection entry content (array)
198
- │ └── images.json # Reusable site-level images (handle → shortcut)
200
+ │ ├── images.json # Reusable site-level images (handle → shortcut)
201
+ │ ├── videos.json # Site-level videos (handle → stock ref or upload)
202
+ │ └── files.json # The site's files, read-only (handle → file, name, url)
199
203
  │
200
204
  ├── public/
201
205
  │ ├── css/<name>.css # Stylesheets
@@ -310,6 +314,8 @@ Models describe the shape of your content. They're JSON-like files with unquoted
310
314
  | `getPages(path, opts?)` | Pages where path starts with prefix; pass `{ collection: true }` for collection pages only |
311
315
  | `getEntry(handle)` | Entry by handle |
312
316
  | `getImage(handle)` | Site-level image by handle (from `images.json`) |
317
+ | `getVideo(handle)` | Site-level video by handle (from `videos.json`); render it with `embed(video, attr?)` |
318
+ | `getFile(handle)` | Site file by handle (from `files.json`): `{ name, url, size, type, ext }`, like a `file` field |
313
319
  | `getSlugs(path)` | Slugs under a collection path |
314
320
  | `path(page)` | Relative URL path of a page (e.g. `/blog/my-post`) |
315
321
  | `url(pathOrPage?)` | Site origin, or a full absolute URL when given a path string or page object |
@@ -474,6 +480,39 @@ For images used in more than one place — logos, recurring icons, shared hero a
474
480
 
475
481
  Then reference any of them from any `image` field with `"cms:<handle>"` (e.g. `"cms:logo"`). Templates can also fetch the resolved object directly via `getImage('logo')`.
476
482
 
483
+ ### Files (`src/content/files.json`)
484
+
485
+ 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. Add files with `node upload.mjs` (below) or in the CMS.
486
+
487
+ ```json
488
+ {
489
+ "lunch-menu": { "file": "lunch-menu.pdf", "name": "Lunch Menu", "url": "https://files.sleekcms.com/…/lunch-menu.pdf" }
490
+ }
491
+ ```
492
+
493
+ 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')`:
494
+
495
+ ```ejs
496
+ <% const menu = getFile('lunch-menu') %>
497
+ <% if (menu) { %><a href="<%= menu.url %>" download><%= menu.name %></a><% } %>
498
+ ```
499
+
500
+ ### Uploading images and documents (`upload.mjs`)
501
+
502
+ Every workspace has an `upload.mjs` next to `build.mjs`, so you or your AI agent can put images and documents from disk into the site without opening the CMS:
503
+
504
+ ```bash
505
+ node upload.mjs ~/Downloads/logo.png --handle logo # image → "cms:logo"
506
+ node upload.mjs photos/*.jpg # one image per file; handle = file name
507
+ node upload.mjs ~/Desktop/menu.pdf # document → Media > Files
508
+ node upload.mjs menu-v2.pdf --name menu.pdf # store it under another file name
509
+ ```
510
+
511
+ - **Images** (png, jpg, jpeg, webp, gif, svg, ico, up to 8 MB) are stored under a handle and added to `src/content/images.json`. Use them as `"cms:<handle>"` in image fields or `img('cms:<handle>')` in templates. Uploading to an existing handle replaces that image everywhere it's used.
512
+ - **Documents** (pdf, docx, xlsx, pptx, csv, tsv, txt, md, up to 10 MB) go to Media > Files under their file name, and `src/content/files.json` is updated. Uploading the same name again replaces the file.
513
+
514
+ The script prints the ref to use for each file (`--json` prints one JSON result per line instead). It exits `0` when everything uploaded, `1` when some uploads failed, and `2` when it couldn't run. Uploading needs an editor or admin on the site. Binary files copied into `src/` aren't synced; the sync log points to `upload.mjs` instead.
515
+
477
516
  ### Images in markdown
478
517
 
479
518
  Inside `markdown` fields, the same shortcuts work in standard image syntax. Append `|<alt>` for alt text and a `<width>x<height>` token to set the rendered dimensions (defaults to `600x400`):
package/dist/AGENTS.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # SleekCMS Site Builder — Agent Guide
2
2
 
3
+ > **The user's request states intent; this guide states the platform.** A request (or a site the user points you at) may describe a feature loosely or in its own words. Build the *intent* with the documented CMS convention every time: a form for scheduling a time is a booking form (§11), a store follows §13, a blog gets the `rss.xml` feed (§12), and images go through the image pipeline (§7). The request guides content, copy and design: follow it by default, and improve on it where there's a clear opportunity. This guide decides how features are wired. The goal is always a fully working site built on CMS features.
4
+
3
5
  ## 1. Mental Model
4
6
 
5
7
  SleekCMS is a headless CMS plus static site generator. There are no servers, no git repos, nothing to compile locally. You build a site by editing three kinds of files, which sync automatically to the platform:
@@ -51,6 +53,8 @@ Pages and entries are either **single** (one record) or a **collection** (many r
51
53
 
52
54
  Never guess at why something isn't rendering — read `sync-errors.log` first, then run `node build.mjs` and read its output.
53
55
 
56
+ A passing build proves the site compiles, not that its interactions work. For anything interactive (menus, tabs, carousels, toggles, forms), check the markup, handlers, responsive variants, active states and keyboard behaviour against the request. In your summary to the user, keep what you reviewed in code apart from what you actually tested in a browser.
57
+
54
58
  ## 3. File Naming Rules (applies to models, views, and content)
55
59
 
56
60
  1. Keys are **lowercase**, words separated by `-` (dash). Example: `getting-started`.
@@ -69,10 +73,11 @@ Never guess at why something isn't rendering — read `sync-errors.log` first, t
69
73
  | `docs_getting-started` | `/docs/getting-started` |
70
74
  | `docs_guides+` | `/docs/guides/<slug>` |
71
75
  | `rss.xml` | `/rss.xml` (no layout) |
76
+ | `redirect.txt` | `/redirect.txt` (no layout) — redirect rules, see §5 |
72
77
 
73
78
  For collection pages, the `<slug>` is the content file's name: `content/pages/blog+/my-post.json` → `/blog/my-post`. Renaming the file renames the URL.
74
79
 
75
- **Renaming.** Rename a page, entry, or block by `mv`-ing only its model file — a collection too (`blog+.model`, not its folder). The sync renames it on the server and moves its view, content, and records to match: `models/pages/about.model` → `about-us.model` moves `/about` to `/about-us`. A collection record is the exception: its content file name is its slug, so to change one record's URL, rename that file within its folder — `content/pages/blog+/old-post.json` → `new-post.json` moves just that record from `/blog/old-post` to `/blog/new-post`. Keep slugs lowercase and dash-separated, and keep the file in its collection's folder. The kind and `+` can't change. Update hard-coded links and `getEntry()` calls that use the old name.
80
+ **Renaming.** Rename a page, entry, or block by `mv`-ing only its model file — a collection too (`blog+.model`, not its folder). The sync renames it on the server and moves its view, content, and records to match: `models/pages/about.model` → `about-us.model` moves `/about` to `/about-us`. A collection record is the exception: its content file name is its slug, so to change one record's URL, rename that file within its folder — `content/pages/blog+/old-post.json` → `new-post.json` moves just that record from `/blog/old-post` to `/blog/new-post`. Keep slugs lowercase and dash-separated, and keep the file in its collection's folder. The kind and `+` can't change. Update hard-coded links and `getEntry()` calls that use the old name, and add a redirect for a page URL that visitors or search engines may already know (§5).
76
81
 
77
82
  ## 4. Folder Structure (under `src/`)
78
83
 
@@ -97,6 +102,7 @@ content/entries/<key>.json Content for a single entry (object)
97
102
  content/entries/<key>+.json Collection entry — ONE file holding a JSON array of all items
98
103
  content/images.json Site-level reusable images: handle → shortcut (§7)
99
104
  content/videos.json Site-level videos: handle → pexels:/pixabay: ref (user uploads show as embed_url) (§7)
105
+ content/files.json The site's files (Media > Files), read-only: handle → { file, name, url } (§7)
100
106
 
101
107
  setup.json Site setup (JSON, comments allowed): default page title, Tailwind on/off, sitemap on/off, cookie consent on/off, analytics tag ids — edit values in place, never delete
102
108
  ```
@@ -110,13 +116,13 @@ setup.json Site setup (JSON, comments allowed): default pag
110
116
  - **Public URL** (what `link()`/`script()` and HTML `href`/`src` take): `/css/styles.css`, `/js/app.js`.
111
117
  - **Page route** (what visitors see): `/about`, `/blog/<slug>`. Computed from the page key.
112
118
 
113
- **Site setup (`setup.json`)** — the set-once site decisions, mirrored from Settings > Website in the CMS. Each key is explained by the comment above it in the file:
119
+ **Site setup (`setup.json`)** — the set-once site decisions, the same values as Manage Site in the CMS. Each key is explained by the comment above it in the file:
114
120
 
115
121
  | Key | Value | Effect |
116
122
  |---|---|---|
117
- | `title` | string or `null` | Default `<title>` for pages that don't set their own (via `title()` or the SEO block). `null` = the site name. |
123
+ | `title` | string or `null` | Default `<title>` for pages that don't set their own (via `title()` or the SEO block). `null` = the site name. Max 255 characters. |
118
124
  | `tw_enabled` | boolean | `true` compiles `css/tailwind.css` and injects it into every page — required for any site styled with Tailwind utilities. |
119
- | `sitemap_enabled` | boolean | `true` publishes `sitemap.xml` + `robots.txt` on every build. Turn it on for public sites and SEO work. |
125
+ | `sitemap_enabled` | boolean | `true` publishes `sitemap.xml` + `robots.txt` on every build. Turn it on for public sites and SEO work. Which pages are *excluded* is a CMS UI setting (Manage Site > Search Visibility), never this file. |
120
126
  | `cookie_consent` | boolean | `true` injects a consent banner into every page and holds the analytics tags, plus any script or iframe tagged `data-sleekcms-consent`, until the visitor accepts its category. A toggle only — banner text, position and styling are authored in HTML (§14). |
121
127
  | `chat_enabled` | boolean | `true` adds the chat bot to every published page; it answers visitor questions from the site's published pages (re-read on every publish). A toggle only — the welcome message, knowledge files and instructions are managed in the CMS. `false` removes it from the next publish and stops open chats right away. |
122
128
  | `analytics` | object | Marketing/analytics tag ids injected into every published page. |
@@ -142,7 +148,7 @@ analytics: {
142
148
 
143
149
  Omit a key (or the whole `analytics` object) to leave that integration alone; write `analytics: {}` to clear every one. Unrecognized keys and unusable values are dropped with a warning, and a malformed id is skipped at build time rather than breaking the build. Put tag ids here rather than pasting vendor snippets into a layout — the build injects the right script for each id, and with `cookie_consent: true` it gates them behind the banner automatically (§14).
144
150
 
145
- Edit values in place and keep the keys. Keys you omit keep their stored values; unknown keys are ignored. Which pages are excluded from the sitemap is a CMS UI setting, not a key in this file. Never delete the file.
151
+ Edit values in place and keep the keys. Keys you omit keep their stored values. An unknown key is dropped with a warning rather than failing the save, but warnings don't reach `sync-errors.log`, so match key names to the comments in the file. Never delete the file.
146
152
 
147
153
  ## 5. Common Tasks
148
154
 
@@ -173,6 +179,34 @@ Edit the matching file under `content/` — **do not** hard-code editable copy i
173
179
 
174
180
  All fields are optional — a missing value simply renders as `undefined`, so guard with `<% if (item.field) { %>` where it matters.
175
181
 
182
+ ### Redirect old URLs
183
+
184
+ When a page's URL changes (a rename, a restructure, a rebuild that retires old paths), keep inbound links working with a `redirect.txt` page. Like `rss.xml`, its key has an extension, so it is emitted raw with no layout (§3). Each line is one rule, `from to [status]` (`/old-about /about 301`). `from` must match the path exactly, and rules apply before a 404.
185
+
186
+ **`models/pages/redirect.txt.model`**
187
+ ```
188
+ {
189
+ redirects: [
190
+ {
191
+ from: text,
192
+ to: link,
193
+ status: text
194
+ }
195
+ ]
196
+ }
197
+ ```
198
+
199
+ **`content/pages/redirect.txt.json`**
200
+ ```json
201
+ { "redirects": [{ "from": "/old-about", "to": "/about", "status": "301" }] }
202
+ ```
203
+
204
+ **`pages/redirect.txt.ejs`** — one rule per line:
205
+ ```ejs
206
+ <% (item.redirects || []).forEach(function (r) { %><%= r.from %> <%= r.to %> <%= r.status || 301 %>
207
+ <% }) %>
208
+ ```
209
+
176
210
  ## 6. Models
177
211
 
178
212
  ### `.model` file format
@@ -203,6 +237,7 @@ Like JSON but with **no quotes**; each value is a field type name:
203
237
  | Model type | Content JSON value (what templates receive) |
204
238
  |---|---|
205
239
  | `text`, `paragraph`, `code`, `color`, `link` | String |
240
+ | `guid` | String — a unique id (e.g. order numbers, external ids). The CMS editor pre-fills a new GUID for each record created in the UI. When you write content, give each record its own unique string, e.g. a UUID |
206
241
  | `richtext` | String of **HTML** — output with `<%- %>` |
207
242
  | `markdown` | String of **raw markdown** — convert with `<%- remark(item.x) %>`, which renders GFM (tables, task lists, footnotes), syntax-highlighted code fences, math (`$…$`, `$$…$$`) and ```` ```mermaid ```` diagrams as SVG (see Helpers — rendering) |
208
243
  | `number` | Number |
@@ -214,7 +249,7 @@ Like JSON but with **no quotes**; each value is a field type name:
214
249
  | `image` | Shortcut string (preferred when writing) or resolved `{ "url", "alt" }` object — see §7. Shortcuts only resolve in `image`/`icon` fields — in a `text` field the string stays literal text |
215
250
  | `icon` | Svg icon. Use for icons the user is expected to edit/swap in the CMS (e.g. features/services). Fill with placeholder `"iconify:<prefix:name or search>"` (or `"cms:<handle>"` pointing at an svg); render with `svg(item.icon)`. Fixed UI icons don't need fields — call `svg('iconify:...')` directly in templates |
216
251
  | `video` | Ref string (preferred when writing) or the resolved object you read back. Write `"cms:<handle>"` to reuse a video from `content/videos.json` (or a user upload by its handle), or `"pexels:<search>\|<title>"` / `"pixabay:<search>\|<title>"` for a one-off — resolved server-side (see §7). **Never invent** `video_id`/`embed_url` objects; render with `embed()` only |
217
- | `file` | Write a plain file name, e.g. `"brochure.pdf"` (pdf, docx, xlsx, pptx, csv, tsv, txt, md — no path, no URL, never an object). On save the CMS attaches the site file with that name, or creates a small placeholder file the user replaces in the CMS UI; reads back as the file name. Render a link with `item.x.url` / `item.x.name` |
252
+ | `file` | Write a plain file name, e.g. `"brochure.pdf"` (pdf, docx, xlsx, pptx, csv, tsv, txt, md — no path, no URL, never an object). A listed file's handle from `content/files.json` works too, and wins over a file with that name. On save the CMS attaches that file, or (for a file name nothing matches) creates a small placeholder file the user replaces in the CMS UI; reads back as the file name. Render a link with `item.x.url` / `item.x.name` |
218
253
  | `json` | Any JSON |
219
254
  | `sheet` | Array of arrays |
220
255
  | `location` | `{ "markers": [{ "lat", "lng" }], "img": "<google static map url>" }` — `img` supports Google Static Maps params; markers are already part of the URL |
@@ -305,6 +340,10 @@ Give **every** image field a resolvable shortcut from the sources above — incl
305
340
 
306
341
  then reference with `"cms:logo"` in any `image`/`icon` field, or render directly in templates — `svg('cms:logo', { class: 'h-8 w-auto' })` for SVGs/icons, `img('cms:hero', '1920x600')` for photos. (`getImage('logo')` returns the resolved image object for a bare handle — rarely needed; the helpers take the value or `cms:` ref directly.)
307
342
 
343
+ Each handle's value is an image source from the list above, never another handle: `"logo": "cms:new-mark"` is ignored. To change what a handle shows, change its own value; every `cms:<handle>` on the site follows.
344
+
345
+ **The user's own images:** upload them from disk with `node upload.mjs` (see *Uploading images and documents* below). It stores the image under a handle and prints the `cms:<handle>` ref to use. Never point a field at a local path: a local path is not an image source, and `url:` needs a public URL. If the image isn't on this machine, use `url:<public url>` when the user gives you one. Otherwise ship a stock placeholder in the slot and tell the user to upload the image in the CMS.
346
+
308
347
  **Images inside markdown:** use the same shortcuts in standard syntax: `![alt](pexels:doctor)`. Append `|<alt>` for alt text and an optional `<W>x<H>` token to set rendered size (default `600x400`): `![doctor](pexels:doctor|Friendly doctor 800x600)`. Refs are rewritten to CDN URLs on save.
309
348
 
310
349
  ### Videos
@@ -321,6 +360,42 @@ Render in templates: `<%- embed(getVideo('hero-loop'), { size: '1280x720' }) %>`
321
360
 
322
361
  Content `video` fields accept the same refs: `"cms:<handle>"` reuses a declared (or user-uploaded) video by handle — preferred when the same video appears in more than one place — while a direct `"pexels:<search>|<title>"` / `"pixabay:<search>|<title>"` creates a record-local video. Stock videos read back as refs; user-uploaded videos read back as objects — echo those untouched.
323
362
 
363
+ ### Files
364
+
365
+ `content/files.json` lists the site's files (Media > Files) as `handle → { file, name, url }`: `file` is the file name, `name` its display name, `url` its public download link.
366
+
367
+ ```json
368
+ { "lunch-menu": { "file": "lunch-menu.pdf", "name": "Lunch Menu", "url": "https://files.sleekcms.com/…/lunch-menu.pdf" } }
369
+ ```
370
+
371
+ It's read-only: edits to it are never saved, and every sync replaces it with the site's current list. Check it before writing a new file name.
372
+
373
+ A `file` field takes a listed handle (`"lunch-menu"`) or file name (`"lunch-menu.pdf"`). Render a file no field points at with `getFile(handle)`, which returns the same `{ name, url, size, type, ext }` as a `file` field (or `undefined` for an unknown handle):
374
+
375
+ ```ejs
376
+ <% const menu = getFile('lunch-menu') %>
377
+ <% if (menu) { %><a href="<%= menu.url %>" download><%= menu.name %></a><% } %>
378
+ ```
379
+
380
+ **The user's own documents:** upload them from disk with `node upload.mjs` (below); the file is listed in `content/files.json` right away. If the document isn't on this machine, a `file` field with a new file name gets a placeholder the user replaces in the CMS (Media > Files).
381
+
382
+ ### Uploading images and documents (`upload.mjs`)
383
+
384
+ When the user gives you an image or document as a file on disk (a logo, team photos, a menu PDF, a price list), upload it. Run this from the workspace root, with one or more paths:
385
+
386
+ ```bash
387
+ node upload.mjs ~/Downloads/logo.png --handle logo # image → cms:logo
388
+ node upload.mjs photos/*.jpg # images; handle = file name without extension
389
+ node upload.mjs ~/Desktop/Lunch\ Menu.pdf # document → Media > Files
390
+ node upload.mjs menu-v2.pdf --name lunch-menu.pdf # store it under another file name
391
+ ```
392
+
393
+ - **Images** (png, jpg, jpeg, webp, gif, svg, ico, up to 8 MB) become site images under a handle. The handle is `--handle`, or by default the file name lowercased and dashed (`Team Photo.jpg` → `team-photo`). The script adds the handle to `content/images.json`. Reference the image as `"cms:<handle>"` in any `image` field, or with `img('cms:<handle>')` / `src('cms:<handle>')` in a view. Uploading to an existing handle replaces that image everywhere it's used.
394
+ - **Documents** (pdf, docx, xlsx, pptx, csv, tsv, txt, md, up to 10 MB) are added to Media > Files under their file name, and `content/files.json` is rewritten to list them. Put the printed file name (or its handle) in a `file` field, or render it with `getFile('<handle>')`. Uploading the same file name again replaces that file.
395
+ - The script prints the ref to use for each file. Copy it from the output rather than guessing it, because the server cleans up file names. Add `--json` for one JSON result per line. Exit code `0` means every file uploaded, `1` means some failed (the reason is printed), and `2` means it couldn't run.
396
+ - Uploading needs an EDITOR or ADMIN on the site.
397
+ - Never copy images or documents into `src/`. Binary files there are not synced; the sync log just points you back to `upload.mjs`.
398
+
324
399
  ### Markdown content files (`.md`)
325
400
 
326
401
  A collection-page record is stored as Markdown with frontmatter instead of JSON when its model meets **all** of:
@@ -385,6 +460,8 @@ There is exactly **one** layout: `layouts/common.ejs`. It wraps every page autom
385
460
  </html>
386
461
  ```
387
462
 
463
+ **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.
464
+
388
465
  ### Helpers — content access
389
466
 
390
467
  | Function | Returns | Notes |
@@ -394,7 +471,8 @@ There is exactly **one** layout: `layouts/common.ejs`. It wraps every page autom
394
471
  | `getEntry(handle)` | Object \| Array | Entry by key. The `+` is optional — `getEntry('authors')` and `getEntry('authors+')` are equivalent. Single → object, collection → array |
395
472
  | `getSlugs(path)` | string[] | Slugs under a collection path |
396
473
  | `getImage(handle)` | Object \| undefined | Resolved image from `images.json` (bare handle, no `cms:` prefix) |
397
- | `getVideo(handle)` | Object \| undefined | Video from `videos.json` |
474
+ | `getVideo(handle)` | Object \| undefined | Video from `videos.json` as `{ title, source, video_id, embed_url }` — render with `embed()` |
475
+ | `getFile(handle)` | Object \| undefined | File from `files.json` as `{ name, url, size, type, ext }`, like a `file` field |
398
476
  | `getContent(query?)` | Any | Full content payload, optionally filtered with JMESPath |
399
477
  | `path(page)` | String | URL path of a page object |
400
478
  | `url(pathOrPage?)` | String | Site origin; pass a path or page object for a full absolute URL |
@@ -432,7 +510,9 @@ Render `icon` fields or direct icon shortcuts with `svg()` — it inlines the sv
432
510
 
433
511
  > **Direct UI Icons vs. CMS-Editable Icons:**
434
512
  > - **Static UI icons** (chevrons, arrows, badges, buttons, decorative bullet checks): render directly in `.ejs` with `svg('iconify:<family>:<name>', { class: '...' })` — no model field or `images.json` entry required.
435
- > - **User-editable icons** (site logo, service/feature icons): when the user is expected to customize/swap the icon in the CMS, expose it via `content/images.json` (for site handles like `"logo"`) or an `icon: icon` model field, providing a placeholder `"iconify:..."` in content so the site ships visually complete. Render with `svg('cms:logo')` or `svg(item.icon)`.
513
+ > - **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.
514
+ > - **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.
515
+ > - **User-uploaded images, logos included,** may be raster images or SVGs. Render them with `img()` and give them `image` model fields. Reserve `svg()` and `icon` fields for assets known to be SVG.
436
516
 
437
517
  ### Helpers — video
438
518
 
@@ -462,7 +542,9 @@ Animation needs no `script()` call at all — `data-motion` attributes are built
462
542
  </section>
463
543
  ```
464
544
 
465
- **CSS/JS rules:** include assets only via `link()` / `script()` — never raw `<link>`/`<script>` tags in views. Exception: creating `css/tailwind.css` enables Tailwind v4 — the file is the Tailwind *input CSS*, compiled against the rendered HTML and injected automatically; never `link()` it. Whenever you write it, keep `@import "tailwindcss";` as its first statement (plus the two `@plugin` lines: `@plugin "@tailwindcss/typography";` and `@plugin "@tailwindcss/forms";`) — dropping the import silently removes all Tailwind styling. Define the site's fonts/colors/radii as `@theme` tokens there (giving utilities like `font-display` / `bg-brand-600`) with a `/* DESIGN */` header comment recording the design direction. v4, not v3: custom utilities go in `@utility name { … }`, never `@layer utilities { .name { … } }` (a v3 pattern that yields "Cannot apply unknown utility class"); config is CSS-first — there is no `tailwind.config.js`. Default to a polished Tailwind design with a deliberate design direction.
545
+ **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`.
546
+
547
+ **Style every page with Tailwind v4 utility classes in the view's `class` attribute**, built from those `@theme` tokens. Layout, spacing, color, type, borders, shadows, gradients, hover/focus states and responsive behaviour are all utilities, so `tailwind.css` stays the site's one stylesheet and the whole design re-themes from it. Keyframes, decorative pseudo-element layers and repeated multi-property effects live in `tailwind.css` too (`@theme --animate-*`, `@layer base`, `@utility`). Default to a polished Tailwind design with a deliberate design direction.
466
548
 
467
549
  ## 9. SEO
468
550
 
@@ -486,7 +568,7 @@ Make every site strongly SEO- and sharing-friendly. The standard pattern is a re
486
568
 
487
569
  Add `seo: block(seo)` to page models and render it **once from the layout head**: `<% if (item.seo) { %><%- render(item.seo) %><% } %>` in `layouts/common.ejs` (as in the layout example above) — not per page view.
488
570
 
489
- Three site-level SEO settings live in `setup.json` (§4), not in views: `sitemap_enabled: true` publishes `sitemap.xml` + `robots.txt` on every build; `title` is the site's default `<title>` for pages whose SEO block leaves the title empty (`null` = the site name); and a Google Search Console verification token goes in `analytics.gsc`. Sitemap page exclusions are chosen in the CMS UI — there is no file setting for them.
571
+ Three site-level SEO settings live in `setup.json` (§4), not in views: `sitemap_enabled: true` publishes `sitemap.xml` + `robots.txt` on every build; `title` is the site's default `<title>` for pages whose SEO block leaves the title empty (`null` = the site name); and a Google Search Console verification token goes in `analytics.gsc`. Sitemap page exclusions are chosen in the CMS UI (Manage Site > Search Visibility) — there is no file setting for them.
490
572
 
491
573
  ## 10. Forms
492
574
 
@@ -601,7 +683,7 @@ Snipcart powers stores. Use it when the business **sells products** and the user
601
683
 
602
684
  Use this when the user wants a **cookie banner** — GDPR / ePrivacy consent for tracking scripts and third-party embeds. One switch turns it on; everything else is HTML. Tag each script or iframe that needs consent with `data-sleekcms-consent="<category>"`, and at build time SleekCMS ships it inert, injects a consent banner and runtime into every page, and activates it only after the visitor accepts that category. Choices are stored in a first-party cookie and honoured on every return visit. **Never add a banner speculatively** — only when the user asks, or the site clearly needs one.
603
685
 
604
- **Turn it on** — `cookie_consent: true` in `setup.json` (§4), the same switch as Settings > Website > Analytics in the CMS. Off (the default): nothing is injected and tagged scripts run as normal.
686
+ **Turn it on** — `cookie_consent: true` in `setup.json` (§4), the same switch as Manage Site > Tracking & Analytics in the CMS. Off (the default): nothing is injected and tagged scripts run as normal.
605
687
 
606
688
  **Tag scripts via `script()`** — views never contain raw `<script>` tags, so the category goes in the attributes object:
607
689
 
@@ -863,7 +945,7 @@ if (!matchMedia('(prefers-reduced-motion: reduce)').matches) {
863
945
  4. `file` field values are plain file names ending in a supported extension (`"brochure.pdf"`) — the CMS attaches or placeholder-creates the file server-side; never write objects or URLs.
864
946
  5. Never invent `video_id`/`embed_url` objects — render videos with `embed()` only. Videos are set via refs: `content/videos.json` handles for shared videos, and `cms:<handle>` / provider refs in `video` fields (§7); values that read back as embed URLs or objects are user uploads — leave them untouched.
865
947
  6. `richtext` → `<%- item.x %>`. `markdown` → `<%- remark(item.x) %>`. Never load highlight.js, KaTeX or Mermaid yourself.
866
- 7. CSS/JS via `link()`/`script()` only; `css/tailwind.css` is auto-injected — never `link()` it. When writing tailwind.css, `@import "tailwindcss";` must stay as its first statement — dropping it silently removes all Tailwind styling.
948
+ 7. CSS/JS via `link()`/`script()` only; `css/tailwind.css` is auto-injected — never `link()` it. When writing tailwind.css, `@import "tailwindcss";` must stay as its first statement — dropping it silently removes all Tailwind styling. Style views with Tailwind v4 utilities in `class`, built from the `@theme` tokens; `tailwind.css` is the site's one stylesheet and also holds keyframes, `@layer base` and `@utility` effects.
867
949
  8. Editable copy belongs in `content/`, not hard-coded in `.ejs`. Fixed micro-labels may stay in views.
868
950
  9. In page keys, `-` separates words and `_` means `/`. `_index` is the home page only; a section index page uses the plain section key.
869
951
  10. After edits, check `sync-errors.log` in the workspace root. New line = failure for that file; line removed = fixed.
@@ -871,6 +953,8 @@ if (!matchMedia('(prefers-reduced-motion: reduce)').matches) {
871
953
  12. Image shortcuts (`"iconify:..."`, `"pexels:..."`, etc.) resolve **only** in `image`/`icon`-typed fields. CMS-editable icons are declared `icon: icon`, never `icon: text` — render with `svg(item.icon)`; fixed UI icons need no field: `svg('iconify:...')` directly in the view.
872
954
  13. Use entries (`entry(key+)`) for shared content (authors, testimonials, nav, footer) — not blocks. Block models cannot contain `block(...)` or `stack` fields; use blocks only when a group of fields also benefits from a reusable view.
873
955
  14. Always create an RSS feed for blogs (`rss.xml` key) and make every site strongly SEO- and sharing-friendly (reusable `seo` block rendered once from the layout head).
874
- 15. Site-wide setup — default page title, Tailwind on/off, sitemap on/off, cookie consent on/off, analytics tag ids — lives in `setup.json` (§4). Change values there rather than hard-coding a site title into the layout, and never delete the file. Sitemap page exclusions are UI-only.
956
+ 15. Site-wide setup — default page title, Tailwind on/off, sitemap on/off, cookie consent on/off, analytics tag ids — lives in `setup.json` (§4). Change values there rather than hard-coding a site title into the layout, and never delete the file. Sitemap page exclusions are UI-only (Manage Site > Search Visibility).
875
957
  16. Cookie consent (§14) is a `setup.json` switch plus `data-sleekcms-consent` attributes — tag third-party scripts through `script({ …, 'data-sleekcms-consent': 'category' })`, never a raw tag, and add a footer "Cookie settings" link. Only when the user asks for a banner.
876
958
  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.
959
+ 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.
960
+ 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 He}from"node:fs";import{fileURLToPath as Fr}from"node:url";import xe from"node:path";import{render as Nr}from"ink";import{execSync as it,spawn as B}from"child_process";import{program as tt,Option as nt}from"commander";function ot(){return tt.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 nt("-v, --version","output the version number").hideHelp()).addOption(new nt("-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),tt.opts()}function L(e){let t=process.platform==="win32"?`where ${e}`:`which ${e}`;try{return it(t,{stdio:"ignore"}),!0}catch{return!1}}function at(e){if(process.platform!=="darwin")return!1;try{return it(`osascript -e 'id of application "${e}"'`,{stdio:"ignore"}),!0}catch{return!1}}function dn(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 rt=[{name:"VS Code",cmd:"code",args:e=>["-n",e]},{name:"Cursor",cmd:"cursor",args:e=>["-n",e]}];function st(e,t){let r=(t?rt.filter(s=>s.cmd===t):rt).find(s=>L(s.cmd));return r?(dn(r.cmd,r.args(e)),r.name):null}function gn(e){if(process.platform==="darwin")return B("open",["-a","Terminal",e],{detached:!0,stdio:"ignore"}).unref(),!0;if(process.platform==="win32"){if(L("wt"))B("wt.exe",["-d",e],{detached:!0,stdio:"ignore"}).unref();else{let t=L("powershell")?"powershell.exe":"cmd.exe";B("cmd.exe",["/c","start","",t],{cwd:e,detached:!0,stdio:"ignore"}).unref()}return!0}return!1}function pn(e){return at("iTerm")?(B("open",["-a","iTerm",e],{detached:!0,stdio:"ignore"}).unref(),!0):!1}function fn(e){return`'${e.replace(/'/g,"'\\''")}'`}var hn=[{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 ct(e,t){if(!L(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 L("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 ${fn(e)} && ${t}; exec bash`;for(let r of hn)if(L(r.cmd))try{return B(r.cmd,r.args(n),{cwd:e,detached:!0,stdio:"ignore"}).unref(),!0}catch{}return!1}function mn(e){return ct(e,"claude")}function wn(e){return ct(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 lt(){let e=process.platform==="darwin",t=process.platform==="win32",n=[];return(e||t)&&n.push({key:"terminal",label:"terminal",run:gn}),at("iTerm")&&n.push({key:"iterm",label:"iterm",run:pn}),L("code")&&n.push({key:"code",label:"vscode",run:r=>st(r,"code")!==null}),L("cursor")&&n.push({key:"cursor",label:"cursor",run:r=>st(r,"cursor")!==null}),L("claude")&&n.push({key:"claude",label:"claude",run:mn}),L("pi")&&n.push({key:"pi",label:"pi",run:wn}),n}import{EventEmitter as nr}from"node:events";import{spawn as rr}from"node:child_process";import re from"fs-extra";import Oe from"path";import sr from"chokidar";import{createHash as bn}from"crypto";import h from"fs-extra";import vn from"os";import b from"path";var ut={localhost:"http://app.sleekcms.test/api/mcp",development:"https://app.sleekcms.dev/api/mcp",production:"https://app.sleekcms.com/api/mcp"},Sn=["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"],W=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 u=await i.text();throw new W(`${n} ${r} \u2192 ${i.status}: ${u}`,i.status,u)}return i.json()}var xn=["AGENT.md","CLAUDE.md",b.join(".vscode","copilot-instructions.md")],Tn="mrmlnc.vscode-json5";async function kn(e,t){t&&(await h.outputFile(b.join(e,"AGENTS.md"),t),await Promise.all(xn.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:[Tn]},null,2))}async function dt(e,t,n){if(!n)return;let r=b.join(e,t),s=new RegExp(`${t.replace(/\./g,"\\.")} v\\d+`),i=n.match(s)?.[0];if(await h.pathExists(r)){let u=(await h.readFile(r,"utf-8")).match(s)?.[0];if(u&&u===i)return}await h.outputFile(r,n)}async function $n(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 En(e,t){let n=`${t.name.substr(0,20)} ${t.id}`.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,""),r=e||b.join(vn.homedir(),"SleekCMS");return b.resolve(r,n)}async function Pe(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=ut[s]||ut.production,u=await ee(i,t,"GET","/get_site"),a=e.viewsDir?b.resolve(e.viewsDir):En(e.path,u);await h.ensureDir(a),await $n(a,t,{env:s,slug:String(u.id)},n),await Promise.all(Sn.map(v=>h.ensureDir(b.join(a,v)))),await dt(a,"build.mjs",e.buildScript),await dt(a,"upload.mjs",e.uploadScript);let y=b.join(a,".cache","state.json");e.flush&&await h.remove(y);let w=!await h.pathExists(y),d=w?{}:(await h.readJson(y)).fileMap||{},g=0,x=0,m=0;if(w)({fileMap:d,pulled:x}=await Gn(a,i,t,n,r)),await kn(a,e.agentMd);else{let v=await Nn(a,d,i,t,n);m=v.renamed,g=await Rn(a,d,i,t,n,r,v.held),await Pn(a,d,i,t,n)}return await h.outputJson(y,{fileMap:d},{spaces:2}),{viewsDir:a,site:u,isFirstRun:w,pushed:g,pulled:x,renamed:m}}var z=e=>bn("sha1").update(e).digest("hex"),Bn=new Set([".DS_Store","Thumbs.db","desktop.ini"]);async function Ce(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 u of i){if(Bn.has(u.name))continue;let a=b.join(s,u.name);u.isDirectory()?await r(a):u.isFile()&&n.push(b.relative(e,a).replace(/\\/g,"/"))}}return await r(t),n}var J="src/content/files.json";async function Pn(e,t,n,r,s){let i;try{i=await ee(n,r,"GET","/get_files?kind=files")}catch(w){if(w instanceof W&&w.status>=400&&w.status<500)return;s(`\u26A0\uFE0F Couldn't refresh ${J}: ${yt(w)}`);return}let u=i.find(w=>w.path===J);if(!u)return;let a=b.join(e,J);(await h.pathExists(a)?await h.readFile(a,"utf-8"):null)!==u.content&&(await h.outputFile(a,u.content),t[J]={mtimeMs:(await h.stat(a)).mtimeMs,hash:z(u.content)},s(`\u{1F4C4} Refreshed ${J}`))}var Cn=e=>e.subarray(0,8e3).includes(0),gt=new Set;function pt(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 ft=10;function Mn(e){let t=[];for(let n=0;n<e.length;n+=ft)t.push(e.slice(n,n+ft));return t}async function Rn(e,t,n,r,s,i,u=new Set){let a=[];for(let g of await Ce(e)){if(u.has(g)||g===J)continue;let x=b.join(e,g),m=t[g],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);if(Cn(k)){let D=`${g}@${v.mtimeMs}`;gt.has(D)||(gt.add(D),s(`\u26A0\uFE0F ${g} is a binary file and isn't synced. Upload it with: node upload.mjs ${g}`));continue}let C=k.toString("utf-8");C.trim()&&a.push({rel:g,full:x,stat:v,content:C,prior:m})}if(a.length===0)return 0;a.sort((g,x)=>pt(g.rel)-pt(x.rel)),s(`\u2B06 pushing ${a.length} file(s)`),i?.(0,a.length);let y=await bt(e),w=0,d=0;for(let g of Mn(a)){let x;try{x=await ee(n,r,"POST","/save_files",g.map(v=>({path:v.rel,content:v.content})))}catch(v){s(`\u274C Error saving files: ${yt(v)}`),s(`\u23F8 ${a.length-d} file(s) not pushed \u2014 saving any file retries them.`);break}let m=Ln(x);for(let v=0;v<g.length;v++){let k=g[v],C=jn(k,v,x,m);if(!C){let D="No save response returned";y[k.rel]=D,s(`\u274C Error saving ${k.rel}: ${D}`);continue}if(C.error){y[k.rel]=C.error,s(`\u274C Error saving ${k.rel}: ${C.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++}d+=g.length,i?.(d,a.length)}return d===0?0:(await vt(e,y),w)}function Ln(e){let t=new Map;for(let n of e)n.path&&t.set(n.path,n);return t}function jn(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 ht=/^src\/(?:(?:models|views)\/(?:pages|entries|blocks)|content\/(?:pages|entries))\//,Dn={PAGE:"pages",ENTRY:"entries",BLOCK:"blocks"},_n=e=>e.split("/").slice(0,3).join("/");function An(e,t,n){let r=new Map,s=(i,u)=>{let a=`${_n(i)}|${u}`;return r.has(a)||r.set(a,{from:[],to:[],hash:u}),r.get(a)};for(let i of e)s(i,n[i].hash).from.push(i);for(let[i,u]of t)s(i,u).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 On(e,t){let n=new Map,r=(s,i,u,a,y)=>{let w=n.get(s);w?(w.pairs.push(y),i<w.rank&&Object.assign(w,{from:u,to:a,rank:i})):n.set(s,{from:u,to:a,rank:i,pairs:[y]})};for(let s of e){let[,i,u,...a]=s.from.split("/"),y=s.to.split("/").slice(3);if(i==="content"&&u==="pages"&&a.length>1){let[d,...g]=a,[x,...m]=y;if(m.length===0)continue;x===d?r(`record:${s.from}`,0,s.from,s.to,s):g.join("/")===m.join("/")&&![...t].some(v=>v.startsWith(`src/content/pages/${d}/`))&&r(`pages:${d}`,3,`src/content/pages/${d}`,`src/content/pages/${x}`,s);continue}let w=a.join("/").replace(/\.[^./]+$/,"");r(`${u}:${w}`,i==="models"?0:i==="views"?1:2,s.from,s.to,s)}return[...n.values()].map(({from:s,to:i,pairs:u})=>({from:s,to:i,pairs:u}))}function mt(e,t){if(e.slug!==void 0&&e.new_slug!==void 0){let u=`src/content/pages/${e.key}/`,a=t.startsWith(u)?t.slice(u.length).match(/^(.+)\.(json|md)$/):null;return a&&a[1].toLowerCase()===e.slug.toLowerCase()?`${u}${e.new_slug}.${a[2]}`:null}let n=Dn[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 In(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 Fn(e,t,n,r){let s=d=>b.join(e,d),i=b.join(e,"src"),u=d=>b.resolve(e,d).startsWith(i+b.sep),a=async(d,g)=>{t[d]={mtimeMs:(await h.stat(s(d))).mtimeMs,hash:g}},y=d=>In(b.dirname(s(d)),b.join(e,...d.split("/").slice(0,3))),w=[];for(let d of r){let g=mt(n.rename,d.from)??d.to;u(g)&&(g!==d.to&&(await h.move(s(d.to),s(g),{overwrite:!0}),await y(d.to),w.push(d.to)),delete t[d.from],w.push(d.from),await a(g,d.hash))}for(let d of new Set([...await Ce(e),...Object.keys(t)])){let g=mt(n.rename,d);if(!g||!u(g))continue;let x=await h.pathExists(s(d));if(x&&await h.pathExists(s(g)))continue;let m=t[d];if(delete t[d],w.push(d),!x)continue;let v=!!m?.hash&&m.mtimeMs===(await h.stat(s(d))).mtimeMs;await h.move(s(d),s(g)),v&&await a(g,m.hash),await y(d)}for(let d of n.files||[]){let g=t[d.path];if(!g||!u(d.path))continue;let x=await h.stat(s(d.path)).catch(()=>null);!x||x.mtimeMs!==g.mtimeMs||await h.readFile(s(d.path),"utf-8")!==d.content&&(await h.outputFile(s(d.path),d.content),await a(d.path,z(d.content)))}return w}function wt(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 yt(e){return e instanceof W?e.body.match(/<title>([^<]*)<\/title>/i)?.[1].trim()||wt(e).replace(/\s+/g," "):e instanceof Error?e.message:String(e)}async function Nn(e,t,n,r,s){let i=new Set,u=await Ce(e),a=new Set(u),y=Object.keys(t).filter(m=>!a.has(m)&&t[m].hash&&ht.test(m));if(y.length===0)return{renamed:0,held:i};let w=new Map;for(let m of u){if(t[m]||!ht.test(m))continue;let v=await h.readFile(b.join(e,m),"utf-8");v.trim()&&w.set(m,z(v))}let d=On(An(y,w,t),a);if(d.length===0)return{renamed:0,held:i};let g=await bt(e),x=0;for(let m of d){let v;try{v=await ee(n,r,"POST","/rename",{from:m.from,to:m.to})}catch(M){let R=M,K=wt(R);if(R.status>=400&&R.status<500)s(`\u26A0\uFE0F Couldn't rename ${m.from} \u2192 ${m.to}: ${K} Saving it as a new file.`);else{for(let _ of m.pairs)i.add(_.to),g[_.to]=`rename from ${_.from} failed: ${K}`;s(`\u274C Error renaming ${m.from} \u2192 ${m.to}: ${K}`)}continue}let k=await Fn(e,t,v,m.pairs);for(let M of[...k,...m.pairs.map(R=>R.to)])delete g[M];let[C,...D]=v.message.split(`
7
+ `);s(`\u{1F500} ${C}`);for(let M of D)s(` ${M}`);x++}return await vt(e,g),{renamed:x,held:i}}var Me="sync-errors.log",Be=new Map;async function bt(e){let t=b.resolve(e),n=Be.get(t);if(n)return n;let r=await Wn(t);return Be.set(t,r),r}async function Wn(e){let t=b.join(e,Me);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 vt(e,t){let n=b.join(e,Me),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 Gn(e,t,n,r,s){Be.delete(b.resolve(e)),await h.remove(b.join(e,Me)),r("\u{1F4E5} Fetching files...");let i=await ee(t,n,"GET","/get_files"),u={},a=0;for(let y of i){let w=b.join(e,y.path);await h.outputFile(w,y.content);let d=(await h.stat(w)).mtimeMs;a++,s?.(a,i.length),u[y.path]={mtimeMs:d,hash:z(y.content)}}return r(`\u2714\uFE0F Synced ${i.length} file(s).`),{fileMap:u,pulled:a}}import Un from"path";import Hn from"chokidar";var Jn=5e3,zn=/(^|[/\\])(\.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??Jn}start(){if(this.watcher)return;let t=Un.join(this.viewsDir,"src");this.watcher=Hn.watch([t],{persistent:!0,ignoreInitial:!0,ignored:zn}).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 Re from"fs-extra";import qn from"os";import xt from"path";var St={localhost:"http://app.sleekcms.test/api",development:"https://app.sleekcms.dev/api",production:"https://app.sleekcms.com/api"};function ne(e){return St[e]||St.production}function je(e){return ne(e).replace(/\/api$/,"")}var Xn=xt.join(qn.homedir(),"SleekCMS"),q=xt.join(Xn,"auth.json"),Le=class extends Error{status;body;constructor(t,n,r=""){super(t),this.name="ApiError",this.status=n,this.body=r}};async function pe(e=q){let t=null;try{t=await Re.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 Tt(e,t=q){await Re.outputJson(t,{sites:e},{spaces:2});try{await Re.chmod(t,384)}catch{}}async function De(e,t=q){let{sites:n}=await pe(t),r=[...n.filter(s=>!(s.env===e.env&&s.siteId===e.siteId)),e];return await Tt(r,t),r}async function _e(e,t,n=q){let{sites:r}=await pe(n),s=r.filter(i=>!(i.env===e&&i.siteId===t));return await Tt(s,n),s}async function Ae(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 Le(`${n} ${r} \u2192 ${s.status}: ${i}`,s.status,i)}return s.json()}function Yn(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}`:"",u=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}${u}${y}: ${a||"(no message)"}`;if(a)return a;try{return JSON.stringify(e)}catch{return"(unknown error)"}}async function kt(e,t){let n;try{n=await Ae(e,t,"GET","/site_build")}catch{return null}if(!Array.isArray(n)||n.length===0)return null;let r=n[0],s=n.find(u=>typeof u.url=="string"&&u.url.length>0),i=Array.isArray(r.errors)?r.errors.map(Yn):[];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 Kn,randomBytes as Vn}from"crypto";var Zn="urn:sleekcms:grant-type:cli_poll",Qn="SleekCMS Sync",$t=e=>e.toString("base64").replace(/=+$/,"").replace(/\+/g,"-").replace(/\//g,"_");function Et(){let e=$t(Vn(32)),t=$t(Kn("sha256").update(e).digest());return{verifier:e,challenge:t}}function Bt(e){let t=e.replace(/[^A-Za-z0-9]/g,"").slice(0,8).toUpperCase();return`${t.slice(0,4)}-${t.slice(4)}`}async function Pt(e,t=fetch){let n={origin:je(e),authorizeEndpoint:`${ne(e)}/oauth/authorize`};try{let r=await t(`${je(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 Ct(e,t){let n=new URLSearchParams({response_type:"code",client_name:Qn,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"}},er=e=>new Promise(t=>setTimeout(t,e));async function Mt(e,t,n={}){let{timeoutMs:r=10*6e4,signal:s,fetchImpl:i=fetch,sleep:u=er}=n,a=n.intervalMs??2e3,y=Date.now()+r,w=`${ne(e)}/oauth/token`;for(;;){if(s?.aborted)throw new X("Cancelled.");let d=null;try{d=await i(w,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({grant_type:Zn,code_verifier:t})})}catch{d=null}if(d){let g=await d.json().catch(()=>({}));if(d.ok&&g.access_token)return g.access_token;if(d.status===429)a=Math.min(a*2,3e4);else if(g.error!=="authorization_pending")throw new X(g.error==="invalid_grant"?"That approval can no longer be used. Add the site again.":`Sign-in failed: ${g.error||`HTTP ${d.status}`}`)}if(Date.now()+a>y)throw new X("Timed out waiting for approval in the browser.");await u(a)}}function tr(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 Rt(e,t){let n=tr(t),r=await Ae(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 Lt=500;function ir(){return new Date().toTimeString().slice(0,8)}function or(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 jt(e){return e instanceof W&&e.status===401}var ar=e=>({id:e.siteId,name:e.name,slug:e.slug});async function Dt(e){e&&(await re.remove(Oe.join(e,"src")),await re.remove(Oe.join(e,".cache","state.json")))}var fe=class extends nr{env="production";saved=[];synced=new Map;connecting=null;message;messageIsError=!1;agentMd;buildScript;uploadScript;configPath;isShuttingDown=!1;basePath;pollOptions={};openBrowser=ge;constructor(t,n={},r=q){super(),this.agentMd=t,this.buildScript=n.build,this.uploadScript=n.upload,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:ir(),text:n}),r.logs.length>Lt&&r.logs.splice(0,r.logs.length-Lt),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 pe(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}=Et(),r=new AbortController;this.connecting={code:Bt(n),url:"",abort:r},this.changed();let s=Ct(await Pt(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 Mt(this.env,t,{...this.pollOptions,signal:r.signal});i=await Rt(this.env,a)}catch(a){this.connecting=null,r.signal.aborted?this.changed():this.flash(or(a),!0);return}this.connecting=null;let u=await De(i,this.configPath);this.saved=u.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=ar(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 Pe({token:t.token,env:this.env,path:this.basePath,agentMd:this.agentMd,buildScript:this.buildScript,uploadScript:this.uploadScript,log:i=>this.appendLog(n.id,i),onProgress:(i,u)=>{r.progress={done:i,total:u},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(jt(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,De({...s},this.configPath).catch(()=>{}))}async revoke(t){let n=this.synced.get(t),r=n?.site.name??this.saved.find(u=>u.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 _e(this.env,t,this.configPath);this.saved=s.filter(u=>u.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 _e(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 Dt(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()}),rr("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 Oe.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=sr.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 kt(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 Pe({token:r.siteToken,env:this.env,viewsDir:r.viewsDir,agentMd:this.agentMd,buildScript:this.buildScript,uploadScript:this.uploadScript,flush:n.flush,log:i=>this.appendLog(t,i),onProgress:(i,u)=>{r.progress={done:i,total:u},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(jt(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 Dt(t.viewsDir)}async exit(){this.isShuttingDown||(this.isShuttingDown=!0,await this.cleanupAll(),this.emit("exit"))}};import{useInput as kr}from"ink";import{Fragment as dr,useEffect as se,useMemo as gr,useRef as pr,useState as j}from"react";import{Box as f,Text as l,useInput as fr,useStdout as hr}from"ink";import mr from"ink-text-input";import{createRequire as cr}from"node:module";var lr=cr(import.meta.url),he=lr("../package.json").version;import ur from"node:os";function Ie(e){if(!e)return"\u2014";let t=ur.homedir();return e.startsWith(t)?"~"+e.slice(t.length):e}function Fe(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 Ne(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 _t(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 We,jsx as o,jsxs as p}from"react/jsx-runtime";var wr=(e,t)=>t<=0?0:Math.max(0,Math.min(e,t-1)),yr=16,br=5,vr="greenBright",At="blueBright",Ot="magentaBright";function It({k:e,bg:t=vr}){return o(l,{color:"black",backgroundColor:t,children:` ${e} `})}function I({k:e,label:t,width:n=16,bg:r}){return p(f,{width:n,children:[o(It,{k:e,bg:r}),o(l,{color:"gray",children:` ${t}`})]})}function E({k:e,label:t,bg:n}){return p(f,{marginBottom:1,children:[o(f,{width:9,children:o(It,{k:e,bg:n})}),o(l,{color:"gray",children:t})]})}function Sr({launchers:e}){return p(f,{flexDirection:"column",borderStyle:"round",borderColor:"green",paddingX:2,paddingY:1,width:66,children:[p(f,{justifyContent:"space-between",children:[o(l,{color:"green",bold:!0,children:"KEYBOARD SHORTCUTS"}),o(l,{color:"gray",children:"? or esc to close"})]}),p(f,{marginTop:1,children:[p(f,{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(f,{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:At}),o(E,{k:"ctrl+c",label:"quit"})]}),p(f,{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:Ot},t.key))]})]})]})}function xr({percent:e,width:t=28}){let n=Math.round(Math.max(0,Math.min(100,e))/100*t);return p(l,{children:[o(l,{color:"green",children:"\u2588".repeat(n)}),o(l,{color:"gray",children:"\u2591".repeat(Math.max(0,t-n))})]})}function Tr({frame:e,width:t=24,seg:n=6}){let r=t+n,s=e%r-n,i="";for(let u=0;u<t;u++)i+=u>=s&&u<s+n?"\u25AE":"\u25AF";return o(l,{color:"yellow",children:i})}function Ft({controller:e}){let{stdout:t}=hr(),n=gr(()=>lt(),[]),[,r]=j(0),[s,i]=j(()=>new Date),[u,a]=j(null),[y,w]=j(""),[d,g]=j(!1),[x,m]=j(0),[v,k]=j(!1),[C,D]=j(!1),[M,R]=j("list"),[K,_]=j(0),qe=()=>r(c=>c+1);se(()=>(e.on("change",qe),()=>{e.off("change",qe)}),[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,F=y.trim().toLowerCase(),P=F?V.filter(c=>c.site.name.toLowerCase().includes(F)||(c.org.name||"").toLowerCase().includes(F)):V,U=P.findIndex(c=>c.site.id===u);U<0&&(U=0);let $=P[U],T=$?.entry,oe=Math.max(3,(t?.rows??30)-26),N=T?.logs??[],Te=Math.max(0,N.length-oe),Zt=Math.min(K,Te),ae=N.length-Zt,Xe=N.slice(Math.max(0,ae-oe),ae),Qt=$?.site.id;se(()=>_(0),[Qt]);let A=T?.lastBuild,ke=A?.status==="building",$e=_t(A),ce=ke||!!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 Ye=P.findIndex(c=>!c.entry),H=Math.max(br,(t?.rows??30)-yr),Z=P.length,le=Z>H,en=Math.floor(H/2),Q=le?Math.max(0,Math.min(U-en,Z-H)):0,Ee=Math.min(Z,Q+H),tn=P.slice(Q,Ee),Ke=pr(!1);se(()=>{Ke.current||(Ke.current=!0,e.saved.length===0&&!e.connecting&&e.connectSite())},[e]);let ue=c=>{if(P.length===0)return;let S=wr(U+c,P.length);a(P[S].site.id)},nn=()=>{if($){if($.entry){e.flash("Already syncing \u2014 press d to disconnect.");return}e.startSite($.saved)}},rn=()=>{$?.entry&&e.removeSite($.site.id)},sn=()=>{$&&e.forgetSite($.site.id)},on=c=>{if(!T){e.flash("Press s to start syncing this site first.",!0);return}D(!0);let S=c.run(T.viewsDir);e.flash(S?`Opened ${c.label} at ${Ie(T.viewsDir)}`:`Couldn't open ${c.label}.`,!S)},an=()=>{if(!T){e.flash("Press s to start syncing this site first.",!0);return}e.togglePause(T.site.id)},Ve=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")},cn=()=>{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)};fr((c,S)=>{if(v){(c==="?"||S.escape)&&k(!1);return}if(G&&S.escape)return void e.cancelConnect();if(d){S.escape?(g(!1),w(""),a(null)):S.return?g(!1):S.upArrow?ue(-1):S.downArrow&&ue(1);return}if(S.tab){M==="logs"?R("list"):T&&R("logs"),_(0);return}if(M==="logs"){if(S.escape){R("list"),_(0);return}if(S.upArrow)return void _(O=>Math.min(O+1,Te));if(S.downArrow)return void _(O=>Math.max(0,O-1))}if(S.escape){w(""),a(null);return}if(c==="?")return void k(!0);if(c==="/")return R("list"),void g(!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 nn();if(c==="d")return rn();if(c==="x")return sn();if(c==="p")return an();if(c==="b")return Ve(!1);if(c==="B")return Ve(!0);if(c==="o")return cn();if(c>="1"&&c<="9"){let O=n[parseInt(c,10)-1];if(O)return on(O)}});let Ze=p(f,{justifyContent:"space-between",paddingX:1,children:[p(f,{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}`})]}),p(f,{children:[p(l,{color:"gray",children:[F?`${P.length}/${V.length}`:V.length," sites \xB7 "]}),p(l,{color:"green",children:[e.watchingCount()," watching"]}),o(l,{color:"gray",children:` \xB7 ${s.toTimeString().slice(0,5)}`})]})]});return v?p(f,{flexDirection:"column",children:[Ze,o(f,{marginTop:1,justifyContent:"center",children:o(Sr,{launchers:n})})]}):p(f,{flexDirection:"column",children:[Ze,p(f,{children:[p(f,{flexDirection:"column",borderStyle:"round",borderColor:"green",width:38,paddingX:1,children:[p(f,{justifyContent:"space-between",children:[o(l,{color:"green",bold:!0,children:"SITE WORKSPACES"}),V.length>H&&!d&&!F?o(l,{color:"gray",children:"/ filter"}):null]}),d?p(f,{children:[o(l,{color:"cyan",children:"/ "}),o(mr,{value:y,onChange:w,onSubmit:()=>g(!1),placeholder:"type to filter\u2026"})]}):F?o(l,{color:"gray",children:`/ ${y} \xB7 ${P.length} match${P.length===1?"":"es"}`}):null,o(f,{flexDirection:"column",marginTop:1,minHeight:le?H+3:void 0,children:P.length===0?o(l,{color:"gray",children:F?`no matches for \u201C${y}\u201D`:"no sites yet \u2014 press a to add one"}):p(We,{children:[le?o(l,{color:"gray",children:Q>0?` \u2191 ${Q} more`:" "}):null,tn.map((c,S)=>{let O=Q+S,Qe=c.entry?ye(c.entry.status):{dot:"\xB7",color:"gray",label:"idle"},et=we(c.entry?.progress),ln=c.entry?c.entry.status==="syncing"&&et!=null?`${et}%`:c.entry.status==="paused"?"paused":"--":"",de=O===U,un=O===Ye&&Ye>0;return p(dr,{children:[un?o(l,{color:"gray",children:"\u2500".repeat(34)}):null,p(f,{justifyContent:"space-between",children:[p(f,{children:[o(l,{color:de?"green":"gray",children:de?"\u276F ":" "}),p(l,{color:c.entry?.status==="watching"||c.entry?.status==="syncing"?"green":Qe.color,children:[c.entry?.status==="syncing"?Ne(x):c.entry?.status==="watching"?"\u25CF":Qe.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:ln})]})]},c.site.id)}),le?o(l,{color:"gray",children:Ee<Z?` \u2193 ${Z-Ee} more`:" "}):null]})}),o(f,{marginTop:1,children:o(l,{color:"gray",children:"+ add site \xB7 a"})})]}),p(f,{flexDirection:"column",flexGrow:1,marginLeft:1,children:[o(f,{flexDirection:"column",borderStyle:"round",borderColor:"gray",paddingX:1,children:$?p(We,{children:[p(l,{children:[$.org.name?p(l,{color:"gray",children:[$.org.name," / "]}):null,o(l,{color:"cyan",bold:!0,children:$.site.name})]}),p(f,{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)"})]}),p(f,{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]}),p(f,{children:[o(l,{color:"gray",children:"path "}),o(l,{wrap:"truncate-end",children:T?.viewsDir?Ie(T.viewsDir):"\u2014"})]}),p(f,{children:[o(l,{color:"gray",children:"synced "}),o(l,{children:Fe(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(f,{marginTop:1,children:o(xr,{percent:T?T.status==="syncing"?we(T.progress)??0:T.status==="watching"?100:0:0})}),T?p(We,{children:[p(f,{marginTop:1,children:[o(l,{color:"gray",children:"build "}),o(l,{color:$e.color,children:`${$e.dot} ${$e.label}`}),o(l,{color:"gray",children:" last "}),o(l,{children:ke?"in progress":Fe(A?.at)})]}),p(f,{children:[o(l,{color:"gray",children:"url "}),A?.url?o(l,{color:"cyan",wrap:"truncate-end",children:A.url}):o(l,{color:"gray",children:"\u2014"})]}),A?.status==="error"&&A.errors?.length?p(f,{children:[o(l,{color:"red",children:"\u258C "}),o(l,{color:"red",wrap:"truncate-end",children:`\u2717 ${A.errors[0]}`})]}):null,ke?p(f,{children:[o(Tr,{frame:x}),o(l,{color:"gray",children:` ${A?.stage??"building"}`})]}):null]}):null]}):o(l,{color:"gray",children:"No site selected."})}),G?p(f,{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."}),p(f,{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."}),p(f,{marginTop:1,children:[p(l,{color:"yellow",children:[Ne(x)," "]}),o(l,{color:"gray",children:"waiting for approval \xB7 esc to cancel"})]})]}):p(f,{flexDirection:"column",borderStyle:"round",borderColor:M==="logs"?"green":"gray",paddingX:1,marginTop:1,height:oe+4,flexShrink:0,overflowY:"hidden",children:[p(f,{justifyContent:"space-between",children:[p(l,{children:[o(l,{color:"cyan",bold:!0,children:"LOGS \xB7 "}),o(l,{color:"white",children:$?.site.name??"\u2014"})]}),T&&N.length>0?M==="logs"?o(l,{color:"green",children:`\u2191\u2193 scroll \xB7 esc back${Te>0?` \xB7 ${ae}/${N.length}`:""}`}):N.length>oe?o(l,{color:"gray",children:"tab to scroll"}):null:null]}),o(f,{flexDirection:"column",marginTop:1,children:!T||N.length===0?o(l,{color:"gray",children:"no activity yet"}):Xe.map((c,S)=>p(l,{wrap:"truncate-end",children:[p(l,{color:"gray",children:[c.time," "]}),c.text]},ae-Xe.length+S))})]})]})]}),G?.url?p(f,{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(f,{paddingX:1,children:p(l,{color:e.messageIsError?"red":"gray",wrap:"truncate-end",children:[e.messageIsError?"\u2717 ":"",e.message]})}):null,p(f,{flexDirection:"column",marginTop:1,children:[p(f,{paddingX:1,justifyContent:"space-between",children:[p(f,{flexWrap:"wrap",children:[o(I,{k:"\u2191\u2193",label:"select"}),o(I,{k:"/",label:"search"}),o(I,{k:"s",label:"start sync"}),o(I,{k:"d",label:"disconnect"}),o(I,{k:"a",label:"add site"}),o(I,{k:"ctrl+c",label:"quit",width:14})]}),o(I,{k:"?",label:"all keys",width:12,bg:At})]}),!C&&n.length>0?o(f,{paddingX:1,marginTop:1,flexWrap:"wrap",children:n.map((c,S)=>o(I,{k:String(S+1),label:c.label,bg:Ot},c.key))}):null]})]})}import{jsx as $r}from"react/jsx-runtime";function Nt({controller:e}){return kr((t,n)=>{n.ctrl&&t==="c"&&e.exit()}),$r(Ft,{controller:e})}import*as zt from"os";import*as Ue from"util";var Er={production:"https://9Q849fBKdCJcK6j8M5SHpWjt@s2790579.us-west-2a.betterstackdata.com/2790579",development:"https://oj8Je2g5rZhXAv6NZ3CxYHFb@s2790583.us-west-2a.betterstackdata.com/2790583"},ie,Wt,Br=()=>Wt??=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)}),Gt=({service:e,environment:t})=>{let n=Er[t];return n&&(ie={dsn:n,environment:t,service:e}),!!n},Ut=(e,t)=>{Br().then(n=>n?.captureException(e,t&&{extra:t}))},Ht=async e=>{await(await Wt)?.flush(e)};var Cr=10*6e4,Mr=3,Rr=30,Lr=1e3;var Se=!1,Ge=!1;var qt=console.error.bind(console),Y=new Map,Jt=new WeakSet,be={start:0,sent:0},jr=()=>process.env.ENV||process.env.AWS_LAMBDA_FUNCTION_NAME?.match(/-(production|development)$/)?.[1],Xt=({service:e,environment:t=jr(),quiet:n=!1})=>{Se=!!t&&Gt({service:e,environment:t}),n||console.log(`[observability] ${e}: ${Se?`reporting errors (${t})`:"off"}`)},Yt=(e,t)=>{if(!(!Se||Ge)){Ge=!0;try{let n=Or(e);if(Jt.has(n))return;Jt.add(n);let r=Dr(_r(n));if(r===!1)return;Ut(Ir(n),r?{...t,suppressed:r}:t)}catch(n){qt("[observability] capture failed:",n?.message)}finally{Ge=!1}}},Kt=async(e=2e3)=>{if(Se)try{await Ht(e)}catch(t){qt("[observability] flush failed:",t?.message)}};var Dr=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>=Cr)&&(r=n?.dropped??0,n={start:t,sent:0,dropped:0},Y.delete(e),Y.set(e,n),Y.size>Lr&&Y.delete(Y.keys().next().value)),n.sent>=Mr||be.sent>=Rr?(n.dropped++,!1):(n.sent++,be.sent++,r)},_r=e=>{let t=e.stack?.split(`
11
+ `).find(n=>/^\s*at /.test(n)&&!/node_modules|node:|[\\/]observability[\\/]/.test(n));return`${e.name}|${Ar(e.message)}|${t?.trim()??""}`},Ar=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),Or=e=>{if(e instanceof Error)return e;let t=new Error(typeof e=="string"?e:Ue.inspect(e,{depth:3,breakLength:1/0}));return t.name="NonErrorThrown",t},ve=(()=>{try{return zt.homedir()}catch{return""}})(),Ir=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 Jr}from"react/jsx-runtime";var Je=ot();Je.version&&(console.log(he),process.exit(0));var Vt=(Je.env??"production").toLowerCase();Xt({service:"cms-sync",environment:process.env.DO_NOT_TRACK==="1"?null:Vt,quiet:!0});var ze=xe.dirname(Fr(import.meta.url)),Wr=He(xe.join(ze,"AGENTS.md"),"utf-8"),Gr=He(xe.join(ze,"build.mjs"),"utf-8"),Ur=He(xe.join(ze,"upload.mjs"),"utf-8");async function Hr(){let e=new fe(Wr,{build:Gr,upload:Ur});e.basePath=Je.path,await e.init(Vt),process.stdout.isTTY&&process.stdout.write("\x1B[2J\x1B[3J\x1B[H");let t=Nr(Jr(Nt,{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()}Hr().catch(async e=>{let t=e;console.error("\u274C",t.body||t.message),t.body||(Yt(e),await Kt()),process.exit(1)});
@@ -0,0 +1,362 @@
1
+ #!/usr/bin/env node
2
+ // upload.mjs v1 — SleekCMS workspace upload script.
3
+ /**
4
+ * Self-contained (no `npm install` needed). Run from anywhere inside a synced
5
+ * SleekCMS workspace — the one `@sleekcms/sync` created, containing `.cache/token`.
6
+ *
7
+ * node upload.mjs <path>... Upload images and documents from disk.
8
+ * node upload.mjs logo.png --handle logo Pick the image's handle (one image only).
9
+ * node upload.mjs menu.pdf --name lunch-menu.pdf
10
+ * Pick the document's file name (one file only).
11
+ * --json Print one JSON result per line instead of text.
12
+ *
13
+ * What each file becomes is decided by its extension:
14
+ * - Images (png, jpg, jpeg, webp, gif, svg, ico) become site images under a
15
+ * handle (default: the file name without its extension) — reference them as
16
+ * `cms:<handle>` in image fields, or `img('cms:<handle>')` in views. An existing
17
+ * handle is replaced in place, so every `cms:<handle>` shows the new image.
18
+ * - Documents (pdf, docx, xlsx, pptx, csv, tsv, txt, md) go to Media > Files
19
+ * under their file name — the value a `file` field takes. An existing file
20
+ * name is replaced in place.
21
+ *
22
+ * It calls POST /api/mcp/create_asset, then sends the bytes straight to the
23
+ * upload URL it returns, and finally updates src/content/images.json or
24
+ * src/content/files.json so the workspace lists the new asset (the running sync
25
+ * pushes images.json back unchanged, which is a no-op on the server).
26
+ *
27
+ * Exit codes: 0 = every file uploaded, 1 = some uploads failed, 2 = couldn't run.
28
+ */
29
+
30
+ import fs from "node:fs";
31
+ import path from "node:path";
32
+ import { pathToFileURL } from "node:url";
33
+
34
+ // Same env → base-URL table the sync engine and build.mjs use.
35
+ const MCP_BASE_URLS = {
36
+ localhost: "http://app.sleekcms.test/api/mcp",
37
+ development: "https://app.sleekcms.dev/api/mcp",
38
+ production: "https://app.sleekcms.com/api/mcp",
39
+ };
40
+
41
+ // Mirrors cms-be handlers/mcp/assets.js.
42
+ export const IMAGE_EXTS = ["png", "jpg", "jpeg", "webp", "gif", "svg", "ico"];
43
+ export const FILE_EXTS = ["pdf", "docx", "xlsx", "pptx", "csv", "tsv", "txt", "md"];
44
+ const TEXT_FILE_EXTS = ["csv", "tsv", "txt", "md"];
45
+ const MAX_IMAGE_MB = 8;
46
+ const MAX_FILE_MB = 10;
47
+ const MAX_INLINE_TEXT_BYTES = 256 * 1024;
48
+
49
+ const IMAGES_INDEX = "src/content/images.json";
50
+ const FILES_INDEX = "src/content/files.json";
51
+
52
+ /** The workspace's env: the override, else what .cache/site.json records, else production. */
53
+ export function workspaceEnv(ws, override) {
54
+ let recorded;
55
+ try {
56
+ recorded = JSON.parse(fs.readFileSync(path.join(ws, ".cache", "site.json"), "utf-8")).env;
57
+ } catch {
58
+ recorded = undefined;
59
+ }
60
+ const env = String(override || recorded || "production").toLowerCase();
61
+ return MCP_BASE_URLS[env] ? env : "production";
62
+ }
63
+
64
+ export function mcpBase(env) {
65
+ return MCP_BASE_URLS[env] || MCP_BASE_URLS.production;
66
+ }
67
+
68
+ /** Walk up from `startDir` until a `.cache/token` is found; null if none. */
69
+ export function findWorkspace(startDir) {
70
+ let dir = path.resolve(startDir);
71
+ // eslint-disable-next-line no-constant-condition
72
+ while (true) {
73
+ if (fs.existsSync(path.join(dir, ".cache", "token"))) return dir;
74
+ const parent = path.dirname(dir);
75
+ if (parent === dir) return null;
76
+ dir = parent;
77
+ }
78
+ }
79
+
80
+ export function extOf(name) {
81
+ const m = /\.([a-z0-9]{1,10})$/i.exec(String(name || ""));
82
+ return m ? m[1].toLowerCase() : "";
83
+ }
84
+
85
+ /** "image", "file", or null for an extension the CMS doesn't take. */
86
+ export function kindOf(name) {
87
+ const ext = extOf(name);
88
+ if (IMAGE_EXTS.includes(ext)) return "image";
89
+ if (FILE_EXTS.includes(ext)) return "file";
90
+ return null;
91
+ }
92
+
93
+ /** A valid image handle from a file name: "Team Photo (1).JPG" → "team-photo-1". */
94
+ export function handleFor(fileName) {
95
+ const base = path.basename(fileName).replace(/\.[^.]*$/, "");
96
+ const handle = base
97
+ .toLowerCase()
98
+ .replace(/[^a-z0-9_-]+/g, "-")
99
+ .replace(/^[-_]+|-+$/g, "")
100
+ .slice(0, 64)
101
+ .replace(/-+$/, "");
102
+ return handle || "image";
103
+ }
104
+
105
+ export function parseFlags(argv) {
106
+ const flags = { files: [], handle: undefined, name: undefined, env: undefined, json: false, help: false };
107
+ for (let i = 0; i < argv.length; i++) {
108
+ const a = argv[i];
109
+ if (a === "--handle") flags.handle = argv[++i];
110
+ else if (a.startsWith("--handle=")) flags.handle = a.slice("--handle=".length);
111
+ else if (a === "--name") flags.name = argv[++i];
112
+ else if (a.startsWith("--name=")) flags.name = a.slice("--name=".length);
113
+ else if (a === "--env") flags.env = argv[++i];
114
+ else if (a.startsWith("--env=")) flags.env = a.slice("--env=".length);
115
+ else if (a === "--json") flags.json = true;
116
+ else if (a === "--help" || a === "-h") flags.help = true;
117
+ else flags.files.push(a);
118
+ }
119
+ return flags;
120
+ }
121
+
122
+ export class UploadError extends Error {
123
+ constructor(message, httpStatus) {
124
+ super(message);
125
+ this.httpStatus = httpStatus;
126
+ }
127
+ }
128
+
129
+ /** The server's own message from an error body (JSON `message`/`error`), else the raw text. */
130
+ function errorText(text) {
131
+ try {
132
+ const body = JSON.parse(text);
133
+ if (body && (body.message || body.error)) return String(body.message || body.error);
134
+ } catch {
135
+ /* not JSON */
136
+ }
137
+ const title = String(text || "").match(/<title>([^<]*)<\/title>/i)?.[1].trim();
138
+ return (title || String(text || "")).replace(/\s+/g, " ").slice(0, 300);
139
+ }
140
+
141
+ async function api(base, token, method, p, body, fetchImpl) {
142
+ const res = await fetchImpl(base + p, {
143
+ method,
144
+ headers: {
145
+ Authorization: `Bearer ${token}`,
146
+ ...(body !== undefined ? { "Content-Type": "application/json" } : {}),
147
+ },
148
+ body: body !== undefined ? JSON.stringify(body) : undefined,
149
+ });
150
+ if (!res.ok) {
151
+ const text = await res.text().catch(() => "");
152
+ throw new UploadError(`${p.split("?")[0]} → ${res.status}: ${errorText(text)}`, res.status);
153
+ }
154
+ return res.json();
155
+ }
156
+
157
+ /** Send the bytes to the upload URL create_asset handed back. Returns the parsed reply, if any. */
158
+ async function sendBytes(upload, data, fetchImpl) {
159
+ const res = await fetchImpl(upload.url, { method: upload.method, headers: upload.headers || {}, body: data });
160
+ const text = await res.text().catch(() => "");
161
+ if (!res.ok) throw new UploadError(`upload → ${res.status}: ${errorText(text)}`, res.status);
162
+ try {
163
+ return text ? JSON.parse(text) : null;
164
+ } catch {
165
+ return null;
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Put one handle into the local images.json, keeping everything else in it as it
171
+ * is (it may hold edits the sync hasn't pushed yet). Returns false when the file
172
+ * isn't plain JSON (comments etc.) and was left alone.
173
+ */
174
+ export function mergeImageHandle(ws, handle, value) {
175
+ const file = path.join(ws, IMAGES_INDEX);
176
+ let map = {};
177
+ if (fs.existsSync(file)) {
178
+ const text = fs.readFileSync(file, "utf-8");
179
+ if (text.trim()) {
180
+ try {
181
+ map = JSON.parse(text);
182
+ } catch {
183
+ return false;
184
+ }
185
+ if (!map || typeof map !== "object" || Array.isArray(map)) return false;
186
+ }
187
+ }
188
+ if (map[handle] === value) return true;
189
+ map[handle] = value;
190
+ fs.mkdirSync(path.dirname(file), { recursive: true });
191
+ fs.writeFileSync(file, `${JSON.stringify(map, null, 2)}\n`);
192
+ return true;
193
+ }
194
+
195
+ /** Write the server's files.json (read-only, server-owned — the sync never pushes it). */
196
+ function writeFilesIndex(ws, files) {
197
+ const index = (files || []).find((f) => f.path === FILES_INDEX);
198
+ if (!index) return;
199
+ const file = path.join(ws, FILES_INDEX);
200
+ fs.mkdirSync(path.dirname(file), { recursive: true });
201
+ fs.writeFileSync(file, index.content);
202
+ }
203
+
204
+ /**
205
+ * Upload one file from disk. `opts`: { ws, base, token, filePath, handle?, name?, fetchImpl? }.
206
+ * Resolves to a result object; throws UploadError on failure.
207
+ */
208
+ export async function uploadOne(opts) {
209
+ const { ws, base, token, filePath, fetchImpl = fetch } = opts;
210
+ const fileName = path.basename(filePath);
211
+ const kind = kindOf(opts.name || fileName);
212
+ if (!kind) {
213
+ throw new UploadError(
214
+ `${fileName}: unsupported type. Images: ${IMAGE_EXTS.join(", ")}. Documents: ${FILE_EXTS.join(", ")}.`,
215
+ );
216
+ }
217
+ let stat;
218
+ try {
219
+ stat = fs.statSync(filePath);
220
+ } catch {
221
+ throw new UploadError(`${filePath}: no such file.`);
222
+ }
223
+ if (!stat.isFile()) throw new UploadError(`${filePath}: not a file.`);
224
+ if (stat.size === 0) throw new UploadError(`${filePath}: the file is empty.`);
225
+ const maxMb = kind === "image" ? MAX_IMAGE_MB : MAX_FILE_MB;
226
+ if (stat.size > maxMb * 1024 * 1024) {
227
+ throw new UploadError(`${fileName}: ${(stat.size / 1024 / 1024).toFixed(1)} MB is over the ${maxMb} MB limit for ${kind === "image" ? "images" : "documents"}.`);
228
+ }
229
+ const data = fs.readFileSync(filePath);
230
+
231
+ if (kind === "image") {
232
+ if (opts.name && kindOf(opts.name) !== "image") throw new UploadError(`--name must keep an image extension.`);
233
+ const handle = opts.handle || handleFor(fileName);
234
+ const name = opts.name || fileName;
235
+ const created = await api(base, token, "POST", "/create_asset", { type: "image", handle, name }, fetchImpl);
236
+ if (created.upload) {
237
+ if (created.max_size_mb && stat.size > created.max_size_mb * 1024 * 1024) {
238
+ throw new UploadError(`${fileName}: over the ${created.max_size_mb} MB limit for images.`);
239
+ }
240
+ await sendBytes(created.upload, data, fetchImpl);
241
+ }
242
+ // What the server lists for the handle now — the value images.json reads back as.
243
+ let value;
244
+ try {
245
+ const images = await api(base, token, "GET", "/get_images", undefined, fetchImpl);
246
+ value = JSON.parse(images.content || "{}")[handle];
247
+ } catch {
248
+ value = undefined;
249
+ }
250
+ const indexed = value ? mergeImageHandle(ws, handle, value) : false;
251
+ return {
252
+ source: filePath,
253
+ type: "image",
254
+ ref: created.ref || `cms:${handle}`,
255
+ handle,
256
+ replaced: !!created.replaces_existing,
257
+ url: typeof value === "string" && value.startsWith("url:") ? value.slice(4) : undefined,
258
+ indexed,
259
+ };
260
+ }
261
+
262
+ // Document → Media > Files.
263
+ if (opts.handle) throw new UploadError(`--handle is for images; a document is named by its file name (use --name).`);
264
+ const name = opts.name || fileName;
265
+ if (extOf(name) !== extOf(fileName)) throw new UploadError(`--name must keep the file's .${extOf(fileName)} extension.`);
266
+ const body = { type: "file", name };
267
+ // Small text documents go inline: the server stores them and confirms the row at once.
268
+ const ext = extOf(name);
269
+ if (TEXT_FILE_EXTS.includes(ext) && data.length <= MAX_INLINE_TEXT_BYTES) body.content = data.toString("utf-8");
270
+ const created = await api(base, token, "POST", "/create_asset", body, fetchImpl);
271
+ if (created.upload) await sendBytes(created.upload, data, fetchImpl);
272
+ writeFilesIndex(ws, created.files);
273
+ return {
274
+ source: filePath,
275
+ type: "file",
276
+ ref: created.ref || name,
277
+ handle: created.handle || undefined,
278
+ replaced: !!created.replaces_existing,
279
+ url: created.url || undefined,
280
+ indexed: !!(created.files || []).some((f) => f.path === FILES_INDEX),
281
+ };
282
+ }
283
+
284
+ /** Human-readable lines for one result. */
285
+ export function describe(r) {
286
+ if (r.type === "image") {
287
+ const lines = [`✅ ${r.replaced ? "Replaced" : "Uploaded"} image ${path.basename(r.source)} → "${r.ref}"${r.url ? ` (${r.url})` : ""}`];
288
+ lines.push(` Use "${r.ref}" as an image field's value, or img('${r.ref}') / src('${r.ref}') in a view.`);
289
+ if (!r.indexed) lines.push(` ${IMAGES_INDEX} wasn't updated here; the handle is on the server and shows up there after the next re-fetch.`);
290
+ return lines;
291
+ }
292
+ const lines = [`✅ ${r.replaced ? "Replaced" : "Uploaded"} file ${path.basename(r.source)} → "${r.ref}"${r.url ? ` (${r.url})` : ""}`];
293
+ lines.push(` Use "${r.ref}"${r.handle ? ` (or its handle "${r.handle}")` : ""} as a file field's value, or getFile('${r.handle || r.ref}') in a view.`);
294
+ return lines;
295
+ }
296
+
297
+ const USAGE = `Usage: node upload.mjs <path>... [--handle <handle>] [--name <file name>] [--json]
298
+
299
+ Images (${IMAGE_EXTS.join(", ")}, up to ${MAX_IMAGE_MB} MB) become site images: cms:<handle>.
300
+ Documents (${FILE_EXTS.join(", ")}, up to ${MAX_FILE_MB} MB) go to Media > Files under their file name.
301
+ --handle the image's handle (default: its file name without the extension); one image only.
302
+ --name the stored file name, extension kept; one file only.`;
303
+
304
+ async function main() {
305
+ const flags = parseFlags(process.argv.slice(2));
306
+ if (flags.help || flags.files.length === 0) {
307
+ (flags.help ? console.log : console.error)(USAGE);
308
+ process.exit(flags.help ? 0 : 2);
309
+ }
310
+ if ((flags.handle || flags.name) && flags.files.length > 1) {
311
+ console.error("❌ --handle and --name name a single upload; pass one file with them.");
312
+ process.exit(2);
313
+ }
314
+
315
+ const ws = findWorkspace(process.cwd());
316
+ if (!ws) {
317
+ console.error("❌ Not inside a SleekCMS workspace (no .cache/token found). Run this from your synced site folder.");
318
+ process.exit(2);
319
+ }
320
+ let token;
321
+ try {
322
+ token = fs.readFileSync(path.join(ws, ".cache", "token"), "utf-8").trim();
323
+ } catch {
324
+ token = "";
325
+ }
326
+ if (!token) {
327
+ console.error("❌ Could not read .cache/token in this workspace.");
328
+ process.exit(2);
329
+ }
330
+ const base = mcpBase(workspaceEnv(ws, flags.env || process.env.SLEEKCMS_ENV));
331
+
332
+ let failed = 0;
333
+ for (const file of flags.files) {
334
+ const filePath = path.resolve(process.cwd(), file);
335
+ try {
336
+ const r = await uploadOne({ ws, base, token, filePath, handle: flags.handle, name: flags.name });
337
+ if (flags.json) console.log(JSON.stringify({ ok: true, ...r }));
338
+ else for (const line of describe(r)) console.log(line);
339
+ } catch (err) {
340
+ failed++;
341
+ const msg = err && err.message ? err.message : String(err);
342
+ const hint =
343
+ err && err.httpStatus === 403
344
+ ? " (uploading needs an EDITOR or ADMIN on this site)"
345
+ : err && err.httpStatus === 401
346
+ ? " (access to this site was revoked — add it again in sleekcms)"
347
+ : "";
348
+ if (flags.json) console.log(JSON.stringify({ ok: false, source: filePath, error: msg + hint }));
349
+ else console.error(`❌ ${path.basename(filePath)}: ${msg}${hint}`);
350
+ }
351
+ }
352
+ process.exit(failed ? 1 : 0);
353
+ }
354
+
355
+ // Run only when invoked directly (`node upload.mjs`), not when imported by tests.
356
+ const isMain = import.meta.url === pathToFileURL(process.argv[1] || "").href;
357
+ if (isMain) {
358
+ main().catch((err) => {
359
+ console.error("❌", err && err.message ? err.message : String(err));
360
+ process.exit(2);
361
+ });
362
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleekcms/sync",
3
- "version": "4.1.0",
3
+ "version": "4.3.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",
@@ -28,7 +28,7 @@
28
28
  "dist"
29
29
  ],
30
30
  "scripts": {
31
- "build": "npm run clean && node esbuild.config.mjs && cp src/AGENTS.md dist/ && cp src/build.mjs dist/ && chmod +x dist/index.js",
31
+ "build": "npm run clean && node esbuild.config.mjs && cp src/AGENTS.md dist/ && cp src/build.mjs src/upload.mjs dist/ && chmod +x dist/index.js",
32
32
  "dev": "tsx src/index.tsx",
33
33
  "typecheck": "tsc --noEmit",
34
34
  "clean": "rm -rf dist",