@sleekcms/sync 4.2.0 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -174,6 +174,8 @@ No manual deploy. No waiting. SleekCMS rebuilds on every sync and the preview re
174
174
  ```
175
175
  <site-name>/
176
176
  ├── AGENTS.md # AI agent reference (auto-generated)
177
+ ├── build.mjs # Build / publish the site: node build.mjs [--publish]
178
+ ├── upload.mjs # Upload images and documents: node upload.mjs <path>...
177
179
  ├── .vscode/
178
180
  │ ├── settings.json # Editor settings (site .json files open as JSON5)
179
181
  │ └── extensions.json # Recommends the JSON5 extension
@@ -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. Upload new files in the CMS.
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
@@ -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:** you can't upload a file from the workspace. A local path is not an image source, and `url:` needs a public URL. When the user wants their own photo or logo on the site, use `url:<public url>` if they give you one. Otherwise ask them to upload it in the CMS under a handle. After the workspace re-fetches from the server, the upload appears in `content/images.json` as `"<handle>": "url:<path>"`. Leave that value as it is and reference the image as `cms:<handle>`. Until then, ship a stock placeholder in the slot as usual.
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: `![alt](pexels:doctor)`. Append `|<alt>` for alt text and an optional `<W>x<H>` token to set rendered size (default `600x400`): `![doctor](pexels:doctor|Friendly doctor 800x600)`. Refs are rewritten to CDN URLs on save.
348
348
 
@@ -377,7 +377,24 @@ A `file` field takes a listed handle (`"lunch-menu"`) or file name (`"lunch-menu
377
377
  <% if (menu) { %><a href="<%= menu.url %>" download><%= menu.name %></a><% } %>
