@sleekcms/sync 2.14.0 → 3.0.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
@@ -12,7 +12,7 @@ Every save auto-syncs. Every change goes live. No build steps. No Git hooks. No
12
12
 
13
13
  - **AI generates your entire site from a description** — models, templates, layouts, styles, and content, all wired up and ready to go.
14
14
  - **AI-generated sites are plain files** — EJS templates, JSON content, CSS, JS. You own every line. Open in VS Code, Cursor, or any editor.
15
- - **Any AI can maintain it** — the CLI injects `AGENT.md`, `CLAUDE.md`, and `.vscode/copilot-instructions.md` into your workspace. GitHub Copilot, Claude, Cursor — they all read the same site reference and understand your structure from day one.
15
+ - **Any AI can maintain it** — the CLI writes `AGENTS.md` into your workspace, the instructions file Claude Code, Codex, Cursor, GitHub Copilot and other agents all read. They get the same site reference and understand your structure from day one.
16
16
  - **Live sync, always** — save a file, it syncs. Open the SleekCMS dashboard and your preview updates in real time.
17
17
  - **One command to start** — no installs required.
18
18
 
@@ -21,10 +21,10 @@ Every save auto-syncs. Every change goes live. No build steps. No Git hooks. No
21
21
  ## Quickstart
22
22
 
23
23
  ```bash
24
- npx @sleekcms/sync --token <YOUR_AUTH_TOKEN>
24
+ npx @sleekcms/sync
25
25
  ```
26
26
 
27
- That's it. The CLI fetches your site, opens an editor prompt, and starts watching for changes. Grab a token from your [SleekCMS > Builder](https://app.sleekcms.com).
27
+ That's it. Your browser opens on SleekCMS: sign in, pick a site (or create a blank one), check the code on the page matches the one in your terminal, and approve. The CLI pulls the site and starts watching for changes. Press `a` to add more sites the same way.
28
28
 
29
29
  ---
30
30
 
@@ -33,14 +33,14 @@ That's it. The CLI fetches your site, opens an editor prompt, and starts watchin
33
33
  ### Run without installing (recommended)
34
34
 
35
35
  ```bash
36
- npx @sleekcms/sync --token <YOUR_AUTH_TOKEN>
36
+ npx @sleekcms/sync
37
37
  ```
38
38
 
39
39
  ### Install globally
40
40
 
41
41
  ```bash
42
42
  npm install -g @sleekcms/sync
43
- sleekcms --token <YOUR_AUTH_TOKEN>
43
+ sleekcms
44
44
  ```
45
45
 
46
46
  ---
@@ -48,15 +48,16 @@ sleekcms --token <YOUR_AUTH_TOKEN>
48
48
  ## Usage
49
49
 
50
50
  ```bash
51
- # Basic — auto-sync on every save
52
- npx @sleekcms/sync --token abc123-xxxx
53
-
54
- # Manual sync — push changes only when you press [s]
55
- npx @sleekcms/sync --token abc123-xxxx --manual
51
+ # Add sites in your browser, then pick which to sync
52
+ npx @sleekcms/sync
56
53
 
54
+ # Keep the workspaces somewhere other than ~/SleekCMS
55
+ npx @sleekcms/sync --path ~/sites
57
56
  ```
58
57
 
59
- Once running, press `r` to re-fetch all files or `x` / `Ctrl+C` to exit. In `--manual` mode, press `s` to push your changes on demand.
58
+ Each site is added on its own (`a`) and approved in the browser; no browser on this machine (over SSH, say)? Open the address the CLI prints on any device. The CLI keeps one token per site in `~/SleekCMS/auth.json` and acts with your role on that site: editors sync content, images and videos, while models, views, setup, building and publishing need an admin.
59
+
60
+ Once running, `s` starts syncing the selected site, `d` disconnects it, `x` forgets a disconnected site, `?` lists every key, and `Ctrl+C` exits.
60
61
 
61
62
  ---
62
63
 
@@ -66,15 +67,9 @@ This is where SleekCMS CLI becomes something different. Instead of hand-crafting
66
67
 
67
68
  ### How it works
68
69
 
69
- When the CLI starts for the first time, it writes three files into your local workspace:
70
-
71
- | File | Picked up by |
72
- |---|---|
73
- | `AGENT.md` | Copilot (agent mode), any generic agent |
74
- | `CLAUDE.md` | Claude / Claude Code |
75
- | `.vscode/copilot-instructions.md` | GitHub Copilot in VS Code |
70
+ When the CLI pulls a site, it writes `AGENTS.md` into the workspace — the shared instructions file AI coding agents read on their own. (Older versions also wrote `AGENT.md`, `CLAUDE.md` and `.vscode/copilot-instructions.md`; those are removed, since a leftover `CLAUDE.md` would stop Claude Code from reading `AGENTS.md`.)
76
71
 
77
- These files contain the complete SleekCMS site-building reference — file naming conventions, model syntax, template helpers, content format, and all the rules an AI needs to produce valid, working code.
72
+ It contains the complete SleekCMS site-building reference — file naming conventions, model syntax, template helpers, content format, and all the rules an AI needs to produce valid, working code.
78
73
 
79
74
  ### Generate a complete site
80
75
 
@@ -120,7 +115,7 @@ There's no lock-in, no proprietary format, no "AI black box". Every file the AI
120
115
  - **Models** are plain JSON-like schema files — add a field, save, done.
121
116
  - **Content** is readable JSON — edit values directly without touching the CMS dashboard.
122
117
 
123
- And because `AGENT.md` / `CLAUDE.md` live in the workspace, any AI session — today or months from now — picks up the same full site context. Ask it to add a dark mode toggle, refactor the blog layout, or generate ten more blog posts. It already knows your site.
118
+ And because `AGENTS.md` lives in the workspace, any AI session — today or months from now — picks up the same full site context. Ask it to add a dark mode toggle, refactor the blog layout, or generate ten more blog posts. It already knows your site.
124
119
 
125
120
  ---
126
121
 
@@ -178,11 +173,10 @@ No manual deploy. No waiting. SleekCMS rebuilds on every sync and the preview re
178
173
 
179
174
  ```
180
175
  <site-name>/
181
- ├── AGENT.md # AI agent reference (auto-generated)
182
- ├── CLAUDE.md # Claude reference (auto-generated)
176
+ ├── AGENTS.md # AI agent reference (auto-generated)
183
177
  ├── .vscode/
184
- │ ├── copilot-instructions.md # GitHub Copilot context (auto-generated)
185
- │ └── settings.json # Editor settings
178
+ │ ├── settings.json # Editor settings (site .json files open as JSON5)
179
+ │ └── extensions.json # Recommends the JSON5 extension
186
180
  │
187
181
  └── src/
188
182
  ├── models/
@@ -447,7 +441,7 @@ Set `cookie_consent: true` in `src/setup.json` and tag any script or iframe that
447
441
  <a data-sleekcms-consent href="#">Cookie settings</a> <!-- reopens the preferences panel; put one in the footer -->
448
442
  ```
449
443
 
450
- Category names are yours (`analytics`, `marketing`, …); the banner offers exactly the ones found on the site. An optional empty `<div data-sleekcms-consent data-position="bottom" data-theme="auto" data-policy-url="/privacy"></div>` in the layout customizes position (`bottom`, `bottom-left`, `bottom-right`, `modal`), theme, policy link, locale and button labels, and `--sleek-consent-bg/text/accent/on-accent/radius` CSS variables restyle it. The full reference is in the `AGENT.md` that `sleekcms sync` writes into your workspace.
444
+ Category names are yours (`analytics`, `marketing`, …); the banner offers exactly the ones found on the site. An optional empty `<div data-sleekcms-consent data-position="bottom" data-theme="auto" data-policy-url="/privacy"></div>` in the layout customizes position (`bottom`, `bottom-left`, `bottom-right`, `modal`), theme, policy link, locale and button labels, and `--sleek-consent-bg/text/accent/on-accent/radius` CSS variables restyle it. The full reference is in the `AGENTS.md` that `sleekcms sync` writes into your workspace.
451
445
 
452
446
  ## Images
453
447
 
@@ -493,7 +487,7 @@ Inside `markdown` fields, the same shortcuts work in standard image syntax. Appe
493
487
  ## Prerequisites
494
488
 
495
489
  - **Node.js** v16 or later
