@sleekcms/sync 4.2.0 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -3
- package/dist/AGENTS.md +36 -17
- package/dist/index.js +7 -7
- package/dist/upload.mjs +372 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -174,6 +174,8 @@ No manual deploy. No waiting. SleekCMS rebuilds on every sync and the preview re
|
|
|
174
174
|
```
|
|
175
175
|
<site-name>/
|
|
176
176
|
├── AGENTS.md # AI agent reference (auto-generated)
|
|
177
|
+
├── build.mjs # Build / publish the site: node build.mjs [--publish]
|
|
178
|
+
├── upload.mjs # Upload images and documents: node upload.mjs <path>...
|
|
177
179
|
├── .vscode/
|
|
178
180
|
│ ├── settings.json # Editor settings (site .json files open as JSON5)
|
|
179
181
|
│ └── extensions.json # Recommends the JSON5 extension
|
|
@@ -396,7 +398,7 @@ Models describe the shape of your content. They're JSON-like files with unquoted
|
|
|
396
398
|
**Forms** (no backend needed)
|
|
397
399
|
|
|
398
400
|
```html
|
|
399
|
-
<form data-
|
|
401
|
+
<form data-sleeksite-form="Contact">
|
|
400
402
|
<input name="name" type="text" required>
|
|
401
403
|
<input name="email" type="email" required>
|
|
402
404
|
<textarea name="message"></textarea>
|
|
@@ -404,7 +406,7 @@ Models describe the shape of your content. They're JSON-like files with unquoted
|
|
|
404
406
|
</form>
|
|
405
407
|
```
|
|
406
408
|
|
|
407
|
-
Any form with `data-
|
|
409
|
+
Any form with `data-sleeksite-form="<name>"` captures and stores submissions in the CMS dashboard automatically (`data-sleeksite-book` makes it a booking form with a slot picker). On sync the form is created if it doesn't exist and the CMS stores the view with the name replaced by the form's key, so the published form stays linked when it's renamed in the app; your local file keeps the name as you wrote it. File uploads work out of the box too — add an `<input type="file">` and visitors can attach documents (PDF, Word, Excel, CSV, text) and images:
|
|
408
410
|
|
|
409
411
|
```html
|
|
410
412
|
<input name="resume" type="file" accept=".pdf">
|
|
@@ -480,7 +482,7 @@ Then reference any of them from any `image` field with `"cms:<handle>"` (e.g. `"
|
|
|
480
482
|
|
|
481
483
|
### Files (`src/content/files.json`)
|
|
482
484
|
|
|
483
|
-
The site's files (Media > Files) are listed read-only as `handle → { file, name, url }`. Every sync refreshes the list from the server, and local edits to it are never pushed.
|
|
485
|
+
The site's files (Media > Files) are listed read-only as `handle → { file, name, url }`. Every sync refreshes the list from the server, and local edits to it are never pushed. Add files with `node upload.mjs` (below) or in the CMS.
|
|
484
486
|
|
|
485
487
|
```json
|
|
486
488
|
{
|
|
@@ -495,6 +497,22 @@ A `file` field takes a listed handle or file name (`"lunch-menu"` or `"lunch-men
|
|
|
495
497
|
<% if (menu) { %><a href="<%= menu.url %>" download><%= menu.name %></a><% } %>
|
|
496
498
|
```
|
|
497
499
|
|
|
500
|
+
### Uploading images and documents (`upload.mjs`)
|
|
501
|
+
|
|
502
|
+
Every workspace has an `upload.mjs` next to `build.mjs`, so you or your AI agent can put images and documents from disk into the site without opening the CMS:
|
|
503
|
+
|
|
504
|
+
```bash
|
|
505
|
+
node upload.mjs ~/Downloads/logo.png --handle logo # image → "cms:logo"
|
|
506
|
+
node upload.mjs photos/*.jpg # one image per file; handle = file name
|
|
507
|
+
node upload.mjs ~/Desktop/menu.pdf # document → Media > Files
|
|
508
|
+
node upload.mjs menu-v2.pdf --name menu.pdf # store it under another file name
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
- **Images** (png, jpg, jpeg, webp, gif, svg, ico, up to 8 MB) are stored under a handle and added to `src/content/images.json`. Use them as `"cms:<handle>"` in image fields or `img('cms:<handle>')` in templates. Uploading to an existing handle replaces that image everywhere it's used.
|
|
512
|
+
- **Documents** (pdf, docx, xlsx, pptx, csv, tsv, txt, md, up to 10 MB) go to Media > Files under their file name, and `src/content/files.json` is updated. Uploading the same name again replaces the file.
|
|
513
|
+
|
|
514
|
+
The script prints the ref to use for each file (`--json` prints one JSON result per line instead). It exits `0` when everything uploaded, `1` when some uploads failed, and `2` when it couldn't run. Uploading needs an editor or admin on the site. Binary files copied into `src/` aren't synced; the sync log points to `upload.mjs` instead.
|
|
515
|
+
|
|
498
516
|
### Images in markdown
|
|
499
517
|
|
|
500
518
|
Inside `markdown` fields, the same shortcuts work in standard image syntax. Append `|<alt>` for alt text and a `<width>x<height>` token to set the rendered dimensions (defaults to `600x400`):
|
package/dist/AGENTS.md
CHANGED
|
@@ -102,7 +102,7 @@ content/entries/<key>.json Content for a single entry (object)
|
|
|
102
102
|
content/entries/<key>+.json Collection entry — ONE file holding a JSON array of all items
|
|
103
103
|
content/images.json Site-level reusable images: handle → shortcut (§7)
|
|
104
104
|
content/videos.json Site-level videos: handle → pexels:/pixabay: ref (user uploads show as embed_url) (§7)
|
|
105
|
-
content/files.json The site's files (Media > Files), read-only:
|
|
105
|
+
content/files.json The site's files (Media > Files), read-only: file name → { file, url, size } (§7)
|
|
106
106
|
|
|
107
107
|
setup.json Site setup (JSON, comments allowed): default page title, Tailwind on/off, sitemap on/off, cookie consent on/off, analytics tag ids — edit values in place, never delete
|
|
108
108
|
```
|
|
@@ -249,7 +249,7 @@ Like JSON but with **no quotes**; each value is a field type name:
|
|
|
249
249
|
| `image` | Shortcut string (preferred when writing) or resolved `{ "url", "alt" }` object — see §7. Shortcuts only resolve in `image`/`icon` fields — in a `text` field the string stays literal text |
|
|
250
250
|
| `icon` | Svg icon. Use for icons the user is expected to edit/swap in the CMS (e.g. features/services). Fill with placeholder `"iconify:<prefix:name or search>"` (or `"cms:<handle>"` pointing at an svg); render with `svg(item.icon)`. Fixed UI icons don't need fields — call `svg('iconify:...')` directly in templates |
|
|
251
251
|
| `video` | Ref string (preferred when writing) or the resolved object you read back. Write `"cms:<handle>"` to reuse a video from `content/videos.json` (or a user upload by its handle), or `"pexels:<search>\|<title>"` / `"pixabay:<search>\|<title>"` for a one-off — resolved server-side (see §7). **Never invent** `video_id`/`embed_url` objects; render with `embed()` only |
|
|
252
|
-
| `file` | Write a plain file name, e.g. `"brochure.pdf"` (pdf, docx, xlsx, pptx, csv, tsv, txt, md — no path, no URL, never an object). A
|
|
252
|
+
| `file` | Write a plain file name, e.g. `"brochure.pdf"` (pdf, docx, xlsx, pptx, csv, tsv, txt, md — no path, no URL, never an object). A file listed in `content/files.json` is written by its listed name (`"forms/booking.docx"` for one in a folder). On save the CMS attaches that file, or (for a file name nothing matches) creates a small placeholder file the user replaces in the CMS UI; reads back as the file name. Render a link with `item.x.url` / `item.x.name` |
|
|
253
253
|
| `json` | Any JSON |
|
|
254
254
|
| `sheet` | Array of arrays |
|
|
255
255
|
| `location` | `{ "markers": [{ "lat", "lng" }], "img": "<google static map url>" }` — `img` supports Google Static Maps params; markers are already part of the URL |
|
|
@@ -342,7 +342,7 @@ then reference with `"cms:logo"` in any `image`/`icon` field, or render directly
|
|
|
342
342
|
|
|
343
343
|
Each handle's value is an image source from the list above, never another handle: `"logo": "cms:new-mark"` is ignored. To change what a handle shows, change its own value; every `cms:<handle>` on the site follows.
|
|
344
344
|
|
|
345
|
-
**The user's own images:**
|
|
345
|
+
**The user's own images:** upload them from disk with `node upload.mjs` (see *Uploading images and documents* below). It stores the image under a handle and prints the `cms:<handle>` ref to use. Never point a field at a local path: a local path is not an image source, and `url:` needs a public URL. If the image isn't on this machine, use `url:<public url>` when the user gives you one. Otherwise ship a stock placeholder in the slot and tell the user to upload the image in the CMS.
|
|
346
346
|
|
|
347
347
|
**Images inside markdown:** use the same shortcuts in standard syntax: ``. Append `|<alt>` for alt text and an optional `<W>x<H>` token to set rendered size (default `600x400`): ``. Refs are rewritten to CDN URLs on save.
|
|
348
348
|
|
|
@@ -362,22 +362,39 @@ Content `video` fields accept the same refs: `"cms:<handle>"` reuses a declared
|
|
|
362
362
|
|
|
363
363
|
### Files
|
|
364
364
|
|
|
365
|
-
`content/files.json` lists the site's files (Media > Files) as `
|
|
365
|
+
`content/files.json` lists the site's files (Media > Files) as `file name → { file, url, size }`: `file` is the value a `file` field takes, `url` the path the site serves it at, `size` its bytes.
|
|
366
366
|
|
|
367
367
|
```json
|
|
368
|
-
{ "lunch-menu": { "file": "lunch-menu.pdf", "
|
|
368
|
+
{ "lunch-menu.pdf": { "file": "lunch-menu.pdf", "url": "/_files/lunch-menu.pdf", "size": 48213 } }
|
|
369
369
|
```
|
|
370
370
|
|
|
371
371
|
It's read-only: edits to it are never saved, and every sync replaces it with the site's current list. Check it before writing a new file name.
|
|
372
372
|
|
|
373
|
-
A `file` field takes a listed
|
|
373
|
+
A `file` field takes a listed file name (`"lunch-menu.pdf"`). Render a file no field points at with `getFile(name)`, which returns the same `{ name, url, size, type, ext }` as a `file` field (or `undefined` for an unknown name):
|
|
374
374
|
|
|
375
375
|
```ejs
|
|
376
|
-
<% const menu = getFile('lunch-menu') %>
|
|
376
|
+
<% const menu = getFile('lunch-menu.pdf') %>
|
|
377
377
|
<% if (menu) { %><a href="<%= menu.url %>" download><%= menu.name %></a><% } %>
|
|
378
378
|
```
|
|
379
379
|
|
|
380
|
-
**The user's own documents:**
|
|
380
|
+
**The user's own documents:** upload them from disk with `node upload.mjs` (below); the file is listed in `content/files.json` right away. If the document isn't on this machine, a `file` field with a new file name gets a placeholder the user replaces in the CMS (Media > Files).
|
|
381
|
+
|
|
382
|
+
### Uploading images and documents (`upload.mjs`)
|
|
383
|
+
|
|
384
|
+
When the user gives you an image or document as a file on disk (a logo, team photos, a menu PDF, a price list), upload it. Run this from the workspace root, with one or more paths:
|
|
385
|
+
|
|
386
|
+
```bash
|
|
387
|
+
node upload.mjs ~/Downloads/logo.png --handle logo # image → cms:logo
|
|
388
|
+
node upload.mjs photos/*.jpg # images; handle = file name without extension
|
|
389
|
+
node upload.mjs ~/Desktop/Lunch\ Menu.pdf # document → Media > Files
|
|
390
|
+
node upload.mjs menu-v2.pdf --name lunch-menu.pdf # store it under another file name
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
- **Images** (png, jpg, jpeg, webp, gif, svg, ico, up to 8 MB) become site images under a handle. The handle is `--handle`, or by default the file name lowercased and dashed (`Team Photo.jpg` → `team-photo`). The script adds the handle to `content/images.json`. Reference the image as `"cms:<handle>"` in any `image` field, or with `img('cms:<handle>')` / `src('cms:<handle>')` in a view. Uploading to an existing handle replaces that image everywhere it's used.
|
|
394
|
+
- **Documents** (pdf, docx, xlsx, pptx, csv, tsv, txt, md, up to 10 MB) are added to Media > Files under their file name, and `content/files.json` is rewritten to list them. Put the printed file name in a `file` field, or render it with `getFile('<file name>')`. Uploading the same file name again replaces that file.
|
|
395
|
+
- The script prints the ref to use for each file. Copy it from the output rather than guessing it, because the server cleans up file names. Add `--json` for one JSON result per line. Exit code `0` means every file uploaded, `1` means some failed (the reason is printed), and `2` means it couldn't run.
|
|
396
|
+
- Uploading needs an EDITOR or ADMIN on the site.
|
|
397
|
+
- Never copy images or documents into `src/`. Binary files there are not synced; the sync log just points you back to `upload.mjs`.
|
|
381
398
|
|
|
382
399
|
### Markdown content files (`.md`)
|
|
383
400
|
|
|
@@ -455,7 +472,7 @@ There is exactly **one** layout: `layouts/common.ejs`. It wraps every page autom
|
|
|
455
472
|
| `getSlugs(path)` | string[] | Slugs under a collection path |
|
|
456
473
|
| `getImage(handle)` | Object \| undefined | Resolved image from `images.json` (bare handle, no `cms:` prefix) |
|
|
457
474
|
| `getVideo(handle)` | Object \| undefined | Video from `videos.json` as `{ title, source, video_id, embed_url }` — render with `embed()` |
|
|
458
|
-
| `getFile(
|
|
475
|
+
| `getFile(name)` | Object \| undefined | File from `files.json` by its name, as `{ name, url, size, type, ext }`, like a `file` field |
|
|
459
476
|
| `getContent(query?)` | Any | Full content payload, optionally filtered with JMESPath |
|
|
460
477
|
| `path(page)` | String | URL path of a page object |
|
|
461
478
|
| `url(pathOrPage?)` | String | Site origin; pass a path or page object for a full absolute URL |
|
|
@@ -555,10 +572,10 @@ Three site-level SEO settings live in `setup.json` (§4), not in views: `sitemap
|
|
|
555
572
|
|
|
556
573
|
## 10. Forms
|
|
557
574
|
|
|
558
|
-
Any `<form>` with `data-
|
|
575
|
+
Any `<form>` with `data-sleeksite-form="<name>"` works automatically — submissions are captured and shown in the CMS dashboard. No backend, no action URL, no JS.
|
|
559
576
|
|
|
560
577
|
```html
|
|
561
|
-
<form data-
|
|
578
|
+
<form data-sleeksite-form="Contact">
|
|
562
579
|
<input name="name" type="text" required>
|
|
563
580
|
<input name="email" type="email" required>
|
|
564
581
|
<textarea name="message"></textarea>
|
|
@@ -566,7 +583,7 @@ Any `<form>` with `data-sleekcms="<hardcoded name>"` works automatically — sub
|
|
|
566
583
|
</form>
|
|
567
584
|
```
|
|
568
585
|
|
|
569
|
-
|
|
586
|
+
Write the form's **name** (e.g. `Contact`, `Newsletter`, `Quote request`), literally — never a template expression. On sync the form is created if it doesn't exist, and the CMS stores the view with the name replaced by the form's key (e.g. `data-sleeksite-form="mh3k2x9a"`), so the published form stays linked when the user renames it in the app. Your local file keeps the name as you wrote it. A key you find in a view is the form's id: keep it as it is. Each input's `name` is stored as-is.
|
|
570
587
|
|
|
571
588
|
**File uploads** are supported out of the box — add an `<input type="file">` and visitors can attach documents (PDF, Word, Excel, CSV, text) and images (add `multiple` for several):
|
|
572
589
|
|
|
@@ -574,25 +591,27 @@ The `data-sleekcms` value groups submissions by form; each input's `name` is sto
|
|
|
574
591
|
<input name="resume" type="file" accept=".pdf,.doc,.docx">
|
|
575
592
|
```
|
|
576
593
|
|
|
594
|
+
For visitors to **book a time**, use `data-sleeksite-book` instead — see §11. A walk-in queue's "Schedule a time" form uses `data-sleeksite-queue="<queue name>"` (fields `queue_date`, an empty `<select name="queue_time">`, `name`, `email`) and goes to the queue, not the inbox; the queue is created on sync, with the same name-to-key rule as forms.
|
|
595
|
+
|
|
577
596
|
## 11. Bookings
|
|
578
597
|
|
|
579
598
|
Use this whenever visitors should **book or schedule** something — an appointment, a call, a demo, a consultation, a class, or a reservation ("pick a time / reserve a slot"). It fits appointment-based businesses: salon, spa, barber, dental/medical clinic, therapist, tutor, photographer, consultant, repair shop, restaurant reservations. **Only add a booking form when the business genuinely takes appointments and the user asks for it (or it is clearly core to the business) — never add one speculatively to an arbitrary site.**
|
|
580
599
|
|
|
581
|
-
**A booking is just a form
|
|
600
|
+
**A booking is just a form** with `data-sleeksite-book="<name>"` instead of `data-sleeksite-form` — on the page it gets an availability slot picker, and submissions become appointments with email confirmation, calendar invites, and no-login cancel/reschedule. Every other field is yours: bookings store arbitrary fields, exactly like form submissions.
|
|
582
601
|
|
|
583
602
|
```html
|
|
584
|
-
<form data-
|
|
603
|
+
<form data-sleeksite-book="Consultation">
|
|
585
604
|
<input name="name" type="text" required placeholder="Your name">
|
|
586
605
|
<input name="email" type="email" required placeholder="Email">
|
|
587
|
-
<
|
|
606
|
+
<div data-sleeksite-slot></div>
|
|
588
607
|
<textarea name="notes" placeholder="Anything we should know?"></textarea>
|
|
589
608
|
<button type="submit">Book appointment</button>
|
|
590
609
|
</form>
|
|
591
610
|
```
|
|
592
611
|
|
|
593
|
-
Rules: **include an email field** (`type="email"` or `name="email"`) — confirmation and manage links go there; the
|
|
612
|
+
Rules: **include an email field** (`type="email"` or `name="email"`) — confirmation and manage links go there; the picker goes where an element with `data-sleeksite-slot` (or an `<input type="datetime-local">`) is, and replaces it — without one it goes just before the submit button, and the chosen time is stored as the booking's slot; style the form like any other — the picker inherits the page's `--accent` CSS variable (or set `data-accent` on the form).
|
|
594
613
|
|
|
595
|
-
The form named `
|
|
614
|
+
The form named `Consultation` is **auto-created on sync** like any form (the same name-to-key rule as §10 applies), and as a booking form it gets default availability: 30-minute slots, one booking at a time, Mon–Fri 9 AM–5 PM in America/New_York, bookings confirmed instantly (email-confirmation off — the owner can require it in settings). The defaults are guesses — tell the user to review the timezone and open hours in the form's **Booking settings** (Customers › Inbox); appointments appear under **Customers › Bookings**. Reusing the same name (or key) across pages shares one booking form. The visitor's confirm and manage links open pages on the site's own domain (`/_book/…`) — nothing to build.
|
|
596
615
|
|
|
597
616
|
## 12. RSS Feeds
|
|
598
617
|
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{readFileSync as
|
|
2
|
+
import{readFileSync as He}from"node:fs";import{fileURLToPath as Fr}from"node:url";import xe from"node:path";import{render as Nr}from"ink";import{execSync as it,spawn as B}from"child_process";import{program as tt,Option as nt}from"commander";function ot(){return tt.name("cms-sync").description("SleekCMS CLI tool to sync and edit CMS templates locally. Add each site you want to sync by approving it in your browser \u2014 changes are watched and pushed automatically.").addOption(new nt("-v, --version","output the version number").hideHelp()).addOption(new nt("-e, --env <env>","Environment (localhost, development, production)").default("production").hideHelp()).option("-p, --path <path>","Directory path for files (default: ~/SleekCMS)").addHelpText("after",`
|
|
3
3
|
Examples:
|
|
4
4
|
$ sleekcms # add sites in your browser, pick which to sync
|
|
5
5
|
$ sleekcms -e development # use a non-production backend
|
|
6
|
-
`).parse(process.argv),Qe.opts()}function R(e){let t=process.platform==="win32"?`where ${e}`:`which ${e}`;try{return rt(t,{stdio:"ignore"}),!0}catch{return!1}}function it(e){if(process.platform!=="darwin")return!1;try{return rt(`osascript -e 'id of application "${e}"'`,{stdio:"ignore"}),!0}catch{return!1}}function ln(e,t){let n={detached:!0,stdio:"ignore",windowsHide:!0};process.platform==="win32"?B("cmd.exe",["/c",e,...t],n).unref():B(e,t,n).unref()}var tt=[{name:"VS Code",cmd:"code",args:e=>["-n",e]},{name:"Cursor",cmd:"cursor",args:e=>["-n",e]}];function nt(e,t){let r=(t?tt.filter(s=>s.cmd===t):tt).find(s=>R(s.cmd));return r?(ln(r.cmd,r.args(e)),r.name):null}function un(e){if(process.platform==="darwin")return B("open",["-a","Terminal",e],{detached:!0,stdio:"ignore"}).unref(),!0;if(process.platform==="win32"){if(R("wt"))B("wt.exe",["-d",e],{detached:!0,stdio:"ignore"}).unref();else{let t=R("powershell")?"powershell.exe":"cmd.exe";B("cmd.exe",["/c","start","",t],{cwd:e,detached:!0,stdio:"ignore"}).unref()}return!0}return!1}function dn(e){return it("iTerm")?(B("open",["-a","iTerm",e],{detached:!0,stdio:"ignore"}).unref(),!0):!1}function gn(e){return`'${e.replace(/'/g,"'\\''")}'`}var fn=[{cmd:"x-terminal-emulator",args:e=>["-e","bash","-c",e]},{cmd:"gnome-terminal",args:e=>["--","bash","-c",e]},{cmd:"konsole",args:e=>["-e","bash","-c",e]},{cmd:"xfce4-terminal",args:e=>["-x","bash","-c",e]},{cmd:"kitty",args:e=>["bash","-c",e]},{cmd:"alacritty",args:e=>["-e","bash","-c",e]},{cmd:"xterm",args:e=>["-e","bash","-c",e]}];function ot(e,t){if(!R(t))return!1;if(process.platform==="darwin"){let r=`tell application "Terminal" to do script ("cd " & quoted form of "${e}" & " && ${t}")`;return B("osascript",["-e",r,"-e",'tell application "Terminal" to activate'],{detached:!0,stdio:"ignore"}).unref(),!0}if(process.platform==="win32")return R("wt")?B("wt.exe",["-d",e,"cmd","/k",t],{detached:!0,stdio:"ignore"}).unref():B("cmd.exe",["/c","start","","cmd","/k",t],{cwd:e,detached:!0,stdio:"ignore"}).unref(),!0;let n=`cd ${gn(e)} && ${t}; exec bash`;for(let r of fn)if(R(r.cmd))try{return B(r.cmd,r.args(n),{cwd:e,detached:!0,stdio:"ignore"}).unref(),!0}catch{}return!1}function pn(e){return ot(e,"claude")}function hn(e){return ot(e,"pi")}function ge(e){if(!e)return!1;try{let t=process.platform==="darwin"?B("open",[e],{detached:!0,stdio:"ignore"}):process.platform==="win32"?B("cmd.exe",["/c","start","",e],{detached:!0,stdio:"ignore",windowsHide:!0}):B("xdg-open",[e],{detached:!0,stdio:"ignore"});return t.on("error",()=>{}),t.unref(),!0}catch{return!1}}function at(){let e=process.platform==="darwin",t=process.platform==="win32",n=[];return(e||t)&&n.push({key:"terminal",label:"terminal",run:un}),it("iTerm")&&n.push({key:"iterm",label:"iterm",run:dn}),R("code")&&n.push({key:"code",label:"vscode",run:r=>nt(r,"code")!==null}),R("cursor")&&n.push({key:"cursor",label:"cursor",run:r=>nt(r,"cursor")!==null}),R("claude")&&n.push({key:"claude",label:"claude",run:pn}),R("pi")&&n.push({key:"pi",label:"pi",run:hn}),n}import{EventEmitter as er}from"node:events";import{spawn as tr}from"node:child_process";import re from"fs-extra";import Ae from"path";import nr from"chokidar";import{createHash as wn}from"crypto";import h from"fs-extra";import yn from"os";import b from"path";var ct={localhost:"http://app.sleekcms.test/api/mcp",development:"https://app.sleekcms.dev/api/mcp",production:"https://app.sleekcms.com/api/mcp"},bn=["src/views/blocks","src/views/entries","src/views/pages","src/views/layouts","src/models/blocks","src/models/entries","src/models/pages","src/content/pages","src/content/entries","src/public/js","src/public/css"],N=class extends Error{status;body;constructor(t,n,r){super(t),this.status=n,this.body=r}};async function ee(e,t,n,r,s){let i=await fetch(e+r,{method:n,headers:{Authorization:`Bearer ${t}`,...s!==void 0?{"Content-Type":"application/json"}:{}},body:s!==void 0?JSON.stringify(s):void 0});if(!i.ok){let d=await i.text();throw new N(`${n} ${r} \u2192 ${i.status}: ${d}`,i.status,d)}return i.json()}var vn=["AGENT.md","CLAUDE.md",b.join(".vscode","copilot-instructions.md")],Sn="mrmlnc.vscode-json5";async function xn(e,t){t&&(await h.outputFile(b.join(e,"AGENTS.md"),t),await Promise.all(vn.map(n=>h.remove(b.join(e,n))))),await h.outputFile(b.join(e,".vscode","settings.json"),JSON.stringify({"files.associations":{"*.model":"javascript","**/src/**/*.json":"json5"},"[javascript]":{"editor.defaultFormatter":"esbenp.prettier-vscode"},"js/ts.validate.enabled":!1},null,2)),await h.outputFile(b.join(e,".vscode","extensions.json"),JSON.stringify({recommendations:[Sn]},null,2))}async function Tn(e,t){if(!t)return;let n=b.join(e,"build.mjs"),r=t.match(/build\.mjs v\d+/)?.[0];if(await h.pathExists(n)){let s=(await h.readFile(n,"utf-8")).match(/build\.mjs v\d+/)?.[0];if(s&&s===r)return}await h.outputFile(n,t)}async function kn(e,t,n,r){let s=b.join(e,".cache","site.json"),i=await h.readJson(s).catch(()=>null);i&&(i.env!==n.env||i.slug!==n.slug)&&(r?.("\u267B\uFE0F Workspace was tied to a different site \u2014 recreating it."),await h.emptyDir(e)),await h.outputJson(s,n),await h.outputFile(b.join(e,".cache","token"),t)}function $n(e,t){let n=`${t.name.substr(0,20)} ${t.id}`.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,""),r=e||b.join(yn.homedir(),"SleekCMS");return b.resolve(r,n)}async function Be(e){let t=(e.token||"").trim();if(!t)throw new Error("syncSite: token is required");let n=e.log??(v=>console.log(v)),r=e.onProgress,s=(e.env||"production").toLowerCase(),i=ct[s]||ct.production,d=await ee(i,t,"GET","/get_site"),a=e.viewsDir?b.resolve(e.viewsDir):$n(e.path,d);await h.ensureDir(a),await kn(a,t,{env:s,slug:String(d.id)},n),await Promise.all(bn.map(v=>h.ensureDir(b.join(a,v)))),await Tn(a,e.buildScript);let y=b.join(a,".cache","state.json");e.flush&&await h.remove(y);let w=!await h.pathExists(y),u=w?{}:(await h.readJson(y)).fileMap||{},f=0,x=0,m=0;if(w)({fileMap:u,pulled:x}=await Nn(a,i,t,n,r)),await xn(a,e.agentMd);else{let v=await In(a,u,i,t,n);m=v.renamed,f=await Cn(a,u,i,t,n,r,v.held),await Bn(a,u,i,t,n)}return await h.outputJson(y,{fileMap:u},{spaces:2}),{viewsDir:a,site:d,isFirstRun:w,pushed:f,pulled:x,renamed:m}}var z=e=>wn("sha1").update(e).digest("hex"),En=new Set([".DS_Store","Thumbs.db","desktop.ini"]);async function Pe(e){let t=b.join(e,"src");if(!await h.pathExists(t))return[];let n=[];async function r(s){let i=await h.readdir(s,{withFileTypes:!0});for(let d of i){if(En.has(d.name))continue;let a=b.join(s,d.name);d.isDirectory()?await r(a):d.isFile()&&n.push(b.relative(e,a).replace(/\\/g,"/"))}}return await r(t),n}var J="src/content/files.json";async function Bn(e,t,n,r,s){let i;try{i=await ee(n,r,"GET","/get_files?kind=files")}catch(w){if(w instanceof N&&w.status>=400&&w.status<500)return;s(`\u26A0\uFE0F Couldn't refresh ${J}: ${pt(w)}`);return}let d=i.find(w=>w.path===J);if(!d)return;let a=b.join(e,J);(await h.pathExists(a)?await h.readFile(a,"utf-8"):null)!==d.content&&(await h.outputFile(a,d.content),t[J]={mtimeMs:(await h.stat(a)).mtimeMs,hash:z(d.content)},s(`\u{1F4C4} Refreshed ${J}`))}function lt(e){let t=e.split(b.sep).join("/");return t==="src/setup.json"?0:t==="src/content/images.json"?1:t==="src/content/videos.json"?2:t.startsWith("src/models/")?3:t.startsWith("src/content/")?4:5}var ut=10;function Pn(e){let t=[];for(let n=0;n<e.length;n+=ut)t.push(e.slice(n,n+ut));return t}async function Cn(e,t,n,r,s,i,d=new Set){let a=[];for(let f of await Pe(e)){if(d.has(f)||f===J)continue;let x=b.join(e,f),m=t[f],v=await h.stat(x);if(m&&m.mtimeMs===v.mtimeMs){m.hash||(m.hash=z(await h.readFile(x,"utf-8")));continue}let k=await h.readFile(x,"utf-8");k.trim()&&a.push({rel:f,full:x,stat:v,content:k,prior:m})}if(a.length===0)return 0;a.sort((f,x)=>lt(f.rel)-lt(x.rel)),s(`\u2B06 pushing ${a.length} file(s)`),i?.(0,a.length);let y=await ht(e),w=0,u=0;for(let f of Pn(a)){let x;try{x=await ee(n,r,"POST","/save_files",f.map(v=>({path:v.rel,content:v.content})))}catch(v){s(`\u274C Error saving files: ${pt(v)}`),s(`\u23F8 ${a.length-u} file(s) not pushed \u2014 saving any file retries them.`);break}let m=Mn(x);for(let v=0;v<f.length;v++){let k=f[v],A=Rn(k,v,x,m);if(!A){let W="No save response returned";y[k.rel]=W,s(`\u274C Error saving ${k.rel}: ${W}`);continue}if(A.error){y[k.rel]=A.error,s(`\u274C Error saving ${k.rel}: ${A.error}`);continue}delete y[k.rel],t[k.rel]={mtimeMs:k.stat.mtimeMs,hash:z(k.content)},s(`\u2705 ${k.prior?"Updated":"Created"} ${k.rel}`),w++}u+=f.length,i?.(u,a.length)}return u===0?0:(await mt(e,y),w)}function Mn(e){let t=new Map;for(let n of e)n.path&&t.set(n.path,n);return t}function Rn(e,t,n,r){let s=r.get(e.rel);if(s)return s;let i=n[t];if(!i?.path||i.path===e.rel)return i}var dt=/^src\/(?:(?:models|views)\/(?:pages|entries|blocks)|content\/(?:pages|entries))\//,Ln={PAGE:"pages",ENTRY:"entries",BLOCK:"blocks"},jn=e=>e.split("/").slice(0,3).join("/");function Dn(e,t,n){let r=new Map,s=(i,d)=>{let a=`${jn(i)}|${d}`;return r.has(a)||r.set(a,{from:[],to:[],hash:d}),r.get(a)};for(let i of e)s(i,n[i].hash).from.push(i);for(let[i,d]of t)s(i,d).to.push(i);return[...r.values()].filter(i=>i.from.length===1&&i.to.length===1).map(i=>({from:i.from[0],to:i.to[0],hash:i.hash}))}function _n(e,t){let n=new Map,r=(s,i,d,a,y)=>{let w=n.get(s);w?(w.pairs.push(y),i<w.rank&&Object.assign(w,{from:d,to:a,rank:i})):n.set(s,{from:d,to:a,rank:i,pairs:[y]})};for(let s of e){let[,i,d,...a]=s.from.split("/"),y=s.to.split("/").slice(3);if(i==="content"&&d==="pages"&&a.length>1){let[u,...f]=a,[x,...m]=y;if(m.length===0)continue;x===u?r(`record:${s.from}`,0,s.from,s.to,s):f.join("/")===m.join("/")&&![...t].some(v=>v.startsWith(`src/content/pages/${u}/`))&&r(`pages:${u}`,3,`src/content/pages/${u}`,`src/content/pages/${x}`,s);continue}let w=a.join("/").replace(/\.[^./]+$/,"");r(`${d}:${w}`,i==="models"?0:i==="views"?1:2,s.from,s.to,s)}return[...n.values()].map(({from:s,to:i,pairs:d})=>({from:s,to:i,pairs:d}))}function gt(e,t){if(e.slug!==void 0&&e.new_slug!==void 0){let d=`src/content/pages/${e.key}/`,a=t.startsWith(d)?t.slice(d.length).match(/^(.+)\.(json|md)$/):null;return a&&a[1].toLowerCase()===e.slug.toLowerCase()?`${d}${e.new_slug}.${a[2]}`:null}let n=Ln[e.type],{key:r,new_key:s}=e;if(!n||!s)return null;if(t===`src/models/${n}/${r}.model`)return`src/models/${n}/${s}.model`;if(t===`src/views/${n}/${r}.ejs`)return`src/views/${n}/${s}.ejs`;if(n!=="blocks"&&(t===`src/content/${n}/${r}.json`||t===`src/content/${n}/${r}.md`))return`src/content/${n}/${s}${b.extname(t)}`;let i=`src/content/pages/${r}/`;return n==="pages"&&r.endsWith("+")&&t.startsWith(i)?`src/content/pages/${s}/${t.slice(i.length)}`:null}async function An(e,t){let n=e;for(;n.startsWith(t+b.sep);){let r=await h.readdir(n).catch(()=>null);if(!r||r.length>0)return;await h.remove(n),n=b.dirname(n)}}async function On(e,t,n,r){let s=u=>b.join(e,u),i=b.join(e,"src"),d=u=>b.resolve(e,u).startsWith(i+b.sep),a=async(u,f)=>{t[u]={mtimeMs:(await h.stat(s(u))).mtimeMs,hash:f}},y=u=>An(b.dirname(s(u)),b.join(e,...u.split("/").slice(0,3))),w=[];for(let u of r){let f=gt(n.rename,u.from)??u.to;d(f)&&(f!==u.to&&(await h.move(s(u.to),s(f),{overwrite:!0}),await y(u.to),w.push(u.to)),delete t[u.from],w.push(u.from),await a(f,u.hash))}for(let u of new Set([...await Pe(e),...Object.keys(t)])){let f=gt(n.rename,u);if(!f||!d(f))continue;let x=await h.pathExists(s(u));if(x&&await h.pathExists(s(f)))continue;let m=t[u];if(delete t[u],w.push(u),!x)continue;let v=!!m?.hash&&m.mtimeMs===(await h.stat(s(u))).mtimeMs;await h.move(s(u),s(f)),v&&await a(f,m.hash),await y(u)}for(let u of n.files||[]){let f=t[u.path];if(!f||!d(u.path))continue;let x=await h.stat(s(u.path)).catch(()=>null);!x||x.mtimeMs!==f.mtimeMs||await h.readFile(s(u.path),"utf-8")!==u.content&&(await h.outputFile(s(u.path),u.content),await a(u.path,z(u.content)))}return w}function ft(e){try{let t=JSON.parse(e.body).message;if(t)return t}catch{}return(e.body||e.message||String(e)).slice(0,300)}function pt(e){return e instanceof N?e.body.match(/<title>([^<]*)<\/title>/i)?.[1].trim()||ft(e).replace(/\s+/g," "):e instanceof Error?e.message:String(e)}async function In(e,t,n,r,s){let i=new Set,d=await Pe(e),a=new Set(d),y=Object.keys(t).filter(m=>!a.has(m)&&t[m].hash&&dt.test(m));if(y.length===0)return{renamed:0,held:i};let w=new Map;for(let m of d){if(t[m]||!dt.test(m))continue;let v=await h.readFile(b.join(e,m),"utf-8");v.trim()&&w.set(m,z(v))}let u=_n(Dn(y,w,t),a);if(u.length===0)return{renamed:0,held:i};let f=await ht(e),x=0;for(let m of u){let v;try{v=await ee(n,r,"POST","/rename",{from:m.from,to:m.to})}catch(C){let M=C,K=ft(M);if(M.status>=400&&M.status<500)s(`\u26A0\uFE0F Couldn't rename ${m.from} \u2192 ${m.to}: ${K} Saving it as a new file.`);else{for(let j of m.pairs)i.add(j.to),f[j.to]=`rename from ${j.from} failed: ${K}`;s(`\u274C Error renaming ${m.from} \u2192 ${m.to}: ${K}`)}continue}let k=await On(e,t,v,m.pairs);for(let C of[...k,...m.pairs.map(M=>M.to)])delete f[C];let[A,...W]=v.message.split(`
|
|
7
|
-
`);s(`\u{1F500} ${
|
|
8
|
-
`)){let i=s.indexOf(": ");i>0&&(r[s.slice(0,i)]=s.slice(i+2))}return r}async function
|
|
6
|
+
`).parse(process.argv),tt.opts()}function L(e){let t=process.platform==="win32"?`where ${e}`:`which ${e}`;try{return it(t,{stdio:"ignore"}),!0}catch{return!1}}function at(e){if(process.platform!=="darwin")return!1;try{return it(`osascript -e 'id of application "${e}"'`,{stdio:"ignore"}),!0}catch{return!1}}function dn(e,t){let n={detached:!0,stdio:"ignore",windowsHide:!0};process.platform==="win32"?B("cmd.exe",["/c",e,...t],n).unref():B(e,t,n).unref()}var rt=[{name:"VS Code",cmd:"code",args:e=>["-n",e]},{name:"Cursor",cmd:"cursor",args:e=>["-n",e]}];function st(e,t){let r=(t?rt.filter(s=>s.cmd===t):rt).find(s=>L(s.cmd));return r?(dn(r.cmd,r.args(e)),r.name):null}function gn(e){if(process.platform==="darwin")return B("open",["-a","Terminal",e],{detached:!0,stdio:"ignore"}).unref(),!0;if(process.platform==="win32"){if(L("wt"))B("wt.exe",["-d",e],{detached:!0,stdio:"ignore"}).unref();else{let t=L("powershell")?"powershell.exe":"cmd.exe";B("cmd.exe",["/c","start","",t],{cwd:e,detached:!0,stdio:"ignore"}).unref()}return!0}return!1}function pn(e){return at("iTerm")?(B("open",["-a","iTerm",e],{detached:!0,stdio:"ignore"}).unref(),!0):!1}function fn(e){return`'${e.replace(/'/g,"'\\''")}'`}var hn=[{cmd:"x-terminal-emulator",args:e=>["-e","bash","-c",e]},{cmd:"gnome-terminal",args:e=>["--","bash","-c",e]},{cmd:"konsole",args:e=>["-e","bash","-c",e]},{cmd:"xfce4-terminal",args:e=>["-x","bash","-c",e]},{cmd:"kitty",args:e=>["bash","-c",e]},{cmd:"alacritty",args:e=>["-e","bash","-c",e]},{cmd:"xterm",args:e=>["-e","bash","-c",e]}];function ct(e,t){if(!L(t))return!1;if(process.platform==="darwin"){let r=`tell application "Terminal" to do script ("cd " & quoted form of "${e}" & " && ${t}")`;return B("osascript",["-e",r,"-e",'tell application "Terminal" to activate'],{detached:!0,stdio:"ignore"}).unref(),!0}if(process.platform==="win32")return L("wt")?B("wt.exe",["-d",e,"cmd","/k",t],{detached:!0,stdio:"ignore"}).unref():B("cmd.exe",["/c","start","","cmd","/k",t],{cwd:e,detached:!0,stdio:"ignore"}).unref(),!0;let n=`cd ${fn(e)} && ${t}; exec bash`;for(let r of hn)if(L(r.cmd))try{return B(r.cmd,r.args(n),{cwd:e,detached:!0,stdio:"ignore"}).unref(),!0}catch{}return!1}function mn(e){return ct(e,"claude")}function wn(e){return ct(e,"pi")}function ge(e){if(!e)return!1;try{let t=process.platform==="darwin"?B("open",[e],{detached:!0,stdio:"ignore"}):process.platform==="win32"?B("cmd.exe",["/c","start","",e],{detached:!0,stdio:"ignore",windowsHide:!0}):B("xdg-open",[e],{detached:!0,stdio:"ignore"});return t.on("error",()=>{}),t.unref(),!0}catch{return!1}}function lt(){let e=process.platform==="darwin",t=process.platform==="win32",n=[];return(e||t)&&n.push({key:"terminal",label:"terminal",run:gn}),at("iTerm")&&n.push({key:"iterm",label:"iterm",run:pn}),L("code")&&n.push({key:"code",label:"vscode",run:r=>st(r,"code")!==null}),L("cursor")&&n.push({key:"cursor",label:"cursor",run:r=>st(r,"cursor")!==null}),L("claude")&&n.push({key:"claude",label:"claude",run:mn}),L("pi")&&n.push({key:"pi",label:"pi",run:wn}),n}import{EventEmitter as nr}from"node:events";import{spawn as rr}from"node:child_process";import re from"fs-extra";import Oe from"path";import sr from"chokidar";import{createHash as bn}from"crypto";import h from"fs-extra";import vn from"os";import b from"path";var ut={localhost:"http://app.sleekcms.test/api/mcp",development:"https://app.sleekcms.dev/api/mcp",production:"https://app.sleekcms.com/api/mcp"},Sn=["src/views/blocks","src/views/entries","src/views/pages","src/views/layouts","src/models/blocks","src/models/entries","src/models/pages","src/content/pages","src/content/entries","src/public/js","src/public/css"],W=class extends Error{status;body;constructor(t,n,r){super(t),this.status=n,this.body=r}};async function ee(e,t,n,r,s){let i=await fetch(e+r,{method:n,headers:{Authorization:`Bearer ${t}`,...s!==void 0?{"Content-Type":"application/json"}:{}},body:s!==void 0?JSON.stringify(s):void 0});if(!i.ok){let u=await i.text();throw new W(`${n} ${r} \u2192 ${i.status}: ${u}`,i.status,u)}return i.json()}var xn=["AGENT.md","CLAUDE.md",b.join(".vscode","copilot-instructions.md")],Tn="mrmlnc.vscode-json5";async function kn(e,t){t&&(await h.outputFile(b.join(e,"AGENTS.md"),t),await Promise.all(xn.map(n=>h.remove(b.join(e,n))))),await h.outputFile(b.join(e,".vscode","settings.json"),JSON.stringify({"files.associations":{"*.model":"javascript","**/src/**/*.json":"json5"},"[javascript]":{"editor.defaultFormatter":"esbenp.prettier-vscode"},"js/ts.validate.enabled":!1},null,2)),await h.outputFile(b.join(e,".vscode","extensions.json"),JSON.stringify({recommendations:[Tn]},null,2))}async function dt(e,t,n){if(!n)return;let r=b.join(e,t),s=new RegExp(`${t.replace(/\./g,"\\.")} v\\d+`),i=n.match(s)?.[0];if(await h.pathExists(r)){let u=(await h.readFile(r,"utf-8")).match(s)?.[0];if(u&&u===i)return}await h.outputFile(r,n)}async function $n(e,t,n,r){let s=b.join(e,".cache","site.json"),i=await h.readJson(s).catch(()=>null);i&&(i.env!==n.env||i.slug!==n.slug)&&(r?.("\u267B\uFE0F Workspace was tied to a different site \u2014 recreating it."),await h.emptyDir(e)),await h.outputJson(s,n),await h.outputFile(b.join(e,".cache","token"),t)}function En(e,t){let n=`${t.name.substr(0,20)} ${t.id}`.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,""),r=e||b.join(vn.homedir(),"SleekCMS");return b.resolve(r,n)}async function Pe(e){let t=(e.token||"").trim();if(!t)throw new Error("syncSite: token is required");let n=e.log??(v=>console.log(v)),r=e.onProgress,s=(e.env||"production").toLowerCase(),i=ut[s]||ut.production,u=await ee(i,t,"GET","/get_site"),a=e.viewsDir?b.resolve(e.viewsDir):En(e.path,u);await h.ensureDir(a),await $n(a,t,{env:s,slug:String(u.id)},n),await Promise.all(Sn.map(v=>h.ensureDir(b.join(a,v)))),await dt(a,"build.mjs",e.buildScript),await dt(a,"upload.mjs",e.uploadScript);let y=b.join(a,".cache","state.json");e.flush&&await h.remove(y);let w=!await h.pathExists(y),d=w?{}:(await h.readJson(y)).fileMap||{},g=0,x=0,m=0;if(w)({fileMap:d,pulled:x}=await Gn(a,i,t,n,r)),await kn(a,e.agentMd);else{let v=await Nn(a,d,i,t,n);m=v.renamed,g=await Rn(a,d,i,t,n,r,v.held),await Pn(a,d,i,t,n)}return await h.outputJson(y,{fileMap:d},{spaces:2}),{viewsDir:a,site:u,isFirstRun:w,pushed:g,pulled:x,renamed:m}}var z=e=>bn("sha1").update(e).digest("hex"),Bn=new Set([".DS_Store","Thumbs.db","desktop.ini"]);async function Ce(e){let t=b.join(e,"src");if(!await h.pathExists(t))return[];let n=[];async function r(s){let i=await h.readdir(s,{withFileTypes:!0});for(let u of i){if(Bn.has(u.name))continue;let a=b.join(s,u.name);u.isDirectory()?await r(a):u.isFile()&&n.push(b.relative(e,a).replace(/\\/g,"/"))}}return await r(t),n}var J="src/content/files.json";async function Pn(e,t,n,r,s){let i;try{i=await ee(n,r,"GET","/get_files?kind=files")}catch(w){if(w instanceof W&&w.status>=400&&w.status<500)return;s(`\u26A0\uFE0F Couldn't refresh ${J}: ${yt(w)}`);return}let u=i.find(w=>w.path===J);if(!u)return;let a=b.join(e,J);(await h.pathExists(a)?await h.readFile(a,"utf-8"):null)!==u.content&&(await h.outputFile(a,u.content),t[J]={mtimeMs:(await h.stat(a)).mtimeMs,hash:z(u.content)},s(`\u{1F4C4} Refreshed ${J}`))}var Cn=e=>e.subarray(0,8e3).includes(0),gt=new Set;function pt(e){let t=e.split(b.sep).join("/");return t==="src/setup.json"?0:t==="src/content/images.json"?1:t==="src/content/videos.json"?2:t.startsWith("src/models/")?3:t.startsWith("src/content/")?4:5}var ft=10;function Mn(e){let t=[];for(let n=0;n<e.length;n+=ft)t.push(e.slice(n,n+ft));return t}async function Rn(e,t,n,r,s,i,u=new Set){let a=[];for(let g of await Ce(e)){if(u.has(g)||g===J)continue;let x=b.join(e,g),m=t[g],v=await h.stat(x);if(m&&m.mtimeMs===v.mtimeMs){m.hash||(m.hash=z(await h.readFile(x,"utf-8")));continue}let k=await h.readFile(x);if(Cn(k)){let D=`${g}@${v.mtimeMs}`;gt.has(D)||(gt.add(D),s(`\u26A0\uFE0F ${g} is a binary file and isn't synced. Upload it with: node upload.mjs ${g}`));continue}let C=k.toString("utf-8");C.trim()&&a.push({rel:g,full:x,stat:v,content:C,prior:m})}if(a.length===0)return 0;a.sort((g,x)=>pt(g.rel)-pt(x.rel)),s(`\u2B06 pushing ${a.length} file(s)`),i?.(0,a.length);let y=await bt(e),w=0,d=0;for(let g of Mn(a)){let x;try{x=await ee(n,r,"POST","/save_files",g.map(v=>({path:v.rel,content:v.content})))}catch(v){s(`\u274C Error saving files: ${yt(v)}`),s(`\u23F8 ${a.length-d} file(s) not pushed \u2014 saving any file retries them.`);break}let m=Ln(x);for(let v=0;v<g.length;v++){let k=g[v],C=jn(k,v,x,m);if(!C){let D="No save response returned";y[k.rel]=D,s(`\u274C Error saving ${k.rel}: ${D}`);continue}if(C.error){y[k.rel]=C.error,s(`\u274C Error saving ${k.rel}: ${C.error}`);continue}delete y[k.rel],t[k.rel]={mtimeMs:k.stat.mtimeMs,hash:z(k.content)},s(`\u2705 ${k.prior?"Updated":"Created"} ${k.rel}`),w++}d+=g.length,i?.(d,a.length)}return d===0?0:(await vt(e,y),w)}function Ln(e){let t=new Map;for(let n of e)n.path&&t.set(n.path,n);return t}function jn(e,t,n,r){let s=r.get(e.rel);if(s)return s;let i=n[t];if(!i?.path||i.path===e.rel)return i}var ht=/^src\/(?:(?:models|views)\/(?:pages|entries|blocks)|content\/(?:pages|entries))\//,Dn={PAGE:"pages",ENTRY:"entries",BLOCK:"blocks"},_n=e=>e.split("/").slice(0,3).join("/");function An(e,t,n){let r=new Map,s=(i,u)=>{let a=`${_n(i)}|${u}`;return r.has(a)||r.set(a,{from:[],to:[],hash:u}),r.get(a)};for(let i of e)s(i,n[i].hash).from.push(i);for(let[i,u]of t)s(i,u).to.push(i);return[...r.values()].filter(i=>i.from.length===1&&i.to.length===1).map(i=>({from:i.from[0],to:i.to[0],hash:i.hash}))}function On(e,t){let n=new Map,r=(s,i,u,a,y)=>{let w=n.get(s);w?(w.pairs.push(y),i<w.rank&&Object.assign(w,{from:u,to:a,rank:i})):n.set(s,{from:u,to:a,rank:i,pairs:[y]})};for(let s of e){let[,i,u,...a]=s.from.split("/"),y=s.to.split("/").slice(3);if(i==="content"&&u==="pages"&&a.length>1){let[d,...g]=a,[x,...m]=y;if(m.length===0)continue;x===d?r(`record:${s.from}`,0,s.from,s.to,s):g.join("/")===m.join("/")&&![...t].some(v=>v.startsWith(`src/content/pages/${d}/`))&&r(`pages:${d}`,3,`src/content/pages/${d}`,`src/content/pages/${x}`,s);continue}let w=a.join("/").replace(/\.[^./]+$/,"");r(`${u}:${w}`,i==="models"?0:i==="views"?1:2,s.from,s.to,s)}return[...n.values()].map(({from:s,to:i,pairs:u})=>({from:s,to:i,pairs:u}))}function mt(e,t){if(e.slug!==void 0&&e.new_slug!==void 0){let u=`src/content/pages/${e.key}/`,a=t.startsWith(u)?t.slice(u.length).match(/^(.+)\.(json|md)$/):null;return a&&a[1].toLowerCase()===e.slug.toLowerCase()?`${u}${e.new_slug}.${a[2]}`:null}let n=Dn[e.type],{key:r,new_key:s}=e;if(!n||!s)return null;if(t===`src/models/${n}/${r}.model`)return`src/models/${n}/${s}.model`;if(t===`src/views/${n}/${r}.ejs`)return`src/views/${n}/${s}.ejs`;if(n!=="blocks"&&(t===`src/content/${n}/${r}.json`||t===`src/content/${n}/${r}.md`))return`src/content/${n}/${s}${b.extname(t)}`;let i=`src/content/pages/${r}/`;return n==="pages"&&r.endsWith("+")&&t.startsWith(i)?`src/content/pages/${s}/${t.slice(i.length)}`:null}async function In(e,t){let n=e;for(;n.startsWith(t+b.sep);){let r=await h.readdir(n).catch(()=>null);if(!r||r.length>0)return;await h.remove(n),n=b.dirname(n)}}async function Fn(e,t,n,r){let s=d=>b.join(e,d),i=b.join(e,"src"),u=d=>b.resolve(e,d).startsWith(i+b.sep),a=async(d,g)=>{t[d]={mtimeMs:(await h.stat(s(d))).mtimeMs,hash:g}},y=d=>In(b.dirname(s(d)),b.join(e,...d.split("/").slice(0,3))),w=[];for(let d of r){let g=mt(n.rename,d.from)??d.to;u(g)&&(g!==d.to&&(await h.move(s(d.to),s(g),{overwrite:!0}),await y(d.to),w.push(d.to)),delete t[d.from],w.push(d.from),await a(g,d.hash))}for(let d of new Set([...await Ce(e),...Object.keys(t)])){let g=mt(n.rename,d);if(!g||!u(g))continue;let x=await h.pathExists(s(d));if(x&&await h.pathExists(s(g)))continue;let m=t[d];if(delete t[d],w.push(d),!x)continue;let v=!!m?.hash&&m.mtimeMs===(await h.stat(s(d))).mtimeMs;await h.move(s(d),s(g)),v&&await a(g,m.hash),await y(d)}for(let d of n.files||[]){let g=t[d.path];if(!g||!u(d.path))continue;let x=await h.stat(s(d.path)).catch(()=>null);!x||x.mtimeMs!==g.mtimeMs||await h.readFile(s(d.path),"utf-8")!==d.content&&(await h.outputFile(s(d.path),d.content),await a(d.path,z(d.content)))}return w}function wt(e){try{let t=JSON.parse(e.body).message;if(t)return t}catch{}return(e.body||e.message||String(e)).slice(0,300)}function yt(e){return e instanceof W?e.body.match(/<title>([^<]*)<\/title>/i)?.[1].trim()||wt(e).replace(/\s+/g," "):e instanceof Error?e.message:String(e)}async function Nn(e,t,n,r,s){let i=new Set,u=await Ce(e),a=new Set(u),y=Object.keys(t).filter(m=>!a.has(m)&&t[m].hash&&ht.test(m));if(y.length===0)return{renamed:0,held:i};let w=new Map;for(let m of u){if(t[m]||!ht.test(m))continue;let v=await h.readFile(b.join(e,m),"utf-8");v.trim()&&w.set(m,z(v))}let d=On(An(y,w,t),a);if(d.length===0)return{renamed:0,held:i};let g=await bt(e),x=0;for(let m of d){let v;try{v=await ee(n,r,"POST","/rename",{from:m.from,to:m.to})}catch(M){let R=M,K=wt(R);if(R.status>=400&&R.status<500)s(`\u26A0\uFE0F Couldn't rename ${m.from} \u2192 ${m.to}: ${K} Saving it as a new file.`);else{for(let _ of m.pairs)i.add(_.to),g[_.to]=`rename from ${_.from} failed: ${K}`;s(`\u274C Error renaming ${m.from} \u2192 ${m.to}: ${K}`)}continue}let k=await Fn(e,t,v,m.pairs);for(let M of[...k,...m.pairs.map(R=>R.to)])delete g[M];let[C,...D]=v.message.split(`
|
|
7
|
+
`);s(`\u{1F500} ${C}`);for(let M of D)s(` ${M}`);x++}return await vt(e,g),{renamed:x,held:i}}var Me="sync-errors.log",Be=new Map;async function bt(e){let t=b.resolve(e),n=Be.get(t);if(n)return n;let r=await Wn(t);return Be.set(t,r),r}async function Wn(e){let t=b.join(e,Me);if(!await h.pathExists(t))return{};let n=await h.readFile(t,"utf-8"),r={};for(let s of n.split(`
|
|
8
|
+
`)){let i=s.indexOf(": ");i>0&&(r[s.slice(0,i)]=s.slice(i+2))}return r}async function vt(e,t){let n=b.join(e,Me),r=Object.entries(t);if(r.length===0){await h.remove(n);return}await h.outputFile(n,r.map(([s,i])=>`${s}: ${i}`).join(`
|
|
9
9
|
`)+`
|
|
10
|
-
`)}async function Nn(e,t,n,r,s){Ee.delete(b.resolve(e)),await h.remove(b.join(e,Ce)),r("\u{1F4E5} Fetching files...");let i=await ee(t,n,"GET","/get_files"),d={},a=0;for(let y of i){let w=b.join(e,y.path);await h.outputFile(w,y.content);let u=(await h.stat(w)).mtimeMs;a++,s?.(a,i.length),d[y.path]={mtimeMs:u,hash:z(y.content)}}return r(`\u2714\uFE0F Synced ${i.length} file(s).`),{fileMap:d,pulled:a}}import Wn from"path";import Gn from"chokidar";var Un=5e3,Hn=/(^|[/\\])(\.DS_Store|Thumbs\.db|desktop\.ini)$/,te=class{viewsDir;onSync;debounceMs;watcher=null;debounceTimer=null;isShuttingDown=!1;dirty=!1;syncInFlight=!1;constructor(t){this.viewsDir=t.viewsDir,this.onSync=t.onSync,this.debounceMs=t.debounceMs??Un}start(){if(this.watcher)return;let t=Wn.join(this.viewsDir,"src");this.watcher=Gn.watch([t],{persistent:!0,ignoreInitial:!0,ignored:Hn}).on("change",()=>this.scheduleSync()).on("add",()=>this.scheduleSync()).on("unlink",()=>this.scheduleSync()).on("addDir",()=>this.scheduleSync()).on("unlinkDir",()=>this.scheduleSync())}setShuttingDown(t){this.isShuttingDown=t}async stop(){this.debounceTimer&&(clearTimeout(this.debounceTimer),this.debounceTimer=null),this.watcher&&(await this.watcher.close(),this.watcher=null)}scheduleSync(){this.isShuttingDown||(this.dirty=!0,this.debounceTimer&&clearTimeout(this.debounceTimer),this.debounceTimer=setTimeout(()=>this.flush(),this.debounceMs))}async flush(){if(this.debounceTimer=null,!(!this.dirty||this.isShuttingDown)){if(this.syncInFlight){this.debounceTimer=setTimeout(()=>this.flush(),this.debounceMs);return}this.dirty=!1,this.syncInFlight=!0;try{await this.onSync()}catch(t){let n=t;console.error("\u274C Sync failed:",n.body||n.message)}finally{this.syncInFlight=!1}}}};import Me from"fs-extra";import Jn from"os";import yt from"path";var wt={localhost:"http://app.sleekcms.test/api",development:"https://app.sleekcms.dev/api",production:"https://app.sleekcms.com/api"};function ne(e){return wt[e]||wt.production}function Le(e){return ne(e).replace(/\/api$/,"")}var zn=yt.join(Jn.homedir(),"SleekCMS"),q=yt.join(zn,"auth.json"),Re=class extends Error{status;body;constructor(t,n,r=""){super(t),this.name="ApiError",this.status=n,this.body=r}};async function fe(e=q){let t=null;try{t=await Me.readJson(e)}catch{return{sites:[],hadLegacyLogin:!1}}return{sites:Array.isArray(t?.sites)?t.sites.filter(r=>r&&typeof r.token=="string"&&typeof r.siteId=="number"):[],hadLegacyLogin:typeof t?.userToken=="string"}}async function bt(e,t=q){await Me.outputJson(t,{sites:e},{spaces:2});try{await Me.chmod(t,384)}catch{}}async function je(e,t=q){let{sites:n}=await fe(t),r=[...n.filter(s=>!(s.env===e.env&&s.siteId===e.siteId)),e];return await bt(r,t),r}async function De(e,t,n=q){let{sites:r}=await fe(n),s=r.filter(i=>!(i.env===e&&i.siteId===t));return await bt(s,n),s}async function _e(e,t,n,r){let s=await fetch(ne(e)+r,{method:n,headers:{Authorization:`Bearer ${t}`}});if(!s.ok){let i=await s.text().catch(()=>"");throw new Re(`${n} ${r} \u2192 ${s.status}: ${i}`,s.status,i)}return s.json()}function qn(e){if(e==null)return"(unknown error)";if(typeof e=="string")return e;if(typeof e!="object")return String(e);let t=e,n=typeof t.file_path=="string"?t.file_path:"",r=typeof t.path=="string"?t.path:typeof t.filename=="string"?t.filename:"",s=n||r,i=t.line!=null?`:${t.line}`:"",d=t.column!=null?`:${t.column}`:"",a=typeof t.message=="string"&&t.message?t.message:typeof t.name=="string"&&t.name?t.name:"",y=n&&r&&r!==n?` (on page ${r})`:"";if(s)return`${s}${i}${d}${y}: ${a||"(no message)"}`;if(a)return a;try{return JSON.stringify(e)}catch{return"(unknown error)"}}async function vt(e,t){let n;try{n=await _e(e,t,"GET","/site_build")}catch{return null}if(!Array.isArray(n)||n.length===0)return null;let r=n[0],s=n.find(d=>typeof d.url=="string"&&d.url.length>0),i=Array.isArray(r.errors)?r.errors.map(qn):[];return i.length>0?{status:"error",mode:"preview",errors:i,url:s?.url||void 0,at:r.updated_at?Date.parse(r.updated_at):void 0}:s?{status:"success",mode:"preview",url:s.url||void 0,at:s.updated_at?Date.parse(s.updated_at):void 0}:null}import{createHash as Xn,randomBytes as Yn}from"crypto";var Kn="urn:sleekcms:grant-type:cli_poll",Vn="SleekCMS Sync",St=e=>e.toString("base64").replace(/=+$/,"").replace(/\+/g,"-").replace(/\//g,"_");function xt(){let e=St(Yn(32)),t=St(Xn("sha256").update(e).digest());return{verifier:e,challenge:t}}function Tt(e){let t=e.replace(/[^A-Za-z0-9]/g,"").slice(0,8).toUpperCase();return`${t.slice(0,4)}-${t.slice(4)}`}async function kt(e,t=fetch){let n={origin:Le(e),authorizeEndpoint:`${ne(e)}/oauth/authorize`};try{let r=await t(`${Le(e)}/.well-known/oauth-authorization-server`);if(!r.ok)return n;let s=await r.json();if(typeof s.issuer=="string"&&typeof s.authorization_endpoint=="string")return{origin:s.issuer.replace(/\/+$/,""),authorizeEndpoint:s.authorization_endpoint}}catch{}return n}function $t(e,t){let n=new URLSearchParams({response_type:"code",client_name:Vn,redirect_uri:`${e.origin}/oauth/cli-done`,code_challenge:t,code_challenge_method:"S256",resource:`${e.origin}/mcp/`});return`${e.authorizeEndpoint}?${n.toString()}`}var X=class extends Error{constructor(t){super(t),this.name="SignInError"}},Zn=e=>new Promise(t=>setTimeout(t,e));async function Et(e,t,n={}){let{timeoutMs:r=10*6e4,signal:s,fetchImpl:i=fetch,sleep:d=Zn}=n,a=n.intervalMs??2e3,y=Date.now()+r,w=`${ne(e)}/oauth/token`;for(;;){if(s?.aborted)throw new X("Cancelled.");let u=null;try{u=await i(w,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({grant_type:Kn,code_verifier:t})})}catch{u=null}if(u){let f=await u.json().catch(()=>({}));if(u.ok&&f.access_token)return f.access_token;if(u.status===429)a=Math.min(a*2,3e4);else if(f.error!=="authorization_pending")throw new X(f.error==="invalid_grant"?"That approval can no longer be used. Add the site again.":`Sign-in failed: ${f.error||`HTTP ${u.status}`}`)}if(Date.now()+a>y)throw new X("Timed out waiting for approval in the browser.");await d(a)}}function Qn(e){let t=e.split(".")[1];if(!t)throw new X("The server returned a token this version can't read.");return JSON.parse(Buffer.from(t,"base64url").toString("utf8"))}async function Bt(e,t){let n=Qn(t),r=await _e(e,t,"GET","/mcp/get_site");return{env:e,siteId:n.sid,slug:n.slug,name:r.name,org:{name:r.org},role:r.role||void 0,token:t,addedAt:Date.now()}}var Pt=500;function rr(){return new Date().toTimeString().slice(0,8)}function sr(e){let t=e;if(t.body){try{let n=JSON.parse(t.body);if(n&&typeof n.message=="string")return n.message;if(n&&typeof n.error=="string")return n.error}catch{}return t.body}return t.message}function Ct(e){return e instanceof N&&e.status===401}var ir=e=>({id:e.siteId,name:e.name,slug:e.slug});async function Mt(e){e&&(await re.remove(Ae.join(e,"src")),await re.remove(Ae.join(e,".cache","state.json")))}var pe=class extends er{env="production";saved=[];synced=new Map;connecting=null;message;messageIsError=!1;agentMd;buildScript;configPath;isShuttingDown=!1;basePath;pollOptions={};openBrowser=ge;constructor(t,n,r=q){super(),this.agentMd=t,this.buildScript=n,this.configPath=r}changed(){this.emit("change")}flash(t,n=!1){this.message=t,this.messageIsError=n,this.changed()}appendLog(t,n){let r=this.synced.get(t);r&&(r.logs.push({time:rr(),text:n}),r.logs.length>Pt&&r.logs.splice(0,r.logs.length-Pt),this.changed())}siteCount(){return this.synced.size}syncingCount(){let t=0;for(let n of this.synced.values())n.status==="syncing"&&t++;return t}watchingCount(){let t=0;for(let n of this.synced.values())(n.status==="watching"||n.status==="syncing")&&t++;return t}async init(t){this.env=t;let{sites:n,hadLegacyLogin:r}=await fe(this.configPath);this.saved=n.filter(s=>s.env===t),r&&this.flash("SleekCMS Sync now connects one site at a time \u2014 press a to add a site in your browser."),this.changed()}isSynced(t){return this.synced.has(t)}async connectSite(){if(this.connecting)return;let{verifier:t,challenge:n}=xt(),r=new AbortController;this.connecting={code:Tt(n),url:"",abort:r},this.changed();let s=$t(await kt(this.env),n);if(r.signal.aborted){this.connecting=null,this.changed();return}this.connecting.url=s,this.message=void 0,this.changed(),this.openBrowser(s)||this.flash("Couldn't open a browser \u2014 open the address shown to continue.",!0);let i;try{let a=await Et(this.env,t,{...this.pollOptions,signal:r.signal});i=await Bt(this.env,a)}catch(a){this.connecting=null,r.signal.aborted?this.changed():this.flash(sr(a),!0);return}this.connecting=null;let d=await je(i,this.configPath);this.saved=d.filter(a=>a.env===this.env),this.flash(`Added "${i.name}".`),this.synced.has(i.siteId)||await this.startSite(i)}cancelConnect(){this.connecting?.abort.abort()}async startSite(t){let n=ir(t);if(this.synced.has(n.id))return;let r={org:t.org,site:n,siteToken:t.token,role:t.role,viewsDir:"",status:"syncing",logs:[]};this.synced.set(n.id,r),this.appendLog(n.id,"\u2795 added \u2014 pulling files"),this.changed();try{let s=await Be({token:t.token,env:this.env,path:this.basePath,agentMd:this.agentMd,buildScript:this.buildScript,log:i=>this.appendLog(n.id,i),onProgress:(i,d)=>{r.progress={done:i,total:d},this.changed()}});this.noteRole(n.id,s.site.role),r.viewsDir=s.viewsDir,r.progress=void 0,r.status="watching",r.lastSyncedAt=Date.now(),r.lastMsg=`pulled ${s.pulled} file(s)`,r.watcher=new te({viewsDir:s.viewsDir,onSync:()=>this.runSync(n.id)}),r.watcher.start(),this.watchBuildStatus(n.id),this.seedLastBuild(n.id),this.appendLog(n.id,"\u{1F440} watching for changes")}catch(s){if(Ct(s))return void await this.revoke(n.id);let i=s;r.status="error",r.progress=void 0,r.lastMsg=i.body||i.message,this.appendLog(n.id,`\u274C ${i.body||i.message}`)}this.changed()}noteRole(t,n){let r=this.synced.get(t);if(!n||!r||r.role===n)return;r.role=n;let s=this.saved.find(i=>i.siteId===t);s&&(s.role=n,je({...s},this.configPath).catch(()=>{}))}async revoke(t){let n=this.synced.get(t),r=n?.site.name??this.saved.find(d=>d.siteId===t)?.name??"this site";n&&(n.watcher?.setShuttingDown(!0),await n.watcher?.stop(),await n.buildWatcher?.close(),this.synced.delete(t));let s=await De(this.env,t,this.configPath);this.saved=s.filter(d=>d.env===this.env);let i=n?.viewsDir?` Local files kept in ${n.viewsDir}.`:"";this.flash(`Access to "${r}" was revoked \u2014 press a to add it again.${i}`,!0)}async forgetSite(t){if(this.synced.has(t)){this.flash("Disconnect it first (d), then press x to forget it.",!0);return}let n=this.saved.find(s=>s.siteId===t)?.name??"the site",r=await De(this.env,t,this.configPath);this.saved=r.filter(s=>s.env===this.env),this.flash(`Forgot "${n}". To revoke its access everywhere, log out of all sessions in SleekCMS.`)}async removeSite(t){let n=this.synced.get(t);n&&(n.watcher?.setShuttingDown(!0),await n.watcher?.stop(),await n.buildWatcher?.close(),await Mt(n.viewsDir),this.synced.delete(t),this.changed())}buildSite(t,n=!1){let r=this.synced.get(t);if(!r||!r.viewsDir)return;if(r.role&&r.role!=="ADMIN"){this.flash(`Building and publishing are for site ADMINs \u2014 you're ${r.role} on "${r.site.name}".`,!0);return}let s=n?"live":"preview";this.writeBuildFile(r.viewsDir,{status:"building",mode:s,stage:"queued",url:r.lastBuild?.url,at:Date.now()}),tr("node",n?["build.mjs","--publish"]:["build.mjs"],{cwd:r.viewsDir,stdio:"ignore"}).on("error",a=>{this.writeBuildFile(r.viewsDir,{status:"error",mode:s,errors:[a.message],url:r.lastBuild?.url,at:Date.now()}),this.appendLog(t,`\u274C build: ${a.message}`)}),this.appendLog(t,n?"\u{1F680} publishing\u2026":"\u{1F528} building\u2026")}buildFile(t){return Ae.join(t,".cache","build.json")}async readBuildFile(t){try{let n=await re.readJson(this.buildFile(t));return n&&n.status?n:null}catch{return null}}async writeBuildFile(t,n){let r=this.buildFile(t),s=`${r}.tmp`;try{await re.outputJson(s,n),await re.rename(s,r)}catch{}}watchBuildStatus(t){let n=this.synced.get(t);!n||!n.viewsDir||(n.buildWatcher=nr.watch(this.buildFile(n.viewsDir),{ignoreInitial:!1}).on("add",()=>{this.readBuildStatus(t)}).on("change",()=>{this.readBuildStatus(t)}))}async readBuildStatus(t){let n=this.synced.get(t);if(!n)return;let r=await this.readBuildFile(n.viewsDir);if(!r)return;let s=n.lastBuild?.url;n.lastBuild={status:r.status,mode:r.mode??"preview",url:r.url??(r.status==="success"?void 0:s),errors:r.errors,stage:r.stage,at:r.at},this.changed()}async seedLastBuild(t){let n=this.synced.get(t);if(!n)return;let r=await vt(this.env,n.siteToken);if(!r)return;let s=this.synced.get(t);if(!s)return;let i=await this.readBuildFile(s.viewsDir);i?.at!=null&&(r.at==null||i.at>=r.at)||await this.writeBuildFile(s.viewsDir,r)}async refetch(t){await this.runSync(t,{flush:!0})}async runSync(t,n={}){let r=this.synced.get(t);if(!(!r||!r.viewsDir||r.status==="paused")){r.status="syncing",this.changed();try{let s=await Be({token:r.siteToken,env:this.env,viewsDir:r.viewsDir,agentMd:this.agentMd,buildScript:this.buildScript,flush:n.flush,log:i=>this.appendLog(t,i),onProgress:(i,d)=>{r.progress={done:i,total:d},this.changed()}});if(this.noteRole(t,s.site.role),r.progress=void 0,r.status="watching",r.lastSyncedAt=Date.now(),n.flush)r.lastMsg=`re-fetched ${s.pulled} file(s)`;else{let i=[s.renamed>0?`renamed ${s.renamed}`:"",s.pushed>0?`pushed ${s.pushed} file(s)`:""].filter(Boolean);r.lastMsg=i.length>0?i.join(", "):"up to date"}}catch(s){if(Ct(s))return void await this.revoke(t);let i=s;r.status="error",r.progress=void 0,r.lastMsg=i.body||i.message,this.appendLog(t,`\u274C ${i.body||i.message}`)}this.changed()}}async togglePause(t){let n=this.synced.get(t);!n||!n.viewsDir||(n.status==="paused"?(n.watcher=new te({viewsDir:n.viewsDir,onSync:()=>this.runSync(t)}),n.watcher.start(),n.status="watching",this.appendLog(t,"\u25B6 resumed \u2014 watching for changes")):(n.watcher?.setShuttingDown(!0),await n.watcher?.stop(),n.watcher=void 0,n.progress=void 0,n.status="paused",this.appendLog(t,"\u23F8 paused")),this.changed())}async cleanupAll(){this.cancelConnect();for(let t of this.synced.values())t.watcher?.setShuttingDown(!0),await t.watcher?.stop(),await t.buildWatcher?.close(),await Mt(t.viewsDir)}async exit(){this.isShuttingDown||(this.isShuttingDown=!0,await this.cleanupAll(),this.emit("exit"))}};import{useInput as xr}from"ink";import{Fragment as lr,useEffect as se,useMemo as ur,useRef as dr,useState as L}from"react";import{Box as p,Text as l,useInput as gr,useStdout as fr}from"ink";import pr from"ink-text-input";import{createRequire as or}from"node:module";var ar=or(import.meta.url),he=ar("../package.json").version;import cr from"node:os";function Oe(e){if(!e)return"\u2014";let t=cr.homedir();return e.startsWith(t)?"~"+e.slice(t.length):e}function Ie(e){if(!e)return"\u2014";let t=Math.floor((Date.now()-e)/1e3);if(t<5)return"just now";if(t<60)return`${t}s ago`;let n=Math.floor(t/60);if(n<60)return`${n}m ago`;let r=Math.floor(n/60);return r<24?`${r}h ago`:`${Math.floor(r/24)}d ago`}function we(e){return!e||e.total<=0?null:Math.round(e.done/e.total*100)}var me=["\u280B","\u2819","\u2839","\u2838","\u283C","\u2834","\u2826","\u2827","\u2807","\u280F"];function Fe(e){return me[(e%me.length+me.length)%me.length]}function ye(e){switch(e){case"syncing":return{dot:"\u25CF",color:"green",label:"syncing"};case"watching":return{dot:"\u25CB",color:"gray",label:"watching"};case"paused":return{dot:"\u2016",color:"yellow",label:"paused"};case"error":return{dot:"\u2717",color:"red",label:"error"}}}function Rt(e){if(!e)return{dot:"\xB7",color:"gray",label:"\u2014"};switch(e.status){case"building":return{dot:"\u2234",color:"yellow",label:"building"};case"error":return{dot:"\u2717",color:"red",label:"failed"};case"success":return{dot:"\u2713",color:"green",label:e.mode==="live"?"live":"preview"}}}import{Fragment as Ne,jsx as o,jsxs as g}from"react/jsx-runtime";var hr=(e,t)=>t<=0?0:Math.max(0,Math.min(e,t-1)),mr=16,wr=5,yr="greenBright",Lt="blueBright",jt="magentaBright";function Dt({k:e,bg:t=yr}){return o(l,{color:"black",backgroundColor:t,children:` ${e} `})}function O({k:e,label:t,width:n=16,bg:r}){return g(p,{width:n,children:[o(Dt,{k:e,bg:r}),o(l,{color:"gray",children:` ${t}`})]})}function E({k:e,label:t,bg:n}){return g(p,{marginBottom:1,children:[o(p,{width:9,children:o(Dt,{k:e,bg:n})}),o(l,{color:"gray",children:t})]})}function br({launchers:e}){return g(p,{flexDirection:"column",borderStyle:"round",borderColor:"green",paddingX:2,paddingY:1,width:66,children:[g(p,{justifyContent:"space-between",children:[o(l,{color:"green",bold:!0,children:"KEYBOARD SHORTCUTS"}),o(l,{color:"gray",children:"? or esc to close"})]}),g(p,{marginTop:1,children:[g(p,{flexDirection:"column",width:34,children:[o(l,{color:"cyan",bold:!0,children:"Navigate"}),o(E,{k:"\u2191\u2193",label:"move selection"}),o(E,{k:"tab",label:"focus logs \xB7 \u2191\u2193 scroll"}),o(E,{k:"esc",label:"clear filter / back"}),o(p,{marginTop:1,children:o(l,{color:"cyan",bold:!0,children:"Global"})}),o(E,{k:"/",label:"search sites"}),o(E,{k:"a",label:"add a site (browser)"}),o(E,{k:"?",label:"toggle this help",bg:Lt}),o(E,{k:"ctrl+c",label:"quit"})]}),g(p,{flexDirection:"column",flexGrow:1,children:[o(l,{color:"cyan",bold:!0,children:"Site actions"}),o(E,{k:"s",label:"start sync"}),o(E,{k:"d",label:"disconnect"}),o(E,{k:"x",label:"forget site"}),o(E,{k:"p",label:"pause / resume sync"}),o(E,{k:"b",label:"build preview"}),o(E,{k:"B",label:"publish live"}),o(E,{k:"o",label:"open preview url"}),e.map((t,n)=>o(E,{k:String(n+1),label:`open in ${t.label}`,bg:jt},t.key))]})]})]})}function vr({percent:e,width:t=28}){let n=Math.round(Math.max(0,Math.min(100,e))/100*t);return g(l,{children:[o(l,{color:"green",children:"\u2588".repeat(n)}),o(l,{color:"gray",children:"\u2591".repeat(Math.max(0,t-n))})]})}function Sr({frame:e,width:t=24,seg:n=6}){let r=t+n,s=e%r-n,i="";for(let d=0;d<t;d++)i+=d>=s&&d<s+n?"\u25AE":"\u25AF";return o(l,{color:"yellow",children:i})}function _t({controller:e}){let{stdout:t}=fr(),n=ur(()=>at(),[]),[,r]=L(0),[s,i]=L(()=>new Date),[d,a]=L(null),[y,w]=L(""),[u,f]=L(!1),[x,m]=L(0),[v,k]=L(!1),[A,W]=L(!1),[C,M]=L("list"),[K,j]=L(0),Je=()=>r(c=>c+1);se(()=>(e.on("change",Je),()=>{e.off("change",Je)}),[e]),se(()=>{let c,S=()=>{i(new Date),c=setTimeout(S,6e4-Date.now()%6e4)};return c=setTimeout(S,6e4-Date.now()%6e4),()=>clearTimeout(c)},[]);let V=e.saved.map(c=>({org:c.org,site:{id:c.siteId,name:c.name,slug:c.slug},saved:c,entry:e.synced.get(c.siteId)})).sort((c,S)=>(c.entry?0:1)-(S.entry?0:1)),G=e.connecting,I=y.trim().toLowerCase(),P=I?V.filter(c=>c.site.name.toLowerCase().includes(I)||(c.org.name||"").toLowerCase().includes(I)):V,U=P.findIndex(c=>c.site.id===d);U<0&&(U=0);let $=P[U],T=$?.entry,oe=Math.max(3,(t?.rows??30)-26),F=T?.logs??[],xe=Math.max(0,F.length-oe),Kt=Math.min(K,xe),ae=F.length-Kt,ze=F.slice(Math.max(0,ae-oe),ae),Vt=$?.site.id;se(()=>j(0),[Vt]);let D=T?.lastBuild,Te=D?.status==="building",ke=Rt(D),ce=Te||!!G;if(!ce){for(let c of e.synced.values())if(c.status==="syncing"){ce=!0;break}}se(()=>{if(!ce)return;let c=setInterval(()=>m(S=>S+1),120);return()=>clearInterval(c)},[ce]);let qe=P.findIndex(c=>!c.entry),H=Math.max(wr,(t?.rows??30)-mr),Z=P.length,le=Z>H,Zt=Math.floor(H/2),Q=le?Math.max(0,Math.min(U-Zt,Z-H)):0,$e=Math.min(Z,Q+H),Qt=P.slice(Q,$e),Xe=dr(!1);se(()=>{Xe.current||(Xe.current=!0,e.saved.length===0&&!e.connecting&&e.connectSite())},[e]);let ue=c=>{if(P.length===0)return;let S=hr(U+c,P.length);a(P[S].site.id)},en=()=>{if($){if($.entry){e.flash("Already syncing \u2014 press d to disconnect.");return}e.startSite($.saved)}},tn=()=>{$?.entry&&e.removeSite($.site.id)},nn=()=>{$&&e.forgetSite($.site.id)},rn=c=>{if(!T){e.flash("Press s to start syncing this site first.",!0);return}W(!0);let S=c.run(T.viewsDir);e.flash(S?`Opened ${c.label} at ${Oe(T.viewsDir)}`:`Couldn't open ${c.label}.`,!S)},sn=()=>{if(!T){e.flash("Press s to start syncing this site first.",!0);return}e.togglePause(T.site.id)},Ye=c=>{if(!T){e.flash("Press s to start syncing this site first.",!0);return}e.buildSite(T.site.id,c),e.flash(c?"Publishing \u2014 making this live\u2026":"Building preview\u2026")},on=()=>{let c=T?.lastBuild?.url;if(!c){e.flash("No build URL yet \u2014 press b to build.",!0);return}e.flash(ge(c)?`Opening ${c}`:"Couldn't open the browser.",!1)};gr((c,S)=>{if(v){(c==="?"||S.escape)&&k(!1);return}if(G&&S.escape)return void e.cancelConnect();if(u){S.escape?(f(!1),w(""),a(null)):S.return?f(!1):S.upArrow?ue(-1):S.downArrow&&ue(1);return}if(S.tab){C==="logs"?M("list"):T&&M("logs"),j(0);return}if(C==="logs"){if(S.escape){M("list"),j(0);return}if(S.upArrow)return void j(_=>Math.min(_+1,xe));if(S.downArrow)return void j(_=>Math.max(0,_-1))}if(S.escape){w(""),a(null);return}if(c==="?")return void k(!0);if(c==="/")return M("list"),void f(!0);if(c==="a"||c==="n")return void e.connectSite();if(S.upArrow)return ue(-1);if(S.downArrow)return ue(1);if(c==="s")return en();if(c==="d")return tn();if(c==="x")return nn();if(c==="p")return sn();if(c==="b")return Ye(!1);if(c==="B")return Ye(!0);if(c==="o")return on();if(c>="1"&&c<="9"){let _=n[parseInt(c,10)-1];if(_)return rn(_)}});let Ke=g(p,{justifyContent:"space-between",paddingX:1,children:[g(p,{children:[o(l,{color:"green",bold:!0,children:"\u25C6 SleekCMS"}),o(l,{color:"gray",children:` v${he} \u2502 `}),o(l,{children:e.env==="production"?"Sync":`Sync \xB7 ${e.env}`})]}),g(p,{children:[g(l,{color:"gray",children:[I?`${P.length}/${V.length}`:V.length," sites \xB7 "]}),g(l,{color:"green",children:[e.watchingCount()," watching"]}),o(l,{color:"gray",children:` \xB7 ${s.toTimeString().slice(0,5)}`})]})]});return v?g(p,{flexDirection:"column",children:[Ke,o(p,{marginTop:1,justifyContent:"center",children:o(br,{launchers:n})})]}):g(p,{flexDirection:"column",children:[Ke,g(p,{children:[g(p,{flexDirection:"column",borderStyle:"round",borderColor:"green",width:38,paddingX:1,children:[g(p,{justifyContent:"space-between",children:[o(l,{color:"green",bold:!0,children:"SITE WORKSPACES"}),V.length>H&&!u&&!I?o(l,{color:"gray",children:"/ filter"}):null]}),u?g(p,{children:[o(l,{color:"cyan",children:"/ "}),o(pr,{value:y,onChange:w,onSubmit:()=>f(!1),placeholder:"type to filter\u2026"})]}):I?o(l,{color:"gray",children:`/ ${y} \xB7 ${P.length} match${P.length===1?"":"es"}`}):null,o(p,{flexDirection:"column",marginTop:1,minHeight:le?H+3:void 0,children:P.length===0?o(l,{color:"gray",children:I?`no matches for \u201C${y}\u201D`:"no sites yet \u2014 press a to add one"}):g(Ne,{children:[le?o(l,{color:"gray",children:Q>0?` \u2191 ${Q} more`:" "}):null,Qt.map((c,S)=>{let _=Q+S,Ve=c.entry?ye(c.entry.status):{dot:"\xB7",color:"gray",label:"idle"},Ze=we(c.entry?.progress),an=c.entry?c.entry.status==="syncing"&&Ze!=null?`${Ze}%`:c.entry.status==="paused"?"paused":"--":"",de=_===U,cn=_===qe&&qe>0;return g(lr,{children:[cn?o(l,{color:"gray",children:"\u2500".repeat(34)}):null,g(p,{justifyContent:"space-between",children:[g(p,{children:[o(l,{color:de?"green":"gray",children:de?"\u276F ":" "}),g(l,{color:c.entry?.status==="watching"||c.entry?.status==="syncing"?"green":Ve.color,children:[c.entry?.status==="syncing"?Fe(x):c.entry?.status==="watching"?"\u25CF":Ve.dot," "]}),o(l,{color:de?"white":c.entry?void 0:"gray",bold:de,children:c.site.name})]}),o(l,{color:c.entry?.status==="syncing"?"green":"gray",children:an})]})]},c.site.id)}),le?o(l,{color:"gray",children:$e<Z?` \u2193 ${Z-$e} more`:" "}):null]})}),o(p,{marginTop:1,children:o(l,{color:"gray",children:"+ add site \xB7 a"})})]}),g(p,{flexDirection:"column",flexGrow:1,marginLeft:1,children:[o(p,{flexDirection:"column",borderStyle:"round",borderColor:"gray",paddingX:1,children:$?g(Ne,{children:[g(l,{children:[$.org.name?g(l,{color:"gray",children:[$.org.name," / "]}):null,o(l,{color:"cyan",bold:!0,children:$.site.name})]}),g(p,{marginTop:1,children:[o(l,{color:"gray",children:"status "}),T?o(l,{color:ye(T.status).color,children:`: ${ye(T.status).label}`}):o(l,{color:"gray",children:": idle \u2014 not syncing (press s to start)"})]}),g(p,{children:[o(l,{color:"gray",children:"role "}),o(l,{children:T?.role??$.saved.role??"\u2014"}),(T?.role??$.saved.role??"ADMIN")!=="ADMIN"?o(l,{color:"gray",children:" \xB7 build & publish need ADMIN"}):null]}),g(p,{children:[o(l,{color:"gray",children:"path "}),o(l,{wrap:"truncate-end",children:T?.viewsDir?Oe(T.viewsDir):"\u2014"})]}),g(p,{children:[o(l,{color:"gray",children:"synced "}),o(l,{children:Ie(T?.lastSyncedAt)}),o(l,{color:"gray",children:" progress "}),o(l,{color:T?.status==="syncing"?"green":"gray",children:T?.status==="syncing"?`${we(T.progress)??0}%`:"\u2014"})]}),o(p,{marginTop:1,children:o(vr,{percent:T?T.status==="syncing"?we(T.progress)??0:T.status==="watching"?100:0:0})}),T?g(Ne,{children:[g(p,{marginTop:1,children:[o(l,{color:"gray",children:"build "}),o(l,{color:ke.color,children:`${ke.dot} ${ke.label}`}),o(l,{color:"gray",children:" last "}),o(l,{children:Te?"in progress":Ie(D?.at)})]}),g(p,{children:[o(l,{color:"gray",children:"url "}),D?.url?o(l,{color:"cyan",wrap:"truncate-end",children:D.url}):o(l,{color:"gray",children:"\u2014"})]}),D?.status==="error"&&D.errors?.length?g(p,{children:[o(l,{color:"red",children:"\u258C "}),o(l,{color:"red",wrap:"truncate-end",children:`\u2717 ${D.errors[0]}`})]}):null,Te?g(p,{children:[o(Sr,{frame:x}),o(l,{color:"gray",children:` ${D?.stage??"building"}`})]}):null]}):null]}):o(l,{color:"gray",children:"No site selected."})}),G?g(p,{flexDirection:"column",borderStyle:"round",borderColor:"blue",paddingX:1,marginTop:1,flexGrow:1,children:[o(l,{color:"cyan",bold:!0,children:"ADD A SITE"}),o(l,{children:"Approve it in your browser: pick a site, or create one."}),g(p,{marginTop:1,children:[o(l,{color:"gray",children:"Check the page shows "}),o(l,{color:"cyan",bold:!0,children:G.code})]}),o(l,{color:"gray",children:"Only approve a page that shows this code."}),g(p,{marginTop:1,children:[g(l,{color:"yellow",children:[Fe(x)," "]}),o(l,{color:"gray",children:"waiting for approval \xB7 esc to cancel"})]})]}):g(p,{flexDirection:"column",borderStyle:"round",borderColor:C==="logs"?"green":"gray",paddingX:1,marginTop:1,height:oe+4,flexShrink:0,overflowY:"hidden",children:[g(p,{justifyContent:"space-between",children:[g(l,{children:[o(l,{color:"cyan",bold:!0,children:"LOGS \xB7 "}),o(l,{color:"white",children:$?.site.name??"\u2014"})]}),T&&F.length>0?C==="logs"?o(l,{color:"green",children:`\u2191\u2193 scroll \xB7 esc back${xe>0?` \xB7 ${ae}/${F.length}`:""}`}):F.length>oe?o(l,{color:"gray",children:"tab to scroll"}):null:null]}),o(p,{flexDirection:"column",marginTop:1,children:!T||F.length===0?o(l,{color:"gray",children:"no activity yet"}):ze.map((c,S)=>g(l,{wrap:"truncate-end",children:[g(l,{color:"gray",children:[c.time," "]}),c.text]},ae-ze.length+S))})]})]})]}),G?.url?g(p,{flexDirection:"column",paddingX:1,marginTop:1,children:[o(l,{color:"gray",children:"No browser? Open this address on any device:"}),o(l,{color:"cyan",children:G.url})]}):null,e.message?o(p,{paddingX:1,children:g(l,{color:e.messageIsError?"red":"gray",wrap:"truncate-end",children:[e.messageIsError?"\u2717 ":"",e.message]})}):null,g(p,{flexDirection:"column",marginTop:1,children:[g(p,{paddingX:1,justifyContent:"space-between",children:[g(p,{flexWrap:"wrap",children:[o(O,{k:"\u2191\u2193",label:"select"}),o(O,{k:"/",label:"search"}),o(O,{k:"s",label:"start sync"}),o(O,{k:"d",label:"disconnect"}),o(O,{k:"a",label:"add site"}),o(O,{k:"ctrl+c",label:"quit",width:14})]}),o(O,{k:"?",label:"all keys",width:12,bg:Lt})]}),!A&&n.length>0?o(p,{paddingX:1,marginTop:1,flexWrap:"wrap",children:n.map((c,S)=>o(O,{k:String(S+1),label:c.label,bg:jt},c.key))}):null]})]})}import{jsx as Tr}from"react/jsx-runtime";function At({controller:e}){return xr((t,n)=>{n.ctrl&&t==="c"&&e.exit()}),Tr(_t,{controller:e})}import*as Gt from"os";import*as Ge from"util";var kr={production:"https://9Q849fBKdCJcK6j8M5SHpWjt@s2790579.us-west-2a.betterstackdata.com/2790579",development:"https://oj8Je2g5rZhXAv6NZ3CxYHFb@s2790583.us-west-2a.betterstackdata.com/2790583"},ie,Ot,$r=()=>Ot??=import("@sentry/node").then(e=>(e.init({dsn:ie.dsn,environment:ie.environment,serverName:ie.service,initialScope:{tags:{service:ie.service}},defaultIntegrations:!1,tracesSampleRate:0,maxBreadcrumbs:0,normalizeDepth:3}),e)).catch(e=>{console.warn("[observability] provider failed to load:",e?.message)}),It=({service:e,environment:t})=>{let n=kr[t];return n&&(ie={dsn:n,environment:t,service:e}),!!n},Ft=(e,t)=>{$r().then(n=>n?.captureException(e,t&&{extra:t}))},Nt=async e=>{await(await Ot)?.flush(e)};var Br=10*6e4,Pr=3,Cr=30,Mr=1e3;var Se=!1,We=!1;var Ut=console.error.bind(console),Y=new Map,Wt=new WeakSet,be={start:0,sent:0},Rr=()=>process.env.ENV||process.env.AWS_LAMBDA_FUNCTION_NAME?.match(/-(production|development)$/)?.[1],Ht=({service:e,environment:t=Rr(),quiet:n=!1})=>{Se=!!t&&It({service:e,environment:t}),n||console.log(`[observability] ${e}: ${Se?`reporting errors (${t})`:"off"}`)},Jt=(e,t)=>{if(!(!Se||We)){We=!0;try{let n=_r(e);if(Wt.has(n))return;Wt.add(n);let r=Lr(jr(n));if(r===!1)return;Ft(Ar(n),r?{...t,suppressed:r}:t)}catch(n){Ut("[observability] capture failed:",n?.message)}finally{We=!1}}},zt=async(e=2e3)=>{if(Se)try{await Nt(e)}catch(t){Ut("[observability] flush failed:",t?.message)}};var Lr=e=>{let t=Date.now();t-be.start>=6e4&&(be={start:t,sent:0});let n=Y.get(e),r=0;return(!n||t-n.start>=Br)&&(r=n?.dropped??0,n={start:t,sent:0,dropped:0},Y.delete(e),Y.set(e,n),Y.size>Mr&&Y.delete(Y.keys().next().value)),n.sent>=Pr||be.sent>=Cr?(n.dropped++,!1):(n.sent++,be.sent++,r)},jr=e=>{let t=e.stack?.split(`
|
|
11
|
-
`).find(n=>/^\s*at /.test(n)&&!/node_modules|node:|[\\/]observability[\\/]/.test(n));return`${e.name}|${
|
|
12
|
-
${e.stack}`.includes(ve))return e;let t=new Error(e.message.split(ve).join("~"));return t.name=e.name,t.stack=e.stack?.split(ve).join("~"),t};import{jsx as
|
|
10
|
+
`)}async function Gn(e,t,n,r,s){Be.delete(b.resolve(e)),await h.remove(b.join(e,Me)),r("\u{1F4E5} Fetching files...");let i=await ee(t,n,"GET","/get_files"),u={},a=0;for(let y of i){let w=b.join(e,y.path);await h.outputFile(w,y.content);let d=(await h.stat(w)).mtimeMs;a++,s?.(a,i.length),u[y.path]={mtimeMs:d,hash:z(y.content)}}return r(`\u2714\uFE0F Synced ${i.length} file(s).`),{fileMap:u,pulled:a}}import Un from"path";import Hn from"chokidar";var Jn=5e3,zn=/(^|[/\\])(\.DS_Store|Thumbs\.db|desktop\.ini)$/,te=class{viewsDir;onSync;debounceMs;watcher=null;debounceTimer=null;isShuttingDown=!1;dirty=!1;syncInFlight=!1;constructor(t){this.viewsDir=t.viewsDir,this.onSync=t.onSync,this.debounceMs=t.debounceMs??Jn}start(){if(this.watcher)return;let t=Un.join(this.viewsDir,"src");this.watcher=Hn.watch([t],{persistent:!0,ignoreInitial:!0,ignored:zn}).on("change",()=>this.scheduleSync()).on("add",()=>this.scheduleSync()).on("unlink",()=>this.scheduleSync()).on("addDir",()=>this.scheduleSync()).on("unlinkDir",()=>this.scheduleSync())}setShuttingDown(t){this.isShuttingDown=t}async stop(){this.debounceTimer&&(clearTimeout(this.debounceTimer),this.debounceTimer=null),this.watcher&&(await this.watcher.close(),this.watcher=null)}scheduleSync(){this.isShuttingDown||(this.dirty=!0,this.debounceTimer&&clearTimeout(this.debounceTimer),this.debounceTimer=setTimeout(()=>this.flush(),this.debounceMs))}async flush(){if(this.debounceTimer=null,!(!this.dirty||this.isShuttingDown)){if(this.syncInFlight){this.debounceTimer=setTimeout(()=>this.flush(),this.debounceMs);return}this.dirty=!1,this.syncInFlight=!0;try{await this.onSync()}catch(t){let n=t;console.error("\u274C Sync failed:",n.body||n.message)}finally{this.syncInFlight=!1}}}};import Re from"fs-extra";import qn from"os";import xt from"path";var St={localhost:"http://app.sleekcms.test/api",development:"https://app.sleekcms.dev/api",production:"https://app.sleekcms.com/api"};function ne(e){return St[e]||St.production}function je(e){return ne(e).replace(/\/api$/,"")}var Xn=xt.join(qn.homedir(),"SleekCMS"),q=xt.join(Xn,"auth.json"),Le=class extends Error{status;body;constructor(t,n,r=""){super(t),this.name="ApiError",this.status=n,this.body=r}};async function pe(e=q){let t=null;try{t=await Re.readJson(e)}catch{return{sites:[],hadLegacyLogin:!1}}return{sites:Array.isArray(t?.sites)?t.sites.filter(r=>r&&typeof r.token=="string"&&typeof r.siteId=="number"):[],hadLegacyLogin:typeof t?.userToken=="string"}}async function Tt(e,t=q){await Re.outputJson(t,{sites:e},{spaces:2});try{await Re.chmod(t,384)}catch{}}async function De(e,t=q){let{sites:n}=await pe(t),r=[...n.filter(s=>!(s.env===e.env&&s.siteId===e.siteId)),e];return await Tt(r,t),r}async function _e(e,t,n=q){let{sites:r}=await pe(n),s=r.filter(i=>!(i.env===e&&i.siteId===t));return await Tt(s,n),s}async function Ae(e,t,n,r){let s=await fetch(ne(e)+r,{method:n,headers:{Authorization:`Bearer ${t}`}});if(!s.ok){let i=await s.text().catch(()=>"");throw new Le(`${n} ${r} \u2192 ${s.status}: ${i}`,s.status,i)}return s.json()}function Yn(e){if(e==null)return"(unknown error)";if(typeof e=="string")return e;if(typeof e!="object")return String(e);let t=e,n=typeof t.file_path=="string"?t.file_path:"",r=typeof t.path=="string"?t.path:typeof t.filename=="string"?t.filename:"",s=n||r,i=t.line!=null?`:${t.line}`:"",u=t.column!=null?`:${t.column}`:"",a=typeof t.message=="string"&&t.message?t.message:typeof t.name=="string"&&t.name?t.name:"",y=n&&r&&r!==n?` (on page ${r})`:"";if(s)return`${s}${i}${u}${y}: ${a||"(no message)"}`;if(a)return a;try{return JSON.stringify(e)}catch{return"(unknown error)"}}async function kt(e,t){let n;try{n=await Ae(e,t,"GET","/site_build")}catch{return null}if(!Array.isArray(n)||n.length===0)return null;let r=n[0],s=n.find(u=>typeof u.url=="string"&&u.url.length>0),i=Array.isArray(r.errors)?r.errors.map(Yn):[];return i.length>0?{status:"error",mode:"preview",errors:i,url:s?.url||void 0,at:r.updated_at?Date.parse(r.updated_at):void 0}:s?{status:"success",mode:"preview",url:s.url||void 0,at:s.updated_at?Date.parse(s.updated_at):void 0}:null}import{createHash as Kn,randomBytes as Vn}from"crypto";var Zn="urn:sleekcms:grant-type:cli_poll",Qn="SleekCMS Sync",$t=e=>e.toString("base64").replace(/=+$/,"").replace(/\+/g,"-").replace(/\//g,"_");function Et(){let e=$t(Vn(32)),t=$t(Kn("sha256").update(e).digest());return{verifier:e,challenge:t}}function Bt(e){let t=e.replace(/[^A-Za-z0-9]/g,"").slice(0,8).toUpperCase();return`${t.slice(0,4)}-${t.slice(4)}`}async function Pt(e,t=fetch){let n={origin:je(e),authorizeEndpoint:`${ne(e)}/oauth/authorize`};try{let r=await t(`${je(e)}/.well-known/oauth-authorization-server`);if(!r.ok)return n;let s=await r.json();if(typeof s.issuer=="string"&&typeof s.authorization_endpoint=="string")return{origin:s.issuer.replace(/\/+$/,""),authorizeEndpoint:s.authorization_endpoint}}catch{}return n}function Ct(e,t){let n=new URLSearchParams({response_type:"code",client_name:Qn,redirect_uri:`${e.origin}/oauth/cli-done`,code_challenge:t,code_challenge_method:"S256",resource:`${e.origin}/mcp/`});return`${e.authorizeEndpoint}?${n.toString()}`}var X=class extends Error{constructor(t){super(t),this.name="SignInError"}},er=e=>new Promise(t=>setTimeout(t,e));async function Mt(e,t,n={}){let{timeoutMs:r=10*6e4,signal:s,fetchImpl:i=fetch,sleep:u=er}=n,a=n.intervalMs??2e3,y=Date.now()+r,w=`${ne(e)}/oauth/token`;for(;;){if(s?.aborted)throw new X("Cancelled.");let d=null;try{d=await i(w,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({grant_type:Zn,code_verifier:t})})}catch{d=null}if(d){let g=await d.json().catch(()=>({}));if(d.ok&&g.access_token)return g.access_token;if(d.status===429)a=Math.min(a*2,3e4);else if(g.error!=="authorization_pending")throw new X(g.error==="invalid_grant"?"That approval can no longer be used. Add the site again.":`Sign-in failed: ${g.error||`HTTP ${d.status}`}`)}if(Date.now()+a>y)throw new X("Timed out waiting for approval in the browser.");await u(a)}}function tr(e){let t=e.split(".")[1];if(!t)throw new X("The server returned a token this version can't read.");return JSON.parse(Buffer.from(t,"base64url").toString("utf8"))}async function Rt(e,t){let n=tr(t),r=await Ae(e,t,"GET","/mcp/get_site");return{env:e,siteId:n.sid,slug:n.slug,name:r.name,org:{name:r.org},role:r.role||void 0,token:t,addedAt:Date.now()}}var Lt=500;function ir(){return new Date().toTimeString().slice(0,8)}function or(e){let t=e;if(t.body){try{let n=JSON.parse(t.body);if(n&&typeof n.message=="string")return n.message;if(n&&typeof n.error=="string")return n.error}catch{}return t.body}return t.message}function jt(e){return e instanceof W&&e.status===401}var ar=e=>({id:e.siteId,name:e.name,slug:e.slug});async function Dt(e){e&&(await re.remove(Oe.join(e,"src")),await re.remove(Oe.join(e,".cache","state.json")))}var fe=class extends nr{env="production";saved=[];synced=new Map;connecting=null;message;messageIsError=!1;agentMd;buildScript;uploadScript;configPath;isShuttingDown=!1;basePath;pollOptions={};openBrowser=ge;constructor(t,n={},r=q){super(),this.agentMd=t,this.buildScript=n.build,this.uploadScript=n.upload,this.configPath=r}changed(){this.emit("change")}flash(t,n=!1){this.message=t,this.messageIsError=n,this.changed()}appendLog(t,n){let r=this.synced.get(t);r&&(r.logs.push({time:ir(),text:n}),r.logs.length>Lt&&r.logs.splice(0,r.logs.length-Lt),this.changed())}siteCount(){return this.synced.size}syncingCount(){let t=0;for(let n of this.synced.values())n.status==="syncing"&&t++;return t}watchingCount(){let t=0;for(let n of this.synced.values())(n.status==="watching"||n.status==="syncing")&&t++;return t}async init(t){this.env=t;let{sites:n,hadLegacyLogin:r}=await pe(this.configPath);this.saved=n.filter(s=>s.env===t),r&&this.flash("SleekCMS Sync now connects one site at a time \u2014 press a to add a site in your browser."),this.changed()}isSynced(t){return this.synced.has(t)}async connectSite(){if(this.connecting)return;let{verifier:t,challenge:n}=Et(),r=new AbortController;this.connecting={code:Bt(n),url:"",abort:r},this.changed();let s=Ct(await Pt(this.env),n);if(r.signal.aborted){this.connecting=null,this.changed();return}this.connecting.url=s,this.message=void 0,this.changed(),this.openBrowser(s)||this.flash("Couldn't open a browser \u2014 open the address shown to continue.",!0);let i;try{let a=await Mt(this.env,t,{...this.pollOptions,signal:r.signal});i=await Rt(this.env,a)}catch(a){this.connecting=null,r.signal.aborted?this.changed():this.flash(or(a),!0);return}this.connecting=null;let u=await De(i,this.configPath);this.saved=u.filter(a=>a.env===this.env),this.flash(`Added "${i.name}".`),this.synced.has(i.siteId)||await this.startSite(i)}cancelConnect(){this.connecting?.abort.abort()}async startSite(t){let n=ar(t);if(this.synced.has(n.id))return;let r={org:t.org,site:n,siteToken:t.token,role:t.role,viewsDir:"",status:"syncing",logs:[]};this.synced.set(n.id,r),this.appendLog(n.id,"\u2795 added \u2014 pulling files"),this.changed();try{let s=await Pe({token:t.token,env:this.env,path:this.basePath,agentMd:this.agentMd,buildScript:this.buildScript,uploadScript:this.uploadScript,log:i=>this.appendLog(n.id,i),onProgress:(i,u)=>{r.progress={done:i,total:u},this.changed()}});this.noteRole(n.id,s.site.role),r.viewsDir=s.viewsDir,r.progress=void 0,r.status="watching",r.lastSyncedAt=Date.now(),r.lastMsg=`pulled ${s.pulled} file(s)`,r.watcher=new te({viewsDir:s.viewsDir,onSync:()=>this.runSync(n.id)}),r.watcher.start(),this.watchBuildStatus(n.id),this.seedLastBuild(n.id),this.appendLog(n.id,"\u{1F440} watching for changes")}catch(s){if(jt(s))return void await this.revoke(n.id);let i=s;r.status="error",r.progress=void 0,r.lastMsg=i.body||i.message,this.appendLog(n.id,`\u274C ${i.body||i.message}`)}this.changed()}noteRole(t,n){let r=this.synced.get(t);if(!n||!r||r.role===n)return;r.role=n;let s=this.saved.find(i=>i.siteId===t);s&&(s.role=n,De({...s},this.configPath).catch(()=>{}))}async revoke(t){let n=this.synced.get(t),r=n?.site.name??this.saved.find(u=>u.siteId===t)?.name??"this site";n&&(n.watcher?.setShuttingDown(!0),await n.watcher?.stop(),await n.buildWatcher?.close(),this.synced.delete(t));let s=await _e(this.env,t,this.configPath);this.saved=s.filter(u=>u.env===this.env);let i=n?.viewsDir?` Local files kept in ${n.viewsDir}.`:"";this.flash(`Access to "${r}" was revoked \u2014 press a to add it again.${i}`,!0)}async forgetSite(t){if(this.synced.has(t)){this.flash("Disconnect it first (d), then press x to forget it.",!0);return}let n=this.saved.find(s=>s.siteId===t)?.name??"the site",r=await _e(this.env,t,this.configPath);this.saved=r.filter(s=>s.env===this.env),this.flash(`Forgot "${n}". To revoke its access everywhere, log out of all sessions in SleekCMS.`)}async removeSite(t){let n=this.synced.get(t);n&&(n.watcher?.setShuttingDown(!0),await n.watcher?.stop(),await n.buildWatcher?.close(),await Dt(n.viewsDir),this.synced.delete(t),this.changed())}buildSite(t,n=!1){let r=this.synced.get(t);if(!r||!r.viewsDir)return;if(r.role&&r.role!=="ADMIN"){this.flash(`Building and publishing are for site ADMINs \u2014 you're ${r.role} on "${r.site.name}".`,!0);return}let s=n?"live":"preview";this.writeBuildFile(r.viewsDir,{status:"building",mode:s,stage:"queued",url:r.lastBuild?.url,at:Date.now()}),rr("node",n?["build.mjs","--publish"]:["build.mjs"],{cwd:r.viewsDir,stdio:"ignore"}).on("error",a=>{this.writeBuildFile(r.viewsDir,{status:"error",mode:s,errors:[a.message],url:r.lastBuild?.url,at:Date.now()}),this.appendLog(t,`\u274C build: ${a.message}`)}),this.appendLog(t,n?"\u{1F680} publishing\u2026":"\u{1F528} building\u2026")}buildFile(t){return Oe.join(t,".cache","build.json")}async readBuildFile(t){try{let n=await re.readJson(this.buildFile(t));return n&&n.status?n:null}catch{return null}}async writeBuildFile(t,n){let r=this.buildFile(t),s=`${r}.tmp`;try{await re.outputJson(s,n),await re.rename(s,r)}catch{}}watchBuildStatus(t){let n=this.synced.get(t);!n||!n.viewsDir||(n.buildWatcher=sr.watch(this.buildFile(n.viewsDir),{ignoreInitial:!1}).on("add",()=>{this.readBuildStatus(t)}).on("change",()=>{this.readBuildStatus(t)}))}async readBuildStatus(t){let n=this.synced.get(t);if(!n)return;let r=await this.readBuildFile(n.viewsDir);if(!r)return;let s=n.lastBuild?.url;n.lastBuild={status:r.status,mode:r.mode??"preview",url:r.url??(r.status==="success"?void 0:s),errors:r.errors,stage:r.stage,at:r.at},this.changed()}async seedLastBuild(t){let n=this.synced.get(t);if(!n)return;let r=await kt(this.env,n.siteToken);if(!r)return;let s=this.synced.get(t);if(!s)return;let i=await this.readBuildFile(s.viewsDir);i?.at!=null&&(r.at==null||i.at>=r.at)||await this.writeBuildFile(s.viewsDir,r)}async refetch(t){await this.runSync(t,{flush:!0})}async runSync(t,n={}){let r=this.synced.get(t);if(!(!r||!r.viewsDir||r.status==="paused")){r.status="syncing",this.changed();try{let s=await Pe({token:r.siteToken,env:this.env,viewsDir:r.viewsDir,agentMd:this.agentMd,buildScript:this.buildScript,uploadScript:this.uploadScript,flush:n.flush,log:i=>this.appendLog(t,i),onProgress:(i,u)=>{r.progress={done:i,total:u},this.changed()}});if(this.noteRole(t,s.site.role),r.progress=void 0,r.status="watching",r.lastSyncedAt=Date.now(),n.flush)r.lastMsg=`re-fetched ${s.pulled} file(s)`;else{let i=[s.renamed>0?`renamed ${s.renamed}`:"",s.pushed>0?`pushed ${s.pushed} file(s)`:""].filter(Boolean);r.lastMsg=i.length>0?i.join(", "):"up to date"}}catch(s){if(jt(s))return void await this.revoke(t);let i=s;r.status="error",r.progress=void 0,r.lastMsg=i.body||i.message,this.appendLog(t,`\u274C ${i.body||i.message}`)}this.changed()}}async togglePause(t){let n=this.synced.get(t);!n||!n.viewsDir||(n.status==="paused"?(n.watcher=new te({viewsDir:n.viewsDir,onSync:()=>this.runSync(t)}),n.watcher.start(),n.status="watching",this.appendLog(t,"\u25B6 resumed \u2014 watching for changes")):(n.watcher?.setShuttingDown(!0),await n.watcher?.stop(),n.watcher=void 0,n.progress=void 0,n.status="paused",this.appendLog(t,"\u23F8 paused")),this.changed())}async cleanupAll(){this.cancelConnect();for(let t of this.synced.values())t.watcher?.setShuttingDown(!0),await t.watcher?.stop(),await t.buildWatcher?.close(),await Dt(t.viewsDir)}async exit(){this.isShuttingDown||(this.isShuttingDown=!0,await this.cleanupAll(),this.emit("exit"))}};import{useInput as kr}from"ink";import{Fragment as dr,useEffect as se,useMemo as gr,useRef as pr,useState as j}from"react";import{Box as f,Text as l,useInput as fr,useStdout as hr}from"ink";import mr from"ink-text-input";import{createRequire as cr}from"node:module";var lr=cr(import.meta.url),he=lr("../package.json").version;import ur from"node:os";function Ie(e){if(!e)return"\u2014";let t=ur.homedir();return e.startsWith(t)?"~"+e.slice(t.length):e}function Fe(e){if(!e)return"\u2014";let t=Math.floor((Date.now()-e)/1e3);if(t<5)return"just now";if(t<60)return`${t}s ago`;let n=Math.floor(t/60);if(n<60)return`${n}m ago`;let r=Math.floor(n/60);return r<24?`${r}h ago`:`${Math.floor(r/24)}d ago`}function we(e){return!e||e.total<=0?null:Math.round(e.done/e.total*100)}var me=["\u280B","\u2819","\u2839","\u2838","\u283C","\u2834","\u2826","\u2827","\u2807","\u280F"];function Ne(e){return me[(e%me.length+me.length)%me.length]}function ye(e){switch(e){case"syncing":return{dot:"\u25CF",color:"green",label:"syncing"};case"watching":return{dot:"\u25CB",color:"gray",label:"watching"};case"paused":return{dot:"\u2016",color:"yellow",label:"paused"};case"error":return{dot:"\u2717",color:"red",label:"error"}}}function _t(e){if(!e)return{dot:"\xB7",color:"gray",label:"\u2014"};switch(e.status){case"building":return{dot:"\u2234",color:"yellow",label:"building"};case"error":return{dot:"\u2717",color:"red",label:"failed"};case"success":return{dot:"\u2713",color:"green",label:e.mode==="live"?"live":"preview"}}}import{Fragment as We,jsx as o,jsxs as p}from"react/jsx-runtime";var wr=(e,t)=>t<=0?0:Math.max(0,Math.min(e,t-1)),yr=16,br=5,vr="greenBright",At="blueBright",Ot="magentaBright";function It({k:e,bg:t=vr}){return o(l,{color:"black",backgroundColor:t,children:` ${e} `})}function I({k:e,label:t,width:n=16,bg:r}){return p(f,{width:n,children:[o(It,{k:e,bg:r}),o(l,{color:"gray",children:` ${t}`})]})}function E({k:e,label:t,bg:n}){return p(f,{marginBottom:1,children:[o(f,{width:9,children:o(It,{k:e,bg:n})}),o(l,{color:"gray",children:t})]})}function Sr({launchers:e}){return p(f,{flexDirection:"column",borderStyle:"round",borderColor:"green",paddingX:2,paddingY:1,width:66,children:[p(f,{justifyContent:"space-between",children:[o(l,{color:"green",bold:!0,children:"KEYBOARD SHORTCUTS"}),o(l,{color:"gray",children:"? or esc to close"})]}),p(f,{marginTop:1,children:[p(f,{flexDirection:"column",width:34,children:[o(l,{color:"cyan",bold:!0,children:"Navigate"}),o(E,{k:"\u2191\u2193",label:"move selection"}),o(E,{k:"tab",label:"focus logs \xB7 \u2191\u2193 scroll"}),o(E,{k:"esc",label:"clear filter / back"}),o(f,{marginTop:1,children:o(l,{color:"cyan",bold:!0,children:"Global"})}),o(E,{k:"/",label:"search sites"}),o(E,{k:"a",label:"add a site (browser)"}),o(E,{k:"?",label:"toggle this help",bg:At}),o(E,{k:"ctrl+c",label:"quit"})]}),p(f,{flexDirection:"column",flexGrow:1,children:[o(l,{color:"cyan",bold:!0,children:"Site actions"}),o(E,{k:"s",label:"start sync"}),o(E,{k:"d",label:"disconnect"}),o(E,{k:"x",label:"forget site"}),o(E,{k:"p",label:"pause / resume sync"}),o(E,{k:"b",label:"build preview"}),o(E,{k:"B",label:"publish live"}),o(E,{k:"o",label:"open preview url"}),e.map((t,n)=>o(E,{k:String(n+1),label:`open in ${t.label}`,bg:Ot},t.key))]})]})]})}function xr({percent:e,width:t=28}){let n=Math.round(Math.max(0,Math.min(100,e))/100*t);return p(l,{children:[o(l,{color:"green",children:"\u2588".repeat(n)}),o(l,{color:"gray",children:"\u2591".repeat(Math.max(0,t-n))})]})}function Tr({frame:e,width:t=24,seg:n=6}){let r=t+n,s=e%r-n,i="";for(let u=0;u<t;u++)i+=u>=s&&u<s+n?"\u25AE":"\u25AF";return o(l,{color:"yellow",children:i})}function Ft({controller:e}){let{stdout:t}=hr(),n=gr(()=>lt(),[]),[,r]=j(0),[s,i]=j(()=>new Date),[u,a]=j(null),[y,w]=j(""),[d,g]=j(!1),[x,m]=j(0),[v,k]=j(!1),[C,D]=j(!1),[M,R]=j("list"),[K,_]=j(0),qe=()=>r(c=>c+1);se(()=>(e.on("change",qe),()=>{e.off("change",qe)}),[e]),se(()=>{let c,S=()=>{i(new Date),c=setTimeout(S,6e4-Date.now()%6e4)};return c=setTimeout(S,6e4-Date.now()%6e4),()=>clearTimeout(c)},[]);let V=e.saved.map(c=>({org:c.org,site:{id:c.siteId,name:c.name,slug:c.slug},saved:c,entry:e.synced.get(c.siteId)})).sort((c,S)=>(c.entry?0:1)-(S.entry?0:1)),G=e.connecting,F=y.trim().toLowerCase(),P=F?V.filter(c=>c.site.name.toLowerCase().includes(F)||(c.org.name||"").toLowerCase().includes(F)):V,U=P.findIndex(c=>c.site.id===u);U<0&&(U=0);let $=P[U],T=$?.entry,oe=Math.max(3,(t?.rows??30)-26),N=T?.logs??[],Te=Math.max(0,N.length-oe),Zt=Math.min(K,Te),ae=N.length-Zt,Xe=N.slice(Math.max(0,ae-oe),ae),Qt=$?.site.id;se(()=>_(0),[Qt]);let A=T?.lastBuild,ke=A?.status==="building",$e=_t(A),ce=ke||!!G;if(!ce){for(let c of e.synced.values())if(c.status==="syncing"){ce=!0;break}}se(()=>{if(!ce)return;let c=setInterval(()=>m(S=>S+1),120);return()=>clearInterval(c)},[ce]);let Ye=P.findIndex(c=>!c.entry),H=Math.max(br,(t?.rows??30)-yr),Z=P.length,le=Z>H,en=Math.floor(H/2),Q=le?Math.max(0,Math.min(U-en,Z-H)):0,Ee=Math.min(Z,Q+H),tn=P.slice(Q,Ee),Ke=pr(!1);se(()=>{Ke.current||(Ke.current=!0,e.saved.length===0&&!e.connecting&&e.connectSite())},[e]);let ue=c=>{if(P.length===0)return;let S=wr(U+c,P.length);a(P[S].site.id)},nn=()=>{if($){if($.entry){e.flash("Already syncing \u2014 press d to disconnect.");return}e.startSite($.saved)}},rn=()=>{$?.entry&&e.removeSite($.site.id)},sn=()=>{$&&e.forgetSite($.site.id)},on=c=>{if(!T){e.flash("Press s to start syncing this site first.",!0);return}D(!0);let S=c.run(T.viewsDir);e.flash(S?`Opened ${c.label} at ${Ie(T.viewsDir)}`:`Couldn't open ${c.label}.`,!S)},an=()=>{if(!T){e.flash("Press s to start syncing this site first.",!0);return}e.togglePause(T.site.id)},Ve=c=>{if(!T){e.flash("Press s to start syncing this site first.",!0);return}e.buildSite(T.site.id,c),e.flash(c?"Publishing \u2014 making this live\u2026":"Building preview\u2026")},cn=()=>{let c=T?.lastBuild?.url;if(!c){e.flash("No build URL yet \u2014 press b to build.",!0);return}e.flash(ge(c)?`Opening ${c}`:"Couldn't open the browser.",!1)};fr((c,S)=>{if(v){(c==="?"||S.escape)&&k(!1);return}if(G&&S.escape)return void e.cancelConnect();if(d){S.escape?(g(!1),w(""),a(null)):S.return?g(!1):S.upArrow?ue(-1):S.downArrow&&ue(1);return}if(S.tab){M==="logs"?R("list"):T&&R("logs"),_(0);return}if(M==="logs"){if(S.escape){R("list"),_(0);return}if(S.upArrow)return void _(O=>Math.min(O+1,Te));if(S.downArrow)return void _(O=>Math.max(0,O-1))}if(S.escape){w(""),a(null);return}if(c==="?")return void k(!0);if(c==="/")return R("list"),void g(!0);if(c==="a"||c==="n")return void e.connectSite();if(S.upArrow)return ue(-1);if(S.downArrow)return ue(1);if(c==="s")return nn();if(c==="d")return rn();if(c==="x")return sn();if(c==="p")return an();if(c==="b")return Ve(!1);if(c==="B")return Ve(!0);if(c==="o")return cn();if(c>="1"&&c<="9"){let O=n[parseInt(c,10)-1];if(O)return on(O)}});let Ze=p(f,{justifyContent:"space-between",paddingX:1,children:[p(f,{children:[o(l,{color:"green",bold:!0,children:"\u25C6 SleekCMS"}),o(l,{color:"gray",children:` v${he} \u2502 `}),o(l,{children:e.env==="production"?"Sync":`Sync \xB7 ${e.env}`})]}),p(f,{children:[p(l,{color:"gray",children:[F?`${P.length}/${V.length}`:V.length," sites \xB7 "]}),p(l,{color:"green",children:[e.watchingCount()," watching"]}),o(l,{color:"gray",children:` \xB7 ${s.toTimeString().slice(0,5)}`})]})]});return v?p(f,{flexDirection:"column",children:[Ze,o(f,{marginTop:1,justifyContent:"center",children:o(Sr,{launchers:n})})]}):p(f,{flexDirection:"column",children:[Ze,p(f,{children:[p(f,{flexDirection:"column",borderStyle:"round",borderColor:"green",width:38,paddingX:1,children:[p(f,{justifyContent:"space-between",children:[o(l,{color:"green",bold:!0,children:"SITE WORKSPACES"}),V.length>H&&!d&&!F?o(l,{color:"gray",children:"/ filter"}):null]}),d?p(f,{children:[o(l,{color:"cyan",children:"/ "}),o(mr,{value:y,onChange:w,onSubmit:()=>g(!1),placeholder:"type to filter\u2026"})]}):F?o(l,{color:"gray",children:`/ ${y} \xB7 ${P.length} match${P.length===1?"":"es"}`}):null,o(f,{flexDirection:"column",marginTop:1,minHeight:le?H+3:void 0,children:P.length===0?o(l,{color:"gray",children:F?`no matches for \u201C${y}\u201D`:"no sites yet \u2014 press a to add one"}):p(We,{children:[le?o(l,{color:"gray",children:Q>0?` \u2191 ${Q} more`:" "}):null,tn.map((c,S)=>{let O=Q+S,Qe=c.entry?ye(c.entry.status):{dot:"\xB7",color:"gray",label:"idle"},et=we(c.entry?.progress),ln=c.entry?c.entry.status==="syncing"&&et!=null?`${et}%`:c.entry.status==="paused"?"paused":"--":"",de=O===U,un=O===Ye&&Ye>0;return p(dr,{children:[un?o(l,{color:"gray",children:"\u2500".repeat(34)}):null,p(f,{justifyContent:"space-between",children:[p(f,{children:[o(l,{color:de?"green":"gray",children:de?"\u276F ":" "}),p(l,{color:c.entry?.status==="watching"||c.entry?.status==="syncing"?"green":Qe.color,children:[c.entry?.status==="syncing"?Ne(x):c.entry?.status==="watching"?"\u25CF":Qe.dot," "]}),o(l,{color:de?"white":c.entry?void 0:"gray",bold:de,children:c.site.name})]}),o(l,{color:c.entry?.status==="syncing"?"green":"gray",children:ln})]})]},c.site.id)}),le?o(l,{color:"gray",children:Ee<Z?` \u2193 ${Z-Ee} more`:" "}):null]})}),o(f,{marginTop:1,children:o(l,{color:"gray",children:"+ add site \xB7 a"})})]}),p(f,{flexDirection:"column",flexGrow:1,marginLeft:1,children:[o(f,{flexDirection:"column",borderStyle:"round",borderColor:"gray",paddingX:1,children:$?p(We,{children:[p(l,{children:[$.org.name?p(l,{color:"gray",children:[$.org.name," / "]}):null,o(l,{color:"cyan",bold:!0,children:$.site.name})]}),p(f,{marginTop:1,children:[o(l,{color:"gray",children:"status "}),T?o(l,{color:ye(T.status).color,children:`: ${ye(T.status).label}`}):o(l,{color:"gray",children:": idle \u2014 not syncing (press s to start)"})]}),p(f,{children:[o(l,{color:"gray",children:"role "}),o(l,{children:T?.role??$.saved.role??"\u2014"}),(T?.role??$.saved.role??"ADMIN")!=="ADMIN"?o(l,{color:"gray",children:" \xB7 build & publish need ADMIN"}):null]}),p(f,{children:[o(l,{color:"gray",children:"path "}),o(l,{wrap:"truncate-end",children:T?.viewsDir?Ie(T.viewsDir):"\u2014"})]}),p(f,{children:[o(l,{color:"gray",children:"synced "}),o(l,{children:Fe(T?.lastSyncedAt)}),o(l,{color:"gray",children:" progress "}),o(l,{color:T?.status==="syncing"?"green":"gray",children:T?.status==="syncing"?`${we(T.progress)??0}%`:"\u2014"})]}),o(f,{marginTop:1,children:o(xr,{percent:T?T.status==="syncing"?we(T.progress)??0:T.status==="watching"?100:0:0})}),T?p(We,{children:[p(f,{marginTop:1,children:[o(l,{color:"gray",children:"build "}),o(l,{color:$e.color,children:`${$e.dot} ${$e.label}`}),o(l,{color:"gray",children:" last "}),o(l,{children:ke?"in progress":Fe(A?.at)})]}),p(f,{children:[o(l,{color:"gray",children:"url "}),A?.url?o(l,{color:"cyan",wrap:"truncate-end",children:A.url}):o(l,{color:"gray",children:"\u2014"})]}),A?.status==="error"&&A.errors?.length?p(f,{children:[o(l,{color:"red",children:"\u258C "}),o(l,{color:"red",wrap:"truncate-end",children:`\u2717 ${A.errors[0]}`})]}):null,ke?p(f,{children:[o(Tr,{frame:x}),o(l,{color:"gray",children:` ${A?.stage??"building"}`})]}):null]}):null]}):o(l,{color:"gray",children:"No site selected."})}),G?p(f,{flexDirection:"column",borderStyle:"round",borderColor:"blue",paddingX:1,marginTop:1,flexGrow:1,children:[o(l,{color:"cyan",bold:!0,children:"ADD A SITE"}),o(l,{children:"Approve it in your browser: pick a site, or create one."}),p(f,{marginTop:1,children:[o(l,{color:"gray",children:"Check the page shows "}),o(l,{color:"cyan",bold:!0,children:G.code})]}),o(l,{color:"gray",children:"Only approve a page that shows this code."}),p(f,{marginTop:1,children:[p(l,{color:"yellow",children:[Ne(x)," "]}),o(l,{color:"gray",children:"waiting for approval \xB7 esc to cancel"})]})]}):p(f,{flexDirection:"column",borderStyle:"round",borderColor:M==="logs"?"green":"gray",paddingX:1,marginTop:1,height:oe+4,flexShrink:0,overflowY:"hidden",children:[p(f,{justifyContent:"space-between",children:[p(l,{children:[o(l,{color:"cyan",bold:!0,children:"LOGS \xB7 "}),o(l,{color:"white",children:$?.site.name??"\u2014"})]}),T&&N.length>0?M==="logs"?o(l,{color:"green",children:`\u2191\u2193 scroll \xB7 esc back${Te>0?` \xB7 ${ae}/${N.length}`:""}`}):N.length>oe?o(l,{color:"gray",children:"tab to scroll"}):null:null]}),o(f,{flexDirection:"column",marginTop:1,children:!T||N.length===0?o(l,{color:"gray",children:"no activity yet"}):Xe.map((c,S)=>p(l,{wrap:"truncate-end",children:[p(l,{color:"gray",children:[c.time," "]}),c.text]},ae-Xe.length+S))})]})]})]}),G?.url?p(f,{flexDirection:"column",paddingX:1,marginTop:1,children:[o(l,{color:"gray",children:"No browser? Open this address on any device:"}),o(l,{color:"cyan",children:G.url})]}):null,e.message?o(f,{paddingX:1,children:p(l,{color:e.messageIsError?"red":"gray",wrap:"truncate-end",children:[e.messageIsError?"\u2717 ":"",e.message]})}):null,p(f,{flexDirection:"column",marginTop:1,children:[p(f,{paddingX:1,justifyContent:"space-between",children:[p(f,{flexWrap:"wrap",children:[o(I,{k:"\u2191\u2193",label:"select"}),o(I,{k:"/",label:"search"}),o(I,{k:"s",label:"start sync"}),o(I,{k:"d",label:"disconnect"}),o(I,{k:"a",label:"add site"}),o(I,{k:"ctrl+c",label:"quit",width:14})]}),o(I,{k:"?",label:"all keys",width:12,bg:At})]}),!C&&n.length>0?o(f,{paddingX:1,marginTop:1,flexWrap:"wrap",children:n.map((c,S)=>o(I,{k:String(S+1),label:c.label,bg:Ot},c.key))}):null]})]})}import{jsx as $r}from"react/jsx-runtime";function Nt({controller:e}){return kr((t,n)=>{n.ctrl&&t==="c"&&e.exit()}),$r(Ft,{controller:e})}import*as zt from"os";import*as Ue from"util";var Er={production:"https://9Q849fBKdCJcK6j8M5SHpWjt@s2790579.us-west-2a.betterstackdata.com/2790579",development:"https://oj8Je2g5rZhXAv6NZ3CxYHFb@s2790583.us-west-2a.betterstackdata.com/2790583"},ie,Wt,Br=()=>Wt??=import("@sentry/node").then(e=>(e.init({dsn:ie.dsn,environment:ie.environment,serverName:ie.service,initialScope:{tags:{service:ie.service}},defaultIntegrations:!1,tracesSampleRate:0,maxBreadcrumbs:0,normalizeDepth:3}),e)).catch(e=>{console.warn("[observability] provider failed to load:",e?.message)}),Gt=({service:e,environment:t})=>{let n=Er[t];return n&&(ie={dsn:n,environment:t,service:e}),!!n},Ut=(e,t)=>{Br().then(n=>n?.captureException(e,t&&{extra:t}))},Ht=async e=>{await(await Wt)?.flush(e)};var Cr=10*6e4,Mr=3,Rr=30,Lr=1e3;var Se=!1,Ge=!1;var qt=console.error.bind(console),Y=new Map,Jt=new WeakSet,be={start:0,sent:0},jr=()=>process.env.ENV||process.env.AWS_LAMBDA_FUNCTION_NAME?.match(/-(production|development)$/)?.[1],Xt=({service:e,environment:t=jr(),quiet:n=!1})=>{Se=!!t&&Gt({service:e,environment:t}),n||console.log(`[observability] ${e}: ${Se?`reporting errors (${t})`:"off"}`)},Yt=(e,t)=>{if(!(!Se||Ge)){Ge=!0;try{let n=Or(e);if(Jt.has(n))return;Jt.add(n);let r=Dr(_r(n));if(r===!1)return;Ut(Ir(n),r?{...t,suppressed:r}:t)}catch(n){qt("[observability] capture failed:",n?.message)}finally{Ge=!1}}},Kt=async(e=2e3)=>{if(Se)try{await Ht(e)}catch(t){qt("[observability] flush failed:",t?.message)}};var Dr=e=>{let t=Date.now();t-be.start>=6e4&&(be={start:t,sent:0});let n=Y.get(e),r=0;return(!n||t-n.start>=Cr)&&(r=n?.dropped??0,n={start:t,sent:0,dropped:0},Y.delete(e),Y.set(e,n),Y.size>Lr&&Y.delete(Y.keys().next().value)),n.sent>=Mr||be.sent>=Rr?(n.dropped++,!1):(n.sent++,be.sent++,r)},_r=e=>{let t=e.stack?.split(`
|
|
11
|
+
`).find(n=>/^\s*at /.test(n)&&!/node_modules|node:|[\\/]observability[\\/]/.test(n));return`${e.name}|${Ar(e.message)}|${t?.trim()??""}`},Ar=e=>e.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi,"<id>").replace(/\b[0-9a-f]{16,}\b/gi,"<id>").replace(/\d+/g,"<n>").slice(0,300),Or=e=>{if(e instanceof Error)return e;let t=new Error(typeof e=="string"?e:Ue.inspect(e,{depth:3,breakLength:1/0}));return t.name="NonErrorThrown",t},ve=(()=>{try{return zt.homedir()}catch{return""}})(),Ir=e=>{if(ve.length<2||!`${e.message}
|
|
12
|
+
${e.stack}`.includes(ve))return e;let t=new Error(e.message.split(ve).join("~"));return t.name=e.name,t.stack=e.stack?.split(ve).join("~"),t};import{jsx as Jr}from"react/jsx-runtime";var Je=ot();Je.version&&(console.log(he),process.exit(0));var Vt=(Je.env??"production").toLowerCase();Xt({service:"cms-sync",environment:process.env.DO_NOT_TRACK==="1"?null:Vt,quiet:!0});var ze=xe.dirname(Fr(import.meta.url)),Wr=He(xe.join(ze,"AGENTS.md"),"utf-8"),Gr=He(xe.join(ze,"build.mjs"),"utf-8"),Ur=He(xe.join(ze,"upload.mjs"),"utf-8");async function Hr(){let e=new fe(Wr,{build:Gr,upload:Ur});e.basePath=Je.path,await e.init(Vt),process.stdout.isTTY&&process.stdout.write("\x1B[2J\x1B[3J\x1B[H");let t=Nr(Jr(Nt,{controller:e}),{exitOnCtrlC:!1}),n=r=>{t.unmount(),console.log(r),process.exit(0)};e.once("exit",()=>n("\u2713 Stopped. Local site files were cleaned up.")),process.on("SIGINT",()=>{e.exit()}),process.on("SIGTERM",()=>{e.exit()}),await t.waitUntilExit()}Hr().catch(async e=>{let t=e;console.error("\u274C",t.body||t.message),t.body||(Yt(e),await Kt()),process.exit(1)});
|
package/dist/upload.mjs
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// upload.mjs v1 — SleekCMS workspace upload script.
|
|
3
|
+
/**
|
|
4
|
+
* Self-contained (no `npm install` needed). Run from anywhere inside a synced
|
|
5
|
+
* SleekCMS workspace — the one `@sleekcms/sync` created, containing `.cache/token`.
|
|
6
|
+
*
|
|
7
|
+
* node upload.mjs <path>... Upload images and documents from disk.
|
|
8
|
+
* node upload.mjs logo.png --handle logo Pick the image's handle (one image only).
|
|
9
|
+
* node upload.mjs menu.pdf --name lunch-menu.pdf
|
|
10
|
+
* Pick the document's file name (one file only).
|
|
11
|
+
* --json Print one JSON result per line instead of text.
|
|
12
|
+
*
|
|
13
|
+
* What each file becomes is decided by its extension:
|
|
14
|
+
* - Images (png, jpg, jpeg, webp, gif, svg, ico) become site images under a
|
|
15
|
+
* handle (default: the file name without its extension) — reference them as
|
|
16
|
+
* `cms:<handle>` in image fields, or `img('cms:<handle>')` in views. An existing
|
|
17
|
+
* handle is replaced in place, so every `cms:<handle>` shows the new image.
|
|
18
|
+
* - Documents (pdf, docx, xlsx, pptx, csv, tsv, txt, md) go to Media > Files
|
|
19
|
+
* under their file name — the value a `file` field takes. An existing file
|
|
20
|
+
* name is replaced in place.
|
|
21
|
+
*
|
|
22
|
+
* It calls POST /api/mcp/create_asset, then sends the bytes straight to the
|
|
23
|
+
* upload URL it returns, and finally updates src/content/images.json or
|
|
24
|
+
* src/content/files.json so the workspace lists the new asset (the running sync
|
|
25
|
+
* pushes images.json back unchanged, which is a no-op on the server).
|
|
26
|
+
*
|
|
27
|
+
* Exit codes: 0 = every file uploaded, 1 = some uploads failed, 2 = couldn't run.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import fs from "node:fs";
|
|
31
|
+
import path from "node:path";
|
|
32
|
+
import { pathToFileURL } from "node:url";
|
|
33
|
+
|
|
34
|
+
// Same env → base-URL table the sync engine and build.mjs use.
|
|
35
|
+
const MCP_BASE_URLS = {
|
|
36
|
+
localhost: "http://app.sleekcms.test/api/mcp",
|
|
37
|
+
development: "https://app.sleekcms.dev/api/mcp",
|
|
38
|
+
production: "https://app.sleekcms.com/api/mcp",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Mirrors cms-be handlers/mcp/assets.js.
|
|
42
|
+
export const IMAGE_EXTS = ["png", "jpg", "jpeg", "webp", "gif", "svg", "ico"];
|
|
43
|
+
export const FILE_EXTS = ["pdf", "docx", "xlsx", "pptx", "csv", "tsv", "txt", "md"];
|
|
44
|
+
const TEXT_FILE_EXTS = ["csv", "tsv", "txt", "md"];
|
|
45
|
+
const MAX_IMAGE_MB = 8;
|
|
46
|
+
const MAX_FILE_MB = 10;
|
|
47
|
+
const MAX_INLINE_TEXT_BYTES = 256 * 1024;
|
|
48
|
+
|
|
49
|
+
const IMAGES_INDEX = "src/content/images.json";
|
|
50
|
+
const FILES_INDEX = "src/content/files.json";
|
|
51
|
+
|
|
52
|
+
/** The workspace's env: the override, else what .cache/site.json records, else production. */
|
|
53
|
+
export function workspaceEnv(ws, override) {
|
|
54
|
+
let recorded;
|
|
55
|
+
try {
|
|
56
|
+
recorded = JSON.parse(fs.readFileSync(path.join(ws, ".cache", "site.json"), "utf-8")).env;
|
|
57
|
+
} catch {
|
|
58
|
+
recorded = undefined;
|
|
59
|
+
}
|
|
60
|
+
const env = String(override || recorded || "production").toLowerCase();
|
|
61
|
+
return MCP_BASE_URLS[env] ? env : "production";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function mcpBase(env) {
|
|
65
|
+
return MCP_BASE_URLS[env] || MCP_BASE_URLS.production;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Walk up from `startDir` until a `.cache/token` is found; null if none. */
|
|
69
|
+
export function findWorkspace(startDir) {
|
|
70
|
+
let dir = path.resolve(startDir);
|
|
71
|
+
// eslint-disable-next-line no-constant-condition
|
|
72
|
+
while (true) {
|
|
73
|
+
if (fs.existsSync(path.join(dir, ".cache", "token"))) return dir;
|
|
74
|
+
const parent = path.dirname(dir);
|
|
75
|
+
if (parent === dir) return null;
|
|
76
|
+
dir = parent;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function extOf(name) {
|
|
81
|
+
const m = /\.([a-z0-9]{1,10})$/i.exec(String(name || ""));
|
|
82
|
+
return m ? m[1].toLowerCase() : "";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** "image", "file", or null for an extension the CMS doesn't take. */
|
|
86
|
+
export function kindOf(name) {
|
|
87
|
+
const ext = extOf(name);
|
|
88
|
+
if (IMAGE_EXTS.includes(ext)) return "image";
|
|
89
|
+
if (FILE_EXTS.includes(ext)) return "file";
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** A valid image handle from a file name: "Team Photo (1).JPG" → "team-photo-1". */
|
|
94
|
+
export function handleFor(fileName) {
|
|
95
|
+
const base = path.basename(fileName).replace(/\.[^.]*$/, "");
|
|
96
|
+
const handle = base
|
|
97
|
+
.toLowerCase()
|
|
98
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
99
|
+
.replace(/^[-_]+|-+$/g, "")
|
|
100
|
+
.slice(0, 64)
|
|
101
|
+
.replace(/-+$/, "");
|
|
102
|
+
return handle || "image";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function parseFlags(argv) {
|
|
106
|
+
const flags = { files: [], handle: undefined, name: undefined, env: undefined, json: false, help: false };
|
|
107
|
+
for (let i = 0; i < argv.length; i++) {
|
|
108
|
+
const a = argv[i];
|
|
109
|
+
if (a === "--handle") flags.handle = argv[++i];
|
|
110
|
+
else if (a.startsWith("--handle=")) flags.handle = a.slice("--handle=".length);
|
|
111
|
+
else if (a === "--name") flags.name = argv[++i];
|
|
112
|
+
else if (a.startsWith("--name=")) flags.name = a.slice("--name=".length);
|
|
113
|
+
else if (a === "--env") flags.env = argv[++i];
|
|
114
|
+
else if (a.startsWith("--env=")) flags.env = a.slice("--env=".length);
|
|
115
|
+
else if (a === "--json") flags.json = true;
|
|
116
|
+
else if (a === "--help" || a === "-h") flags.help = true;
|
|
117
|
+
else flags.files.push(a);
|
|
118
|
+
}
|
|
119
|
+
return flags;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export class UploadError extends Error {
|
|
123
|
+
constructor(message, httpStatus) {
|
|
124
|
+
super(message);
|
|
125
|
+
this.httpStatus = httpStatus;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The server's own message from an error body (JSON `message`/`error`), else the raw text. */
|
|
130
|
+
function errorText(text) {
|
|
131
|
+
try {
|
|
132
|
+
const body = JSON.parse(text);
|
|
133
|
+
if (body && (body.message || body.error)) return String(body.message || body.error);
|
|
134
|
+
} catch {
|
|
135
|
+
/* not JSON */
|
|
136
|
+
}
|
|
137
|
+
const title = String(text || "").match(/<title>([^<]*)<\/title>/i)?.[1].trim();
|
|
138
|
+
return (title || String(text || "")).replace(/\s+/g, " ").slice(0, 300);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function api(base, token, method, p, body, fetchImpl) {
|
|
142
|
+
const res = await fetchImpl(base + p, {
|
|
143
|
+
method,
|
|
144
|
+
headers: {
|
|
145
|
+
Authorization: `Bearer ${token}`,
|
|
146
|
+
...(body !== undefined ? { "Content-Type": "application/json" } : {}),
|
|
147
|
+
},
|
|
148
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
149
|
+
});
|
|
150
|
+
if (!res.ok) {
|
|
151
|
+
const text = await res.text().catch(() => "");
|
|
152
|
+
throw new UploadError(`${p.split("?")[0]} → ${res.status}: ${errorText(text)}`, res.status);
|
|
153
|
+
}
|
|
154
|
+
return res.json();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Send the bytes to the upload URL create_asset handed back. Returns the parsed reply, if any. */
|
|
158
|
+
async function sendBytes(upload, data, fetchImpl) {
|
|
159
|
+
const res = await fetchImpl(upload.url, { method: upload.method, headers: upload.headers || {}, body: data });
|
|
160
|
+
const text = await res.text().catch(() => "");
|
|
161
|
+
if (!res.ok) throw new UploadError(`upload → ${res.status}: ${errorText(text)}`, res.status);
|
|
162
|
+
try {
|
|
163
|
+
return text ? JSON.parse(text) : null;
|
|
164
|
+
} catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Put one handle into the local images.json, keeping everything else in it as it
|
|
171
|
+
* is (it may hold edits the sync hasn't pushed yet). Returns false when the file
|
|
172
|
+
* isn't plain JSON (comments etc.) and was left alone.
|
|
173
|
+
*/
|
|
174
|
+
export function mergeImageHandle(ws, handle, value) {
|
|
175
|
+
const file = path.join(ws, IMAGES_INDEX);
|
|
176
|
+
let map = {};
|
|
177
|
+
if (fs.existsSync(file)) {
|
|
178
|
+
const text = fs.readFileSync(file, "utf-8");
|
|
179
|
+
if (text.trim()) {
|
|
180
|
+
try {
|
|
181
|
+
map = JSON.parse(text);
|
|
182
|
+
} catch {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
if (!map || typeof map !== "object" || Array.isArray(map)) return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (map[handle] === value) return true;
|
|
189
|
+
map[handle] = value;
|
|
190
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
191
|
+
fs.writeFileSync(file, `${JSON.stringify(map, null, 2)}\n`);
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Write the server's files.json (read-only, server-owned — the sync never pushes it). */
|
|
196
|
+
function writeFilesIndex(ws, files) {
|
|
197
|
+
const index = (files || []).find((f) => f.path === FILES_INDEX);
|
|
198
|
+
if (!index) return;
|
|
199
|
+
const file = path.join(ws, FILES_INDEX);
|
|
200
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
201
|
+
fs.writeFileSync(file, index.content);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Upload one file from disk. `opts`: { ws, base, token, filePath, handle?, name?, fetchImpl? }.
|
|
206
|
+
* Resolves to a result object; throws UploadError on failure.
|
|
207
|
+
*/
|
|
208
|
+
export async function uploadOne(opts) {
|
|
209
|
+
const { ws, base, token, filePath, fetchImpl = fetch } = opts;
|
|
210
|
+
const fileName = path.basename(filePath);
|
|
211
|
+
const kind = kindOf(opts.name || fileName);
|
|
212
|
+
if (!kind) {
|
|
213
|
+
throw new UploadError(
|
|
214
|
+
`${fileName}: unsupported type. Images: ${IMAGE_EXTS.join(", ")}. Documents: ${FILE_EXTS.join(", ")}.`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
let stat;
|
|
218
|
+
try {
|
|
219
|
+
stat = fs.statSync(filePath);
|
|
220
|
+
} catch {
|
|
221
|
+
throw new UploadError(`${filePath}: no such file.`);
|
|
222
|
+
}
|
|
223
|
+
if (!stat.isFile()) throw new UploadError(`${filePath}: not a file.`);
|
|
224
|
+
if (stat.size === 0) throw new UploadError(`${filePath}: the file is empty.`);
|
|
225
|
+
const maxMb = kind === "image" ? MAX_IMAGE_MB : MAX_FILE_MB;
|
|
226
|
+
if (stat.size > maxMb * 1024 * 1024) {
|
|
227
|
+
throw new UploadError(`${fileName}: ${(stat.size / 1024 / 1024).toFixed(1)} MB is over the ${maxMb} MB limit for ${kind === "image" ? "images" : "documents"}.`);
|
|
228
|
+
}
|
|
229
|
+
const data = fs.readFileSync(filePath);
|
|
230
|
+
|
|
231
|
+
if (kind === "image") {
|
|
232
|
+
if (opts.name && kindOf(opts.name) !== "image") throw new UploadError(`--name must keep an image extension.`);
|
|
233
|
+
const handle = opts.handle || handleFor(fileName);
|
|
234
|
+
const name = opts.name || fileName;
|
|
235
|
+
const created = await api(base, token, "POST", "/create_asset", { type: "image", handle, name }, fetchImpl);
|
|
236
|
+
if (created.upload) {
|
|
237
|
+
if (created.max_size_mb && stat.size > created.max_size_mb * 1024 * 1024) {
|
|
238
|
+
throw new UploadError(`${fileName}: over the ${created.max_size_mb} MB limit for images.`);
|
|
239
|
+
}
|
|
240
|
+
await sendBytes(created.upload, data, fetchImpl);
|
|
241
|
+
}
|
|
242
|
+
// What the server lists for the handle now — the value images.json reads back as.
|
|
243
|
+
let value;
|
|
244
|
+
try {
|
|
245
|
+
const images = await api(base, token, "GET", "/get_images", undefined, fetchImpl);
|
|
246
|
+
value = JSON.parse(images.content || "{}")[handle];
|
|
247
|
+
} catch {
|
|
248
|
+
value = undefined;
|
|
249
|
+
}
|
|
250
|
+
const indexed = value ? mergeImageHandle(ws, handle, value) : false;
|
|
251
|
+
return {
|
|
252
|
+
source: filePath,
|
|
253
|
+
type: "image",
|
|
254
|
+
ref: created.ref || `cms:${handle}`,
|
|
255
|
+
handle,
|
|
256
|
+
replaced: !!created.replaces_existing,
|
|
257
|
+
url: typeof value === "string" && value.startsWith("url:") ? value.slice(4) : undefined,
|
|
258
|
+
indexed,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Document → Media > Files.
|
|
263
|
+
if (opts.handle) throw new UploadError(`--handle is for images; a document is named by its file name (use --name).`);
|
|
264
|
+
const name = opts.name || fileName;
|
|
265
|
+
if (extOf(name) !== extOf(fileName)) throw new UploadError(`--name must keep the file's .${extOf(fileName)} extension.`);
|
|
266
|
+
const body = { type: "file", name };
|
|
267
|
+
// Small text documents go inline: the server stores them and confirms the row at once.
|
|
268
|
+
const ext = extOf(name);
|
|
269
|
+
if (TEXT_FILE_EXTS.includes(ext) && data.length <= MAX_INLINE_TEXT_BYTES) body.content = data.toString("utf-8");
|
|
270
|
+
const created = await api(base, token, "POST", "/create_asset", body, fetchImpl);
|
|
271
|
+
let files = created.files;
|
|
272
|
+
if (created.upload) {
|
|
273
|
+
await sendBytes(created.upload, data, fetchImpl);
|
|
274
|
+
// The listing create_asset returned predates the bytes (a new file is only
|
|
275
|
+
// listed once stored), so ask again now that they are.
|
|
276
|
+
try {
|
|
277
|
+
files = await api(base, token, "GET", "/get_files?kind=files", undefined, fetchImpl);
|
|
278
|
+
} catch {
|
|
279
|
+
// keep the earlier listing; the next sync pull corrects it
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
writeFilesIndex(ws, files);
|
|
283
|
+
return {
|
|
284
|
+
source: filePath,
|
|
285
|
+
type: "file",
|
|
286
|
+
ref: created.ref || name,
|
|
287
|
+
handle: created.handle || undefined,
|
|
288
|
+
replaced: !!created.replaces_existing,
|
|
289
|
+
url: created.url || undefined,
|
|
290
|
+
indexed: !!(files || []).some((f) => f.path === FILES_INDEX),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Human-readable lines for one result. */
|
|
295
|
+
export function describe(r) {
|
|
296
|
+
if (r.type === "image") {
|
|
297
|
+
const lines = [`✅ ${r.replaced ? "Replaced" : "Uploaded"} image ${path.basename(r.source)} → "${r.ref}"${r.url ? ` (${r.url})` : ""}`];
|
|
298
|
+
lines.push(` Use "${r.ref}" as an image field's value, or img('${r.ref}') / src('${r.ref}') in a view.`);
|
|
299
|
+
if (!r.indexed) lines.push(` ${IMAGES_INDEX} wasn't updated here; the handle is on the server and shows up there after the next re-fetch.`);
|
|
300
|
+
return lines;
|
|
301
|
+
}
|
|
302
|
+
const lines = [`✅ ${r.replaced ? "Replaced" : "Uploaded"} file ${path.basename(r.source)} → "${r.ref}"${r.url ? ` (${r.url})` : ""}`];
|
|
303
|
+
lines.push(` Use "${r.ref}" as a file field's value, or getFile('${r.ref}') in a view.`);
|
|
304
|
+
return lines;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const USAGE = `Usage: node upload.mjs <path>... [--handle <handle>] [--name <file name>] [--json]
|
|
308
|
+
|
|
309
|
+
Images (${IMAGE_EXTS.join(", ")}, up to ${MAX_IMAGE_MB} MB) become site images: cms:<handle>.
|
|
310
|
+
Documents (${FILE_EXTS.join(", ")}, up to ${MAX_FILE_MB} MB) go to Media > Files under their file name.
|
|
311
|
+
--handle the image's handle (default: its file name without the extension); one image only.
|
|
312
|
+
--name the stored file name, extension kept; one file only.`;
|
|
313
|
+
|
|
314
|
+
async function main() {
|
|
315
|
+
const flags = parseFlags(process.argv.slice(2));
|
|
316
|
+
if (flags.help || flags.files.length === 0) {
|
|
317
|
+
(flags.help ? console.log : console.error)(USAGE);
|
|
318
|
+
process.exit(flags.help ? 0 : 2);
|
|
319
|
+
}
|
|
320
|
+
if ((flags.handle || flags.name) && flags.files.length > 1) {
|
|
321
|
+
console.error("❌ --handle and --name name a single upload; pass one file with them.");
|
|
322
|
+
process.exit(2);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const ws = findWorkspace(process.cwd());
|
|
326
|
+
if (!ws) {
|
|
327
|
+
console.error("❌ Not inside a SleekCMS workspace (no .cache/token found). Run this from your synced site folder.");
|
|
328
|
+
process.exit(2);
|
|
329
|
+
}
|
|
330
|
+
let token;
|
|
331
|
+
try {
|
|
332
|
+
token = fs.readFileSync(path.join(ws, ".cache", "token"), "utf-8").trim();
|
|
333
|
+
} catch {
|
|
334
|
+
token = "";
|
|
335
|
+
}
|
|
336
|
+
if (!token) {
|
|
337
|
+
console.error("❌ Could not read .cache/token in this workspace.");
|
|
338
|
+
process.exit(2);
|
|
339
|
+
}
|
|
340
|
+
const base = mcpBase(workspaceEnv(ws, flags.env || process.env.SLEEKCMS_ENV));
|
|
341
|
+
|
|
342
|
+
let failed = 0;
|
|
343
|
+
for (const file of flags.files) {
|
|
344
|
+
const filePath = path.resolve(process.cwd(), file);
|
|
345
|
+
try {
|
|
346
|
+
const r = await uploadOne({ ws, base, token, filePath, handle: flags.handle, name: flags.name });
|
|
347
|
+
if (flags.json) console.log(JSON.stringify({ ok: true, ...r }));
|
|
348
|
+
else for (const line of describe(r)) console.log(line);
|
|
349
|
+
} catch (err) {
|
|
350
|
+
failed++;
|
|
351
|
+
const msg = err && err.message ? err.message : String(err);
|
|
352
|
+
const hint =
|
|
353
|
+
err && err.httpStatus === 403
|
|
354
|
+
? " (uploading needs an EDITOR or ADMIN on this site)"
|
|
355
|
+
: err && err.httpStatus === 401
|
|
356
|
+
? " (access to this site was revoked — add it again in sleekcms)"
|
|
357
|
+
: "";
|
|
358
|
+
if (flags.json) console.log(JSON.stringify({ ok: false, source: filePath, error: msg + hint }));
|
|
359
|
+
else console.error(`❌ ${path.basename(filePath)}: ${msg}${hint}`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
process.exit(failed ? 1 : 0);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Run only when invoked directly (`node upload.mjs`), not when imported by tests.
|
|
366
|
+
const isMain = import.meta.url === pathToFileURL(process.argv[1] || "").href;
|
|
367
|
+
if (isMain) {
|
|
368
|
+
main().catch((err) => {
|
|
369
|
+
console.error("❌", err && err.message ? err.message : String(err));
|
|
370
|
+
process.exit(2);
|
|
371
|
+
});
|
|
372
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sleekcms/sync",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.4.0",
|
|
4
4
|
"description": "Edit SleekCMS sites locally — models, content, templates, images — with live two-way sync and AI agent support.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sleekcms",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"dist"
|
|
29
29
|
],
|
|
30
30
|
"scripts": {
|
|
31
|
-
"build": "npm run clean && node esbuild.config.mjs && cp src/AGENTS.md dist/ && cp src/build.mjs dist/ && chmod +x dist/index.js",
|
|
31
|
+
"build": "npm run clean && node esbuild.config.mjs && cp src/AGENTS.md dist/ && cp src/build.mjs src/upload.mjs dist/ && chmod +x dist/index.js",
|
|
32
32
|
"dev": "tsx src/index.tsx",
|
|
33
33
|
"typecheck": "tsc --noEmit",
|
|
34
34
|
"clean": "rm -rf dist",
|