378
378
  ```
379
379
 
380
- **The user's own documents:** you can't upload a file from the workspace. Ask the user to upload it in the CMS (Media > Files); it appears in `content/files.json` after the next sync. Until then, a `file` field with a new file name gets a placeholder the user replaces in the CMS.
380
+ **The user's own documents:** upload them from disk with `node upload.mjs` (below); the file is listed in `content/files.json` right away. If the document isn't on this machine, a `file` field with a new file name gets a placeholder the user replaces in the CMS (Media > Files).
381
+
382
+ ### Uploading images and documents (`upload.mjs`)
383
+
384
+ When the user gives you an image or document as a file on disk (a logo, team photos, a menu PDF, a price list), upload it. Run this from the workspace root, with one or more paths:
385
+
386
+ ```bash
387
+ node upload.mjs ~/Downloads/logo.png --handle logo # image → cms:logo
388
+ node upload.mjs photos/*.jpg # images; handle = file name without extension
389
+ node upload.mjs ~/Desktop/Lunch\ Menu.pdf # document → Media > Files
390
+ node upload.mjs menu-v2.pdf --name lunch-menu.pdf # store it under another file name
391
+ ```
392
+
393
+ - **Images** (png, jpg, jpeg, webp, gif, svg, ico, up to 8 MB) become site images under a handle. The handle is `--handle`, or by default the file name lowercased and dashed (`Team Photo.jpg` → `team-photo`). The script adds the handle to `content/images.json`. Reference the image as `"cms:<handle>"` in any `image` field, or with `img('cms:<handle>')` / `src('cms:<handle>')` in a view. Uploading to an existing handle replaces that image everywhere it's used.
394
+ - **Documents** (pdf, docx, xlsx, pptx, csv, tsv, txt, md, up to 10 MB) are added to Media > Files under their file name, and `content/files.json` is rewritten to list them. Put the printed file name (or its handle) in a `file` field, or render it with `getFile('<handle>')`. Uploading the same file name again replaces that file.
395
+ - The script prints the ref to use for each file. Copy it from the output rather than guessing it, because the server cleans up file names. Add `--json` for one JSON result per line. Exit code `0` means every file uploaded, `1` means some failed (the reason is printed), and `2` means it couldn't run.
396
+ - Uploading needs an EDITOR or ADMIN on the site.
397
+ - Never copy images or documents into `src/`. Binary files there are not synced; the sync log just points you back to `upload.mjs`.
381
398
 
382
399
  ### Markdown content files (`.md`)
383
400
 
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import{readFileSync as qt}from"node:fs";import{fileURLToPath as Or}from"node:url";import Ue from"node:path";import{render as Ir}from"ink";import{execSync as rt,spawn as B}from"child_process";import{program as Qe,Option as et}from"commander";function st(){return Qe.name("cms-sync").description("SleekCMS CLI tool to sync and edit CMS templates locally. Add each site you want to sync by approving it in your browser \u2014 changes are watched and pushed automatically.").addOption(new et("-v, --version","output the version number").hideHelp()).addOption(new et("-e, --env <env>","Environment (localhost, development, production)").default("production").hideHelp()).option("-p, --path <path>","Directory path for files (default: ~/SleekCMS)").addHelpText("after",`
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} ${A}`);for(let C of W)s(` ${C}`);x++}return await mt(e,f),{renamed:x,held:i}}var Ce="sync-errors.log",Ee=new Map;async function ht(e){let t=b.resolve(e),n=Ee.get(t);if(n)return n;let r=await Fn(t);return Ee.set(t,r),r}async function Fn(e){let t=b.join(e,Ce);if(!await h.pathExists(t))return{};let n=await h.readFile(t,"utf-8"),r={};for(let s of n.split(`
8
- `)){let i=s.indexOf(": ");i>0&&(r[s.slice(0,i)]=s.slice(i+2))}return r}async function mt(e,t){let n=b.join(e,Ce),r=Object.entries(t);if(r.length===0){await h.remove(n);return}await h.outputFile(n,r.map(([s,i])=>`${s}: ${i}`).join(`
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}|${Dr(e.message)}|${t?.trim()??""}`},Dr=e=>e.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi,"<id>").replace(/\b[0-9a-f]{16,}\b/gi,"<id>").replace(/\d+/g,"<n>").slice(0,300),_r=e=>{if(e instanceof Error)return e;let t=new Error(typeof e=="string"?e:Ge.inspect(e,{depth:3,breakLength:1/0}));return t.name="NonErrorThrown",t},ve=(()=>{try{return Gt.homedir()}catch{return""}})(),Ar=e=>{if(ve.length<2||!`${e.message}
12
- ${e.stack}`.includes(ve))return e;let t=new Error(e.message.split(ve).join("~"));return t.name=e.name,t.stack=e.stack?.split(ve).join("~"),t};import{jsx as Gr}from"react/jsx-runtime";var He=st();He.version&&(console.log(he),process.exit(0));var Xt=(He.env??"production").toLowerCase();Ht({service:"cms-sync",environment:process.env.DO_NOT_TRACK==="1"?null:Xt,quiet:!0});var Yt=Ue.dirname(Or(import.meta.url)),Fr=qt(Ue.join(Yt,"AGENTS.md"),"utf-8"),Nr=qt(Ue.join(Yt,"build.mjs"),"utf-8");async function Wr(){let e=new pe(Fr,Nr);e.basePath=He.path,await e.init(Xt),process.stdout.isTTY&&process.stdout.write("\x1B[2J\x1B[3J\x1B[H");let t=Ir(Gr(At,{controller:e}),{exitOnCtrlC:!1}),n=r=>{t.unmount(),console.log(r),process.exit(0)};e.once("exit",()=>n("\u2713 Stopped. Local site files were cleaned up.")),process.on("SIGINT",()=>{e.exit()}),process.on("SIGTERM",()=>{e.exit()}),await t.waitUntilExit()}Wr().catch(async e=>{let t=e;console.error("\u274C",t.body||t.message),t.body||(Jt(e),await zt()),process.exit(1)});
10
+ `)}async function Gn(e,t,n,r,s){Be.delete(b.resolve(e)),await h.remove(b.join(e,Me)),r("\u{1F4E5} Fetching files...");let i=await ee(t,n,"GET","/get_files"),u={},a=0;for(let y of i){let w=b.join(e,y.path);await h.outputFile(w,y.content);let d=(await h.stat(w)).mtimeMs;a++,s?.(a,i.length),u[y.path]={mtimeMs:d,hash:z(y.content)}}return r(`\u2714\uFE0F Synced ${i.length} file(s).`),{fileMap:u,pulled:a}}import Un from"path";import Hn from"chokidar";var Jn=5e3,zn=/(^|[/\\])(\.DS_Store|Thumbs\.db|desktop\.ini)$/,te=class{viewsDir;onSync;debounceMs;watcher=null;debounceTimer=null;isShuttingDown=!1;dirty=!1;syncInFlight=!1;constructor(t){this.viewsDir=t.viewsDir,this.onSync=t.onSync,this.debounceMs=t.debounceMs??Jn}start(){if(this.watcher)return;let t=Un.join(this.viewsDir,"src");this.watcher=Hn.watch([t],{persistent:!0,ignoreInitial:!0,ignored:zn}).on("change",()=>this.scheduleSync()).on("add",()=>this.scheduleSync()).on("unlink",()=>this.scheduleSync()).on("addDir",()=>this.scheduleSync()).on("unlinkDir",()=>this.scheduleSync())}setShuttingDown(t){this.isShuttingDown=t}async stop(){this.debounceTimer&&(clearTimeout(this.debounceTimer),this.debounceTimer=null),this.watcher&&(await this.watcher.close(),this.watcher=null)}scheduleSync(){this.isShuttingDown||(this.dirty=!0,this.debounceTimer&&clearTimeout(this.debounceTimer),this.debounceTimer=setTimeout(()=>this.flush(),this.debounceMs))}async flush(){if(this.debounceTimer=null,!(!this.dirty||this.isShuttingDown)){if(this.syncInFlight){this.debounceTimer=setTimeout(()=>this.flush(),this.debounceMs);return}this.dirty=!1,this.syncInFlight=!0;try{await this.onSync()}catch(t){let n=t;console.error("\u274C Sync failed:",n.body||n.message)}finally{this.syncInFlight=!1}}}};import Re from"fs-extra";import qn from"os";import xt from"path";var St={localhost:"http://app.sleekcms.test/api",development:"https://app.sleekcms.dev/api",production:"https://app.sleekcms.com/api"};function ne(e){return St[e]||St.production}function je(e){return ne(e).replace(/\/api$/,"")}var Xn=xt.join(qn.homedir(),"SleekCMS"),q=xt.join(Xn,"auth.json"),Le=class extends Error{status;body;constructor(t,n,r=""){super(t),this.name="ApiError",this.status=n,this.body=r}};async function pe(e=q){let t=null;try{t=await Re.readJson(e)}catch{return{sites:[],hadLegacyLogin:!1}}return{sites:Array.isArray(t?.sites)?t.sites.filter(r=>r&&typeof r.token=="string"&&typeof r.siteId=="number"):[],hadLegacyLogin:typeof t?.userToken=="string"}}async function Tt(e,t=q){await Re.outputJson(t,{sites:e},{spaces:2});try{await Re.chmod(t,384)}catch{}}async function De(e,t=q){let{sites:n}=await pe(t),r=[...n.filter(s=>!(s.env===e.env&&s.siteId===e.siteId)),e];return await Tt(r,t),r}async function _e(e,t,n=q){let{sites:r}=await pe(n),s=r.filter(i=>!(i.env===e&&i.siteId===t));return await Tt(s,n),s}async function Ae(e,t,n,r){let s=await fetch(ne(e)+r,{method:n,headers:{Authorization:`Bearer ${t}`}});if(!s.ok){let i=await s.text().catch(()=>"");throw new Le(`${n} ${r} \u2192 ${s.status}: ${i}`,s.status,i)}return s.json()}function Yn(e){if(e==null)return"(unknown error)";if(typeof e=="string")return e;if(typeof e!="object")return String(e);let t=e,n=typeof t.file_path=="string"?t.file_path:"",r=typeof t.path=="string"?t.path:typeof t.filename=="string"?t.filename:"",s=n||r,i=t.line!=null?`:${t.line}`:"",u=t.column!=null?`:${t.column}`:"",a=typeof t.message=="string"&&t.message?t.message:typeof t.name=="string"&&t.name?t.name:"",y=n&&r&&r!==n?` (on page ${r})`:"";if(s)return`${s}${i}${u}${y}: ${a||"(no message)"}`;if(a)return a;try{return JSON.stringify(e)}catch{return"(unknown error)"}}async function kt(e,t){let n;try{n=await Ae(e,t,"GET","/site_build")}catch{return null}if(!Array.isArray(n)||n.length===0)return null;let r=n[0],s=n.find(u=>typeof u.url=="string"&&u.url.length>0),i=Array.isArray(r.errors)?r.errors.map(Yn):[];return i.length>0?{status:"error",mode:"preview",errors:i,url:s?.url||void 0,at:r.updated_at?Date.parse(r.updated_at):void 0}:s?{status:"success",mode:"preview",url:s.url||void 0,at:s.updated_at?Date.parse(s.updated_at):void 0}:null}import{createHash as Kn,randomBytes as Vn}from"crypto";var Zn="urn:sleekcms:grant-type:cli_poll",Qn="SleekCMS Sync",$t=e=>e.toString("base64").replace(/=+$/,"").replace(/\+/g,"-").replace(/\//g,"_");function Et(){let e=$t(Vn(32)),t=$t(Kn("sha256").update(e).digest());return{verifier:e,challenge:t}}function Bt(e){let t=e.replace(/[^A-Za-z0-9]/g,"").slice(0,8).toUpperCase();return`${t.slice(0,4)}-${t.slice(4)}`}async function Pt(e,t=fetch){let n={origin:je(e),authorizeEndpoint:`${ne(e)}/oauth/authorize`};try{let r=await t(`${je(e)}/.well-known/oauth-authorization-server`);if(!r.ok)return n;let s=await r.json();if(typeof s.issuer=="string"&&typeof s.authorization_endpoint=="string")return{origin:s.issuer.replace(/\/+$/,""),authorizeEndpoint:s.authorization_endpoint}}catch{}return n}function Ct(e,t){let n=new URLSearchParams({response_type:"code",client_name:Qn,redirect_uri:`${e.origin}/oauth/cli-done`,code_challenge:t,code_challenge_method:"S256",resource:`${e.origin}/mcp/`});return`${e.authorizeEndpoint}?${n.toString()}`}var X=class extends Error{constructor(t){super(t),this.name="SignInError"}},er=e=>new Promise(t=>setTimeout(t,e));async function Mt(e,t,n={}){let{timeoutMs:r=10*6e4,signal:s,fetchImpl:i=fetch,sleep:u=er}=n,a=n.intervalMs??2e3,y=Date.now()+r,w=`${ne(e)}/oauth/token`;for(;;){if(s?.aborted)throw new X("Cancelled.");let d=null;try{d=await i(w,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({grant_type:Zn,code_verifier:t})})}catch{d=null}if(d){let g=await d.json().catch(()=>({}));if(d.ok&&g.access_token)return g.access_token;if(d.status===429)a=Math.min(a*2,3e4);else if(g.error!=="authorization_pending")throw new X(g.error==="invalid_grant"?"That approval can no longer be used. Add the site again.":`Sign-in failed: ${g.error||`HTTP ${d.status}`}`)}if(Date.now()+a>y)throw new X("Timed out waiting for approval in the browser.");await u(a)}}function tr(e){let t=e.split(".")[1];if(!t)throw new X("The server returned a token this version can't read.");return JSON.parse(Buffer.from(t,"base64url").toString("utf8"))}async function Rt(e,t){let n=tr(t),r=await Ae(e,t,"GET","/mcp/get_site");return{env:e,siteId:n.sid,slug:n.slug,name:r.name,org:{name:r.org},role:r.role||void 0,token:t,addedAt:Date.now()}}var Lt=500;function ir(){return new Date().toTimeString().slice(0,8)}function or(e){let t=e;if(t.body){try{let n=JSON.parse(t.body);if(n&&typeof n.message=="string")return n.message;if(n&&typeof n.error=="string")return n.error}catch{}return t.body}return t.message}function jt(e){return e instanceof W&&e.status===401}var ar=e=>({id:e.siteId,name:e.name,slug:e.slug});async function Dt(e){e&&(await re.remove(Oe.join(e,"src")),await re.remove(Oe.join(e,".cache","state.json")))}var fe=class extends nr{env="production";saved=[];synced=new Map;connecting=null;message;messageIsError=!1;agentMd;buildScript;uploadScript;configPath;isShuttingDown=!1;basePath;pollOptions={};openBrowser=ge;constructor(t,n={},r=q){super(),this.agentMd=t,this.buildScript=n.build,this.uploadScript=n.upload,this.configPath=r}changed(){this.emit("change")}flash(t,n=!1){this.message=t,this.messageIsError=n,this.changed()}appendLog(t,n){let r=this.synced.get(t);r&&(r.logs.push({time:ir(),text:n}),r.logs.length>Lt&&r.logs.splice(0,r.logs.length-Lt),this.changed())}siteCount(){return this.synced.size}syncingCount(){let t=0;for(let n of this.synced.values())n.status==="syncing"&&t++;return t}watchingCount(){let t=0;for(let n of this.synced.values())(n.status==="watching"||n.status==="syncing")&&t++;return t}async init(t){this.env=t;let{sites:n,hadLegacyLogin:r}=await pe(this.configPath);this.saved=n.filter(s=>s.env===t),r&&this.flash("SleekCMS Sync now connects one site at a time \u2014 press a to add a site in your browser."),this.changed()}isSynced(t){return this.synced.has(t)}async connectSite(){if(this.connecting)return;let{verifier:t,challenge:n}=Et(),r=new AbortController;this.connecting={code:Bt(n),url:"",abort:r},this.changed();let s=Ct(await Pt(this.env),n);if(r.signal.aborted){this.connecting=null,this.changed();return}this.connecting.url=s,this.message=void 0,this.changed(),this.openBrowser(s)||this.flash("Couldn't open a browser \u2014 open the address shown to continue.",!0);let i;try{let a=await Mt(this.env,t,{...this.pollOptions,signal:r.signal});i=await Rt(this.env,a)}catch(a){this.connecting=null,r.signal.aborted?this.changed():this.flash(or(a),!0);return}this.connecting=null;let u=await De(i,this.configPath);this.saved=u.filter(a=>a.env===this.env),this.flash(`Added "${i.name}".`),this.synced.has(i.siteId)||await this.startSite(i)}cancelConnect(){this.connecting?.abort.abort()}async startSite(t){let n=ar(t);if(this.synced.has(n.id))return;let r={org:t.org,site:n,siteToken:t.token,role:t.role,viewsDir:"",status:"syncing",logs:[]};this.synced.set(n.id,r),this.appendLog(n.id,"\u2795 added \u2014 pulling files"),this.changed();try{let s=await Pe({token:t.token,env:this.env,path:this.basePath,agentMd:this.agentMd,buildScript:this.buildScript,uploadScript:this.uploadScript,log:i=>this.appendLog(n.id,i),onProgress:(i,u)=>{r.progress={done:i,total:u},this.changed()}});this.noteRole(n.id,s.site.role),r.viewsDir=s.viewsDir,r.progress=void 0,r.status="watching",r.lastSyncedAt=Date.now(),r.lastMsg=`pulled ${s.pulled} file(s)`,r.watcher=new te({viewsDir:s.viewsDir,onSync:()=>this.runSync(n.id)}),r.watcher.start(),this.watchBuildStatus(n.id),this.seedLastBuild(n.id),this.appendLog(n.id,"\u{1F440} watching for changes")}catch(s){if(jt(s))return void await this.revoke(n.id);let i=s;r.status="error",r.progress=void 0,r.lastMsg=i.body||i.message,this.appendLog(n.id,`\u274C ${i.body||i.message}`)}this.changed()}noteRole(t,n){let r=this.synced.get(t);if(!n||!r||r.role===n)return;r.role=n;let s=this.saved.find(i=>i.siteId===t);s&&(s.role=n,De({...s},this.configPath).catch(()=>{}))}async revoke(t){let n=this.synced.get(t),r=n?.site.name??this.saved.find(u=>u.siteId===t)?.name??"this site";n&&(n.watcher?.setShuttingDown(!0),await n.watcher?.stop(),await n.buildWatcher?.close(),this.synced.delete(t));let s=await _e(this.env,t,this.configPath);this.saved=s.filter(u=>u.env===this.env);let i=n?.viewsDir?` Local files kept in ${n.viewsDir}.`:"";this.flash(`Access to "${r}" was revoked \u2014 press a to add it again.${i}`,!0)}async forgetSite(t){if(this.synced.has(t)){this.flash("Disconnect it first (d), then press x to forget it.",!0);return}let n=this.saved.find(s=>s.siteId===t)?.name??"the site",r=await _e(this.env,t,this.configPath);this.saved=r.filter(s=>s.env===this.env),this.flash(`Forgot "${n}". To revoke its access everywhere, log out of all sessions in SleekCMS.`)}async removeSite(t){let n=this.synced.get(t);n&&(n.watcher?.setShuttingDown(!0),await n.watcher?.stop(),await n.buildWatcher?.close(),await Dt(n.viewsDir),this.synced.delete(t),this.changed())}buildSite(t,n=!1){let r=this.synced.get(t);if(!r||!r.viewsDir)return;if(r.role&&r.role!=="ADMIN"){this.flash(`Building and publishing are for site ADMINs \u2014 you're ${r.role} on "${r.site.name}".`,!0);return}let s=n?"live":"preview";this.writeBuildFile(r.viewsDir,{status:"building",mode:s,stage:"queued",url:r.lastBuild?.url,at:Date.now()}),rr("node",n?["build.mjs","--publish"]:["build.mjs"],{cwd:r.viewsDir,stdio:"ignore"}).on("error",a=>{this.writeBuildFile(r.viewsDir,{status:"error",mode:s,errors:[a.message],url:r.lastBuild?.url,at:Date.now()}),this.appendLog(t,`\u274C build: ${a.message}`)}),this.appendLog(t,n?"\u{1F680} publishing\u2026":"\u{1F528} building\u2026")}buildFile(t){return Oe.join(t,".cache","build.json")}async readBuildFile(t){try{let n=await re.readJson(this.buildFile(t));return n&&n.status?n:null}catch{return null}}async writeBuildFile(t,n){let r=this.buildFile(t),s=`${r}.tmp`;try{await re.outputJson(s,n),await re.rename(s,r)}catch{}}watchBuildStatus(t){let n=this.synced.get(t);!n||!n.viewsDir||(n.buildWatcher=sr.watch(this.buildFile(n.viewsDir),{ignoreInitial:!1}).on("add",()=>{this.readBuildStatus(t)}).on("change",()=>{this.readBuildStatus(t)}))}async readBuildStatus(t){let n=this.synced.get(t);if(!n)return;let r=await this.readBuildFile(n.viewsDir);if(!r)return;let s=n.lastBuild?.url;n.lastBuild={status:r.status,mode:r.mode??"preview",url:r.url??(r.status==="success"?void 0:s),errors:r.errors,stage:r.stage,at:r.at},this.changed()}async seedLastBuild(t){let n=this.synced.get(t);if(!n)return;let r=await kt(this.env,n.siteToken);if(!r)return;let s=this.synced.get(t);if(!s)return;let i=await this.readBuildFile(s.viewsDir);i?.at!=null&&(r.at==null||i.at>=r.at)||await this.writeBuildFile(s.viewsDir,r)}async refetch(t){await this.runSync(t,{flush:!0})}async runSync(t,n={}){let r=this.synced.get(t);if(!(!r||!r.viewsDir||r.status==="paused")){r.status="syncing",this.changed();try{let s=await Pe({token:r.siteToken,env:this.env,viewsDir:r.viewsDir,agentMd:this.agentMd,buildScript:this.buildScript,uploadScript:this.uploadScript,flush:n.flush,log:i=>this.appendLog(t,i),onProgress:(i,u)=>{r.progress={done:i,total:u},this.changed()}});if(this.noteRole(t,s.site.role),r.progress=void 0,r.status="watching",r.lastSyncedAt=Date.now(),n.flush)r.lastMsg=`re-fetched ${s.pulled} file(s)`;else{let i=[s.renamed>0?`renamed ${s.renamed}`:"",s.pushed>0?`pushed ${s.pushed} file(s)`:""].filter(Boolean);r.lastMsg=i.length>0?i.join(", "):"up to date"}}catch(s){if(jt(s))return void await this.revoke(t);let i=s;r.status="error",r.progress=void 0,r.lastMsg=i.body||i.message,this.appendLog(t,`\u274C ${i.body||i.message}`)}this.changed()}}async togglePause(t){let n=this.synced.get(t);!n||!n.viewsDir||(n.status==="paused"?(n.watcher=new te({viewsDir:n.viewsDir,onSync:()=>this.runSync(t)}),n.watcher.start(),n.status="watching",this.appendLog(t,"\u25B6 resumed \u2014 watching for changes")):(n.watcher?.setShuttingDown(!0),await n.watcher?.stop(),n.watcher=void 0,n.progress=void 0,n.status="paused",this.appendLog(t,"\u23F8 paused")),this.changed())}async cleanupAll(){this.cancelConnect();for(let t of this.synced.values())t.watcher?.setShuttingDown(!0),await t.watcher?.stop(),await t.buildWatcher?.close(),await Dt(t.viewsDir)}async exit(){this.isShuttingDown||(this.isShuttingDown=!0,await this.cleanupAll(),this.emit("exit"))}};import{useInput as kr}from"ink";import{Fragment as dr,useEffect as se,useMemo as gr,useRef as pr,useState as j}from"react";import{Box as f,Text as l,useInput as fr,useStdout as hr}from"ink";import mr from"ink-text-input";import{createRequire as cr}from"node:module";var lr=cr(import.meta.url),he=lr("../package.json").version;import ur from"node:os";function Ie(e){if(!e)return"\u2014";let t=ur.homedir();return e.startsWith(t)?"~"+e.slice(t.length):e}function Fe(e){if(!e)return"\u2014";let t=Math.floor((Date.now()-e)/1e3);if(t<5)return"just now";if(t<60)return`${t}s ago`;let n=Math.floor(t/60);if(n<60)return`${n}m ago`;let r=Math.floor(n/60);return r<24?`${r}h ago`:`${Math.floor(r/24)}d ago`}function we(e){return!e||e.total<=0?null:Math.round(e.done/e.total*100)}var me=["\u280B","\u2819","\u2839","\u2838","\u283C","\u2834","\u2826","\u2827","\u2807","\u280F"];function Ne(e){return me[(e%me.length+me.length)%me.length]}function ye(e){switch(e){case"syncing":return{dot:"\u25CF",color:"green",label:"syncing"};case"watching":return{dot:"\u25CB",color:"gray",label:"watching"};case"paused":return{dot:"\u2016",color:"yellow",label:"paused"};case"error":return{dot:"\u2717",color:"red",label:"error"}}}function _t(e){if(!e)return{dot:"\xB7",color:"gray",label:"\u2014"};switch(e.status){case"building":return{dot:"\u2234",color:"yellow",label:"building"};case"error":return{dot:"\u2717",color:"red",label:"failed"};case"success":return{dot:"\u2713",color:"green",label:e.mode==="live"?"live":"preview"}}}import{Fragment as We,jsx as o,jsxs as p}from"react/jsx-runtime";var wr=(e,t)=>t<=0?0:Math.max(0,Math.min(e,t-1)),yr=16,br=5,vr="greenBright",At="blueBright",Ot="magentaBright";function It({k:e,bg:t=vr}){return o(l,{color:"black",backgroundColor:t,children:` ${e} `})}function I({k:e,label:t,width:n=16,bg:r}){return p(f,{width:n,children:[o(It,{k:e,bg:r}),o(l,{color:"gray",children:` ${t}`})]})}function E({k:e,label:t,bg:n}){return p(f,{marginBottom:1,children:[o(f,{width:9,children:o(It,{k:e,bg:n})}),o(l,{color:"gray",children:t})]})}function Sr({launchers:e}){return p(f,{flexDirection:"column",borderStyle:"round",borderColor:"green",paddingX:2,paddingY:1,width:66,children:[p(f,{justifyContent:"space-between",children:[o(l,{color:"green",bold:!0,children:"KEYBOARD SHORTCUTS"}),o(l,{color:"gray",children:"? or esc to close"})]}),p(f,{marginTop:1,children:[p(f,{flexDirection:"column",width:34,children:[o(l,{color:"cyan",bold:!0,children:"Navigate"}),o(E,{k:"\u2191\u2193",label:"move selection"}),o(E,{k:"tab",label:"focus logs \xB7 \u2191\u2193 scroll"}),o(E,{k:"esc",label:"clear filter / back"}),o(f,{marginTop:1,children:o(l,{color:"cyan",bold:!0,children:"Global"})}),o(E,{k:"/",label:"search sites"}),o(E,{k:"a",label:"add a site (browser)"}),o(E,{k:"?",label:"toggle this help",bg:At}),o(E,{k:"ctrl+c",label:"quit"})]}),p(f,{flexDirection:"column",flexGrow:1,children:[o(l,{color:"cyan",bold:!0,children:"Site actions"}),o(E,{k:"s",label:"start sync"}),o(E,{k:"d",label:"disconnect"}),o(E,{k:"x",label:"forget site"}),o(E,{k:"p",label:"pause / resume sync"}),o(E,{k:"b",label:"build preview"}),o(E,{k:"B",label:"publish live"}),o(E,{k:"o",label:"open preview url"}),e.map((t,n)=>o(E,{k:String(n+1),label:`open in ${t.label}`,bg:Ot},t.key))]})]})]})}function xr({percent:e,width:t=28}){let n=Math.round(Math.max(0,Math.min(100,e))/100*t);return p(l,{children:[o(l,{color:"green",children:"\u2588".repeat(n)}),o(l,{color:"gray",children:"\u2591".repeat(Math.max(0,t-n))})]})}function Tr({frame:e,width:t=24,seg:n=6}){let r=t+n,s=e%r-n,i="";for(let u=0;u<t;u++)i+=u>=s&&u<s+n?"\u25AE":"\u25AF";return o(l,{color:"yellow",children:i})}function Ft({controller:e}){let{stdout:t}=hr(),n=gr(()=>lt(),[]),[,r]=j(0),[s,i]=j(()=>new Date),[u,a]=j(null),[y,w]=j(""),[d,g]=j(!1),[x,m]=j(0),[v,k]=j(!1),[C,D]=j(!1),[M,R]=j("list"),[K,_]=j(0),qe=()=>r(c=>c+1);se(()=>(e.on("change",qe),()=>{e.off("change",qe)}),[e]),se(()=>{let c,S=()=>{i(new Date),c=setTimeout(S,6e4-Date.now()%6e4)};return c=setTimeout(S,6e4-Date.now()%6e4),()=>clearTimeout(c)},[]);let V=e.saved.map(c=>({org:c.org,site:{id:c.siteId,name:c.name,slug:c.slug},saved:c,entry:e.synced.get(c.siteId)})).sort((c,S)=>(c.entry?0:1)-(S.entry?0:1)),G=e.connecting,F=y.trim().toLowerCase(),P=F?V.filter(c=>c.site.name.toLowerCase().includes(F)||(c.org.name||"").toLowerCase().includes(F)):V,U=P.findIndex(c=>c.site.id===u);U<0&&(U=0);let $=P[U],T=$?.entry,oe=Math.max(3,(t?.rows??30)-26),N=T?.logs??[],Te=Math.max(0,N.length-oe),Zt=Math.min(K,Te),ae=N.length-Zt,Xe=N.slice(Math.max(0,ae-oe),ae),Qt=$?.site.id;se(()=>_(0),[Qt]);let A=T?.lastBuild,ke=A?.status==="building",$e=_t(A),ce=ke||!!G;if(!ce){for(let c of e.synced.values())if(c.status==="syncing"){ce=!0;break}}se(()=>{if(!ce)return;let c=setInterval(()=>m(S=>S+1),120);return()=>clearInterval(c)},[ce]);let Ye=P.findIndex(c=>!c.entry),H=Math.max(br,(t?.rows??30)-yr),Z=P.length,le=Z>H,en=Math.floor(H/2),Q=le?Math.max(0,Math.min(U-en,Z-H)):0,Ee=Math.min(Z,Q+H),tn=P.slice(Q,Ee),Ke=pr(!1);se(()=>{Ke.current||(Ke.current=!0,e.saved.length===0&&!e.connecting&&e.connectSite())},[e]);let ue=c=>{if(P.length===0)return;let S=wr(U+c,P.length);a(P[S].site.id)},nn=()=>{if($){if($.entry){e.flash("Already syncing \u2014 press d to disconnect.");return}e.startSite($.saved)}},rn=()=>{$?.entry&&e.removeSite($.site.id)},sn=()=>{$&&e.forgetSite($.site.id)},on=c=>{if(!T){e.flash("Press s to start syncing this site first.",!0);return}D(!0);let S=c.run(T.viewsDir);e.flash(S?`Opened ${c.label} at ${Ie(T.viewsDir)}`:`Couldn't open ${c.label}.`,!S)},an=()=>{if(!T){e.flash("Press s to start syncing this site first.",!0);return}e.togglePause(T.site.id)},Ve=c=>{if(!T){e.flash("Press s to start syncing this site first.",!0);return}e.buildSite(T.site.id,c),e.flash(c?"Publishing \u2014 making this live\u2026":"Building preview\u2026")},cn=()=>{let c=T?.lastBuild?.url;if(!c){e.flash("No build URL yet \u2014 press b to build.",!0);return}e.flash(ge(c)?`Opening ${c}`:"Couldn't open the browser.",!1)};fr((c,S)=>{if(v){(c==="?"||S.escape)&&k(!1);return}if(G&&S.escape)return void e.cancelConnect();if(d){S.escape?(g(!1),w(""),a(null)):S.return?g(!1):S.upArrow?ue(-1):S.downArrow&&ue(1);return}if(S.tab){M==="logs"?R("list"):T&&R("logs"),_(0);return}if(M==="logs"){if(S.escape){R("list"),_(0);return}if(S.upArrow)return void _(O=>Math.min(O+1,Te));if(S.downArrow)return void _(O=>Math.max(0,O-1))}if(S.escape){w(""),a(null);return}if(c==="?")return void k(!0);if(c==="/")return R("list"),void g(!0);if(c==="a"||c==="n")return void e.connectSite();if(S.upArrow)return ue(-1);if(S.downArrow)return ue(1);if(c==="s")return nn();if(c==="d")return rn();if(c==="x")return sn();if(c==="p")return an();if(c==="b")return Ve(!1);if(c==="B")return Ve(!0);if(c==="o")return cn();if(c>="1"&&c<="9"){let O=n[parseInt(c,10)-1];if(O)return on(O)}});let Ze=p(f,{justifyContent:"space-between",paddingX:1,children:[p(f,{children:[o(l,{color:"green",bold:!0,children:"\u25C6 SleekCMS"}),o(l,{color:"gray",children:` v${he} \u2502 `}),o(l,{children:e.env==="production"?"Sync":`Sync \xB7 ${e.env}`})]}),p(f,{children:[p(l,{color:"gray",children:[F?`${P.length}/${V.length}`:V.length," sites \xB7 "]}),p(l,{color:"green",children:[e.watchingCount()," watching"]}),o(l,{color:"gray",children:` \xB7 ${s.toTimeString().slice(0,5)}`})]})]});return v?p(f,{flexDirection:"column",children:[Ze,o(f,{marginTop:1,justifyContent:"center",children:o(Sr,{launchers:n})})]}):p(f,{flexDirection:"column",children:[Ze,p(f,{children:[p(f,{flexDirection:"column",borderStyle:"round",borderColor:"green",width:38,paddingX:1,children:[p(f,{justifyContent:"space-between",children:[o(l,{color:"green",bold:!0,children:"SITE WORKSPACES"}),V.length>H&&!d&&!F?o(l,{color:"gray",children:"/ filter"}):null]}),d?p(f,{children:[o(l,{color:"cyan",children:"/ "}),o(mr,{value:y,onChange:w,onSubmit:()=>g(!1),placeholder:"type to filter\u2026"})]}):F?o(l,{color:"gray",children:`/ ${y} \xB7 ${P.length} match${P.length===1?"":"es"}`}):null,o(f,{flexDirection:"column",marginTop:1,minHeight:le?H+3:void 0,children:P.length===0?o(l,{color:"gray",children:F?`no matches for \u201C${y}\u201D`:"no sites yet \u2014 press a to add one"}):p(We,{children:[le?o(l,{color:"gray",children:Q>0?` \u2191 ${Q} more`:" "}):null,tn.map((c,S)=>{let O=Q+S,Qe=c.entry?ye(c.entry.status):{dot:"\xB7",color:"gray",label:"idle"},et=we(c.entry?.progress),ln=c.entry?c.entry.status==="syncing"&&et!=null?`${et}%`:c.entry.status==="paused"?"paused":"--":"",de=O===U,un=O===Ye&&Ye>0;return p(dr,{children:[un?o(l,{color:"gray",children:"\u2500".repeat(34)}):null,p(f,{justifyContent:"space-between",children:[p(f,{children:[o(l,{color:de?"green":"gray",children:de?"\u276F ":" "}),p(l,{color:c.entry?.status==="watching"||c.entry?.status==="syncing"?"green":Qe.color,children:[c.entry?.status==="syncing"?Ne(x):c.entry?.status==="watching"?"\u25CF":Qe.dot," "]}),o(l,{color:de?"white":c.entry?void 0:"gray",bold:de,children:c.site.name})]}),o(l,{color:c.entry?.status==="syncing"?"green":"gray",children:ln})]})]},c.site.id)}),le?o(l,{color:"gray",children:Ee<Z?` \u2193 ${Z-Ee} more`:" "}):null]})}),o(f,{marginTop:1,children:o(l,{color:"gray",children:"+ add site \xB7 a"})})]}),p(f,{flexDirection:"column",flexGrow:1,marginLeft:1,children:[o(f,{flexDirection:"column",borderStyle:"round",borderColor:"gray",paddingX:1,children:$?p(We,{children:[p(l,{children:[$.org.name?p(l,{color:"gray",children:[$.org.name," / "]}):null,o(l,{color:"cyan",bold:!0,children:$.site.name})]}),p(f,{marginTop:1,children:[o(l,{color:"gray",children:"status "}),T?o(l,{color:ye(T.status).color,children:`: ${ye(T.status).label}`}):o(l,{color:"gray",children:": idle \u2014 not syncing (press s to start)"})]}),p(f,{children:[o(l,{color:"gray",children:"role "}),o(l,{children:T?.role??$.saved.role??"\u2014"}),(T?.role??$.saved.role??"ADMIN")!=="ADMIN"?o(l,{color:"gray",children:" \xB7 build & publish need ADMIN"}):null]}),p(f,{children:[o(l,{color:"gray",children:"path "}),o(l,{wrap:"truncate-end",children:T?.viewsDir?Ie(T.viewsDir):"\u2014"})]}),p(f,{children:[o(l,{color:"gray",children:"synced "}),o(l,{children:Fe(T?.lastSyncedAt)}),o(l,{color:"gray",children:" progress "}),o(l,{color:T?.status==="syncing"?"green":"gray",children:T?.status==="syncing"?`${we(T.progress)??0}%`:"\u2014"})]}),o(f,{marginTop:1,children:o(xr,{percent:T?T.status==="syncing"?we(T.progress)??0:T.status==="watching"?100:0:0})}),T?p(We,{children:[p(f,{marginTop:1,children:[o(l,{color:"gray",children:"build "}),o(l,{color:$e.color,children:`${$e.dot} ${$e.label}`}),o(l,{color:"gray",children:" last "}),o(l,{children:ke?"in progress":Fe(A?.at)})]}),p(f,{children:[o(l,{color:"gray",children:"url "}),A?.url?o(l,{color:"cyan",wrap:"truncate-end",children:A.url}):o(l,{color:"gray",children:"\u2014"})]}),A?.status==="error"&&A.errors?.length?p(f,{children:[o(l,{color:"red",children:"\u258C "}),o(l,{color:"red",wrap:"truncate-end",children:`\u2717 ${A.errors[0]}`})]}):null,ke?p(f,{children:[o(Tr,{frame:x}),o(l,{color:"gray",children:` ${A?.stage??"building"}`})]}):null]}):null]}):o(l,{color:"gray",children:"No site selected."})}),G?p(f,{flexDirection:"column",borderStyle:"round",borderColor:"blue",paddingX:1,marginTop:1,flexGrow:1,children:[o(l,{color:"cyan",bold:!0,children:"ADD A SITE"}),o(l,{children:"Approve it in your browser: pick a site, or create one."}),p(f,{marginTop:1,children:[o(l,{color:"gray",children:"Check the page shows "}),o(l,{color:"cyan",bold:!0,children:G.code})]}),o(l,{color:"gray",children:"Only approve a page that shows this code."}),p(f,{marginTop:1,children:[p(l,{color:"yellow",children:[Ne(x)," "]}),o(l,{color:"gray",children:"waiting for approval \xB7 esc to cancel"})]})]}):p(f,{flexDirection:"column",borderStyle:"round",borderColor:M==="logs"?"green":"gray",paddingX:1,marginTop:1,height:oe+4,flexShrink:0,overflowY:"hidden",children:[p(f,{justifyContent:"space-between",children:[p(l,{children:[o(l,{color:"cyan",bold:!0,children:"LOGS \xB7 "}),o(l,{color:"white",children:$?.site.name??"\u2014"})]}),T&&N.length>0?M==="logs"?o(l,{color:"green",children:`\u2191\u2193 scroll \xB7 esc back${Te>0?` \xB7 ${ae}/${N.length}`:""}`}):N.length>oe?o(l,{color:"gray",children:"tab to scroll"}):null:null]}),o(f,{flexDirection:"column",marginTop:1,children:!T||N.length===0?o(l,{color:"gray",children:"no activity yet"}):Xe.map((c,S)=>p(l,{wrap:"truncate-end",children:[p(l,{color:"gray",children:[c.time," "]}),c.text]},ae-Xe.length+S))})]})]})]}),G?.url?p(f,{flexDirection:"column",paddingX:1,marginTop:1,children:[o(l,{color:"gray",children:"No browser? Open this address on any device:"}),o(l,{color:"cyan",children:G.url})]}):null,e.message?o(f,{paddingX:1,children:p(l,{color:e.messageIsError?"red":"gray",wrap:"truncate-end",children:[e.messageIsError?"\u2717 ":"",e.message]})}):null,p(f,{flexDirection:"column",marginTop:1,children:[p(f,{paddingX:1,justifyContent:"space-between",children:[p(f,{flexWrap:"wrap",children:[o(I,{k:"\u2191\u2193",label:"select"}),o(I,{k:"/",label:"search"}),o(I,{k:"s",label:"start sync"}),o(I,{k:"d",label:"disconnect"}),o(I,{k:"a",label:"add site"}),o(I,{k:"ctrl+c",label:"quit",width:14})]}),o(I,{k:"?",label:"all keys",width:12,bg:At})]}),!C&&n.length>0?o(f,{paddingX:1,marginTop:1,flexWrap:"wrap",children:n.map((c,S)=>o(I,{k:String(S+1),label:c.label,bg:Ot},c.key))}):null]})]})}import{jsx as $r}from"react/jsx-runtime";function Nt({controller:e}){return kr((t,n)=>{n.ctrl&&t==="c"&&e.exit()}),$r(Ft,{controller:e})}import*as zt from"os";import*as Ue from"util";var Er={production:"https://9Q849fBKdCJcK6j8M5SHpWjt@s2790579.us-west-2a.betterstackdata.com/2790579",development:"https://oj8Je2g5rZhXAv6NZ3CxYHFb@s2790583.us-west-2a.betterstackdata.com/2790583"},ie,Wt,Br=()=>Wt??=import("@sentry/node").then(e=>(e.init({dsn:ie.dsn,environment:ie.environment,serverName:ie.service,initialScope:{tags:{service:ie.service}},defaultIntegrations:!1,tracesSampleRate:0,maxBreadcrumbs:0,normalizeDepth:3}),e)).catch(e=>{console.warn("[observability] provider failed to load:",e?.message)}),Gt=({service:e,environment:t})=>{let n=Er[t];return n&&(ie={dsn:n,environment:t,service:e}),!!n},Ut=(e,t)=>{Br().then(n=>n?.captureException(e,t&&{extra:t}))},Ht=async e=>{await(await Wt)?.flush(e)};var Cr=10*6e4,Mr=3,Rr=30,Lr=1e3;var Se=!1,Ge=!1;var qt=console.error.bind(console),Y=new Map,Jt=new WeakSet,be={start:0,sent:0},jr=()=>process.env.ENV||process.env.AWS_LAMBDA_FUNCTION_NAME?.match(/-(production|development)$/)?.[1],Xt=({service:e,environment:t=jr(),quiet:n=!1})=>{Se=!!t&&Gt({service:e,environment:t}),n||console.log(`[observability] ${e}: ${Se?`reporting errors (${t})`:"off"}`)},Yt=(e,t)=>{if(!(!Se||Ge)){Ge=!0;try{let n=Or(e);if(Jt.has(n))return;Jt.add(n);let r=Dr(_r(n));if(r===!1)return;Ut(Ir(n),r?{...t,suppressed:r}:t)}catch(n){qt("[observability] capture failed:",n?.message)}finally{Ge=!1}}},Kt=async(e=2e3)=>{if(Se)try{await Ht(e)}catch(t){qt("[observability] flush failed:",t?.message)}};var Dr=e=>{let t=Date.now();t-be.start>=6e4&&(be={start:t,sent:0});let n=Y.get(e),r=0;return(!n||t-n.start>=Cr)&&(r=n?.dropped??0,n={start:t,sent:0,dropped:0},Y.delete(e),Y.set(e,n),Y.size>Lr&&Y.delete(Y.keys().next().value)),n.sent>=Mr||be.sent>=Rr?(n.dropped++,!1):(n.sent++,be.sent++,r)},_r=e=>{let t=e.stack?.split(`
11
+ `).find(n=>/^\s*at /.test(n)&&!/node_modules|node:|[\\/]observability[\\/]/.test(n));return`${e.name}|${Ar(e.message)}|${t?.trim()??""}`},Ar=e=>e.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi,"<id>").replace(/\b[0-9a-f]{16,}\b/gi,"<id>").replace(/\d+/g,"<n>").slice(0,300),Or=e=>{if(e instanceof Error)return e;let t=new Error(typeof e=="string"?e:Ue.inspect(e,{depth:3,breakLength:1/0}));return t.name="NonErrorThrown",t},ve=(()=>{try{return zt.homedir()}catch{return""}})(),Ir=e=>{if(ve.length<2||!`${e.message}
12
+ ${e.stack}`.includes(ve))return e;let t=new Error(e.message.split(ve).join("~"));return t.name=e.name,t.stack=e.stack?.split(ve).join("~"),t};import{jsx as Jr}from"react/jsx-runtime";var Je=ot();Je.version&&(console.log(he),process.exit(0));var Vt=(Je.env??"production").toLowerCase();Xt({service:"cms-sync",environment:process.env.DO_NOT_TRACK==="1"?null:Vt,quiet:!0});var ze=xe.dirname(Fr(import.meta.url)),Wr=He(xe.join(ze,"AGENTS.md"),"utf-8"),Gr=He(xe.join(ze,"build.mjs"),"utf-8"),Ur=He(xe.join(ze,"upload.mjs"),"utf-8");async function Hr(){let e=new fe(Wr,{build:Gr,upload:Ur});e.basePath=Je.path,await e.init(Vt),process.stdout.isTTY&&process.stdout.write("\x1B[2J\x1B[3J\x1B[H");let t=Nr(Jr(Nt,{controller:e}),{exitOnCtrlC:!1}),n=r=>{t.unmount(),console.log(r),process.exit(0)};e.once("exit",()=>n("\u2713 Stopped. Local site files were cleaned up.")),process.on("SIGINT",()=>{e.exit()}),process.on("SIGTERM",()=>{e.exit()}),await t.waitUntilExit()}Hr().catch(async e=>{let t=e;console.error("\u274C",t.body||t.message),t.body||(Yt(e),await Kt()),process.exit(1)});
@@ -0,0 +1,362 @@
1
+ #!/usr/bin/env node
2
+ // upload.mjs v1 — SleekCMS workspace upload script.
3
+ /**
4
+ * Self-contained (no `npm install` needed). Run from anywhere inside a synced
5
+ * SleekCMS workspace — the one `@sleekcms/sync` created, containing `.cache/token`.
6
+ *
7
+ * node upload.mjs <path>... Upload images and documents from disk.
8
+ * node upload.mjs logo.png --handle logo Pick the image's handle (one image only).
9
+ * node upload.mjs menu.pdf --name lunch-menu.pdf
10
+ * Pick the document's file name (one file only).
11
+ * --json Print one JSON result per line instead of text.
12
+ *
13
+ * What each file becomes is decided by its extension:
14
+ * - Images (png, jpg, jpeg, webp, gif, svg, ico) become site images under a
15
+ * handle (default: the file name without its extension) — reference them as
16
+ * `cms:<handle>` in image fields, or `img('cms:<handle>')` in views. An existing
17
+ * handle is replaced in place, so every `cms:<handle>` shows the new image.
18
+ * - Documents (pdf, docx, xlsx, pptx, csv, tsv, txt, md) go to Media > Files
19
+ * under their file name — the value a `file` field takes. An existing file
20
+ * name is replaced in place.
21
+ *
22
+ * It calls POST /api/mcp/create_asset, then sends the bytes straight to the
23
+ * upload URL it returns, and finally updates src/content/images.json or
24
+ * src/content/files.json so the workspace lists the new asset (the running sync
25
+ * pushes images.json back unchanged, which is a no-op on the server).
26
+ *
27
+ * Exit codes: 0 = every file uploaded, 1 = some uploads failed, 2 = couldn't run.
28
+ */
29
+
30
+ import fs from "node:fs";
31
+ import path from "node:path";
32
+ import { pathToFileURL } from "node:url";
33
+
34
+ // Same env → base-URL table the sync engine and build.mjs use.
35
+ const MCP_BASE_URLS = {
36
+ localhost: "http://app.sleekcms.test/api/mcp",
37
+ development: "https://app.sleekcms.dev/api/mcp",
38
+ production: "https://app.sleekcms.com/api/mcp",
39
+ };
40
+
41
+ // Mirrors cms-be handlers/mcp/assets.js.
42
+ export const IMAGE_EXTS = ["png", "jpg", "jpeg", "webp", "gif", "svg", "ico"];
43
+ export const FILE_EXTS = ["pdf", "docx", "xlsx", "pptx", "csv", "tsv", "txt", "md"];
44
+ const TEXT_FILE_EXTS = ["csv", "tsv", "txt", "md"];
45
+ const MAX_IMAGE_MB = 8;
46
+ const MAX_FILE_MB = 10;
47
+ const MAX_INLINE_TEXT_BYTES = 256 * 1024;
48
+
49
+ const IMAGES_INDEX = "src/content/images.json";
50
+ const FILES_INDEX = "src/content/files.json";
51
+
52
+ /** The workspace's env: the override, else what .cache/site.json records, else production. */
53
+ export function workspaceEnv(ws, override) {
54
+ let recorded;
55
+ try {
56
+ recorded = JSON.parse(fs.readFileSync(path.join(ws, ".cache", "site.json"), "utf-8")).env;
57
+ } catch {
58
+ recorded = undefined;
59
+ }
60
+ const env = String(override || recorded || "production").toLowerCase();
61
+ return MCP_BASE_URLS[env] ? env : "production";
62
+ }
63
+
64
+ export function mcpBase(env) {
65
+ return MCP_BASE_URLS[env] || MCP_BASE_URLS.production;
66
+ }
67
+
68
+ /** Walk up from `startDir` until a `.cache/token` is found; null if none. */
69
+ export function findWorkspace(startDir) {
70
+ let dir = path.resolve(startDir);
71
+ // eslint-disable-next-line no-constant-condition
72
+ while (true) {
73
+ if (fs.existsSync(path.join(dir, ".cache", "token"))) return dir;
74
+ const parent = path.dirname(dir);
75
+ if (parent === dir) return null;
76
+ dir = parent;
77
+ }
78
+ }
79
+
80
+ export function extOf(name) {
81
+ const m = /\.([a-z0-9]{1,10})$/i.exec(String(name || ""));
82
+ return m ? m[1].toLowerCase() : "";
83
+ }
84
+
85
+ /** "image", "file", or null for an extension the CMS doesn't take. */
86
+ export function kindOf(name) {
87
+ const ext = extOf(name);
88
+ if (IMAGE_EXTS.includes(ext)) return "image";
89
+ if (FILE_EXTS.includes(ext)) return "file";
90
+ return null;
91
+ }
92
+
93
+ /** A valid image handle from a file name: "Team Photo (1).JPG" → "team-photo-1". */
94
+ export function handleFor(fileName) {
95
+ const base = path.basename(fileName).replace(/\.[^.]*$/, "");
96
+ const handle = base
97
+ .toLowerCase()
98
+ .replace(/[^a-z0-9_-]+/g, "-")
99
+ .replace(/^[-_]+|-+$/g, "")
100
+ .slice(0, 64)
101
+ .replace(/-+$/, "");
102
+ return handle || "image";
103
+ }
104
+
105
+ export function parseFlags(argv) {
106
+ const flags = { files: [], handle: undefined, name: undefined, env: undefined, json: false, help: false };
107
+ for (let i = 0; i < argv.length; i++) {
108
+ const a = argv[i];
109
+ if (a === "--handle") flags.handle = argv[++i];
110
+ else if (a.startsWith("--handle=")) flags.handle = a.slice("--handle=".length);
111
+ else if (a === "--name") flags.name = argv[++i];
112
+ else if (a.startsWith("--name=")) flags.name = a.slice("--name=".length);
113
+ else if (a === "--env") flags.env = argv[++i];
114
+ else if (a.startsWith("--env=")) flags.env = a.slice("--env=".length);
115
+ else if (a === "--json") flags.json = true;
116
+ else if (a === "--help" || a === "-h") flags.help = true;
117
+ else flags.files.push(a);
118
+ }
119
+ return flags;
120
+ }
121
+
122
+ export class UploadError extends Error {
123
+ constructor(message, httpStatus) {
124
+ super(message);
125
+ this.httpStatus = httpStatus;
126
+ }
127
+ }
128
+
129
+ /** The server's own message from an error body (JSON `message`/`error`), else the raw text. */
130
+ function errorText(text) {
131
+ try {
132
+ const body = JSON.parse(text);
133
+ if (body && (body.message || body.error)) return String(body.message || body.error);
134
+ } catch {
135
+ /* not JSON */
136
+ }
137
+ const title = String(text || "").match(/<title>([^<]*)<\/title>/i)?.[1].trim();
138
+ return (title || String(text || "")).replace(/\s+/g, " ").slice(0, 300);
139
+ }
140
+
141
+ async function api(base, token, method, p, body, fetchImpl) {
142
+ const res = await fetchImpl(base + p, {
143
+ method,
144
+ headers: {
145
+ Authorization: `Bearer ${token}`,
146
+ ...(body !== undefined ? { "Content-Type": "application/json" } : {}),
147
+ },
148
+ body: body !== undefined ? JSON.stringify(body) : undefined,
149
+ });
150
+ if (!res.ok) {
151
+ const text = await res.text().catch(() => "");
152
+ throw new UploadError(`${p.split("?")[0]} → ${res.status}: ${errorText(text)}`, res.status);
153
+ }
154
+ return res.json();
155
+ }
156
+
157
+ /** Send the bytes to the upload URL create_asset handed back. Returns the parsed reply, if any. */
158
+ async function sendBytes(upload, data, fetchImpl) {
159
+ const res = await fetchImpl(upload.url, { method: upload.method, headers: upload.headers || {}, body: data });
160
+ const text = await res.text().catch(() => "");
161
+ if (!res.ok) throw new UploadError(`upload → ${res.status}: ${errorText(text)}`, res.status);
162
+ try {
163
+ return text ? JSON.parse(text) : null;
164
+ } catch {
165
+ return null;
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Put one handle into the local images.json, keeping everything else in it as it
171
+ * is (it may hold edits the sync hasn't pushed yet). Returns false when the file
172
+ * isn't plain JSON (comments etc.) and was left alone.
173
+ */
174
+ export function mergeImageHandle(ws, handle, value) {
175
+ const file = path.join(ws, IMAGES_INDEX);
176
+ let map = {};
177
+ if (fs.existsSync(file)) {
178
+ const text = fs.readFileSync(file, "utf-8");
179
+ if (text.trim()) {
180
+ try {
181
+ map = JSON.parse(text);
182
+ } catch {
183
+ return false;
184
+ }
185
+ if (!map || typeof map !== "object" || Array.isArray(map)) return false;
186
+ }
187
+ }
188
+ if (map[handle] === value) return true;
189
+ map[handle] = value;
190
+ fs.mkdirSync(path.dirname(file), { recursive: true });
191
+ fs.writeFileSync(file, `${JSON.stringify(map, null, 2)}\n`);
192
+ return true;
193
+ }
194
+
195
+ /** Write the server's files.json (read-only, server-owned — the sync never pushes it). */
196
+ function writeFilesIndex(ws, files) {
197
+ const index = (files || []).find((f) => f.path === FILES_INDEX);
198
+ if (!index) return;
199
+ const file = path.join(ws, FILES_INDEX);
200
+ fs.mkdirSync(path.dirname(file), { recursive: true });
201
+ fs.writeFileSync(file, index.content);
202
+ }
203
+
204
+ /**
205
+ * Upload one file from disk. `opts`: { ws, base, token, filePath, handle?, name?, fetchImpl? }.
206
+ * Resolves to a result object; throws UploadError on failure.
207
+ */
208
+ export async function uploadOne(opts) {
209
+ const { ws, base, token, filePath, fetchImpl = fetch } = opts;
210
+ const fileName = path.basename(filePath);
211
+ const kind = kindOf(opts.name || fileName);
212
+ if (!kind) {
213
+ throw new UploadError(
214
+ `${fileName}: unsupported type. Images: ${IMAGE_EXTS.join(", ")}. Documents: ${FILE_EXTS.join(", ")}.`,
215
+ );
216
+ }
217
+ let stat;
218
+ try {
219
+ stat = fs.statSync(filePath);
220
+ } catch {
221
+ throw new UploadError(`${filePath}: no such file.`);
222
+ }
223
+ if (!stat.isFile()) throw new UploadError(`${filePath}: not a file.`);
224
+ if (stat.size === 0) throw new UploadError(`${filePath}: the file is empty.`);
225
+ const maxMb = kind === "image" ? MAX_IMAGE_MB : MAX_FILE_MB;
226
+ if (stat.size > maxMb * 1024 * 1024) {
227
+ throw new UploadError(`${fileName}: ${(stat.size / 1024 / 1024).toFixed(1)} MB is over the ${maxMb} MB limit for ${kind === "image" ? "images" : "documents"}.`);
228
+ }
229
+ const data = fs.readFileSync(filePath);
230
+
231
+ if (kind === "image") {
232
+ if (opts.name && kindOf(opts.name) !== "image") throw new UploadError(`--name must keep an image extension.`);
233
+ const handle = opts.handle || handleFor(fileName);
234
+ const name = opts.name || fileName;
235
+ const created = await api(base, token, "POST", "/create_asset", { type: "image", handle, name }, fetchImpl);
236
+ if (created.upload) {
237
+ if (created.max_size_mb && stat.size > created.max_size_mb * 1024 * 1024) {
238
+ throw new UploadError(`${fileName}: over the ${created.max_size_mb} MB limit for images.`);
239
+ }
240
+ await sendBytes(created.upload, data, fetchImpl);
241
+ }
242
+ // What the server lists for the handle now — the value images.json reads back as.
243
+ let value;
244
+ try {
245
+ const images = await api(base, token, "GET", "/get_images", undefined, fetchImpl);
246
+ value = JSON.parse(images.content || "{}")[handle];
247
+ } catch {
248
+ value = undefined;
249
+ }
250
+ const indexed = value ? mergeImageHandle(ws, handle, value) : false;
251
+ return {
252
+ source: filePath,
253
+ type: "image",
254
+ ref: created.ref || `cms:${handle}`,
255
+ handle,
256
+ replaced: !!created.replaces_existing,
257
+ url: typeof value === "string" && value.startsWith("url:") ? value.slice(4) : undefined,
258
+ indexed,
259
+ };
260
+ }
261
+
262
+ // Document → Media > Files.
263
+ if (opts.handle) throw new UploadError(`--handle is for images; a document is named by its file name (use --name).`);
264
+ const name = opts.name || fileName;
265
+ if (extOf(name) !== extOf(fileName)) throw new UploadError(`--name must keep the file's .${extOf(fileName)} extension.`);
266
+ const body = { type: "file", name };
267
+ // Small text documents go inline: the server stores them and confirms the row at once.
268
+ const ext = extOf(name);
269
+ if (TEXT_FILE_EXTS.includes(ext) && data.length <= MAX_INLINE_TEXT_BYTES) body.content = data.toString("utf-8");
270
+ const created = await api(base, token, "POST", "/create_asset", body, fetchImpl);
271
+ if (created.upload) await sendBytes(created.upload, data, fetchImpl);
272
+ writeFilesIndex(ws, created.files);
273
+ return {
274
+ source: filePath,
275
+ type: "file",
276
+ ref: created.ref || name,
277
+ handle: created.handle || undefined,
278
+ replaced: !!created.replaces_existing,
279
+ url: created.url || undefined,
280
+ indexed: !!(created.files || []).some((f) => f.path === FILES_INDEX),
281
+ };
282
+ }
283
+
284
+ /** Human-readable lines for one result. */
285
+ export function describe(r) {
286
+ if (r.type === "image") {
287
+ const lines = [`✅ ${r.replaced ? "Replaced" : "Uploaded"} image ${path.basename(r.source)} → "${r.ref}"${r.url ? ` (${r.url})` : ""}`];
288
+ lines.push(` Use "${r.ref}" as an image field's value, or img('${r.ref}') / src('${r.ref}') in a view.`);
289
+ if (!r.indexed) lines.push(` ${IMAGES_INDEX} wasn't updated here; the handle is on the server and shows up there after the next re-fetch.`);
290
+ return lines;
291
+ }
292
+ const lines = [`✅ ${r.replaced ? "Replaced" : "Uploaded"} file ${path.basename(r.source)} → "${r.ref}"${r.url ? ` (${r.url})` : ""}`];
293
+ lines.push(` Use "${r.ref}"${r.handle ? ` (or its handle "${r.handle}")` : ""} as a file field's value, or getFile('${r.handle || r.ref}') in a view.`);
294
+ return lines;
295
+ }
296
+
297
+ const USAGE = `Usage: node upload.mjs <path>... [--handle <handle>] [--name <file name>] [--json]
298
+
299
+ Images (${IMAGE_EXTS.join(", ")}, up to ${MAX_IMAGE_MB} MB) become site images: cms:<handle>.
300
+ Documents (${FILE_EXTS.join(", ")}, up to ${MAX_FILE_MB} MB) go to Media > Files under their file name.
301
+ --handle the image's handle (default: its file name without the extension); one image only.
302
+ --name the stored file name, extension kept; one file only.`;
303
+
304
+ async function main() {
305
+ const flags = parseFlags(process.argv.slice(2));
306
+ if (flags.help || flags.files.length === 0) {
307
+ (flags.help ? console.log : console.error)(USAGE);
308
+ process.exit(flags.help ? 0 : 2);
309
+ }
310
+ if ((flags.handle || flags.name) && flags.files.length > 1) {
311
+ console.error("❌ --handle and --name name a single upload; pass one file with them.");
312
+ process.exit(2);
313
+ }
314
+
315
+ const ws = findWorkspace(process.cwd());
316
+ if (!ws) {
317
+ console.error("❌ Not inside a SleekCMS workspace (no .cache/token found). Run this from your synced site folder.");
318
+ process.exit(2);
319
+ }
320
+ let token;
321
+ try {
322
+ token = fs.readFileSync(path.join(ws, ".cache", "token"), "utf-8").trim();
323
+ } catch {
324
+ token = "";
325
+ }
326
+ if (!token) {
327
+ console.error("❌ Could not read .cache/token in this workspace.");
328
+ process.exit(2);
329
+ }
330
+ const base = mcpBase(workspaceEnv(ws, flags.env || process.env.SLEEKCMS_ENV));
331
+
332
+ let failed = 0;
333
+ for (const file of flags.files) {
334
+ const filePath = path.resolve(process.cwd(), file);
335
+ try {
336
+ const r = await uploadOne({ ws, base, token, filePath, handle: flags.handle, name: flags.name });
337
+ if (flags.json) console.log(JSON.stringify({ ok: true, ...r }));
338
+ else for (const line of describe(r)) console.log(line);
339
+ } catch (err) {
340
+ failed++;
341
+ const msg = err && err.message ? err.message : String(err);
342
+ const hint =
343
+ err && err.httpStatus === 403
344
+ ? " (uploading needs an EDITOR or ADMIN on this site)"
345
+ : err && err.httpStatus === 401
346
+ ? " (access to this site was revoked — add it again in sleekcms)"
347
+ : "";
348
+ if (flags.json) console.log(JSON.stringify({ ok: false, source: filePath, error: msg + hint }));
349
+ else console.error(`❌ ${path.basename(filePath)}: ${msg}${hint}`);
350
+ }
351
+ }
352
+ process.exit(failed ? 1 : 0);
353
+ }
354
+
355
+ // Run only when invoked directly (`node upload.mjs`), not when imported by tests.
356
+ const isMain = import.meta.url === pathToFileURL(process.argv[1] || "").href;
357
+ if (isMain) {
358
+ main().catch((err) => {
359
+ console.error("❌", err && err.message ? err.message : String(err));
360
+ process.exit(2);
361
+ });
362
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleekcms/sync",
3
- "version": "4.2.0",
3
+ "version": "4.3.0",
4
4
  "description": "Edit SleekCMS sites locally — models, content, templates, images — with live two-way sync and AI agent support.",
5
5
  "keywords": [
6
6
  "sleekcms",
@@ -28,7 +28,7 @@
28
28
  "dist"
29
29
  ],
30
30
  "scripts": {
31
- "build": "npm run clean && node esbuild.config.mjs && cp src/AGENTS.md dist/ && cp src/build.mjs dist/ && chmod +x dist/index.js",
31
+ "build": "npm run clean && node esbuild.config.mjs && cp src/AGENTS.md dist/ && cp src/build.mjs src/upload.mjs dist/ && chmod +x dist/index.js",
32
32
  "dev": "tsx src/index.tsx",
33
33
  "typecheck": "tsc --noEmit",
34
34
  "clean": "rm -rf dist",