496
- - A **SleekCMS account** with a CLI auth token ([get one here](https://app.sleekcms.com))
490
+ - A **SleekCMS account** ([sign up](https://app.sleekcms.com)) with access to at least one site, or an organization to create one in
497
491
 
498
492
  ---
499
493
 
@@ -501,11 +495,12 @@ Inside `markdown` fields, the same shortcuts work in standard image syntax. Appe
501
495
 
502
496
  | Problem | Fix |
503
497
  |---|---|
504
- | Authentication error | Verify your token in the SleekCMS dashboard |
505
- | No files downloaded | Check your token and `--env` setting |
498
+ | "Access was revoked" | You logged out of all sessions or lost access to the site. Press `a` and add it again; local files are kept and your edits are pushed |
499
+ | Browser didn't open | Open the address printed under the dashboard, on this or any other device |
500
+ | "admin only" on build or publish | Building and publishing need an admin on the site; ask one to build, or to make you an admin |
501
+ | No files downloaded | Check the `--env` setting |
506
502
  | Changes not syncing | Check terminal output; changes are debounced by 5 seconds |
507
- | Wrong workspace opened | Each token maps to a specific workspace folder |
508
- | Workspace disappeared | Expected — the local workspace is wiped on `Ctrl+C` / `x`. Your edits are already on the server; re-run the CLI to pull a fresh copy. |
503
+ | Workspace disappeared | Expected — the local site files are wiped on `Ctrl+C` or `d`. Your edits are already on the server; re-run the CLI to pull a fresh copy. |
509
504
 
510
505
  ---
511
506
 
@@ -450,16 +450,16 @@ Callable from **any** view (page, block, entry, layout); duplicates are removed
450
450
  | `style(css, order?)` | Add `<style>` |
451
451
  | `script(value, order?)` | Add `<script>` — `.js` URL → external, otherwise inline |
452
452
 
453
- `script()`/`link()` also accept a predefined library name — `gsap` (includes ScrollTrigger, pre-registered), `aos` (auto-`AOS.init()`, JS+CSS), `swiper`, `animejs`, `chartjs`, `threejs` (import map — pair with a `type: 'module'` script), `lottie`, `typedjs`, `splide`, `htmx`, `vanilla-tilt` — either call injects all of the library's CDN assets, deduped. Prefer these names over hand-picked CDN URLs, and only add a library the site actually uses.
453
+ `script()`/`link()` also accept a predefined library name — `motion` (Motion, formerly Framer Motion: the `Motion` global, plus `import … from 'motion'` in a `type: 'module'` script), `gsap` (includes ScrollTrigger, pre-registered), `swiper`, `animejs`, `chartjs`, `threejs` (import map — pair with a `type: 'module'` script), `lottie`, `typedjs`, `splide`, `htmx`, `vanilla-tilt`, and legacy `aos` — either call injects all of the library's CDN assets, deduped. Prefer these names over hand-picked CDN URLs, and only add a library the site actually uses.
454
454
 
455
455
  `script()` also takes an attributes object — `script({ src: '/js/app.js', defer: '' })`, `script({ content: '...', type: 'module' })` — so a tag that needs attributes still never needs a raw `<script>`. That is how a third-party script is put behind cookie consent: `script({ src: 'https://…', 'data-sleekcms-consent': 'marketing' })` (§14).
456
456
 
457
+ Animation needs no `script()` call at all — `data-motion` attributes are built in (§15):
458
+
457
459
  ```ejs
458
- <% script('aos') %> <!-- injects aos.js + aos.css + AOS.init() -->
459
- <section data-aos="fade-up">
460
+ <section data-motion="fade-up">
460
461
  <h2><%= item.title %></h2>
461
462
  </section>
462
- <section data-aos="fade-up" data-aos-delay="150">...</section>
463
463
  ```
464
464
 
465
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.
@@ -666,7 +666,196 @@ No config element → all defaults. One per site: a second one is ignored with a
666
666
 
667
667
  **Notes** — gating happens at build time: tagged scripts ship as `type="text/plain"` with their `src` stashed, and gated iframes ship without `src`, so nothing can fire before consent even on slow connections. The banner is a fixed overlay (or modal), so no layout shift. `<noscript>` content cannot be gated, and `document.write` inside a gated script is unsupported. SleekCMS provides the consent *mechanism*; which script belongs to which category, and the privacy policy itself, are the site owner's call — ask when unsure, and link the policy page via `data-policy-url`.
668
668
 
669
- ## 15. Critical Rules Checklist
669
+ ## 15. Motion & Animation
670
+
671
+ Motion directs attention and makes the site feel responsive; every animation has to buy back the moment it costs the visitor. A well-animated page usually has **one** composed entrance above the fold, scroll reveals with **2-3** related treatments (not one fade-up on everything), tactile hover/focus feedback on every interactive element, **at most one** showpiece scroll moment (a parallax hero, a pinned zoom, a horizontal gallery) and **at most one** ambient loop. A calm brochure site may need only the hover feedback.
672
+
673
+ **Motion that hurts:** animating everything; entrances that hold content hostage (above the fold finishes in ~800ms); re-animating on every scroll pass; animating `width`/`height`/`top`/`left`/`margin` instead of `transform`/`opacity`; scroll-hijacking that takes the scroll wheel away from the visitor (parallax driven by their own scrolling is fine); and any failure that leaves content stuck at `opacity: 0`.
674
+
675
+ **Pick the smallest tool:**
676
+
677
+ | Need | Use |
678
+ |---|---|
679
+ | Hover, focus, active feedback; a looping ambient effect | **Tailwind alone** — `transition`, `hover:`, `active:`, `animate-*` |
680
+ | Reveals, staggered grids, split headlines, counters, parallax, scroll-scrubbed zoom/slide/fade, a pinned horizontal gallery, a marquee, tilt or magnetic hover | **`data-motion` attributes** — built in; add the attribute and the build loads everything. No `script()` call. |
681
+ | An effect the attributes can't express — a click-driven sequence, a scroll callback, a cursor follower | **`script('motion')`** — Motion (formerly Framer Motion) for your own script |
682
+ | A swipeable carousel · typewriter · Lottie | `swiper` / `splide` · `typedjs` · `lottie` (§8) |
683
+
684
+ Never hand-write an `IntersectionObserver` reveal, a parallax scroll handler, or a count-up — the attributes are exactly that. Never load two motion libraries: `data-motion` and `script('motion')` are one (Motion); adding `aos` or `gsap` to the same page makes two.
685
+
686
+ ### `data-motion` attributes
687
+
688
+ Any page whose markup uses a `data-motion*` attribute gets Motion and the SleekCMS motion runtime injected at build time. Each element reveals **once**, when its top passes 90% of the screen height. `prefers-reduced-motion` is handled for you (nothing moves; only scroll-linked fades remain), content is never left hidden (if anything fails to load, the page shows normally within 3 seconds), and elements added later are picked up too.
689
+
690
+ | Attribute | What it does |
691
+ |---|---|
692
+ | `data-motion="fade-up"` | Reveal on scroll. Presets: `fade` `fade-up` `fade-down` `fade-left` `fade-right` · `zoom-in` `zoom-out` · `blur-in` · `pop` (springy scale) · `flip-up` `flip-down` `flip-left` `flip-right` · `reveal-up` `reveal-down` `reveal-left` `reveal-right` (clip-path wipe) · `draw` (stroked SVG draws itself) |
693
+ | `data-motion="y: 60; rotate: -3; opacity: 0"` | A custom start state instead of a preset — keys `opacity` `x` `y` `scale` `rotate` `rotateX` `rotateY` `blur` `clip-path`; numbers are px / deg |
694
+ | `data-motion-delay="150"` `data-motion-duration="800"` `data-motion-easing="ease-out"` `data-motion-distance="40"` | Tuning, in ms like AOS (`"0.4s"` also works). Easing also takes `linear` `ease-in-out` `ease-out-cubic` `ease-out-back` `spring` or `[0.2,0.8,0.2,1]`. Defaults: 600ms, a soft ease-out, 24px. |
695
+ | `data-motion-stagger="90"` | On a container: its `data-motion` children that come into view together play in order, 90ms apart — no index math in loops. |
696
+ | `data-motion-text="words"` / `"chars"` | Splits the text and reveals it piece by piece with the element's own `data-motion` preset (default `fade-up`); `data-motion-stagger` sets the step. Headings and short lines. |
697
+ | `data-motion-counter` | Counts up to the number written inside it — `1,250+`, `$4.9M`, `98%` keep their formatting. |
698
+ | `data-motion-parallax="0.2"` | Drifts against the scroll — larger is slower and feels further away (0.1–0.3 is plenty); negative feels closer. |
699
+ | `data-motion-scroll="scale: .85 1; opacity: .4 1"` | Scrubs values with the scrollbar. Keys as above plus `scaleX` `scaleY` and any CSS property; more than two values spread evenly across the range. |
700
+ | `data-motion-scroll-range="enter"` | `enter` (default — while it scrolls into view) · `exit` · `cover` (its whole trip across the screen) · `pin` (while a tall section's sticky child is pinned) · `page` (the whole page) |
701
+ | `data-motion-scroll-target="section"` | Measure another element — the nearest ancestor matching the selector (or the first match on the page). |
702
+ | `data-motion-horizontal` + `data-motion-track` | Pinned horizontal gallery: vertical scrolling slides the track sideways. |
703
+ | `data-motion-hover="tilt"` / `"magnetic"` | Spring-physics tilt toward the pointer, or a pull toward it (`"tilt 12"`, `"magnetic 0.4"` set the strength). Mouse and trackpad only. |
704
+ | `data-motion-marquee="40"` | Endless ticker on the row it sits on, px per second (negative runs right); slows on hover, pauses off screen. |
705
+
706
+ **One job per element:** an element with `data-motion-parallax`, `-scroll` or `-hover` owns its `transform` — wrap it rather than stacking another motion attribute or Tailwind `translate-*`/`scale-*`/`rotate-*` on it. **Sticky needs unclipped ancestors:** `pin` and `horizontal` rely on `position: sticky`, which an `overflow-hidden` ancestor silently breaks — use `overflow-x-clip`.
707
+
708
+ ### Recipes
709
+
710
+ ```ejs
711
+ <%# Hero entrance — one composed moment, in order %>
712
+ <div data-motion-stagger="120" class="mx-auto max-w-4xl px-6 pt-36 pb-24 text-center">
713
+ <p data-motion="fade-up" class="text-sm font-semibold uppercase tracking-[0.2em] text-brand-600"><%= item.eyebrow %></p>
714
+ <h1 data-motion="fade-up" class="mt-5 font-display text-6xl tracking-tight md:text-8xl"><%= item.title %></h1>
715
+ <p data-motion="fade-up" class="mx-auto mt-6 max-w-[46ch] text-lg text-muted"><%= item.lede %></p>
716
+ <a data-motion="pop" href="/book" class="mt-10 inline-flex rounded-full bg-brand-600 px-8 py-4 text-white">Book a table</a>
717
+ </div>
718
+
719
+ <%# Staggered cards from a loop %>
720
+ <div data-motion-stagger="90" class="grid gap-6 md:grid-cols-3">
721
+ <% item.services.forEach(function (s) { %><article data-motion="fade-up" class="rounded-card bg-surface p-8">…</article><% }) %>
722
+ </div>
723
+
724
+ <%# Split headline — words rise out of a mask; chars + fade reads like typing %>
725
+ <h2 data-motion="reveal-up" data-motion-text="words" class="font-display text-6xl md:text-8xl">Bread worth waking up for.</h2>
726
+ <p data-motion="fade" data-motion-text="chars" data-motion-stagger="30" class="font-mono text-lg">Baked fresh at 5am.</p>
727
+
728
+ <%# Image wipe %>
729
+ <figure data-motion="reveal-left" data-motion-duration="1100" class="overflow-hidden rounded-card"><%- img(item.photo, { w: 1200, h: 800, class: 'w-full object-cover' }) %></figure>
730
+
731
+ <%# Parallax background — the layer drifts at most speed × half the screen height,
732
+ so extend it past its frame by at least that: -inset-y-[12vh] covers 0.2 %>
733
+ <section class="relative isolate h-[80vh] overflow-hidden">
734
+ <div data-motion-parallax="0.2" class="absolute inset-x-0 -inset-y-[12vh] -z-10"><%- img(item.backdrop, { w: 1920, h: 1400, class: 'h-full w-full object-cover' }) %></div>
735
+ <div class="mx-auto max-w-5xl px-6 pt-48 text-white">…</div>
736
+ </section>
737
+
738
+ <%# Layered depth — further back = larger speed; negative comes forward %>
739
+ <section class="relative isolate h-screen overflow-hidden">
740
+ <div data-motion-parallax="0.3" class="absolute inset-x-0 -inset-y-[16vh] -z-20"><%- img(item.sky, { class: 'h-full w-full object-cover' }) %></div>
741
+ <div data-motion-parallax="0.12" class="absolute inset-x-0 bottom-0 -z-10"><%- img(item.hills, { class: 'w-full' }) %></div>
742
+ <h1 data-motion-parallax="-0.06" class="relative pt-[30vh] text-center font-display text-8xl text-white"><%= item.title %></h1>
743
+ </section>
744
+
745
+ <%# Reading progress bar — in layouts/common.ejs %>
746
+ <div data-motion-scroll="scaleX: 0 1" data-motion-scroll-range="page" class="fixed inset-x-0 top-0 z-50 h-1 origin-left bg-brand-600 motion-reduce:hidden"></div>
747
+
748
+ <%# Zoom into view %>
749
+ <figure data-motion-scroll="scale: .85 1; opacity: .5 1" class="mx-auto max-w-6xl overflow-hidden rounded-[2rem]"><%- img(item.showcase, { w: 1600, h: 1000, class: 'w-full' }) %></figure>
750
+
751
+ <%# Sticky media with scrolling steps — CSS sticky does the pinning %>
752
+ <section class="mx-auto grid max-w-6xl gap-16 px-6 py-32 md:grid-cols-2">
753
+ <div class="md:sticky md:top-24 md:h-[70vh]"><%- img(item.media, { class: 'h-full w-full rounded-card object-cover' }) %></div>
754
+ <div class="space-y-[30vh] md:py-[20vh]">
755
+ <% item.steps.forEach(function (step) { %><article data-motion="fade-up"><h3 class="font-display text-3xl"><%= step.title %></h3><p class="mt-3 text-muted"><%= step.body %></p></article><% }) %>
756
+ </div>
757
+ </section>
758
+
759
+ <%# Pinned scrollytelling — section height sets how long it pins; each caption holds for its third (two "1" stops) and cross-fades %>
760
+ <section class="relative h-[300vh]">
761
+ <div class="sticky top-0 grid h-screen place-items-center overflow-hidden">
762
+ <div data-motion-scroll="scale: .6 1; opacity: .3 1" data-motion-scroll-range="pin" data-motion-scroll-target="section" class="w-[min(90vw,1100px)] overflow-hidden rounded-[2rem]"><%- img(item.feature, { w: 1600, h: 1000, class: 'w-full' }) %></div>
763
+ <p data-motion-scroll="opacity: 1 1 0 0 0 0" data-motion-scroll-range="pin" data-motion-scroll-target="section" class="absolute bottom-16 text-2xl">Sourced locally.</p>
764
+ <p data-motion-scroll="opacity: 0 0 1 1 0 0" data-motion-scroll-range="pin" data-motion-scroll-target="section" class="absolute bottom-16 text-2xl">Milled by hand.</p>
765
+ <p data-motion-scroll="opacity: 0 0 0 0 1 1" data-motion-scroll-range="pin" data-motion-scroll-target="section" class="absolute bottom-16 text-2xl">Baked every morning.</p>
766
+ </div>
767
+ </section>
768
+
769
+ <%# Horizontal gallery — sized by the runtime; a plain swipeable row without JS or with reduced motion %>
770
+ <section data-motion-horizontal class="relative">
771
+ <div class="sticky top-0 flex h-screen items-center overflow-x-auto">
772
+ <div data-motion-track class="flex gap-6 px-[8vw]">
773
+ <% item.projects.forEach(function (p) { %><article class="w-[80vw] shrink-0 md:w-[42vw]"><%- img(p.image, { w: 900, h: 1100, class: 'rounded-card' }) %></article><% }) %>
774
+ </div>
775
+ </div>
776
+ </section>
777
+
778
+ <%# Oversized type sliding sideways — x in % of its own width %>
779
+ <div class="overflow-x-clip py-16"><p data-motion-scroll="x: 10% -35%" data-motion-scroll-range="cover" class="whitespace-nowrap font-display text-[18vw] leading-none">Fresh · Local · Daily ·</p></div>
780
+
781
+ <%# Stat counters — write the real figure: it's what search engines and reduced-motion visitors read %>
782
+ <dl data-motion-stagger="100" class="grid grid-cols-2 gap-8 md:grid-cols-4">
783
+ <% item.stats.forEach(function (s) { %><div data-motion="fade-up" class="flex flex-col-reverse"><dt class="mt-2 text-sm text-muted"><%= s.label %></dt><dd data-motion-counter class="font-display text-5xl tabular-nums"><%= s.value %></dd></div><% }) %>
784
+ </dl>
785
+
786
+ <%# Logo marquee — the runtime clones the row for a seamless loop %>
787
+ <div class="overflow-hidden py-10 [mask-image:linear-gradient(to_right,transparent,black_12%,black_88%,transparent)]">
788
+ <div data-motion-marquee="40" class="flex items-center gap-16"><% item.logos.forEach(function (logo) { %><%- img(logo, { h: 40, class: 'h-10 w-auto opacity-70' }) %><% }) %></div>
789
+ </div>
790
+
791
+ <%# Tilt card, magnetic button %>
792
+ <a href="/work/harbor" data-motion-hover="tilt" class="block rounded-card bg-surface p-8 shadow-xl">…</a>
793
+ <a href="/contact" data-motion-hover="magnetic" class="inline-flex rounded-full bg-brand-600 px-8 py-4 text-white">Start a project</a>
794
+
795
+ <%# Floating shape — an ambient loop is Tailwind: an @theme --animate-* keyframe token in css/tailwind.css %>
796
+ <div class="absolute -right-24 top-10 -z-10 size-72 rounded-full bg-brand-500/20 blur-3xl animate-drift motion-reduce:animate-none"></div>
797
+
798
+ <%# SVG line draw — stroked outlines only; data-motion-stagger on the <svg> draws shapes in turn %>
799
+ <svg data-motion="draw" data-motion-duration="1400" viewBox="0 0 400 120" fill="none" stroke="currentColor" stroke-width="3" class="w-full text-brand-600"><path d="M5 100 C 120 10, 260 10, 395 60" /></svg>
800
+ ```
801
+
802
+ **Page transitions** — a cross-fade between pages with no JS, in `css/tailwind.css` (browsers without support just navigate):
803
+
804
+ ```css
805
+ @media (prefers-reduced-motion: no-preference) {
806
+ @view-transition { navigation: auto; }
807
+ }
808
+ ```
809
+
810
+ ### Custom effects with the Motion API
811
+
812
+ When the attributes can't express it, load Motion in the layout and write your own script **as a module**, importing Motion exactly as its documentation does:
813
+
814
+ ```ejs
815
+ <%# layouts/common.ejs %>
816
+ <% script('motion') %>
817
+ <% script({ type: 'module', src: '/js/site.js' }) %>
818
+ ```
819
+
820
+ ```js
821
+ // js/site.js
822
+ import { animate, scroll, stagger } from 'motion';
823
+
824
+ if (!matchMedia('(prefers-reduced-motion: reduce)').matches) {
825
+ // Header hides on the way down, returns on the way up
826
+ const header = document.querySelector('.js-header');
827
+ let last = 0;
828
+ scroll((_, info) => {
829
+ const y = info.y.current;
830
+ header.classList.toggle('-translate-y-full', y > last && y > 120);
831
+ last = y;
832
+ });
833
+
834
+ // Menu opens as a sequence: panel first, then its links in turn
835
+ document.querySelector('.js-menu-open')?.addEventListener('click', () => {
836
+ animate([
837
+ ['.js-menu', { opacity: [0, 1], transform: ['translateY(-12px)', 'none'] }, { duration: 0.25 }],
838
+ ['.js-menu a', { opacity: [0, 1], transform: ['translateX(-8px)', 'none'] }, { delay: stagger(0.05), at: '-0.1' }],
839
+ ]);
840
+ });
841
+
842
+ // A dot that springs after the pointer
843
+ const dot = document.querySelector('.js-cursor');
844
+ if (dot && matchMedia('(pointer: fine)').matches) {
845
+ addEventListener('pointermove', (e) => animate(dot, { x: e.clientX, y: e.clientY }, { type: 'spring', stiffness: 400, damping: 30 }));
846
+ }
847
+ }
848
+ ```
849
+
850
+ - The API: `animate` (elements, values, or a sequence of steps), `scroll`, `inView`, `hover`, `press`, `stagger`, `spring`. A classic script works too: `script('/js/site.js')` with `const { animate, scroll } = Motion;`.
851
+ - **Always `script('motion')` first.** Never import Motion from a CDN URL, and never rely on the copy the build adds for `data-motion` — it has no `'motion'` import and loads after your scripts.
852
+ - Entrances belong to `data-motion`: it hides content before the first paint, while your own script runs after the page has already shown.
853
+
854
+ **Other libraries** (§8): `gsap` only for what Motion can't do, with one `matchMedia('(prefers-reduced-motion: reduce)')` guard around the whole file; `aos` is legacy — on a page you rework, swap `data-aos*` for the matching `data-motion*` attributes and drop `script('aos')`; never run two motion libraries on one page.
855
+
856
+ **Reduced motion & performance:** `data-motion` handles reduced motion itself; Tailwind motion takes `motion-reduce:` variants; any hand-written animation sits behind a `matchMedia` guard. With reduced motion on, every element must be visible, in place, immediately. A `data-motion` element waits for Motion (about 50 KB) before it appears, so never put an entrance on the hero's largest image — animate the words around it instead.
857
+
858
+ ## 16. Critical Rules Checklist
670
859
 
671
860
  1. **`+` is part of the key.** A collection's `+` appears on the model, the view, and the content file/folder: `models/pages/blog+.model`, `pages/blog+.ejs`, `content/pages/blog+/<slug>.json`. A file without the `+` is a *different*, single-type key.
672
861
  2. Models first: a content field that isn't in the `.model` won't sync. Update the model, then the content.
@@ -684,3 +873,4 @@ No config element → all defaults. One per site: a second one is ignored with a
684
873
  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).
685
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.
686
875
  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
+ 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.
package/dist/build.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // build.mjs v3 — SleekCMS workspace build/publish script.
2
+ // build.mjs v4 — SleekCMS workspace build/publish script.
3
3
  /**
4
4
  * Self-contained (no `npm install` needed). Run from anywhere inside a synced
5
5
  * SleekCMS workspace — the one `@sleekcms/sync` created, containing `.cache/token`.
@@ -26,8 +26,8 @@ import fs from "node:fs";
26
26
  import path from "node:path";
27
27
  import { pathToFileURL } from "node:url";
28
28
 
29
- // Same env → base-URL table the sync engine uses; env is the 3rd `-`-segment of
30
- // the per-site token (see sync-engine.ts), defaulting to production.
29
+ // Same env → base-URL table the sync engine uses. The workspace records its env in
30
+ // .cache/site.json (see sync-engine.ts); --env / SLEEKCMS_ENV override it.
31
31
  const MCP_BASE_URLS = {
32
32
  localhost: "http://app.sleekcms.test/api/mcp",
33
33
  development: "https://app.sleekcms.dev/api/mcp",
@@ -37,9 +37,15 @@ const MCP_BASE_URLS = {
37
37
  const TERMINAL = new Set(["COMPLETED", "FAILED", "EXPIRED"]);
38
38
  const DEFAULT_MAX_POLLS = 6;
39
39
 
40
- /** Env baked into the per-site token (`<a>-<b>-<env>-…`); falls back to production. */
41
- export function deriveEnv(token, override) {
42
- const env = (override || (token || "").split("-")[2] || "production").toLowerCase();
40
+ /** The workspace's env: the override, else what .cache/site.json records, else production. */
41
+ export function workspaceEnv(ws, override) {
42
+ let recorded;
43
+ try {
44
+ recorded = JSON.parse(fs.readFileSync(path.join(ws, ".cache", "site.json"), "utf-8")).env;
45
+ } catch {
46
+ recorded = undefined;
47
+ }
48
+ const env = String(override || recorded || "production").toLowerCase();
43
49
  return MCP_BASE_URLS[env] ? env : "production";
44
50
  }
45
51
 
@@ -182,7 +188,7 @@ async function main() {
182
188
  process.exit(2);
183
189
  }
184
190
 
185
- const env = deriveEnv(token, flags.env || process.env.SLEEKCMS_ENV);
191
+ const env = workspaceEnv(ws, flags.env || process.env.SLEEKCMS_ENV);
186
192
  const base = mcpBase(env);
187
193
  const maxPolls = Number(process.env.SLEEKCMS_MAX_BUILD_POLLS) || DEFAULT_MAX_POLLS;
188
194
 
@@ -205,7 +211,12 @@ async function main() {
205
211
  });
206
212
  } catch (err) {
207
213
  const msg = err && err.message ? err.message : String(err);
208
- const hint = err && err.httpStatus === 403 ? " (the workspace token must belong to a site ADMIN)" : "";
214
+ const hint =
215
+ err && err.httpStatus === 403
216
+ ? " (building and publishing are for site ADMINs)"
217
+ : err && err.httpStatus === 401
218
+ ? " (access to this site was revoked — add it again in sleekcms)"
219
+ : "";
209
220
  console.error(`❌ Build request failed${hint}: ${msg}`);
210
221
  writeStatus(ws, { status: "error", mode, errors: [msg], at: Date.now() });
211
222
  appendLog(ws, `${new Date().toISOString()}\t${mode}\tERROR\t${msg}`);
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import{readFileSync as Ne}from"node:fs";import{fileURLToPath as ar}from"node:url";import{createRequire as cr}from"node:module";import Ut from"node:path";import{render as lr}from"ink";import{execSync as ne,spawn as M}from"child_process";import{program as Zt,Option as Ct}from"commander";function re(){return Zt.name("cms-sync").description("SleekCMS CLI tool to sync and edit CMS templates locally. Log in once, then add the sites you want to sync \u2014 changes are watched and pushed automatically.").addOption(new Ct("-v, --version","output the version number").hideHelp()).addOption(new Ct("-e, --env <env>","Environment (localhost, development, production)").default("production").hideHelp()).addOption(new Ct("-d, --dev <secret>","Dev secret for non-production envs (sent as the _dev cookie; enables OTP-in-response). Falls back to SLEEKCMS_DEV_SECRET.").hideHelp()).option("-p, --path <path>","Directory path for files (default: ~/SleekCMS)").addHelpText("after",`
2
+ import{readFileSync as kt}from"node:fs";import{fileURLToPath as Vn}from"node:url";import{createRequire as Qn}from"node:module";import Ae from"node:path";import{render as Zn}from"ink";import{execSync as Ye,spawn as B}from"child_process";import{program as Je,Option as He}from"commander";function Xe(){return Je.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 He("-v, --version","output the version number").hideHelp()).addOption(new He("-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
- $ sleekcms # log in and pick sites to sync
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),Zt.opts()}function j(t){let e=process.platform==="win32"?`where ${t}`:`which ${t}`;try{return ne(e,{stdio:"ignore"}),!0}catch{return!1}}function se(t){if(process.platform!=="darwin")return!1;try{return ne(`osascript -e 'id of application "${t}"'`,{stdio:"ignore"}),!0}catch{return!1}}function sn(t,e){let n={detached:!0,stdio:"ignore",windowsHide:!0};process.platform==="win32"?M("cmd.exe",["/c",t,...e],n).unref():M(t,e,n).unref()}var te=[{name:"VS Code",cmd:"code",args:t=>["-n",t]},{name:"Cursor",cmd:"cursor",args:t=>["-n",t]}];function ee(t,e){let r=(e?te.filter(s=>s.cmd===e):te).find(s=>j(s.cmd));return r?(sn(r.cmd,r.args(t)),r.name):null}function on(t){if(process.platform==="darwin")return M("open",["-a","Terminal",t],{detached:!0,stdio:"ignore"}).unref(),!0;if(process.platform==="win32"){if(j("wt"))M("wt.exe",["-d",t],{detached:!0,stdio:"ignore"}).unref();else{let e=j("powershell")?"powershell.exe":"cmd.exe";M("cmd.exe",["/c","start","",e],{cwd:t,detached:!0,stdio:"ignore"}).unref()}return!0}return!1}function an(t){return se("iTerm")?(M("open",["-a","iTerm",t],{detached:!0,stdio:"ignore"}).unref(),!0):!1}function cn(t){return`'${t.replace(/'/g,"'\\''")}'`}var ln=[{cmd:"x-terminal-emulator",args:t=>["-e","bash","-c",t]},{cmd:"gnome-terminal",args:t=>["--","bash","-c",t]},{cmd:"konsole",args:t=>["-e","bash","-c",t]},{cmd:"xfce4-terminal",args:t=>["-x","bash","-c",t]},{cmd:"kitty",args:t=>["bash","-c",t]},{cmd:"alacritty",args:t=>["-e","bash","-c",t]},{cmd:"xterm",args:t=>["-e","bash","-c",t]}];function ie(t,e){if(!j(e))return!1;if(process.platform==="darwin"){let r=`tell application "Terminal" to do script ("cd " & quoted form of "${t}" & " && ${e}")`;return M("osascript",["-e",r,"-e",'tell application "Terminal" to activate'],{detached:!0,stdio:"ignore"}).unref(),!0}if(process.platform==="win32")return j("wt")?M("wt.exe",["-d",t,"cmd","/k",e],{detached:!0,stdio:"ignore"}).unref():M("cmd.exe",["/c","start","","cmd","/k",e],{cwd:t,detached:!0,stdio:"ignore"}).unref(),!0;let n=`cd ${cn(t)} && ${e}; exec bash`;for(let r of ln)if(j(r.cmd))try{return M(r.cmd,r.args(n),{cwd:t,detached:!0,stdio:"ignore"}).unref(),!0}catch{}return!1}function un(t){return ie(t,"claude")}function dn(t){return ie(t,"pi")}function oe(t){if(!t)return!1;try{return process.platform==="darwin"?M("open",[t],{detached:!0,stdio:"ignore"}).unref():process.platform==="win32"?M("cmd.exe",["/c","start","",t],{detached:!0,stdio:"ignore",windowsHide:!0}).unref():M("xdg-open",[t],{detached:!0,stdio:"ignore"}).unref(),!0}catch{return!1}}function ae(){let t=process.platform==="darwin",e=process.platform==="win32",n=[];return(t||e)&&n.push({key:"terminal",label:"terminal",run:on}),se("iTerm")&&n.push({key:"iterm",label:"iterm",run:an}),j("code")&&n.push({key:"code",label:"vscode",run:r=>ee(r,"code")!==null}),j("cursor")&&n.push({key:"cursor",label:"cursor",run:r=>ee(r,"cursor")!==null}),j("claude")&&n.push({key:"claude",label:"claude",run:un}),j("pi")&&n.push({key:"pi",label:"pi",run:dn}),n}import{EventEmitter as Gn}from"node:events";import{spawn as Un}from"node:child_process";import nt from"fs-extra";import It from"path";import qn from"chokidar";import{createHash as mn}from"crypto";import y from"fs-extra";import hn from"os";import v from"path";var ce={localhost:"http://app.sleekcms.test/api/mcp",development:"https://app.sleekcms.dev/api/mcp",production:"https://app.sleekcms.com/api/mcp"},fn=["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"],Pt=class extends Error{status;body;constructor(e,n,r){super(e),this.status=n,this.body=r}};async function ft(t,e,n,r,s){let i=await fetch(t+r,{method:n,headers:{Authorization:`Bearer ${e}`,...s!==void 0?{"Content-Type":"application/json"}:{}},body:s!==void 0?JSON.stringify(s):void 0});if(!i.ok){let a=await i.text();throw new Pt(`${n} ${r} \u2192 ${i.status}: ${a}`,i.status,a)}return i.json()}async function pn(t,e){e&&(await y.outputFile(v.join(t,"AGENT.md"),e),await y.outputFile(v.join(t,"CLAUDE.md"),e),await y.outputFile(v.join(t,".vscode","copilot-instructions.md"),e)),await y.outputFile(v.join(t,".vscode","settings.json"),JSON.stringify({"files.associations":{"*.model":"javascript"},"[javascript]":{"editor.defaultFormatter":"esbenp.prettier-vscode"},"js/ts.validate.enabled":!1},null,2))}async function yn(t,e){if(!e)return;let n=v.join(t,"build.mjs"),r=e.match(/build\.mjs v\d+/)?.[0];if(await y.pathExists(n)){let s=(await y.readFile(n,"utf-8")).match(/build\.mjs v\d+/)?.[0];if(s&&s===r)return}await y.outputFile(n,e)}async function wn(t,e,n){let r=v.join(t,".cache","token");if(await y.pathExists(r)){if((await y.readFile(r,"utf-8")).trim()===e)return;n?.("\u267B\uFE0F Workspace was tied to a different site \u2014 recreating it."),await y.emptyDir(t)}await y.outputFile(r,e)}function bn(t,e){let n=`${e.name.substr(0,20)} ${e.id}`.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,""),r=t||v.join(hn.homedir(),"SleekCMS");return v.resolve(r,n)}async function Mt(t){let e=(t.token||"").trim();if(!e)throw new Error("syncSite: token is required");let n=t.log??(x=>console.log(x)),r=t.onProgress,s=(t.env||e.split("-")[2]||"production").toLowerCase(),i=ce[s]||ce.production,a=await ft(i,e,"GET","/get_site"),u=t.viewsDir?v.resolve(t.viewsDir):bn(t.path,a);await y.ensureDir(u),await wn(u,e,n),await Promise.all(fn.map(x=>y.ensureDir(v.join(u,x)))),await yn(u,t.buildScript);let h=v.join(u,".cache","state.json");t.flush&&await y.remove(h);let w=!await y.pathExists(h),l=w?{}:(await y.readJson(h)).fileMap||{},b=0,S=0,g=0;if(w)({fileMap:l,pulled:S}=await Dn(u,i,e,n,r)),await pn(u,t.agentMd);else{let x=await Mn(u,l,i,e,n);g=x.renamed,b=await xn(u,l,i,e,n,r,x.held)}return await y.outputJson(h,{fileMap:l},{spaces:2}),{viewsDir:u,site:a,isFirstRun:w,pushed:b,pulled:S,renamed:g}}var tt=t=>mn("sha1").update(t).digest("hex"),Sn=new Set([".DS_Store","Thumbs.db","desktop.ini"]);async function Rt(t){let e=v.join(t,"src");if(!await y.pathExists(e))return[];let n=[];async function r(s){let i=await y.readdir(s,{withFileTypes:!0});for(let a of i){if(Sn.has(a.name))continue;let u=v.join(s,a.name);a.isDirectory()?await r(u):a.isFile()&&n.push(v.relative(t,u).replace(/\\/g,"/"))}}return await r(e),n}async function xn(t,e,n,r,s,i,a=new Set){let u=[];for(let S of await Rt(t)){if(a.has(S))continue;let g=v.join(t,S),x=e[S],C=await y.stat(g);if(x&&x.mtimeMs===C.mtimeMs){x.hash||(x.hash=tt(await y.readFile(g,"utf-8")));continue}let k=await y.readFile(g,"utf-8");k.trim()&&u.push({rel:S,full:g,stat:C,content:k,prior:x})}if(u.length===0)return 0;let h;try{h=await ft(n,r,"POST","/save_files",u.map(S=>({path:S.rel,content:S.content})))}catch(S){let g=S;return s(`\u274C Error saving files: ${g.body||g.message}`),0}let w=await de(t),l=0,b=vn(h);for(let S=0;S<u.length;S++){let g=u[S],x=Tn(g,S,h,b);if(i?.(S+1,u.length),!x){let C="No save response returned";w[g.rel]=C,s(`\u274C Error saving ${g.rel}: ${C}`);continue}if(x.error){w[g.rel]=x.error,s(`\u274C Error saving ${g.rel}: ${x.error}`);continue}delete w[g.rel],e[g.rel]={mtimeMs:g.stat.mtimeMs,hash:tt(g.content)},s(`\u2705 ${g.prior?"Updated":"Created"} ${g.rel}`),l++}return await ge(t,w),l}function vn(t){let e=new Map;for(let n of t)n.path&&e.set(n.path,n);return e}function Tn(t,e,n,r){let s=r.get(t.rel);if(s)return s;let i=n[e];if(!i?.path||i.path===t.rel)return i}var le=/^src\/(?:(?:models|views)\/(?:pages|entries|blocks)|content\/(?:pages|entries))\//,kn={PAGE:"pages",ENTRY:"entries",BLOCK:"blocks"},$n=t=>t.split("/").slice(0,3).join("/");function En(t,e,n){let r=new Map,s=(i,a)=>{let u=`${$n(i)}|${a}`;return r.has(u)||r.set(u,{from:[],to:[],hash:a}),r.get(u)};for(let i of t)s(i,n[i].hash).from.push(i);for(let[i,a]of e)s(i,a).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 Bn(t,e){let n=new Map,r=(s,i,a,u,h)=>{let w=n.get(s);w?(w.pairs.push(h),i<w.rank&&Object.assign(w,{from:a,to:u,rank:i})):n.set(s,{from:a,to:u,rank:i,pairs:[h]})};for(let s of t){let[,i,a,...u]=s.from.split("/"),h=s.to.split("/").slice(3);if(i==="content"&&a==="pages"&&u.length>1){let[l,...b]=u,[S,...g]=h;if(g.length===0)continue;S===l?r(`record:${s.from}`,0,s.from,s.to,s):b.join("/")===g.join("/")&&![...e].some(x=>x.startsWith(`src/content/pages/${l}/`))&&r(`pages:${l}`,3,`src/content/pages/${l}`,`src/content/pages/${S}`,s);continue}let w=u.join("/").replace(/\.[^./]+$/,"");r(`${a}:${w}`,i==="models"?0:i==="views"?1:2,s.from,s.to,s)}return[...n.values()].map(({from:s,to:i,pairs:a})=>({from:s,to:i,pairs:a}))}function ue(t,e){if(t.slug!==void 0&&t.new_slug!==void 0){let a=`src/content/pages/${t.key}/`,u=e.startsWith(a)?e.slice(a.length).match(/^(.+)\.(json|md)$/):null;return u&&u[1].toLowerCase()===t.slug.toLowerCase()?`${a}${t.new_slug}.${u[2]}`:null}let n=kn[t.type],{key:r,new_key:s}=t;if(!n||!s)return null;if(e===`src/models/${n}/${r}.model`)return`src/models/${n}/${s}.model`;if(e===`src/views/${n}/${r}.ejs`)return`src/views/${n}/${s}.ejs`;if(n!=="blocks"&&(e===`src/content/${n}/${r}.json`||e===`src/content/${n}/${r}.md`))return`src/content/${n}/${s}${v.extname(e)}`;let i=`src/content/pages/${r}/`;return n==="pages"&&r.endsWith("+")&&e.startsWith(i)?`src/content/pages/${s}/${e.slice(i.length)}`:null}async function Cn(t,e){let n=t;for(;n.startsWith(e+v.sep);){let r=await y.readdir(n).catch(()=>null);if(!r||r.length>0)return;await y.remove(n),n=v.dirname(n)}}async function Pn(t,e,n,r){let s=l=>v.join(t,l),i=v.join(t,"src"),a=l=>v.resolve(t,l).startsWith(i+v.sep),u=async(l,b)=>{e[l]={mtimeMs:(await y.stat(s(l))).mtimeMs,hash:b}},h=l=>Cn(v.dirname(s(l)),v.join(t,...l.split("/").slice(0,3))),w=[];for(let l of r){let b=ue(n.rename,l.from)??l.to;a(b)&&(b!==l.to&&(await y.move(s(l.to),s(b),{overwrite:!0}),await h(l.to),w.push(l.to)),delete e[l.from],w.push(l.from),await u(b,l.hash))}for(let l of new Set([...await Rt(t),...Object.keys(e)])){let b=ue(n.rename,l);if(!b||!a(b))continue;let S=await y.pathExists(s(l));if(S&&await y.pathExists(s(b)))continue;let g=e[l];if(delete e[l],w.push(l),!S)continue;let x=!!g?.hash&&g.mtimeMs===(await y.stat(s(l))).mtimeMs;await y.move(s(l),s(b)),x&&await u(b,g.hash),await h(l)}for(let l of n.files||[]){let b=e[l.path];if(!b||!a(l.path))continue;let S=await y.stat(s(l.path)).catch(()=>null);!S||S.mtimeMs!==b.mtimeMs||await y.readFile(s(l.path),"utf-8")!==l.content&&(await y.outputFile(s(l.path),l.content),await u(l.path,tt(l.content)))}return w}function On(t){try{let e=JSON.parse(t.body).message;if(e)return e}catch{}return(t.body||t.message||String(t)).slice(0,300)}async function Mn(t,e,n,r,s){let i=new Set,a=await Rt(t),u=new Set(a),h=Object.keys(e).filter(g=>!u.has(g)&&e[g].hash&&le.test(g));if(h.length===0)return{renamed:0,held:i};let w=new Map;for(let g of a){if(e[g]||!le.test(g))continue;let x=await y.readFile(v.join(t,g),"utf-8");x.trim()&&w.set(g,tt(x))}let l=Bn(En(h,w,e),u);if(l.length===0)return{renamed:0,held:i};let b=await de(t),S=0;for(let g of l){let x;try{x=await ft(n,r,"POST","/rename",{from:g.from,to:g.to})}catch(D){let L=D,q=On(L);if(L.status>=400&&L.status<500)s(`\u26A0\uFE0F Couldn't rename ${g.from} \u2192 ${g.to}: ${q} Saving it as a new file.`);else{for(let z of g.pairs)i.add(z.to),b[z.to]=`rename from ${z.from} failed: ${q}`;s(`\u274C Error renaming ${g.from} \u2192 ${g.to}: ${q}`)}continue}let C=await Pn(t,e,x,g.pairs);for(let D of[...C,...g.pairs.map(L=>L.to)])delete b[D];let[k,...P]=x.message.split(`
7
- `);s(`\u{1F500} ${k}`);for(let D of P)s(` ${D}`);S++}return await ge(t,b),{renamed:S,held:i}}var Dt="sync-errors.log",Ot=new Map;async function de(t){let e=v.resolve(t),n=Ot.get(e);if(n)return n;let r=await Rn(e);return Ot.set(e,r),r}async function Rn(t){let e=v.join(t,Dt);if(!await y.pathExists(e))return{};let n=await y.readFile(e,"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 ge(t,e){let n=v.join(t,Dt),r=Object.entries(e);if(r.length===0){await y.remove(n);return}await y.outputFile(n,r.map(([s,i])=>`${s}: ${i}`).join(`
6
+ `).parse(process.argv),Je.opts()}function R(e){let t=process.platform==="win32"?`where ${e}`:`which ${e}`;try{return Ye(t,{stdio:"ignore"}),!0}catch{return!1}}function Ke(e){if(process.platform!=="darwin")return!1;try{return Ye(`osascript -e 'id of application "${e}"'`,{stdio:"ignore"}),!0}catch{return!1}}function Ot(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 ze=[{name:"VS Code",cmd:"code",args:e=>["-n",e]},{name:"Cursor",cmd:"cursor",args:e=>["-n",e]}];function qe(e,t){let r=(t?ze.filter(s=>s.cmd===t):ze).find(s=>R(s.cmd));return r?(Ot(r.cmd,r.args(e)),r.name):null}function It(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 Nt(e){return Ke("iTerm")?(B("open",["-a","iTerm",e],{detached:!0,stdio:"ignore"}).unref(),!0):!1}function Wt(e){return`'${e.replace(/'/g,"'\\''")}'`}var Gt=[{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 Ve(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 ${Wt(e)} && ${t}; exec bash`;for(let r of Gt)if(R(r.cmd))try{return B(r.cmd,r.args(n),{cwd:e,detached:!0,stdio:"ignore"}).unref(),!0}catch{}return!1}function Ut(e){return Ve(e,"claude")}function Jt(e){return Ve(e,"pi")}function ae(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 Qe(){let e=process.platform==="darwin",t=process.platform==="win32",n=[];return(e||t)&&n.push({key:"terminal",label:"terminal",run:It}),Ke("iTerm")&&n.push({key:"iterm",label:"iterm",run:Nt}),R("code")&&n.push({key:"code",label:"vscode",run:r=>qe(r,"code")!==null}),R("cursor")&&n.push({key:"cursor",label:"cursor",run:r=>qe(r,"cursor")!==null}),R("claude")&&n.push({key:"claude",label:"claude",run:Ut}),R("pi")&&n.push({key:"pi",label:"pi",run:Jt}),n}import{EventEmitter as Cn}from"node:events";import{spawn as Mn}from"node:child_process";import Z from"fs-extra";import Ce from"path";import Rn from"chokidar";import{createHash as zt}from"crypto";import f from"fs-extra";import qt from"os";import b from"path";var Ze={localhost:"http://app.sleekcms.test/api/mcp",development:"https://app.sleekcms.dev/api/mcp",production:"https://app.sleekcms.com/api/mcp"},Yt=["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"],X=class extends Error{status;body;constructor(t,n,r){super(t),this.status=n,this.body=r}};async function ce(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 X(`${n} ${r} \u2192 ${i.status}: ${d}`,i.status,d)}return i.json()}var Xt=["AGENT.md","CLAUDE.md",b.join(".vscode","copilot-instructions.md")],Kt="mrmlnc.vscode-json5";async function Vt(e,t){t&&(await f.outputFile(b.join(e,"AGENTS.md"),t),await Promise.all(Xt.map(n=>f.remove(b.join(e,n))))),await f.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 f.outputFile(b.join(e,".vscode","extensions.json"),JSON.stringify({recommendations:[Kt]},null,2))}async function Qt(e,t){if(!t)return;let n=b.join(e,"build.mjs"),r=t.match(/build\.mjs v\d+/)?.[0];if(await f.pathExists(n)){let s=(await f.readFile(n,"utf-8")).match(/build\.mjs v\d+/)?.[0];if(s&&s===r)return}await f.outputFile(n,t)}async function Zt(e,t,n,r){let s=b.join(e,".cache","site.json"),i=await f.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 f.emptyDir(e)),await f.outputJson(s,n),await f.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(qt.homedir(),"SleekCMS");return b.resolve(r,n)}async function ve(e){let t=(e.token||"").trim();if(!t)throw new Error("syncSite: token is required");let n=e.log??(S=>console.log(S)),r=e.onProgress,s=(e.env||"production").toLowerCase(),i=Ze[s]||Ze.production,d=await ce(i,t,"GET","/get_site"),l=e.viewsDir?b.resolve(e.viewsDir):en(e.path,d);await f.ensureDir(l),await Zt(l,t,{env:s,slug:String(d.id)},n),await Promise.all(Yt.map(S=>f.ensureDir(b.join(l,S)))),await Qt(l,e.buildScript);let y=b.join(l,".cache","state.json");e.flush&&await f.remove(y);let v=!await f.pathExists(y),u=v?{}:(await f.readJson(y)).fileMap||{},m=0,w=0,h=0;if(v)({fileMap:u,pulled:w}=await fn(l,i,t,n,r)),await Vt(l,e.agentMd);else{let S=await hn(l,u,i,t,n);h=S.renamed,m=await nn(l,u,i,t,n,r,S.held)}return await f.outputJson(y,{fileMap:u},{spaces:2}),{viewsDir:l,site:d,isFirstRun:v,pushed:m,pulled:w,renamed:h}}var K=e=>zt("sha1").update(e).digest("hex"),tn=new Set([".DS_Store","Thumbs.db","desktop.ini"]);async function Se(e){let t=b.join(e,"src");if(!await f.pathExists(t))return[];let n=[];async function r(s){let i=await f.readdir(s,{withFileTypes:!0});for(let d of i){if(tn.has(d.name))continue;let l=b.join(s,d.name);d.isDirectory()?await r(l):d.isFile()&&n.push(b.relative(e,l).replace(/\\/g,"/"))}}return await r(t),n}async function nn(e,t,n,r,s,i,d=new Set){let l=[];for(let w of await Se(e)){if(d.has(w))continue;let h=b.join(e,w),S=t[w],E=await f.stat(h);if(S&&S.mtimeMs===E.mtimeMs){S.hash||(S.hash=K(await f.readFile(h,"utf-8")));continue}let I=await f.readFile(h,"utf-8");I.trim()&&l.push({rel:w,full:h,stat:E,content:I,prior:S})}if(l.length===0)return 0;let y;try{y=await ce(n,r,"POST","/save_files",l.map(w=>({path:w.rel,content:w.content})))}catch(w){let h=w;return s(`\u274C Error saving files: ${h.body||h.message}`),0}let v=await nt(e),u=0,m=rn(y);for(let w=0;w<l.length;w++){let h=l[w],S=sn(h,w,y,m);if(i?.(w+1,l.length),!S){let E="No save response returned";v[h.rel]=E,s(`\u274C Error saving ${h.rel}: ${E}`);continue}if(S.error){v[h.rel]=S.error,s(`\u274C Error saving ${h.rel}: ${S.error}`);continue}delete v[h.rel],t[h.rel]={mtimeMs:h.stat.mtimeMs,hash:K(h.content)},s(`\u2705 ${h.prior?"Updated":"Created"} ${h.rel}`),u++}return await rt(e,v),u}function rn(e){let t=new Map;for(let n of e)n.path&&t.set(n.path,n);return t}function sn(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 et=/^src\/(?:(?:models|views)\/(?:pages|entries|blocks)|content\/(?:pages|entries))\//,on={PAGE:"pages",ENTRY:"entries",BLOCK:"blocks"},an=e=>e.split("/").slice(0,3).join("/");function cn(e,t,n){let r=new Map,s=(i,d)=>{let l=`${an(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,y)=>{let v=n.get(s);v?(v.pairs.push(y),i<v.rank&&Object.assign(v,{from:d,to:l,rank:i})):n.set(s,{from:d,to:l,rank:i,pairs:[y]})};for(let s of e){let[,i,d,...l]=s.from.split("/"),y=s.to.split("/").slice(3);if(i==="content"&&d==="pages"&&l.length>1){let[u,...m]=l,[w,...h]=y;if(h.length===0)continue;w===u?r(`record:${s.from}`,0,s.from,s.to,s):m.join("/")===h.join("/")&&![...t].some(S=>S.startsWith(`src/content/pages/${u}/`))&&r(`pages:${u}`,3,`src/content/pages/${u}`,`src/content/pages/${w}`,s);continue}let v=l.join("/").replace(/\.[^./]+$/,"");r(`${d}:${v}`,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 tt(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=on[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 un(e,t){let n=e;for(;n.startsWith(t+b.sep);){let r=await f.readdir(n).catch(()=>null);if(!r||r.length>0)return;await f.remove(n),n=b.dirname(n)}}async function dn(e,t,n,r){let s=u=>b.join(e,u),i=b.join(e,"src"),d=u=>b.resolve(e,u).startsWith(i+b.sep),l=async(u,m)=>{t[u]={mtimeMs:(await f.stat(s(u))).mtimeMs,hash:m}},y=u=>un(b.dirname(s(u)),b.join(e,...u.split("/").slice(0,3))),v=[];for(let u of r){let m=tt(n.rename,u.from)??u.to;d(m)&&(m!==u.to&&(await f.move(s(u.to),s(m),{overwrite:!0}),await y(u.to),v.push(u.to)),delete t[u.from],v.push(u.from),await l(m,u.hash))}for(let u of new Set([...await Se(e),...Object.keys(t)])){let m=tt(n.rename,u);if(!m||!d(m))continue;let w=await f.pathExists(s(u));if(w&&await f.pathExists(s(m)))continue;let h=t[u];if(delete t[u],v.push(u),!w)continue;let S=!!h?.hash&&h.mtimeMs===(await f.stat(s(u))).mtimeMs;await f.move(s(u),s(m)),S&&await l(m,h.hash),await y(u)}for(let u of n.files||[]){let m=t[u.path];if(!m||!d(u.path))continue;let w=await f.stat(s(u.path)).catch(()=>null);!w||w.mtimeMs!==m.mtimeMs||await f.readFile(s(u.path),"utf-8")!==u.content&&(await f.outputFile(s(u.path),u.content),await l(u.path,K(u.content)))}return v}function gn(e){try{let t=JSON.parse(e.body).message;if(t)return t}catch{}return(e.body||e.message||String(e)).slice(0,300)}async function hn(e,t,n,r,s){let i=new Set,d=await Se(e),l=new Set(d),y=Object.keys(t).filter(h=>!l.has(h)&&t[h].hash&&et.test(h));if(y.length===0)return{renamed:0,held:i};let v=new Map;for(let h of d){if(t[h]||!et.test(h))continue;let S=await f.readFile(b.join(e,h),"utf-8");S.trim()&&v.set(h,K(S))}let u=ln(cn(y,v,t),l);if(u.length===0)return{renamed:0,held:i};let m=await nt(e),w=0;for(let h of u){let S;try{S=await ce(n,r,"POST","/rename",{from:h.from,to:h.to})}catch(C){let M=C,H=gn(M);if(M.status>=400&&M.status<500)s(`\u26A0\uFE0F Couldn't rename ${h.from} \u2192 ${h.to}: ${H} Saving it as a new file.`);else{for(let D of h.pairs)i.add(D.to),m[D.to]=`rename from ${D.from} failed: ${H}`;s(`\u274C Error renaming ${h.from} \u2192 ${h.to}: ${H}`)}continue}let E=await dn(e,t,S,h.pairs);for(let C of[...E,...h.pairs.map(M=>M.to)])delete m[C];let[I,...pe]=S.message.split(`
7
+ `);s(`\u{1F500} ${I}`);for(let C of pe)s(` ${C}`);w++}return await rt(e,m),{renamed:w,held:i}}var xe="sync-errors.log",be=new Map;async function nt(e){let t=b.resolve(e),n=be.get(t);if(n)return n;let r=await pn(t);return be.set(t,r),r}async function pn(e){let t=b.join(e,xe);if(!await f.pathExists(t))return{};let n=await f.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 rt(e,t){let n=b.join(e,xe),r=Object.entries(t);if(r.length===0){await f.remove(n);return}await f.outputFile(n,r.map(([s,i])=>`${s}: ${i}`).join(`
9
9
  `)+`
10
- `)}async function Dn(t,e,n,r,s){Ot.delete(v.resolve(t)),await y.remove(v.join(t,Dt)),r("\u{1F4E5} Fetching files...");let i=await ft(e,n,"GET","/get_files"),a={},u=0;for(let h of i){let w=v.join(t,h.path);await y.outputFile(w,h.content);let l=(await y.stat(w)).mtimeMs;u++,s?.(u,i.length),a[h.path]={mtimeMs:l,hash:tt(h.content)}}return r(`\u2714\uFE0F Synced ${i.length} file(s).`),{fileMap:a,pulled:u}}import Ln from"path";import jn from"chokidar";var Fn=5e3,An=/(^|[/\\])(\.DS_Store|Thumbs\.db|desktop\.ini)$/,et=class{viewsDir;onSync;debounceMs;watcher=null;debounceTimer=null;isShuttingDown=!1;dirty=!1;syncInFlight=!1;constructor(e){this.viewsDir=e.viewsDir,this.onSync=e.onSync,this.debounceMs=e.debounceMs??Fn}start(){if(this.watcher)return;let e=Ln.join(this.viewsDir,"src");this.watcher=jn.watch([e],{persistent:!0,ignoreInitial:!0,ignored:An}).on("change",()=>this.scheduleSync()).on("add",()=>this.scheduleSync()).on("unlink",()=>this.scheduleSync()).on("addDir",()=>this.scheduleSync()).on("unlinkDir",()=>this.scheduleSync())}setShuttingDown(e){this.isShuttingDown=e}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(e){let n=e;console.error("\u274C Sync failed:",n.body||n.message)}finally{this.syncInFlight=!1}}}};import pt from"fs-extra";import _n from"os";import he from"path";var me={localhost:"http://app.sleekcms.test/api",development:"https://app.sleekcms.dev/api",production:"https://app.sleekcms.com/api"};function jt(t){return me[t]||me.production}var In=he.join(_n.homedir(),"SleekCMS"),yt=he.join(In,"auth.json"),Lt;function fe(t){Lt=t||void 0}function Ft(t){let e=[];return t&&e.push(`_auth=${t}`),Lt&&e.push(`_dev=${Lt}`),e.length?e.join("; "):void 0}var F=class extends Error{status;body;constructor(e,n,r=""){super(e),this.name="UserAuthError",this.status=n,this.body=r}};async function pe(){try{let t=await pt.readJson(yt);return t&&t.userToken?t:null}catch{return null}}async function ye(t){await pt.outputJson(yt,t,{spaces:2});try{await pt.chmod(yt,384)}catch{}}async function we(){await pt.remove(yt)}async function K(t){let e={},n=Ft(t.userToken);n&&(e.Cookie=n),t.subdomain&&(e["x-subdomain"]=t.subdomain),t.slug&&(e.site=t.slug),t.body!==void 0&&(e["Content-Type"]="application/json");let r=await fetch(jt(t.env)+t.path,{method:t.method,headers:e,body:t.body!==void 0?JSON.stringify(t.body):void 0});if(!r.ok){let s=await r.text().catch(()=>"");throw new F(`${t.method} ${t.path} \u2192 ${r.status}: ${s}`,r.status,s)}return r.json()}async function be(t,e){let n={"Content-Type":"application/json"},r=Ft();r&&(n.Cookie=r);let s=await fetch(jt(t)+"/auth/otp",{method:"PUT",headers:n,body:JSON.stringify({email:e})});if(!s.ok){let a=await s.text().catch(()=>"");throw new F(`Failed to send OTP \u2192 ${s.status}: ${a}`,s.status,a)}return{otp:(await s.json().catch(()=>({}))).do_not_use_in_code}}async function At(t,e,n){let r={"Content-Type":"application/json"},s=Ft();s&&(r.Cookie=s);let i=await fetch(jt(t)+"/auth/login",{method:"POST",headers:r,body:JSON.stringify({email:e,otp:n})});if(!i.ok){let h=await i.text().catch(()=>"");throw new F(`Login failed \u2192 ${i.status}: ${h}`,i.status,h)}let a=i.headers.getSetCookie?.()??[],u=Nn(a);if(!u)throw new F("Login succeeded but no _auth cookie was returned.",500);return u}function Nn(t){for(let e of t){let n=e.match(/(?:^|;\s*)_auth=([^;]+)/);if(n&&n[1])return decodeURIComponent(n[1])}return null}async function _t(t,e){return K({env:t,userToken:e,method:"GET",path:"/org"})}async function Se(t,e,n){return K({env:t,userToken:e,method:"GET",path:"/site",subdomain:n})}function Wn(t){if(t==null)return"(unknown error)";if(typeof t=="string")return t;if(typeof t!="object")return String(t);let e=t,n=typeof e.file_path=="string"?e.file_path:"",r=typeof e.path=="string"?e.path:typeof e.filename=="string"?e.filename:"",s=n||r,i=e.line!=null?`:${e.line}`:"",a=e.column!=null?`:${e.column}`:"",u=typeof e.message=="string"&&e.message?e.message:typeof e.name=="string"&&e.name?e.name:"",h=n&&r&&r!==n?` (on page ${r})`:"";if(s)return`${s}${i}${a}${h}: ${u||"(no message)"}`;if(u)return u;try{return JSON.stringify(t)}catch{return"(unknown error)"}}async function xe(t,e,n,r){let s;try{s=await K({env:t,userToken:e,method:"GET",path:"/site_build",subdomain:n,slug:r})}catch{return null}if(!Array.isArray(s)||s.length===0)return null;let i=s[0],a=s.find(h=>typeof h.url=="string"&&h.url.length>0),u=Array.isArray(i.errors)?i.errors.map(Wn):[];return u.length>0?{status:"error",mode:"preview",errors:u,url:a?.url||void 0,at:i.updated_at?Date.parse(i.updated_at):void 0}:a?{status:"success",mode:"preview",url:a.url||void 0,at:a.updated_at?Date.parse(a.updated_at):void 0}:null}async function ve(t,e,n,r){let{token:s}=await K({env:t,userToken:e,method:"POST",path:"/site_user/api_token",subdomain:n,slug:r,body:{refresh:!1}});return s}async function Te(t,e,n){let r=await K({env:t,userToken:e,method:"POST",path:"/org",body:n?{name:n}:{}});return{id:r.id,name:r.name,subdomain:r.subdomain}}async function ke(t,e,n,r){let s=await K({env:t,userToken:e,method:"POST",path:"/site",subdomain:n,body:{name:r}});return{id:s.id,name:s.name,slug:s.slug}}var $e=500;function Hn(){return new Date().toTimeString().slice(0,8)}function wt(t){let e=t;if(e.body){try{let n=JSON.parse(e.body);if(n&&typeof n.message=="string")return n.message;if(n&&typeof n.error=="string")return n.error}catch{}return e.body}return e.message}async function Ee(t){t&&(await nt.remove(It.join(t,"src")),await nt.remove(It.join(t,".cache","state.json")))}var bt=class extends Gn{env="production";userToken="";email;catalog=[];synced=new Map;message;messageIsError=!1;agentMd;buildScript;isShuttingDown=!1;constructor(e,n){super(),this.agentMd=e,this.buildScript=n}changed(){this.emit("change")}flash(e,n=!1){this.message=e,this.messageIsError=n,this.changed()}appendLog(e,n){let r=this.synced.get(e);r&&(r.logs.push({time:Hn(),text:n}),r.logs.length>$e&&r.logs.splice(0,r.logs.length-$e),this.changed())}siteCount(){return this.synced.size}syncingCount(){let e=0;for(let n of this.synced.values())n.status==="syncing"&&e++;return e}watchingCount(){let e=0;for(let n of this.synced.values())(n.status==="watching"||n.status==="syncing")&&e++;return e}async init(e,n,r){this.env=e,this.userToken=n,r&&(this.email=r);let s=await _t(e,n);await this.buildCatalog(s)}async buildCatalog(e){let n=[];for(let r of e)try{n.push({org:r,sites:await Se(this.env,this.userToken,r.subdomain)})}catch(s){if(s instanceof F&&s.status===401)throw s;n.push({org:r,sites:[]})}this.catalog=n,this.changed()}isSynced(e){return this.synced.has(e)}availableSites(){let e=[];for(let n of this.catalog)for(let r of n.sites)this.synced.has(r.id)||e.push({org:n.org,site:r});return e}async addSite(e,n){if(this.synced.has(n.id))return;let r;try{r=await ve(this.env,this.userToken,e.subdomain,n.slug)}catch(i){this.flash(`Failed to authorize "${n.name}": ${wt(i)}`,!0);return}let s={org:e,site:n,siteToken:r,viewsDir:"",status:"syncing",logs:[]};this.synced.set(n.id,s),this.appendLog(n.id,"\u2795 added \u2014 pulling files"),this.changed();try{let i=await Mt({token:r,env:this.env,agentMd:this.agentMd,buildScript:this.buildScript,log:a=>this.appendLog(n.id,a),onProgress:(a,u)=>{s.progress={done:a,total:u},this.changed()}});s.viewsDir=i.viewsDir,s.progress=void 0,s.status="watching",s.lastSyncedAt=Date.now(),s.lastMsg=`pulled ${i.pulled} file(s)`,s.watcher=new et({viewsDir:i.viewsDir,onSync:()=>this.runSync(n.id)}),s.watcher.start(),this.watchBuildStatus(n.id),this.seedLastBuild(n.id),this.appendLog(n.id,"\u{1F440} watching for changes")}catch(i){let a=i;s.status="error",s.progress=void 0,s.lastMsg=a.body||a.message,this.appendLog(n.id,`\u274C ${a.body||a.message}`)}this.changed()}async removeSite(e){let n=this.synced.get(e);n&&(n.watcher?.setShuttingDown(!0),await n.watcher?.stop(),await n.buildWatcher?.close(),await Ee(n.viewsDir),this.synced.delete(e),this.changed())}buildSite(e,n=!1){let r=this.synced.get(e);if(!r||!r.viewsDir)return;let s=n?"live":"preview";this.writeBuildFile(r.viewsDir,{status:"building",mode:s,stage:"queued",url:r.lastBuild?.url,at:Date.now()}),Un("node",n?["build.mjs","--publish"]:["build.mjs"],{cwd:r.viewsDir,stdio:"ignore"}).on("error",u=>{this.writeBuildFile(r.viewsDir,{status:"error",mode:s,errors:[u.message],url:r.lastBuild?.url,at:Date.now()}),this.appendLog(e,`\u274C build: ${u.message}`)}),this.appendLog(e,n?"\u{1F680} publishing\u2026":"\u{1F528} building\u2026")}buildFile(e){return It.join(e,".cache","build.json")}async readBuildFile(e){try{let n=await nt.readJson(this.buildFile(e));return n&&n.status?n:null}catch{return null}}async writeBuildFile(e,n){let r=this.buildFile(e),s=`${r}.tmp`;try{await nt.outputJson(s,n),await nt.rename(s,r)}catch{}}watchBuildStatus(e){let n=this.synced.get(e);!n||!n.viewsDir||(n.buildWatcher=qn.watch(this.buildFile(n.viewsDir),{ignoreInitial:!1}).on("add",()=>{this.readBuildStatus(e)}).on("change",()=>{this.readBuildStatus(e)}))}async readBuildStatus(e){let n=this.synced.get(e);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(e){let n=this.synced.get(e);if(!n)return;let r=await xe(this.env,this.userToken,n.org.subdomain,n.site.slug);if(!r)return;let s=this.synced.get(e);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 ensureOrg(e){if(e)return e;if(this.catalog[0])return this.catalog[0].org;let n;try{n=await _t(this.env,this.userToken)}catch(r){return this.flash(`Couldn't check your organizations: ${wt(r)}`,!0),null}if(n.length>0){try{await this.buildCatalog(n)}catch{}return n[0]}try{let r=this.email?`${this.email.split("@")[0]}'s Sites`:"My Sites";this.flash("No organization yet \u2014 creating one\u2026");let s=await Te(this.env,this.userToken,r);return this.catalog=[...this.catalog,{org:s,sites:[]}],this.changed(),s}catch(r){return this.flash(`Could not create an organization: ${wt(r)}`,!0),null}}async createNewSite(e,n){let r=await this.ensureOrg(n);if(!r)return;let s;try{s=await ke(this.env,this.userToken,r.subdomain,e)}catch(a){this.flash(`Could not create "${e}": ${wt(a)}`,!0);return}let i=this.catalog.find(a=>a.org.id===r.id);i&&!i.sites.some(a=>a.id===s.id)&&(i.sites=[...i.sites,s]),this.flash(`Created "${s.name}".`),await this.addSite(r,s)}async refetch(e){await this.runSync(e,{flush:!0})}async runSync(e,n={}){let r=this.synced.get(e);if(!(!r||!r.viewsDir||r.status==="paused")){r.status="syncing",this.changed();try{let s=await Mt({token:r.siteToken,env:this.env,viewsDir:r.viewsDir,agentMd:this.agentMd,buildScript:this.buildScript,flush:n.flush,log:i=>this.appendLog(e,i),onProgress:(i,a)=>{r.progress={done:i,total:a},this.changed()}});if(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){let i=s;r.status="error",r.progress=void 0,r.lastMsg=i.body||i.message,this.appendLog(e,`\u274C ${i.body||i.message}`)}this.changed()}}async togglePause(e){let n=this.synced.get(e);!n||!n.viewsDir||(n.status==="paused"?(n.watcher=new et({viewsDir:n.viewsDir,onSync:()=>this.runSync(e)}),n.watcher.start(),n.status="watching",this.appendLog(e,"\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(e,"\u23F8 paused")),this.changed())}async cleanupAll(){for(let e of this.synced.values())e.watcher?.setShuttingDown(!0),await e.watcher?.stop(),await e.buildWatcher?.close(),await Ee(e.viewsDir)}async exit(){this.isShuttingDown||(this.isShuttingDown=!0,await this.cleanupAll(),this.emit("exit"))}async logout(){this.isShuttingDown||(this.isShuttingDown=!0,await we(),await this.cleanupAll(),this.emit("logout"))}};import{useEffect as rr,useState as Ae}from"react";import{Box as sr,Text as ir,useInput as or}from"ink";import{useState as rt}from"react";import{Box as st,Text as Y}from"ink";import Be from"ink-text-input";import{jsx as G,jsxs as X}from"react/jsx-runtime";function Ce(t){let e=t;return e?.body||e?.message||String(t)}function Pe({env:t,initialError:e,onAuthed:n}){let[r,s]=rt("email"),[i,a]=rt(""),[u,h]=rt(""),[w,l]=rt(e),[b,S]=rt(),g=async C=>{let k=C.trim();if(k){l(void 0),s("busy");try{let{otp:P}=await be(t,k);if(a(k),P){let D=await At(t,k,P);await n(D,k);return}S(`OTP sent to ${k}`),s("otp")}catch(P){l(Ce(P)),s("email")}}},x=async C=>{let k=C.trim();if(k){l(void 0),s("busy");try{let P=await At(t,i,k);await n(P,i)}catch(P){l(Ce(P)),h(""),s("otp")}}};return X(st,{flexDirection:"column",children:[X(Y,{bold:!0,children:["\u25C6 SleekCMS \u2014 sign in",t!=="production"?` (${t})`:""]}),w?X(Y,{color:"red",children:["\u2717 ",w]}):null,r==="email"&&X(st,{marginTop:1,children:[G(Y,{children:"Email: "}),G(Be,{value:i,onChange:a,onSubmit:g,placeholder:"you@example.com"})]}),r==="otp"&&X(st,{flexDirection:"column",marginTop:1,children:[b?G(Y,{color:"green",children:b}):null,X(st,{children:[G(Y,{children:"Enter OTP: "}),G(Be,{value:u,onChange:h,onSubmit:x})]})]}),r==="busy"&&G(st,{marginTop:1,children:G(Y,{children:"Working\u2026"})})]})}import{Fragment as Kn,useEffect as it,useMemo as Yn,useState as O}from"react";import{Box as p,Text as d,useInput as Xn,useStdout as zn}from"ink";import Re from"ink-text-input";import Jn from"node:os";function Nt(t){if(!t)return"\u2014";let e=Jn.homedir();return t.startsWith(e)?"~"+t.slice(e.length):t}function Wt(t){if(!t)return"\u2014";let e=Math.floor((Date.now()-t)/1e3);if(e<5)return"just now";if(e<60)return`${e}s ago`;let n=Math.floor(e/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 xt(t){return!t||t.total<=0?null:Math.round(t.done/t.total*100)}var St=["\u280B","\u2819","\u2839","\u2838","\u283C","\u2834","\u2826","\u2827","\u2807","\u280F"];function Oe(t){return St[(t%St.length+St.length)%St.length]}function vt(t){switch(t){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 Me(t){if(!t)return{dot:"\xB7",color:"gray",label:"\u2014"};switch(t.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:t.mode==="live"?"live":"preview"}}}import{Fragment as ot,jsx as o,jsxs as m}from"react/jsx-runtime";var U=(t,e)=>e<=0?0:Math.max(0,Math.min(t,e-1)),Vn=16,Qn=5,Zn="greenBright",De="blueBright",Le="magentaBright";function je({k:t,bg:e=Zn}){return o(d,{color:"black",backgroundColor:e,children:` ${t} `})}function I({k:t,label:e,width:n=16,bg:r}){return m(p,{width:n,children:[o(je,{k:t,bg:r}),o(d,{color:"gray",children:` ${e}`})]})}function B({k:t,label:e,bg:n}){return m(p,{marginBottom:1,children:[o(p,{width:9,children:o(je,{k:t,bg:n})}),o(d,{color:"gray",children:e})]})}function tr({launchers:t}){return m(p,{flexDirection:"column",borderStyle:"round",borderColor:"green",paddingX:2,paddingY:1,width:66,children:[m(p,{justifyContent:"space-between",children:[o(d,{color:"green",bold:!0,children:"KEYBOARD SHORTCUTS"}),o(d,{color:"gray",children:"? or esc to close"})]}),m(p,{marginTop:1,children:[m(p,{flexDirection:"column",width:34,children:[o(d,{color:"cyan",bold:!0,children:"Navigate"}),o(B,{k:"\u2191\u2193",label:"move selection"}),o(B,{k:"tab",label:"focus logs \xB7 \u2191\u2193 scroll"}),o(B,{k:"esc",label:"clear filter / back"}),o(p,{marginTop:1,children:o(d,{color:"cyan",bold:!0,children:"Global"})}),o(B,{k:"/",label:"search sites"}),o(B,{k:"n",label:"new site"}),o(B,{k:"?",label:"toggle this help",bg:De}),o(B,{k:"q",label:"logout"}),o(B,{k:"ctrl+c",label:"quit"})]}),m(p,{flexDirection:"column",flexGrow:1,children:[o(d,{color:"cyan",bold:!0,children:"Site actions"}),o(B,{k:"s",label:"start sync"}),o(B,{k:"d",label:"disconnect"}),o(B,{k:"p",label:"pause / resume sync"}),o(B,{k:"b",label:"build preview"}),o(B,{k:"B",label:"publish live"}),o(B,{k:"o",label:"open preview url"}),t.map((e,n)=>o(B,{k:String(n+1),label:`open in ${e.label}`,bg:Le},e.key))]})]})]})}function er({percent:t,width:e=28}){let n=Math.round(Math.max(0,Math.min(100,t))/100*e);return m(d,{children:[o(d,{color:"green",children:"\u2588".repeat(n)}),o(d,{color:"gray",children:"\u2591".repeat(Math.max(0,e-n))})]})}function nr({frame:t,width:e=24,seg:n=6}){let r=e+n,s=t%r-n,i="";for(let a=0;a<e;a++)i+=a>=s&&a<s+n?"\u25AE":"\u25AF";return o(d,{color:"yellow",children:i})}function Fe({controller:t}){let{stdout:e}=zn(),n=Yn(()=>ae(),[]),[,r]=O(0),[s,i]=O(()=>new Date),[a,u]=O(null),[h,w]=O(()=>t.synced.size>0||t.catalog.some(f=>f.sites.length>0)?"dashboard":t.catalog.length<=1?"newName":"newOrg"),[l,b]=O(0),[S,g]=O(""),[x,C]=O(!1),[k,P]=O(""),[D,L]=O(!1),[q,z]=O(0),[qt,Ht]=O(!1),[Ge,Ue]=O(!1),[at,ct]=O("list"),[qe,V]=O(0),Jt=()=>r(c=>c+1);it(()=>(t.on("change",Jt),()=>{t.off("change",Jt)}),[t]),it(()=>{let c,f=()=>{i(new Date),c=setTimeout(f,6e4-Date.now()%6e4)};return c=setTimeout(f,6e4-Date.now()%6e4),()=>clearTimeout(c)},[]);let _=t.catalog.flatMap(c=>c.sites.map(f=>({org:c.org,site:f,entry:t.synced.get(f.id)}))).sort((c,f)=>(c.entry?0:1)-(f.entry?0:1)),N=k.trim().toLowerCase(),R=N?_.filter(c=>c.site.name.toLowerCase().includes(N)||(c.org.name||c.org.subdomain||"").toLowerCase().includes(N)):_,H=R.findIndex(c=>c.site.id===a);H<0&&(H=0);let $=R[H],T=$?.entry,lt=Math.max(3,(e?.rows??30)-26),W=T?.logs??[],kt=Math.max(0,W.length-lt),He=Math.min(qe,kt),ut=W.length-He,Kt=W.slice(Math.max(0,ut-lt),ut),Je=$?.site.id;it(()=>V(0),[Je]);let A=T?.lastBuild,$t=A?.status==="building",Et=Me(A),dt=$t;if(!dt){for(let c of t.synced.values())if(c.status==="syncing"){dt=!0;break}}it(()=>{if(!dt)return;let c=setInterval(()=>z(f=>f+1),120);return()=>clearInterval(c)},[dt]);let Yt=R.findIndex(c=>!c.entry),J=Math.max(Qn,(e?.rows??30)-Vn),Q=R.length,gt=Q>J,Ke=Math.floor(J/2),Z=gt?Math.max(0,Math.min(H-Ke,Q-J)):0,Bt=Math.min(Q,Z+J),Ye=R.slice(Z,Bt);it(()=>{h!=="dashboard"||x||_.length>0||(g(""),t.catalog.length<=1?w("newName"):(b(0),w("newOrg")))},[_.length,h,x,t.catalog.length]);let mt=c=>{if(R.length===0)return;let f=U(H+c,R.length);u(R[f].site.id)},Xe=()=>{if($){if($.entry){t.flash("Already syncing \u2014 press d to disconnect.");return}t.addSite($.org,$.site)}},ze=()=>{$?.entry&&t.removeSite($.site.id)},Ve=()=>{g(""),t.catalog.length<=1?w("newName"):(b(0),w("newOrg"))},Qe=c=>{if(!T){t.flash("Press s to start syncing this site first.",!0);return}Ue(!0);let f=c.run(T.viewsDir);t.flash(f?`Opened ${c.label} at ${Nt(T.viewsDir)}`:`Couldn't open ${c.label}.`,!f)},Ze=()=>{if(!T){t.flash("Press s to start syncing this site first.",!0);return}t.togglePause(T.site.id)},Xt=c=>{if(!T){t.flash("Press s to start syncing this site first.",!0);return}t.buildSite(T.site.id,c),t.flash(c?"Publishing \u2014 making this live\u2026":"Building preview\u2026")},tn=()=>{let c=T?.lastBuild?.url;if(!c){t.flash("No build URL yet \u2014 press b to build.",!0);return}t.flash(oe(c)?`Opening ${c}`:"Couldn't open the browser.",!1)};Xn((c,f)=>{if(qt){(c==="?"||f.escape)&&Ht(!1);return}if(h==="newOrg"){f.escape?w("dashboard"):f.upArrow?b(E=>U(E-1,t.catalog.length)):f.downArrow?b(E=>U(E+1,t.catalog.length)):f.return&&w("newName");return}if(D){f.escape?(L(!1),P(""),u(null)):f.return?L(!1):f.upArrow?mt(-1):f.downArrow&&mt(1);return}if(f.tab){at==="logs"?ct("list"):T&&ct("logs"),V(0);return}if(at==="logs"){if(f.escape){ct("list"),V(0);return}if(f.upArrow)return void V(E=>Math.min(E+1,kt));if(f.downArrow)return void V(E=>Math.max(0,E-1))}if(f.escape){P(""),u(null);return}if(c==="?")return void Ht(!0);if(c==="/")return ct("list"),void L(!0);if(c==="n")return void Ve();if(c==="q")return void t.logout();if(f.upArrow)return mt(-1);if(f.downArrow)return mt(1);if(c==="s")return Xe();if(c==="d")return ze();if(c==="p")return Ze();if(c==="b")return Xt(!1);if(c==="B")return Xt(!0);if(c==="o")return tn();if(c>="1"&&c<="9"){let E=n[parseInt(c,10)-1];if(E)return Qe(E)}},{isActive:h!=="newName"});let en=async c=>{let f=c.trim();if(!f){w("dashboard");return}let E=t.catalog[U(l,t.catalog.length)]?.org??t.catalog[0]?.org;C(!0),w("dashboard"),await t.createNewSite(f,E),C(!1)},zt=m(p,{justifyContent:"space-between",paddingX:1,children:[m(p,{children:[o(d,{color:"green",bold:!0,children:"\u25C6 SleekCMS"}),o(d,{color:"gray",children:" \u2502 "}),o(d,{children:t.email??"signed in"})]}),m(p,{children:[m(d,{color:"gray",children:[N?`${R.length}/${_.length}`:_.length," sites \xB7 "]}),m(d,{color:"green",children:[t.watchingCount()," watching"]}),o(d,{color:"gray",children:` \xB7 ${s.toTimeString().slice(0,5)}`})]})]});return qt?m(p,{flexDirection:"column",children:[zt,o(p,{marginTop:1,justifyContent:"center",children:o(tr,{launchers:n})})]}):m(p,{flexDirection:"column",children:[zt,m(p,{children:[m(p,{flexDirection:"column",borderStyle:"round",borderColor:"green",width:38,paddingX:1,children:[m(p,{justifyContent:"space-between",children:[o(d,{color:"green",bold:!0,children:"SITE WORKSPACES"}),_.length>J&&!D&&!N?o(d,{color:"gray",children:"/ filter"}):null]}),D?m(p,{children:[o(d,{color:"cyan",children:"/ "}),o(Re,{value:k,onChange:P,onSubmit:()=>L(!1),placeholder:"type to filter\u2026"})]}):N?o(d,{color:"gray",children:`/ ${k} \xB7 ${R.length} match${R.length===1?"":"es"}`}):null,o(p,{flexDirection:"column",marginTop:1,minHeight:gt?J+3:void 0,children:R.length===0?o(d,{color:"gray",children:N?`no matches for \u201C${k}\u201D`:"no sites \u2014 press n to create one"}):m(ot,{children:[gt?o(d,{color:"gray",children:Z>0?` \u2191 ${Z} more`:" "}):null,Ye.map((c,f)=>{let E=Z+f,Vt=c.entry?vt(c.entry.status):{dot:"\xB7",color:"gray",label:"idle"},Qt=xt(c.entry?.progress),nn=c.entry?c.entry.status==="syncing"&&Qt!=null?`${Qt}%`:c.entry.status==="paused"?"paused":"--":"",ht=E===H,rn=E===Yt&&Yt>0;return m(Kn,{children:[rn?o(d,{color:"gray",children:"\u2500".repeat(34)}):null,m(p,{justifyContent:"space-between",children:[m(p,{children:[o(d,{color:ht?"green":"gray",children:ht?"\u276F ":" "}),m(d,{color:c.entry?.status==="watching"||c.entry?.status==="syncing"?"green":Vt.color,children:[c.entry?.status==="syncing"?Oe(q):c.entry?.status==="watching"?"\u25CF":Vt.dot," "]}),o(d,{color:ht?"white":c.entry?void 0:"gray",bold:ht,children:c.site.name})]}),o(d,{color:c.entry?.status==="syncing"?"green":"gray",children:nn})]})]},c.site.id)}),gt?o(d,{color:"gray",children:Bt<Q?` \u2193 ${Q-Bt} more`:" "}):null]})}),o(p,{marginTop:1,children:o(d,{color:"gray",children:"+ new site \xB7 n"})})]}),m(p,{flexDirection:"column",flexGrow:1,marginLeft:1,children:[o(p,{flexDirection:"column",borderStyle:"round",borderColor:"gray",paddingX:1,children:$?m(ot,{children:[m(d,{children:[$.org.name||$.org.subdomain?m(d,{color:"gray",children:[$.org.name||$.org.subdomain," / "]}):null,o(d,{color:"cyan",bold:!0,children:$.site.name})]}),m(p,{marginTop:1,children:[o(d,{color:"gray",children:"status "}),T?o(d,{color:vt(T.status).color,children:`: ${vt(T.status).label}`}):o(d,{color:"gray",children:": idle \u2014 not syncing (press s to start)"})]}),m(p,{children:[o(d,{color:"gray",children:"path "}),o(d,{wrap:"truncate-end",children:T?.viewsDir?Nt(T.viewsDir):"\u2014"})]}),m(p,{children:[o(d,{color:"gray",children:"synced "}),o(d,{children:Wt(T?.lastSyncedAt)}),o(d,{color:"gray",children:" progress "}),o(d,{color:T?.status==="syncing"?"green":"gray",children:T?.status==="syncing"?`${xt(T.progress)??0}%`:"\u2014"})]}),o(p,{marginTop:1,children:o(er,{percent:T?T.status==="syncing"?xt(T.progress)??0:T.status==="watching"?100:0:0})}),T?m(ot,{children:[m(p,{marginTop:1,children:[o(d,{color:"gray",children:"build "}),o(d,{color:Et.color,children:`${Et.dot} ${Et.label}`}),o(d,{color:"gray",children:" last "}),o(d,{children:$t?"in progress":Wt(A?.at)})]}),m(p,{children:[o(d,{color:"gray",children:"url "}),A?.url?o(d,{color:"cyan",wrap:"truncate-end",children:A.url}):o(d,{color:"gray",children:"\u2014"})]}),A?.status==="error"&&A.errors?.length?m(p,{children:[o(d,{color:"red",children:"\u258C "}),o(d,{color:"red",wrap:"truncate-end",children:`\u2717 ${A.errors[0]}`})]}):null,$t?m(p,{children:[o(nr,{frame:q}),o(d,{color:"gray",children:` ${A?.stage??"building"}`})]}):null]}):null]}):o(d,{color:"gray",children:"No site selected."})}),h==="dashboard"?m(p,{flexDirection:"column",borderStyle:"round",borderColor:at==="logs"?"green":"gray",paddingX:1,marginTop:1,height:lt+4,flexShrink:0,overflowY:"hidden",children:[m(p,{justifyContent:"space-between",children:[m(d,{children:[o(d,{color:"cyan",bold:!0,children:"LOGS \xB7 "}),o(d,{color:"white",children:$?.site.name??"\u2014"})]}),T&&W.length>0?at==="logs"?o(d,{color:"green",children:`\u2191\u2193 scroll \xB7 esc back${kt>0?` \xB7 ${ut}/${W.length}`:""}`}):W.length>lt?o(d,{color:"gray",children:"tab to scroll"}):null:null]}),o(p,{flexDirection:"column",marginTop:1,children:!T||W.length===0?o(d,{color:"gray",children:"no activity yet"}):Kt.map((c,f)=>m(d,{wrap:"truncate-end",children:[m(d,{color:"gray",children:[c.time," "]}),c.text]},ut-Kt.length+f))})]}):m(p,{flexDirection:"column",borderStyle:"round",borderColor:"blue",paddingX:1,marginTop:1,flexGrow:1,children:[h==="newOrg"&&m(ot,{children:[o(d,{color:"cyan",bold:!0,children:"CREATE SITE \u2014 PICK ORG"}),o(d,{color:"gray",children:"\u2191/\u2193 select \xB7 \u23CE next \xB7 esc cancel"}),o(p,{flexDirection:"column",marginTop:1,children:t.catalog.map((c,f)=>m(d,{color:f===U(l,t.catalog.length)?"blue":void 0,bold:f===U(l,t.catalog.length),children:[f===U(l,t.catalog.length)?"\u276F ":" ",c.org.name||c.org.subdomain]},c.org.id))})]}),h==="newName"&&m(ot,{children:[o(d,{color:"cyan",bold:!0,children:"CREATE SITE"}),m(p,{marginTop:1,children:[o(d,{children:"New site name: "}),o(Re,{value:S,onChange:g,onSubmit:en})]}),o(d,{color:"gray",children:"\u23CE create \xB7 empty + \u23CE to cancel"})]})]})]})]}),t.message?o(p,{paddingX:1,children:m(d,{color:t.messageIsError?"red":"gray",wrap:"truncate-end",children:[t.messageIsError?"\u2717 ":"",t.message]})}):null,m(p,{flexDirection:"column",marginTop:1,children:[m(p,{paddingX:1,justifyContent:"space-between",children:[m(p,{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"}),_.length===0?o(I,{k:"n",label:"new site"}):null,o(I,{k:"ctrl+c",label:"quit",width:14})]}),o(I,{k:"?",label:"all keys",width:12,bg:De})]}),!Ge&&n.length>0?o(p,{paddingX:1,marginTop:1,flexWrap:"wrap",children:n.map((c,f)=>o(I,{k:String(f+1),label:c.label,bg:Le},c.key))}):null]})]})}import{jsx as Tt}from"react/jsx-runtime";function _e(t){let e=t;return e?.body||e?.message||String(t)}function Ie({controller:t,env:e,devSecret:n,savedToken:r,savedEmail:s}){let[i,a]=Ae(r?"loading":"auth"),[u,h]=Ae();or((l,b)=>{b.ctrl&&l==="c"&&t.exit()}),rr(()=>{r&&t.init(e,r,s).then(()=>a("manage")).catch(l=>{l instanceof F&&l.status===401?h("Saved session expired \u2014 please sign in again."):h(_e(l)),a("auth")})},[]);let w=async(l,b)=>{h(void 0),await ye({env:e,userToken:l,devSecret:n,email:b}),a("loading");try{await t.init(e,l,b),a("manage")}catch(S){h(_e(S)),a("auth")}};return i==="auth"?Tt(Pe,{env:e,initialError:u,onAuthed:w}):i==="loading"?Tt(sr,{children:Tt(ir,{children:"Loading your sites\u2026 "})}):Tt(Fe,{controller:t})}import{jsx as hr}from"react/jsx-runtime";var ur=cr(import.meta.url),Gt=re();if(Gt.version){let t=ur("../package.json");console.log(t.version),process.exit(0)}var We=Ut.dirname(ar(import.meta.url)),dr=Ne(Ut.join(We,"AGENT.md"),"utf-8"),gr=Ne(Ut.join(We,"build.mjs"),"utf-8");async function mr(){let t=new bt(dr,gr),e=await pe(),n=(Gt.env??e?.env??"production").toLowerCase(),r=Gt.dev??process.env.SLEEKCMS_DEV_SECRET??e?.devSecret;fe(r),process.stdout.isTTY&&process.stdout.write("\x1B[2J\x1B[3J\x1B[H");let s=lr(hr(Ie,{controller:t,env:n,devSecret:r,savedToken:e?.userToken,savedEmail:e?.email}),{exitOnCtrlC:!1}),i=a=>{s.unmount(),console.log(a),process.exit(0)};t.once("exit",()=>i("\u2713 Stopped. Local site files were cleaned up.")),t.once("logout",()=>i("\u{1F44B} Logged out. Run `sleekcms` to sign in again.")),process.on("SIGINT",()=>{t.exit()}),process.on("SIGTERM",()=>{t.exit()}),await s.waitUntilExit()}mr().catch(t=>{let e=t;console.error("\u274C",e.body||e.message),process.exit(1)});
10
+ `)}async function fn(e,t,n,r,s){be.delete(b.resolve(e)),await f.remove(b.join(e,xe)),r("\u{1F4E5} Fetching files...");let i=await ce(t,n,"GET","/get_files"),d={},l=0;for(let y of i){let v=b.join(e,y.path);await f.outputFile(v,y.content);let u=(await f.stat(v)).mtimeMs;l++,s?.(l,i.length),d[y.path]={mtimeMs:u,hash:K(y.content)}}return r(`\u2714\uFE0F Synced ${i.length} file(s).`),{fileMap:d,pulled:l}}import mn from"path";import wn from"chokidar";var yn=5e3,bn=/(^|[/\\])(\.DS_Store|Thumbs\.db|desktop\.ini)$/,V=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??yn}start(){if(this.watcher)return;let t=mn.join(this.viewsDir,"src");this.watcher=wn.watch([t],{persistent:!0,ignoreInitial:!0,ignored:bn}).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 Te from"fs-extra";import vn from"os";import it from"path";var st={localhost:"http://app.sleekcms.test/api",development:"https://app.sleekcms.dev/api",production:"https://app.sleekcms.com/api"};function Q(e){return st[e]||st.production}function $e(e){return Q(e).replace(/\/api$/,"")}var Sn=it.join(vn.homedir(),"SleekCMS"),U=it.join(Sn,"auth.json"),ke=class extends Error{status;body;constructor(t,n,r=""){super(t),this.name="ApiError",this.status=n,this.body=r}};async function le(e=U){let t=null;try{t=await Te.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 ot(e,t=U){await Te.outputJson(t,{sites:e},{spaces:2});try{await Te.chmod(t,384)}catch{}}async function Be(e,t=U){let{sites:n}=await le(t),r=[...n.filter(s=>!(s.env===e.env&&s.siteId===e.siteId)),e];return await ot(r,t),r}async function Pe(e,t,n=U){let{sites:r}=await le(n),s=r.filter(i=>!(i.env===e&&i.siteId===t));return await ot(s,n),s}async function Ee(e,t,n,r){let s=await fetch(Q(e)+r,{method:n,headers:{Authorization:`Bearer ${t}`}});if(!s.ok){let i=await s.text().catch(()=>"");throw new ke(`${n} ${r} \u2192 ${s.status}: ${i}`,s.status,i)}return s.json()}function xn(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:"",y=n&&r&&r!==n?` (on page ${r})`:"";if(s)return`${s}${i}${d}${y}: ${l||"(no message)"}`;if(l)return l;try{return JSON.stringify(e)}catch{return"(unknown error)"}}async function at(e,t){let n;try{n=await Ee(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(xn):[];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 Tn,randomBytes as kn}from"crypto";var $n="urn:sleekcms:grant-type:cli_poll",Bn="SleekCMS Sync",ct=e=>e.toString("base64").replace(/=+$/,"").replace(/\+/g,"-").replace(/\//g,"_");function lt(){let e=ct(kn(32)),t=ct(Tn("sha256").update(e).digest());return{verifier:e,challenge:t}}function ut(e){let t=e.replace(/[^A-Za-z0-9]/g,"").slice(0,8).toUpperCase();return`${t.slice(0,4)}-${t.slice(4)}`}async function dt(e,t=fetch){let n={origin:$e(e),authorizeEndpoint:`${Q(e)}/oauth/authorize`};try{let r=await t(`${$e(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 gt(e,t){let n=new URLSearchParams({response_type:"code",client_name:Bn,redirect_uri:`${e.origin}/oauth/cli-done`,code_challenge:t,code_challenge_method:"S256",resource:`${e.origin}/mcp/`});return`${e.authorizeEndpoint}?${n.toString()}`}var J=class extends Error{constructor(t){super(t),this.name="SignInError"}},Pn=e=>new Promise(t=>setTimeout(t,e));async function ht(e,t,n={}){let{timeoutMs:r=10*6e4,signal:s,fetchImpl:i=fetch,sleep:d=Pn}=n,l=n.intervalMs??2e3,y=Date.now()+r,v=`${Q(e)}/oauth/token`;for(;;){if(s?.aborted)throw new J("Cancelled.");let u=null;try{u=await i(v,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({grant_type:$n,code_verifier:t})})}catch{u=null}if(u){let m=await u.json().catch(()=>({}));if(u.ok&&m.access_token)return m.access_token;if(u.status===429)l=Math.min(l*2,3e4);else if(m.error!=="authorization_pending")throw new J(m.error==="invalid_grant"?"That approval can no longer be used. Add the site again.":`Sign-in failed: ${m.error||`HTTP ${u.status}`}`)}if(Date.now()+l>y)throw new J("Timed out waiting for approval in the browser.");await d(l)}}function En(e){let t=e.split(".")[1];if(!t)throw new J("The server returned a token this version can't read.");return JSON.parse(Buffer.from(t,"base64url").toString("utf8"))}async function pt(e,t){let n=En(t),r=await Ee(e,t,"GET","/mcp/get_site");return{env:e,siteId:n.sid,slug:n.slug,name:r.name,org:{name:r.org,subdomain:r.subdomain||n.sub},role:r.role||void 0,token:t,addedAt:Date.now()}}var ft=500;function Ln(){return new Date().toTimeString().slice(0,8)}function Dn(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 mt(e){return e instanceof X&&e.status===401}var jn=e=>({id:e.siteId,name:e.name,slug:e.slug});async function wt(e){e&&(await Z.remove(Ce.join(e,"src")),await Z.remove(Ce.join(e,".cache","state.json")))}var ue=class extends Cn{env="production";saved=[];synced=new Map;connecting=null;message;messageIsError=!1;agentMd;buildScript;configPath;isShuttingDown=!1;basePath;pollOptions={};openBrowser=ae;constructor(t,n,r=U){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:Ln(),text:n}),r.logs.length>ft&&r.logs.splice(0,r.logs.length-ft),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 le(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}=lt(),r=new AbortController;this.connecting={code:ut(n),url:"",abort:r},this.changed();let s=gt(await dt(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 ht(this.env,t,{...this.pollOptions,signal:r.signal});i=await pt(this.env,l)}catch(l){this.connecting=null,r.signal.aborted?this.changed():this.flash(Dn(l),!0);return}this.connecting=null;let d=await Be(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=jn(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 ve({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 V({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(mt(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,Be({...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 Pe(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 Pe(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 wt(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()}),Mn("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 Ce.join(t,".cache","build.json")}async readBuildFile(t){try{let n=await Z.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 Z.outputJson(s,n),await Z.rename(s,r)}catch{}}watchBuildStatus(t){let n=this.synced.get(t);!n||!n.viewsDir||(n.buildWatcher=Rn.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 at(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 ve({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(mt(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 V({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 wt(t.viewsDir)}async exit(){this.isShuttingDown||(this.isShuttingDown=!0,await this.cleanupAll(),this.emit("exit"))}};import{useInput as Xn}from"ink";import{Fragment as Fn,useEffect as ee,useMemo as _n,useRef as On,useState as L}from"react";import{Box as p,Text as c,useInput as In,useStdout as Nn}from"ink";import Wn from"ink-text-input";import An from"node:os";function Me(e){if(!e)return"\u2014";let t=An.homedir();return e.startsWith(t)?"~"+e.slice(t.length):e}function Re(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 ge(e){return!e||e.total<=0?null:Math.round(e.done/e.total*100)}var de=["\u280B","\u2819","\u2839","\u2838","\u283C","\u2834","\u2826","\u2827","\u2807","\u280F"];function Le(e){return de[(e%de.length+de.length)%de.length]}function he(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 yt(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 De,jsx as o,jsxs as g}from"react/jsx-runtime";var Gn=(e,t)=>t<=0?0:Math.max(0,Math.min(e,t-1)),Un=16,Jn=5,Hn="greenBright",bt="blueBright",vt="magentaBright";function St({k:e,bg:t=Hn}){return o(c,{color:"black",backgroundColor:t,children:` ${e} `})}function F({k:e,label:t,width:n=16,bg:r}){return g(p,{width:n,children:[o(St,{k:e,bg:r}),o(c,{color:"gray",children:` ${t}`})]})}function $({k:e,label:t,bg:n}){return g(p,{marginBottom:1,children:[o(p,{width:9,children:o(St,{k:e,bg:n})}),o(c,{color:"gray",children:t})]})}function zn({launchers:e}){return g(p,{flexDirection:"column",borderStyle:"round",borderColor:"green",paddingX:2,paddingY:1,width:66,children:[g(p,{justifyContent:"space-between",children:[o(c,{color:"green",bold:!0,children:"KEYBOARD SHORTCUTS"}),o(c,{color:"gray",children:"? or esc to close"})]}),g(p,{marginTop:1,children:[g(p,{flexDirection:"column",width:34,children:[o(c,{color:"cyan",bold:!0,children:"Navigate"}),o($,{k:"\u2191\u2193",label:"move selection"}),o($,{k:"tab",label:"focus logs \xB7 \u2191\u2193 scroll"}),o($,{k:"esc",label:"clear filter / back"}),o(p,{marginTop:1,children:o(c,{color:"cyan",bold:!0,children:"Global"})}),o($,{k:"/",label:"search sites"}),o($,{k:"a",label:"add a site (browser)"}),o($,{k:"?",label:"toggle this help",bg:bt}),o($,{k:"ctrl+c",label:"quit"})]}),g(p,{flexDirection:"column",flexGrow:1,children:[o(c,{color:"cyan",bold:!0,children:"Site actions"}),o($,{k:"s",label:"start sync"}),o($,{k:"d",label:"disconnect"}),o($,{k:"x",label:"forget site"}),o($,{k:"p",label:"pause / resume sync"}),o($,{k:"b",label:"build preview"}),o($,{k:"B",label:"publish live"}),o($,{k:"o",label:"open preview url"}),e.map((t,n)=>o($,{k:String(n+1),label:`open in ${t.label}`,bg:vt},t.key))]})]})]})}function qn({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 Yn({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 xt({controller:e}){let{stdout:t}=Nn(),n=_n(()=>Qe(),[]),[,r]=L(0),[s,i]=L(()=>new Date),[d,l]=L(null),[y,v]=L(""),[u,m]=L(!1),[w,h]=L(0),[S,E]=L(!1),[I,pe]=L(!1),[C,M]=L("list"),[H,D]=L(0),Fe=()=>r(a=>a+1);ee(()=>(e.on("change",Fe),()=>{e.off("change",Fe)}),[e]),ee(()=>{let a,x=()=>{i(new Date),a=setTimeout(x,6e4-Date.now()%6e4)};return a=setTimeout(x,6e4-Date.now()%6e4),()=>clearTimeout(a)},[]);let z=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,x)=>(a.entry?0:1)-(x.entry?0:1)),N=e.connecting,_=y.trim().toLowerCase(),P=_?z.filter(a=>a.site.name.toLowerCase().includes(_)||(a.org.name||a.org.subdomain||"").toLowerCase().includes(_)):z,W=P.findIndex(a=>a.site.id===d);W<0&&(W=0);let k=P[W],T=k?.entry,te=Math.max(3,(t?.rows??30)-26),O=T?.logs??[],fe=Math.max(0,O.length-te),Bt=Math.min(H,fe),ne=O.length-Bt,_e=O.slice(Math.max(0,ne-te),ne),Pt=k?.site.id;ee(()=>D(0),[Pt]);let j=T?.lastBuild,me=j?.status==="building",we=yt(j),re=me||!!N;if(!re){for(let a of e.synced.values())if(a.status==="syncing"){re=!0;break}}ee(()=>{if(!re)return;let a=setInterval(()=>h(x=>x+1),120);return()=>clearInterval(a)},[re]);let Oe=P.findIndex(a=>!a.entry),G=Math.max(Jn,(t?.rows??30)-Un),q=P.length,se=q>G,Et=Math.floor(G/2),Y=se?Math.max(0,Math.min(W-Et,q-G)):0,ye=Math.min(q,Y+G),Ct=P.slice(Y,ye),Ie=On(!1);ee(()=>{Ie.current||(Ie.current=!0,e.saved.length===0&&!e.connecting&&e.connectSite())},[e]);let ie=a=>{if(P.length===0)return;let x=Gn(W+a,P.length);l(P[x].site.id)},Mt=()=>{if(k){if(k.entry){e.flash("Already syncing \u2014 press d to disconnect.");return}e.startSite(k.saved)}},Rt=()=>{k?.entry&&e.removeSite(k.site.id)},Lt=()=>{k&&e.forgetSite(k.site.id)},Dt=a=>{if(!T){e.flash("Press s to start syncing this site first.",!0);return}pe(!0);let x=a.run(T.viewsDir);e.flash(x?`Opened ${a.label} at ${Me(T.viewsDir)}`:`Couldn't open ${a.label}.`,!x)},jt=()=>{if(!T){e.flash("Press s to start syncing this site first.",!0);return}e.togglePause(T.site.id)},Ne=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")},At=()=>{let a=T?.lastBuild?.url;if(!a){e.flash("No build URL yet \u2014 press b to build.",!0);return}e.flash(ae(a)?`Opening ${a}`:"Couldn't open the browser.",!1)};In((a,x)=>{if(S){(a==="?"||x.escape)&&E(!1);return}if(N&&x.escape)return void e.cancelConnect();if(u){x.escape?(m(!1),v(""),l(null)):x.return?m(!1):x.upArrow?ie(-1):x.downArrow&&ie(1);return}if(x.tab){C==="logs"?M("list"):T&&M("logs"),D(0);return}if(C==="logs"){if(x.escape){M("list"),D(0);return}if(x.upArrow)return void D(A=>Math.min(A+1,fe));if(x.downArrow)return void D(A=>Math.max(0,A-1))}if(x.escape){v(""),l(null);return}if(a==="?")return void E(!0);if(a==="/")return M("list"),void m(!0);if(a==="a"||a==="n")return void e.connectSite();if(x.upArrow)return ie(-1);if(x.downArrow)return ie(1);if(a==="s")return Mt();if(a==="d")return Rt();if(a==="x")return Lt();if(a==="p")return jt();if(a==="b")return Ne(!1);if(a==="B")return Ne(!0);if(a==="o")return At();if(a>="1"&&a<="9"){let A=n[parseInt(a,10)-1];if(A)return Dt(A)}});let We=g(p,{justifyContent:"space-between",paddingX:1,children:[g(p,{children:[o(c,{color:"green",bold:!0,children:"\u25C6 SleekCMS"}),o(c,{color:"gray",children:" \u2502 "}),o(c,{children:e.env==="production"?"Sync":`Sync \xB7 ${e.env}`})]}),g(p,{children:[g(c,{color:"gray",children:[_?`${P.length}/${z.length}`:z.length," sites \xB7 "]}),g(c,{color:"green",children:[e.watchingCount()," watching"]}),o(c,{color:"gray",children:` \xB7 ${s.toTimeString().slice(0,5)}`})]})]});return S?g(p,{flexDirection:"column",children:[We,o(p,{marginTop:1,justifyContent:"center",children:o(zn,{launchers:n})})]}):g(p,{flexDirection:"column",children:[We,g(p,{children:[g(p,{flexDirection:"column",borderStyle:"round",borderColor:"green",width:38,paddingX:1,children:[g(p,{justifyContent:"space-between",children:[o(c,{color:"green",bold:!0,children:"SITE WORKSPACES"}),z.length>G&&!u&&!_?o(c,{color:"gray",children:"/ filter"}):null]}),u?g(p,{children:[o(c,{color:"cyan",children:"/ "}),o(Wn,{value:y,onChange:v,onSubmit:()=>m(!1),placeholder:"type to filter\u2026"})]}):_?o(c,{color:"gray",children:`/ ${y} \xB7 ${P.length} match${P.length===1?"":"es"}`}):null,o(p,{flexDirection:"column",marginTop:1,minHeight:se?G+3:void 0,children:P.length===0?o(c,{color:"gray",children:_?`no matches for \u201C${y}\u201D`:"no sites yet \u2014 press a to add one"}):g(De,{children:[se?o(c,{color:"gray",children:Y>0?` \u2191 ${Y} more`:" "}):null,Ct.map((a,x)=>{let A=Y+x,Ge=a.entry?he(a.entry.status):{dot:"\xB7",color:"gray",label:"idle"},Ue=ge(a.entry?.progress),Ft=a.entry?a.entry.status==="syncing"&&Ue!=null?`${Ue}%`:a.entry.status==="paused"?"paused":"--":"",oe=A===W,_t=A===Oe&&Oe>0;return g(Fn,{children:[_t?o(c,{color:"gray",children:"\u2500".repeat(34)}):null,g(p,{justifyContent:"space-between",children:[g(p,{children:[o(c,{color:oe?"green":"gray",children:oe?"\u276F ":" "}),g(c,{color:a.entry?.status==="watching"||a.entry?.status==="syncing"?"green":Ge.color,children:[a.entry?.status==="syncing"?Le(w):a.entry?.status==="watching"?"\u25CF":Ge.dot," "]}),o(c,{color:oe?"white":a.entry?void 0:"gray",bold:oe,children:a.site.name})]}),o(c,{color:a.entry?.status==="syncing"?"green":"gray",children:Ft})]})]},a.site.id)}),se?o(c,{color:"gray",children:ye<q?` \u2193 ${q-ye} more`:" "}):null]})}),o(p,{marginTop:1,children:o(c,{color:"gray",children:"+ add site \xB7 a"})})]}),g(p,{flexDirection:"column",flexGrow:1,marginLeft:1,children:[o(p,{flexDirection:"column",borderStyle:"round",borderColor:"gray",paddingX:1,children:k?g(De,{children:[g(c,{children:[k.org.name||k.org.subdomain?g(c,{color:"gray",children:[k.org.name||k.org.subdomain," / "]}):null,o(c,{color:"cyan",bold:!0,children:k.site.name})]}),g(p,{marginTop:1,children:[o(c,{color:"gray",children:"status "}),T?o(c,{color:he(T.status).color,children:`: ${he(T.status).label}`}):o(c,{color:"gray",children:": idle \u2014 not syncing (press s to start)"})]}),g(p,{children:[o(c,{color:"gray",children:"role "}),o(c,{children:T?.role??k.saved.role??"\u2014"}),(T?.role??k.saved.role??"ADMIN")!=="ADMIN"?o(c,{color:"gray",children:" \xB7 build & publish need ADMIN"}):null]}),g(p,{children:[o(c,{color:"gray",children:"path "}),o(c,{wrap:"truncate-end",children:T?.viewsDir?Me(T.viewsDir):"\u2014"})]}),g(p,{children:[o(c,{color:"gray",children:"synced "}),o(c,{children:Re(T?.lastSyncedAt)}),o(c,{color:"gray",children:" progress "}),o(c,{color:T?.status==="syncing"?"green":"gray",children:T?.status==="syncing"?`${ge(T.progress)??0}%`:"\u2014"})]}),o(p,{marginTop:1,children:o(qn,{percent:T?T.status==="syncing"?ge(T.progress)??0:T.status==="watching"?100:0:0})}),T?g(De,{children:[g(p,{marginTop:1,children:[o(c,{color:"gray",children:"build "}),o(c,{color:we.color,children:`${we.dot} ${we.label}`}),o(c,{color:"gray",children:" last "}),o(c,{children:me?"in progress":Re(j?.at)})]}),g(p,{children:[o(c,{color:"gray",children:"url "}),j?.url?o(c,{color:"cyan",wrap:"truncate-end",children:j.url}):o(c,{color:"gray",children:"\u2014"})]}),j?.status==="error"&&j.errors?.length?g(p,{children:[o(c,{color:"red",children:"\u258C "}),o(c,{color:"red",wrap:"truncate-end",children:`\u2717 ${j.errors[0]}`})]}):null,me?g(p,{children:[o(Yn,{frame:w}),o(c,{color:"gray",children:` ${j?.stage??"building"}`})]}):null]}):null]}):o(c,{color:"gray",children:"No site selected."})}),N?g(p,{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(p,{marginTop:1,children:[o(c,{color:"gray",children:"Check the page shows "}),o(c,{color:"cyan",bold:!0,children:N.code})]}),o(c,{color:"gray",children:"Only approve a page that shows this code."}),g(p,{marginTop:1,children:[g(c,{color:"yellow",children:[Le(w)," "]}),o(c,{color:"gray",children:"waiting for approval \xB7 esc to cancel"})]})]}):g(p,{flexDirection:"column",borderStyle:"round",borderColor:C==="logs"?"green":"gray",paddingX:1,marginTop:1,height:te+4,flexShrink:0,overflowY:"hidden",children:[g(p,{justifyContent:"space-between",children:[g(c,{children:[o(c,{color:"cyan",bold:!0,children:"LOGS \xB7 "}),o(c,{color:"white",children:k?.site.name??"\u2014"})]}),T&&O.length>0?C==="logs"?o(c,{color:"green",children:`\u2191\u2193 scroll \xB7 esc back${fe>0?` \xB7 ${ne}/${O.length}`:""}`}):O.length>te?o(c,{color:"gray",children:"tab to scroll"}):null:null]}),o(p,{flexDirection:"column",marginTop:1,children:!T||O.length===0?o(c,{color:"gray",children:"no activity yet"}):_e.map((a,x)=>g(c,{wrap:"truncate-end",children:[g(c,{color:"gray",children:[a.time," "]}),a.text]},ne-_e.length+x))})]})]})]}),N?.url?g(p,{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:N.url})]}):null,e.message?o(p,{paddingX:1,children:g(c,{color:e.messageIsError?"red":"gray",wrap:"truncate-end",children:[e.messageIsError?"\u2717 ":"",e.message]})}):null,g(p,{flexDirection:"column",marginTop:1,children:[g(p,{paddingX:1,justifyContent:"space-between",children:[g(p,{flexWrap:"wrap",children:[o(F,{k:"\u2191\u2193",label:"select"}),o(F,{k:"/",label:"search"}),o(F,{k:"s",label:"start sync"}),o(F,{k:"d",label:"disconnect"}),o(F,{k:"a",label:"add site"}),o(F,{k:"ctrl+c",label:"quit",width:14})]}),o(F,{k:"?",label:"all keys",width:12,bg:bt})]}),!I&&n.length>0?o(p,{paddingX:1,marginTop:1,flexWrap:"wrap",children:n.map((a,x)=>o(F,{k:String(x+1),label:a.label,bg:vt},a.key))}):null]})]})}import{jsx as Kn}from"react/jsx-runtime";function Tt({controller:e}){return Xn((t,n)=>{n.ctrl&&t==="c"&&e.exit()}),Kn(xt,{controller:e})}import{jsx as sr}from"react/jsx-runtime";var er=Qn(import.meta.url),je=Xe();if(je.version){let e=er("../package.json");console.log(e.version),process.exit(0)}var $t=Ae.dirname(Vn(import.meta.url)),tr=kt(Ae.join($t,"AGENTS.md"),"utf-8"),nr=kt(Ae.join($t,"build.mjs"),"utf-8");async function rr(){let e=new ue(tr,nr);e.basePath=je.path,await e.init((je.env??"production").toLowerCase()),process.stdout.isTTY&&process.stdout.write("\x1B[2J\x1B[3J\x1B[H");let t=Zn(sr(Tt,{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()}rr().catch(e=>{let t=e;console.error("\u274C",t.body||t.message),process.exit(1)});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleekcms/sync",
3
- "version": "2.14.0",
3
+ "version": "3.0.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/AGENT.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 dist/ && chmod +x dist/index.js",
32
32
  "dev": "tsx src/index.tsx",
33
33
  "typecheck": "tsc --noEmit",
34
34
  "clean": "rm -rf dist",