@sleekcms/sync 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AGENT.md +18 -7
- package/dist/build.mjs +238 -0
- package/dist/index.js +4 -5
- package/package.json +2 -2
package/dist/AGENT.md
CHANGED
|
@@ -20,17 +20,26 @@ Three model kinds:
|
|
|
20
20
|
|
|
21
21
|
Pages and entries are either **single** (one record) or a **collection** (many records). A collection is marked by a `+` suffix on the key, e.g. `blog+`.
|
|
22
22
|
|
|
23
|
-
## 2. The Edit → Sync → Verify Loop
|
|
23
|
+
## 2. The Edit → Sync → Build → Verify Loop
|
|
24
24
|
|
|
25
25
|
1. **Edit/create files** in the workspace under `src/`.
|
|
26
|
-
2. The `@sleekcms/sync` utility runs in the background and pushes every save automatically
|
|
27
|
-
3. **
|
|
26
|
+
2. The `@sleekcms/sync` utility runs in the background and pushes every save automatically — there is nothing to run for syncing.
|
|
27
|
+
3. **Check `sync-errors.log`** in the workspace root after a batch of edits — this catches *per-file* save/validation problems:
|
|
28
28
|
- A failed sync **adds a line** describing the error for that file.
|
|
29
29
|
- When the file is fixed and re-synced, **its line is removed**.
|
|
30
|
-
- Empty file (or no new lines) =
|
|
31
|
-
4.
|
|
30
|
+
- Empty file (or no new lines) = every file saved successfully.
|
|
31
|
+
4. **Build the whole site** to compile it and get a viewable URL. Once `sync-errors.log` is clean, run from the workspace root:
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
```bash
|
|
34
|
+
node build.mjs # build + validate; prints the PREVIEW url, or build errors
|
|
35
|
+
node build.mjs --deploy # build + publish LIVE; prints the live url
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- On success it prints `✅ Build … → <url>` — **give that URL to the user** so they can view the result.
|
|
39
|
+
- On failure it prints `❌ Build failed:` followed by the errors (e.g. `views/pages/home.ejs:14: …`). Fix them and build again.
|
|
40
|
+
- This is a *whole-site* check (templates, links, references) that `sync-errors.log` does not cover. Every build is also appended to `build.log`.
|
|
41
|
+
|
|
42
|
+
Never guess at why something isn't rendering — read `sync-errors.log` first, then run `node build.mjs` and read its output.
|
|
34
43
|
|
|
35
44
|
## 3. File Naming Rules (applies to models, views, and content)
|
|
36
45
|
|
|
@@ -342,7 +351,7 @@ There is exactly **one** layout: `layouts/common.ejs`. It wraps every page autom
|
|
|
342
351
|
|
|
343
352
|
### Helpers — bookings
|
|
344
353
|
|
|
345
|
-
`bookings(name, options?)` renders the appointment-booking widget mount div (see §11
|
|
354
|
+
`bookings(name, options?)` renders the appointment-booking / scheduling widget mount div — use it when visitors should book or schedule a time (appointment, call, demo, consultation, class, reservation); see §11 for when it's appropriate. `options` is `{ accent, class, style }`.
|
|
346
355
|
|
|
347
356
|
### Helpers — head injection
|
|
348
357
|
|
|
@@ -397,6 +406,8 @@ The `data-sleekcms` value groups submissions by form; each input's `name` is sto
|
|
|
397
406
|
|
|
398
407
|
## 11. Bookings
|
|
399
408
|
|
|
409
|
+
Use this whenever visitors should **book or schedule** something — an appointment, a call, a demo, a consultation, a class, or a reservation ("pick a time / reserve a slot"). It fits appointment-based businesses: salon, spa, barber, dental/medical clinic, therapist, tutor, photographer, consultant, repair shop, restaurant reservations. **Only add a booking widget when the business genuinely takes appointments and the user asks for it (or it is clearly core to the business) — never add one speculatively to an arbitrary site.**
|
|
410
|
+
|
|
400
411
|
Drop an appointment-booking widget on any page with the `bookings()` helper. Visitors pick a time slot and book right on the page; email confirmation, calendar invites, and cancel/reschedule are handled by the CMS — no backend, no JS.
|
|
401
412
|
|
|
402
413
|
```ejs
|
package/dist/build.mjs
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// build.mjs v2 — SleekCMS workspace build/deploy 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 build.mjs Build + validate the whole site. Prints the PREVIEW
|
|
8
|
+
* URL on success, or the build errors to fix.
|
|
9
|
+
* node build.mjs --deploy Build + publish LIVE. Prints the live URL.
|
|
10
|
+
*
|
|
11
|
+
* It calls POST /api/mcp/deploy and polls it to a terminal state (mirrors the
|
|
12
|
+
* cms-agent build loop). Results go to three places:
|
|
13
|
+
* - stdout — for you / your AI agent to read directly,
|
|
14
|
+
* - ./build.log — appended history of every build,
|
|
15
|
+
* - ./.cache/build.json — the single source of truth the `sleekcms` dashboard watches.
|
|
16
|
+
*
|
|
17
|
+
* Exit codes: 0 = built/deployed, 1 = build errors, 2 = couldn't run / still building.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import fs from "node:fs";
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
import { pathToFileURL } from "node:url";
|
|
23
|
+
|
|
24
|
+
// Same env → base-URL table the sync engine uses; env is the 3rd `-`-segment of
|
|
25
|
+
// the per-site token (see sync-engine.ts), defaulting to production.
|
|
26
|
+
const MCP_BASE_URLS = {
|
|
27
|
+
localhost: "http://app.sleekcms.test/api/mcp",
|
|
28
|
+
development: "https://app.sleekcms.dev/api/mcp",
|
|
29
|
+
production: "https://app.sleekcms.com/api/mcp",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const TERMINAL = new Set(["COMPLETED", "FAILED", "EXPIRED"]);
|
|
33
|
+
const DEFAULT_MAX_POLLS = 6;
|
|
34
|
+
|
|
35
|
+
/** Env baked into the per-site token (`<a>-<b>-<env>-…`); falls back to production. */
|
|
36
|
+
export function deriveEnv(token, override) {
|
|
37
|
+
const env = (override || (token || "").split("-")[2] || "production").toLowerCase();
|
|
38
|
+
return MCP_BASE_URLS[env] ? env : "production";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function mcpBase(env) {
|
|
42
|
+
return MCP_BASE_URLS[env] || MCP_BASE_URLS.production;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Walk up from `startDir` until a `.cache/token` is found; null if none. */
|
|
46
|
+
export function findWorkspace(startDir) {
|
|
47
|
+
let dir = path.resolve(startDir);
|
|
48
|
+
// eslint-disable-next-line no-constant-condition
|
|
49
|
+
while (true) {
|
|
50
|
+
if (fs.existsSync(path.join(dir, ".cache", "token"))) return dir;
|
|
51
|
+
const parent = path.dirname(dir);
|
|
52
|
+
if (parent === dir) return null;
|
|
53
|
+
dir = parent;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Friendly progress label from the raw job status + how many times we've polled. */
|
|
58
|
+
export function stageFor(status, poll) {
|
|
59
|
+
if (status === "PENDING") return "queued";
|
|
60
|
+
if (poll >= 2) return "finalizing";
|
|
61
|
+
return "building";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Coerce the deploy endpoint's error entries to plain strings. */
|
|
65
|
+
export function toErrorStrings(errors) {
|
|
66
|
+
if (!Array.isArray(errors)) return [];
|
|
67
|
+
return errors.map((e) =>
|
|
68
|
+
typeof e === "string" ? e : e && e.message ? String(e.message) : JSON.stringify(e),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
class DeployError extends Error {
|
|
73
|
+
constructor(message, httpStatus) {
|
|
74
|
+
super(message);
|
|
75
|
+
this.httpStatus = httpStatus;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function callDeploy(base, token, deploy, force, fetchImpl) {
|
|
80
|
+
const res = await fetchImpl(base + "/deploy", {
|
|
81
|
+
method: "POST",
|
|
82
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
83
|
+
body: JSON.stringify({ build_only: !deploy, force }),
|
|
84
|
+
});
|
|
85
|
+
if (!res.ok) {
|
|
86
|
+
const text = await res.text().catch(() => "");
|
|
87
|
+
throw new DeployError(`/deploy → ${res.status}: ${text}`, res.status);
|
|
88
|
+
}
|
|
89
|
+
return res.json();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Trigger a build, then poll the SAME job to a terminal state. Mirrors
|
|
94
|
+
* cms-agent's build_site(): first call force=true (enqueue a fresh build),
|
|
95
|
+
* subsequent polls force=false (don't re-trigger). `onPoll(status, poll)` fires
|
|
96
|
+
* after each response so the caller can surface progress.
|
|
97
|
+
*/
|
|
98
|
+
export async function buildAndPoll(opts) {
|
|
99
|
+
const { base, token, deploy = false, maxPolls = DEFAULT_MAX_POLLS, fetchImpl = fetch, onPoll } = opts;
|
|
100
|
+
let r = await callDeploy(base, token, deploy, true, fetchImpl);
|
|
101
|
+
let poll = 0;
|
|
102
|
+
if (onPoll) onPoll(r.status, poll);
|
|
103
|
+
while (
|
|
104
|
+
!TERMINAL.has(r.status) &&
|
|
105
|
+
!(Array.isArray(r.errors) && r.errors.length > 0) &&
|
|
106
|
+
poll < maxPolls
|
|
107
|
+
) {
|
|
108
|
+
poll++;
|
|
109
|
+
r = await callDeploy(base, token, deploy, false, fetchImpl);
|
|
110
|
+
if (onPoll) onPoll(r.status, poll);
|
|
111
|
+
}
|
|
112
|
+
return r;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// CLI
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
function parseFlags(argv) {
|
|
120
|
+
const flags = { deploy: false, env: undefined };
|
|
121
|
+
for (let i = 0; i < argv.length; i++) {
|
|
122
|
+
const a = argv[i];
|
|
123
|
+
if (a === "--deploy" || a === "--live") flags.deploy = true;
|
|
124
|
+
else if (a === "--env") flags.env = argv[++i];
|
|
125
|
+
else if (a.startsWith("--env=")) flags.env = a.slice("--env=".length);
|
|
126
|
+
}
|
|
127
|
+
return flags;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function writeStatus(ws, obj) {
|
|
131
|
+
const dir = path.join(ws, ".cache");
|
|
132
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
133
|
+
const file = path.join(dir, "build.json");
|
|
134
|
+
const tmp = `${file}.tmp`;
|
|
135
|
+
// Write-then-rename so the dashboard never reads a half-written file.
|
|
136
|
+
fs.writeFileSync(tmp, JSON.stringify(obj));
|
|
137
|
+
fs.renameSync(tmp, file);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function appendLog(ws, line) {
|
|
141
|
+
try {
|
|
142
|
+
fs.appendFileSync(path.join(ws, "build.log"), `${line}\n`);
|
|
143
|
+
} catch {
|
|
144
|
+
/* logging is best-effort */
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function main() {
|
|
149
|
+
const flags = parseFlags(process.argv.slice(2));
|
|
150
|
+
const mode = flags.deploy ? "live" : "preview";
|
|
151
|
+
|
|
152
|
+
const ws = findWorkspace(process.cwd());
|
|
153
|
+
if (!ws) {
|
|
154
|
+
console.error(
|
|
155
|
+
"❌ Not inside a SleekCMS workspace (no .cache/token found). Run this from your synced site folder.",
|
|
156
|
+
);
|
|
157
|
+
process.exit(2);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let token;
|
|
161
|
+
try {
|
|
162
|
+
token = fs.readFileSync(path.join(ws, ".cache", "token"), "utf-8").trim();
|
|
163
|
+
} catch {
|
|
164
|
+
console.error("❌ Could not read .cache/token in this workspace.");
|
|
165
|
+
process.exit(2);
|
|
166
|
+
}
|
|
167
|
+
if (!token) {
|
|
168
|
+
console.error("❌ .cache/token is empty.");
|
|
169
|
+
process.exit(2);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const env = deriveEnv(token, flags.env || process.env.SLEEKCMS_ENV);
|
|
173
|
+
const base = mcpBase(env);
|
|
174
|
+
const maxPolls = Number(process.env.SLEEKCMS_MAX_BUILD_POLLS) || DEFAULT_MAX_POLLS;
|
|
175
|
+
|
|
176
|
+
console.log(`⏳ ${flags.deploy ? "Deploying" : "Building"} site…`);
|
|
177
|
+
writeStatus(ws, { status: "building", mode, stage: "queued", at: Date.now() });
|
|
178
|
+
|
|
179
|
+
let r;
|
|
180
|
+
try {
|
|
181
|
+
r = await buildAndPoll({
|
|
182
|
+
base,
|
|
183
|
+
token,
|
|
184
|
+
deploy: flags.deploy,
|
|
185
|
+
maxPolls,
|
|
186
|
+
onPoll: (status, poll) => {
|
|
187
|
+
if (!TERMINAL.has(status)) {
|
|
188
|
+
writeStatus(ws, { status: "building", mode, stage: stageFor(status, poll), at: Date.now() });
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
} catch (err) {
|
|
193
|
+
const msg = err && err.message ? err.message : String(err);
|
|
194
|
+
const hint = err && err.httpStatus === 403 ? " (the workspace token must belong to a site ADMIN)" : "";
|
|
195
|
+
console.error(`❌ Build request failed${hint}: ${msg}`);
|
|
196
|
+
writeStatus(ws, { status: "error", mode, errors: [msg], at: Date.now() });
|
|
197
|
+
appendLog(ws, `${new Date().toISOString()}\t${mode}\tERROR\t${msg}`);
|
|
198
|
+
process.exit(2);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const errors = toErrorStrings(r.errors);
|
|
202
|
+
|
|
203
|
+
// 1) Build failed (compile errors, or the job itself failed/expired).
|
|
204
|
+
if (errors.length > 0 || r.status === "FAILED" || r.status === "EXPIRED") {
|
|
205
|
+
const list = errors.length ? errors : [r.message || "Build failed."];
|
|
206
|
+
console.error("❌ Build failed:");
|
|
207
|
+
for (const e of list) console.error(` - ${e}`);
|
|
208
|
+
writeStatus(ws, { status: "error", mode, errors: list, at: Date.now() });
|
|
209
|
+
appendLog(ws, `${new Date().toISOString()}\t${mode}\tFAILED\n${list.map((e) => ` - ${e}`).join("\n")}`);
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 2) Success.
|
|
214
|
+
if (r.status === "COMPLETED") {
|
|
215
|
+
const url = r.url || "";
|
|
216
|
+
console.log(`✅ Build ${flags.deploy ? "deployed (live)" : "succeeded (preview)"}${url ? ` → ${url}` : ""}`);
|
|
217
|
+
writeStatus(ws, { status: "success", mode, url: url || undefined, at: Date.now() });
|
|
218
|
+
appendLog(ws, `${new Date().toISOString()}\t${mode}\tSUCCESS\t${url}`);
|
|
219
|
+
process.exit(0);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 3) Still running after the poll budget — surface the preview URL, leave it
|
|
223
|
+
// as building; re-running (or the dashboard) will resolve the final state.
|
|
224
|
+
const url = r.url || "";
|
|
225
|
+
console.log(`⏳ Still building${url ? ` → ${url}` : ""}. Re-run \`node build.mjs\` to check again.`);
|
|
226
|
+
writeStatus(ws, { status: "building", mode, stage: "finalizing", url: url || undefined, at: Date.now() });
|
|
227
|
+
appendLog(ws, `${new Date().toISOString()}\t${mode}\t${r.status}\t${url}`);
|
|
228
|
+
process.exit(2);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Run only when invoked directly (`node build.mjs`), not when imported by tests.
|
|
232
|
+
const isMain = import.meta.url === pathToFileURL(process.argv[1] || "").href;
|
|
233
|
+
if (isMain) {
|
|
234
|
+
main().catch((err) => {
|
|
235
|
+
console.error("❌", err && err.message ? err.message : String(err));
|
|
236
|
+
process.exit(2);
|
|
237
|
+
});
|
|
238
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{readFileSync as
|
|
2
|
+
import{readFileSync as ye}from"node:fs";import{fileURLToPath as xr}from"node:url";import{createRequire as Tr}from"node:module";import Mt from"node:path";import{render as kr}from"ink";import{execSync as Wt,spawn as D}from"child_process";import{program as It,Option as ft}from"commander";function Gt(){return It.name("cms-sync").description("SleekCMS CLI tool to sync and edit CMS templates locally. Log in once, then add the sites you want to sync \u2014 changes are watched and pushed automatically.").addOption(new ft("-v, --version","output the version number").hideHelp()).addOption(new ft("-e, --env <env>","Environment (localhost, development, production)").default("production").hideHelp()).addOption(new ft("-d, --dev <secret>","Dev secret for non-production envs (sent as the _dev cookie; enables OTP-in-response). Falls back to SLEEKCMS_DEV_SECRET.").hideHelp()).option("-p, --path <path>","Directory path for files (default: ~/SleekCMS)").addHelpText("after",`
|
|
3
3
|
Examples:
|
|
4
4
|
$ sleekcms # log in and pick sites to sync
|
|
5
5
|
$ sleekcms -e development # use a non-production backend
|
|
6
|
-
`).parse(process.argv),It.opts()}function
|
|
7
|
-
`)){let s=i.indexOf(": ");s>0&&(
|
|
6
|
+
`).parse(process.argv),It.opts()}function j(t){let e=process.platform==="win32"?`where ${t}`:`which ${t}`;try{return Wt(e,{stdio:"ignore"}),!0}catch{return!1}}function Ut(t){if(process.platform!=="darwin")return!1;try{return Wt(`osascript -e 'id of application "${t}"'`,{stdio:"ignore"}),!0}catch{return!1}}function Me(t,e){let r={detached:!0,stdio:"ignore",windowsHide:!0};process.platform==="win32"?D("cmd.exe",["/c",t,...e],r).unref():D(t,e,r).unref()}var _t=[{name:"VS Code",cmd:"code",args:t=>["-n",t]},{name:"Cursor",cmd:"cursor",args:t=>["-n",t]}];function Nt(t,e){let n=(e?_t.filter(i=>i.cmd===e):_t).find(i=>j(i.cmd));return n?(Me(n.cmd,n.args(t)),n.name):null}function $e(t){if(process.platform==="darwin")return D("open",["-a","Terminal",t],{detached:!0,stdio:"ignore"}).unref(),!0;if(process.platform==="win32"){if(j("wt"))D("wt.exe",["-d",t],{detached:!0,stdio:"ignore"}).unref();else{let e=j("powershell")?"powershell.exe":"cmd.exe";D("cmd.exe",["/c","start","",e],{cwd:t,detached:!0,stdio:"ignore"}).unref()}return!0}return!1}function Le(t){return Ut("iTerm")?(D("open",["-a","iTerm",t],{detached:!0,stdio:"ignore"}).unref(),!0):!1}function Re(t){if(process.platform!=="darwin"||!j("claude"))return!1;let e=`tell application "Terminal" to do script ("cd " & quoted form of "${t}" & " && claude")`;return D("osascript",["-e",e,"-e",'tell application "Terminal" to activate'],{detached:!0,stdio:"ignore"}).unref(),!0}function qt(t){if(!t)return!1;try{return process.platform==="darwin"?D("open",[t],{detached:!0,stdio:"ignore"}).unref():process.platform==="win32"?D("cmd.exe",["/c","start","",t],{detached:!0,stdio:"ignore",windowsHide:!0}).unref():D("xdg-open",[t],{detached:!0,stdio:"ignore"}).unref(),!0}catch{return!1}}function Jt(){let t=process.platform==="darwin",e=process.platform==="win32",r=[];return(t||e)&&r.push({key:"terminal",label:"terminal",run:$e}),Ut("iTerm")&&r.push({key:"iterm",label:"iterm",run:Le}),j("code")&&r.push({key:"code",label:"vscode",run:n=>Nt(n,"code")!==null}),j("cursor")&&r.push({key:"cursor",label:"cursor",run:n=>Nt(n,"cursor")!==null}),t&&j("claude")&&r.push({key:"claude",label:"claude",run:Re}),r}import{EventEmitter as ir}from"node:events";import{spawn as sr}from"node:child_process";import K from"fs-extra";import Bt from"path";import or from"chokidar";import f from"fs-extra";import Ae from"os";import v from"path";var Ht={localhost:"http://app.sleekcms.test/api/mcp",development:"https://app.sleekcms.dev/api/mcp",production:"https://app.sleekcms.com/api/mcp"},je=["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"],yt=class extends Error{status;body;constructor(e,r,n){super(e),this.status=r,this.body=n}};async function bt(t,e,r,n,i){let s=await fetch(t+n,{method:r,headers:{Authorization:`Bearer ${e}`,...i!==void 0?{"Content-Type":"application/json"}:{}},body:i!==void 0?JSON.stringify(i):void 0});if(!s.ok){let c=await s.text();throw new yt(`${r} ${n} \u2192 ${s.status}: ${c}`,s.status,c)}return s.json()}async function Ie(t,e){e&&(await f.outputFile(v.join(t,"AGENT.md"),e),await f.outputFile(v.join(t,"CLAUDE.md"),e),await f.outputFile(v.join(t,".vscode","copilot-instructions.md"),e)),await f.outputFile(v.join(t,".vscode","settings.json"),JSON.stringify({"files.associations":{"*.model":"javascript"},"[javascript]":{"editor.defaultFormatter":"esbenp.prettier-vscode"},"js/ts.validate.enabled":!1},null,2))}async function _e(t,e){if(!e)return;let r=v.join(t,"build.mjs"),n=e.match(/build\.mjs v\d+/)?.[0];if(await f.pathExists(r)){let i=(await f.readFile(r,"utf-8")).match(/build\.mjs v\d+/)?.[0];if(i&&i===n)return}await f.outputFile(r,e)}async function Ne(t,e,r){let n=v.join(t,".cache","token");if(await f.pathExists(n)){if((await f.readFile(n,"utf-8")).trim()===e)return;r?.("\u267B\uFE0F Workspace was tied to a different site \u2014 recreating it."),await f.emptyDir(t)}await f.outputFile(n,e)}function We(t,e){let r=`${e.name.substr(0,20)} ${e.id}`.replace(/[\s_]+/g,"-").toLowerCase(),n=t||v.join(Ae.homedir(),"SleekCMS");return v.resolve(n,r)}async function St(t){let e=(t.token||"").trim();if(!e)throw new Error("syncSite: token is required");let r=t.log??(x=>console.log(x)),n=t.onProgress,i=(t.env||e.split("-")[2]||"production").toLowerCase(),s=Ht[i]||Ht.production,c=await bt(s,e,"GET","/get_site"),d=t.viewsDir?v.resolve(t.viewsDir):We(t.path,c);await f.ensureDir(d),await Ne(d,e,r),await Promise.all(je.map(x=>f.ensureDir(v.join(d,x)))),await _e(d,t.buildScript);let h=v.join(d,".cache","state.json");t.flush&&await f.remove(h);let S=!await f.pathExists(h),y=S?{}:(await f.readJson(h)).fileMap||{},m=0,w=0;return S?({fileMap:y,pulled:w}=await Ve(d,s,e,r,n),await Ie(d,t.agentMd)):m=await qe(d,y,s,e,r,n),await f.outputJson(h,{fileMap:y},{spaces:2}),{viewsDir:d,site:c,isFirstRun:S,pushed:m,pulled:w}}var Ge=new Set([".DS_Store","Thumbs.db","desktop.ini"]);async function Ue(t){let e=v.join(t,"src");if(!await f.pathExists(e))return[];let r=[];async function n(i){let s=await f.readdir(i,{withFileTypes:!0});for(let c of s){if(Ge.has(c.name))continue;let d=v.join(i,c.name);c.isDirectory()?await n(d):c.isFile()&&r.push(v.relative(t,d).replace(/\\/g,"/"))}}return await n(e),r}async function qe(t,e,r,n,i,s){let c=[];for(let m of await Ue(t)){let w=v.join(t,m),x=e[m],P=await f.stat(w);if(x&&x.mtimeMs===P.mtimeMs)continue;let L=await f.readFile(w,"utf-8");L.trim()&&c.push({rel:m,full:w,stat:P,content:L,prior:x})}if(c.length===0)return 0;let d;try{d=await bt(r,n,"POST","/save_files",c.map(m=>({path:m.rel,content:m.content})))}catch(m){let w=m;return i(`\u274C Error saving files: ${w.body||w.message}`),0}let h=await Xe(t),S=0,y=Je(d);for(let m=0;m<c.length;m++){let w=c[m],x=He(w,m,d,y);if(s?.(m+1,c.length),!x){let P="No save response returned";h[w.rel]=P,i(`\u274C Error saving ${w.rel}: ${P}`);continue}if(x.error){h[w.rel]=x.error,i(`\u274C Error saving ${w.rel}: ${x.error}`);continue}delete h[w.rel],e[w.rel]={mtimeMs:w.stat.mtimeMs},i(`\u2705 ${w.prior?"Updated":"Created"} ${w.rel}`),S++}return await Ke(t,h),S}function Je(t){let e=new Map;for(let r of t)r.path&&e.set(r.path,r);return e}function He(t,e,r,n){let i=n.get(t.rel);if(i)return i;let s=r[e];if(!s?.path||s.path===t.rel)return s}var vt="sync-errors.log",wt=new Map;async function Xe(t){let e=v.resolve(t),r=wt.get(e);if(r)return r;let n=await ze(e);return wt.set(e,n),n}async function ze(t){let e=v.join(t,vt);if(!await f.pathExists(e))return{};let r=await f.readFile(e,"utf-8"),n={};for(let i of r.split(`
|
|
7
|
+
`)){let s=i.indexOf(": ");s>0&&(n[i.slice(0,s)]=i.slice(s+2))}return n}async function Ke(t,e){let r=v.join(t,vt),n=Object.entries(e);if(n.length===0){await f.remove(r);return}await f.outputFile(r,n.map(([i,s])=>`${i}: ${s}`).join(`
|
|
8
8
|
`)+`
|
|
9
|
-
`)}async function qe(t,e,n,r,i){ht.delete(v.resolve(t)),await w.remove(v.join(t,ft)),r("\u{1F4E5} Fetching files...");let s=await pt(e,n,"GET","/get_files"),a={},d=0;for(let h of s){let S=v.join(t,h.path);await w.outputFile(S,h.content);let f=(await w.stat(S)).mtimeMs;d++,i?.(d,s.length),a[h.path]={mtimeMs:f}}return r(`\u2714\uFE0F Synced ${s.length} file(s).`),{fileMap:a,pulled:d}}import Je from"path";import Ue from"chokidar";var He=5e3,Wt=3600*1e3,Xe=60*1e3,ze=/(^|[/\\])(\.DS_Store|Thumbs\.db|desktop\.ini)$/,z=class{viewsDir;onSync;onActivity;debounceMs;watcher=null;debounceTimer=null;isShuttingDown=!1;dirty=!1;syncInFlight=!1;constructor(e){this.viewsDir=e.viewsDir,this.onSync=e.onSync,this.onActivity=e.onActivity??(()=>{}),this.debounceMs=e.debounceMs??He}start(){if(this.watcher)return;let e=Je.join(this.viewsDir,"src");this.watcher=Ue.watch([e],{persistent:!0,ignoreInitial:!0,ignored:ze}).on("change",()=>this.scheduleSync()).on("add",()=>this.scheduleSync()).on("unlink",()=>this.scheduleSync())}setShuttingDown(e){this.isShuttingDown=e}async stop(){this.debounceTimer&&(clearTimeout(this.debounceTimer),this.debounceTimer=null),this.watcher&&(await this.watcher.close(),this.watcher=null)}scheduleSync(){this.isShuttingDown||(this.dirty=!0,this.debounceTimer&&clearTimeout(this.debounceTimer),this.debounceTimer=setTimeout(()=>this.flush(),this.debounceMs),W(),this.onActivity())}async flush(){if(this.debounceTimer=null,!(!this.dirty||this.isShuttingDown)){if(this.syncInFlight){this.debounceTimer=setTimeout(()=>this.flush(),this.debounceMs);return}this.dirty=!1,this.syncInFlight=!0;try{await this.onSync()}catch(e){let n=e;console.error("\u274C Sync failed:",n.body||n.message)}finally{this.syncInFlight=!1}}}},G=null,St=0,yt=null,wt=Wt,xt=!1;function qt(t,e=Wt){yt=t,wt=e,xt=!1,St=Date.now(),!G&&(G=setInterval(()=>{xt||!yt||Date.now()-St<wt||(vt(),console.log(`
|
|
10
|
-
\u{1F4A4} No changes for ${Math.round(wt/6e4)} minutes. Terminating.`),yt())},Xe),G.unref?.())}function W(){St=Date.now()}function vt(){xt=!0,G&&(clearInterval(G),G=null)}import nt from"fs-extra";import Ke from"os";import Ut from"path";var Jt={localhost:"http://app.sleekcms.test/api",development:"https://app.sleekcms.dev/api",production:"https://app.sleekcms.com/api"};function Tt(t){return Jt[t]||Jt.production}var Ve=Ut.join(Ke.homedir(),"SleekCMS"),rt=Ut.join(Ve,"auth.json"),bt;function Ht(t){bt=t||void 0}function kt(t){let e=[];return t&&e.push(`_auth=${t}`),bt&&e.push(`_dev=${bt}`),e.length?e.join("; "):void 0}var P=class extends Error{status;body;constructor(e,n,r=""){super(e),this.name="UserAuthError",this.status=n,this.body=r}};async function Xt(){try{let t=await nt.readJson(rt);return t&&t.userToken?t:null}catch{return null}}async function zt(t){await nt.outputJson(rt,t,{spaces:2});try{await nt.chmod(rt,384)}catch{}}async function Kt(){await nt.remove(rt)}async function K(t){let e={},n=kt(t.userToken);n&&(e.Cookie=n),t.subdomain&&(e["x-subdomain"]=t.subdomain),t.slug&&(e.site=t.slug),t.body!==void 0&&(e["Content-Type"]="application/json");let r=await fetch(Tt(t.env)+t.path,{method:t.method,headers:e,body:t.body!==void 0?JSON.stringify(t.body):void 0});if(!r.ok){let i=await r.text().catch(()=>"");throw new P(`${t.method} ${t.path} \u2192 ${r.status}: ${i}`,r.status,i)}return r.json()}async function Vt(t,e){let n={"Content-Type":"application/json"},r=kt();r&&(n.Cookie=r);let i=await fetch(Tt(t)+"/auth/otp",{method:"PUT",headers:n,body:JSON.stringify({email:e})});if(!i.ok){let a=await i.text().catch(()=>"");throw new P(`Failed to send OTP \u2192 ${i.status}: ${a}`,i.status,a)}return{otp:(await i.json().catch(()=>({}))).do_not_use_in_code}}async function Et(t,e,n){let r={"Content-Type":"application/json"},i=kt();i&&(r.Cookie=i);let s=await fetch(Tt(t)+"/auth/login",{method:"POST",headers:r,body:JSON.stringify({email:e,otp:n})});if(!s.ok){let h=await s.text().catch(()=>"");throw new P(`Login failed \u2192 ${s.status}: ${h}`,s.status,h)}let a=s.headers.getSetCookie?.()??[],d=Ye(a);if(!d)throw new P("Login succeeded but no _auth cookie was returned.",500);return d}function Ye(t){for(let e of t){let n=e.match(/(?:^|;\s*)_auth=([^;]+)/);if(n&&n[1])return decodeURIComponent(n[1])}return null}async function Ct(t,e){return K({env:t,userToken:e,method:"GET",path:"/org"})}async function Yt(t,e,n){return K({env:t,userToken:e,method:"GET",path:"/site",subdomain:n})}async function Qt(t,e,n,r){let{token:i}=await K({env:t,userToken:e,method:"POST",path:"/site_user/api_token",subdomain:n,slug:r,body:{refresh:!1}});return i}async function Zt(t,e,n){let r=await K({env:t,userToken:e,method:"POST",path:"/org",body:n?{name:n}:{}});return{id:r.id,name:r.name,subdomain:r.subdomain}}async function te(t,e,n,r){let i=await K({env:t,userToken:e,method:"POST",path:"/site",subdomain:n,body:{name:r}});return{id:i.id,name:i.name,slug:i.slug}}var re=500;function Ze(){return new Date().toTimeString().slice(0,8)}function it(t){let e=t;if(e.body){try{let n=JSON.parse(e.body);if(n&&typeof n.message=="string")return n.message;if(n&&typeof n.error=="string")return n.error}catch{}return e.body}return e.message}async function ie(t){t&&(await ee.remove(ne.join(t,"src")),await ee.remove(ne.join(t,".cache","state.json")))}var st=class extends Qe{env="production";userToken="";email;catalog=[];synced=new Map;message;messageIsError=!1;agentMd;isShuttingDown=!1;constructor(e){super(),this.agentMd=e}changed(){this.emit("change")}flash(e,n=!1){this.message=e,this.messageIsError=n,this.changed()}appendLog(e,n){let r=this.synced.get(e);r&&(r.logs.push({time:Ze(),text:n}),r.logs.length>re&&r.logs.splice(0,r.logs.length-re),this.changed())}siteCount(){return this.synced.size}syncingCount(){let e=0;for(let n of this.synced.values())n.status==="syncing"&&e++;return e}watchingCount(){let e=0;for(let n of this.synced.values())(n.status==="watching"||n.status==="syncing")&&e++;return e}async init(e,n,r){this.env=e,this.userToken=n,r&&(this.email=r);let i=await Ct(e,n);await this.buildCatalog(i),qt(()=>{this.exit()})}async buildCatalog(e){let n=[];for(let r of e)try{n.push({org:r,sites:await Yt(this.env,this.userToken,r.subdomain)})}catch(i){if(i instanceof P&&i.status===401)throw i;n.push({org:r,sites:[]})}this.catalog=n,this.changed()}isSynced(e){return this.synced.has(e)}availableSites(){let e=[];for(let n of this.catalog)for(let r of n.sites)this.synced.has(r.id)||e.push({org:n.org,site:r});return e}async addSite(e,n){if(this.synced.has(n.id))return;let r;try{r=await Qt(this.env,this.userToken,e.subdomain,n.slug)}catch(s){this.flash(`Failed to authorize "${n.name}": ${it(s)}`,!0);return}let i={org:e,site:n,siteToken:r,viewsDir:"",status:"syncing",logs:[]};this.synced.set(n.id,i),this.appendLog(n.id,"\u2795 added \u2014 pulling files"),W(),this.changed();try{let s=await mt({token:r,env:this.env,agentMd:this.agentMd,log:a=>this.appendLog(n.id,a),onProgress:(a,d)=>{i.progress={done:a,total:d},this.changed()}});i.viewsDir=s.viewsDir,i.progress=void 0,i.status="watching",i.lastSyncedAt=Date.now(),i.lastMsg=`pulled ${s.pulled} file(s)`,i.watcher=new z({viewsDir:s.viewsDir,onSync:()=>this.runSync(n.id),onActivity:W}),i.watcher.start(),this.appendLog(n.id,"\u{1F440} watching for changes")}catch(s){let a=s;i.status="error",i.progress=void 0,i.lastMsg=a.body||a.message,this.appendLog(n.id,`\u274C ${a.body||a.message}`)}this.changed()}async removeSite(e){let n=this.synced.get(e);n&&(n.watcher?.setShuttingDown(!0),await n.watcher?.stop(),await ie(n.viewsDir),this.synced.delete(e),W(),this.changed())}async ensureOrg(e){if(e)return e;if(this.catalog[0])return this.catalog[0].org;let n;try{n=await Ct(this.env,this.userToken)}catch(r){return this.flash(`Couldn't check your organizations: ${it(r)}`,!0),null}if(n.length>0){try{await this.buildCatalog(n)}catch{}return n[0]}try{let r=this.email?`${this.email.split("@")[0]}'s Sites`:"My Sites";this.flash("No organization yet \u2014 creating one\u2026");let i=await Zt(this.env,this.userToken,r);return this.catalog=[...this.catalog,{org:i,sites:[]}],this.changed(),i}catch(r){return this.flash(`Could not create an organization: ${it(r)}`,!0),null}}async createNewSite(e,n){let r=await this.ensureOrg(n);if(!r)return;let i;try{i=await te(this.env,this.userToken,r.subdomain,e)}catch(a){this.flash(`Could not create "${e}": ${it(a)}`,!0);return}let s=this.catalog.find(a=>a.org.id===r.id);s&&!s.sites.some(a=>a.id===i.id)&&(s.sites=[...s.sites,i]),this.flash(`Created "${i.name}".`),await this.addSite(r,i)}async refetch(e){await this.runSync(e,{flush:!0})}async runSync(e,n={}){let r=this.synced.get(e);if(!(!r||!r.viewsDir||r.status==="paused")){r.status="syncing",this.changed();try{let i=await mt({token:r.siteToken,env:this.env,viewsDir:r.viewsDir,agentMd:this.agentMd,flush:n.flush,log:s=>this.appendLog(e,s),onProgress:(s,a)=>{r.progress={done:s,total:a},this.changed()}});r.progress=void 0,r.status="watching",r.lastSyncedAt=Date.now(),n.flush?r.lastMsg=`re-fetched ${i.pulled} file(s)`:r.lastMsg=i.pushed>0?`pushed ${i.pushed} file(s)`:"up to date"}catch(i){let s=i;r.status="error",r.progress=void 0,r.lastMsg=s.body||s.message,this.appendLog(e,`\u274C ${s.body||s.message}`)}this.changed()}}async togglePause(e){let n=this.synced.get(e);!n||!n.viewsDir||(n.status==="paused"?(n.watcher=new z({viewsDir:n.viewsDir,onSync:()=>this.runSync(e),onActivity:W}),n.watcher.start(),n.status="watching",this.appendLog(e,"\u25B6 resumed \u2014 watching for changes")):(n.watcher?.setShuttingDown(!0),await n.watcher?.stop(),n.watcher=void 0,n.progress=void 0,n.status="paused",this.appendLog(e,"\u23F8 paused")),this.changed())}async cleanupAll(){vt();for(let e of this.synced.values())e.watcher?.setShuttingDown(!0),await e.watcher?.stop(),await ie(e.viewsDir)}async exit(){this.isShuttingDown||(this.isShuttingDown=!0,await this.cleanupAll(),this.emit("exit"))}async logout(){this.isShuttingDown||(this.isShuttingDown=!0,await Kt(),await this.cleanupAll(),this.emit("logout"))}};import{useEffect as ln,useState as ge}from"react";import{Box as gn,Text as dn,useInput as hn}from"ink";import{useState as V}from"react";import{Box as Y,Text as q}from"ink";import se from"ink-text-input";import{jsx as _,jsxs as J}from"react/jsx-runtime";function oe(t){let e=t;return e?.body||e?.message||String(t)}function ae({env:t,initialError:e,onAuthed:n}){let[r,i]=V("email"),[s,a]=V(""),[d,h]=V(""),[S,f]=V(e),[p,y]=V(),b=async D=>{let T=D.trim();if(T){f(void 0),i("busy");try{let{otp:E}=await Vt(t,T);if(a(T),E){let U=await Et(t,T,E);await n(U,T);return}y(`OTP sent to ${T}`),i("otp")}catch(E){f(oe(E)),i("email")}}},O=async D=>{let T=D.trim();if(T){f(void 0),i("busy");try{let E=await Et(t,s,T);await n(E,s)}catch(E){f(oe(E)),h(""),i("otp")}}};return J(Y,{flexDirection:"column",children:[J(q,{bold:!0,children:["\u25C6 SleekCMS \u2014 sign in",t!=="production"?` (${t})`:""]}),S?J(q,{color:"red",children:["\u2717 ",S]}):null,r==="email"&&J(Y,{marginTop:1,children:[_(q,{children:"Email: "}),_(se,{value:s,onChange:a,onSubmit:b,placeholder:"you@example.com"})]}),r==="otp"&&J(Y,{flexDirection:"column",marginTop:1,children:[p?_(q,{color:"green",children:p}):null,J(Y,{children:[_(q,{children:"Enter OTP: "}),_(se,{value:d,onChange:h,onSubmit:O})]})]}),r==="busy"&&_(Y,{marginTop:1,children:_(q,{children:"Working\u2026"})})]})}import{Fragment as en,useEffect as Ot,useMemo as nn,useState as $}from"react";import{Box as m,Text as u,useInput as rn,useStdout as sn}from"ink";import ue from"ink-text-input";import tn from"node:os";function Mt(t){if(!t)return"\u2014";let e=tn.homedir();return t.startsWith(e)?"~"+t.slice(e.length):t}function ce(t){if(!t)return"\u2014";let e=Math.floor((Date.now()-t)/1e3);if(e<5)return"just now";if(e<60)return`${e}s ago`;let n=Math.floor(e/60);if(n<60)return`${n}m ago`;let r=Math.floor(n/60);return r<24?`${r}h ago`:`${Math.floor(r/24)}d ago`}function ot(t){return!t||t.total<=0?null:Math.round(t.done/t.total*100)}function at(t){switch(t){case"syncing":return{dot:"\u25CF",color:"green",label:"syncing"};case"watching":return{dot:"\u25CB",color:"gray",label:"watching"};case"paused":return{dot:"\u2016",color:"yellow",label:"paused"};case"error":return{dot:"\u2717",color:"red",label:"error"}}}import{Fragment as ct,jsx as o,jsxs as g}from"react/jsx-runtime";var F=(t,e)=>e<=0?0:Math.max(0,Math.min(t,e-1)),on=16,an=5;function cn({k:t}){return o(u,{color:"black",backgroundColor:"green",children:` ${t} `})}function A({k:t,label:e,width:n=16}){return g(m,{width:n,children:[o(cn,{k:t}),o(u,{color:"gray",children:` ${e}`})]})}function un({percent:t,width:e=28}){let n=Math.round(Math.max(0,Math.min(100,t))/100*e);return g(u,{children:[o(u,{color:"green",children:"\u2588".repeat(n)}),o(u,{color:"gray",children:"\u2591".repeat(Math.max(0,e-n))})]})}function le({controller:t}){let{stdout:e}=sn(),n=nn(()=>Nt(),[]),[,r]=$(0),[i,s]=$(()=>new Date),[a,d]=$(null),[h,S]=$(()=>t.synced.size>0||t.catalog.some(l=>l.sites.length>0)?"dashboard":t.catalog.length<=1?"newName":"newOrg"),[f,p]=$(0),[y,b]=$(""),[O,D]=$(!1),[T,E]=$(""),[U,Q]=$(!1),Dt=()=>r(c=>c+1);Ot(()=>(t.on("change",Dt),()=>{t.off("change",Dt)}),[t]),Ot(()=>{let c=setInterval(()=>s(new Date),1e3);return()=>clearInterval(c)},[]);let R=t.catalog.flatMap(c=>c.sites.map(l=>({org:c.org,site:l,entry:t.synced.get(l.id)}))).sort((c,l)=>(c.entry?0:1)-(l.entry?0:1)),I=T.trim().toLowerCase(),C=I?R.filter(c=>c.site.name.toLowerCase().includes(I)||(c.org.name||c.org.subdomain||"").toLowerCase().includes(I)):R,j=C.findIndex(c=>c.site.id===a);j<0&&(j=0);let k=C[j],x=k?.entry,$t=C.findIndex(c=>!c.entry),N=Math.max(an,(e?.rows??30)-on),H=C.length,Z=H>N,me=Math.floor(N/2),X=Z?Math.max(0,Math.min(j-me,H-N)):0,lt=Math.min(H,X+N),fe=C.slice(X,lt);Ot(()=>{h!=="dashboard"||O||R.length>0||(b(""),t.catalog.length<=1?S("newName"):(p(0),S("newOrg")))},[R.length,h,O,t.catalog.length]);let tt=c=>{if(C.length===0)return;let l=F(j+c,C.length);d(C[l].site.id)},ye=()=>{k&&(k.entry?t.removeSite(k.site.id):t.addSite(k.org,k.site))},we=()=>{b(""),t.catalog.length<=1?S("newName"):(p(0),S("newOrg"))},Se=c=>{if(!x){t.flash("Press \u23CE to start syncing this site first.",!0);return}let l=c.run(x.viewsDir);t.flash(l?`Opened ${c.label} at ${Mt(x.viewsDir)}`:`Couldn't open ${c.label}.`,!l)},xe=()=>{if(!x){t.flash("Press \u23CE to start syncing this site first.",!0);return}t.togglePause(x.site.id)};rn((c,l)=>{if(h==="newOrg"){l.escape?S("dashboard"):l.upArrow?p(M=>F(M-1,t.catalog.length)):l.downArrow?p(M=>F(M+1,t.catalog.length)):l.return&&S("newName");return}if(U){l.escape?(Q(!1),E(""),d(null)):l.return?Q(!1):l.upArrow?tt(-1):l.downArrow&&tt(1);return}if(l.escape){E(""),d(null);return}if(c==="/")return void Q(!0);if(c==="n")return void we();if(c==="q")return void t.logout();if(l.upArrow)return tt(-1);if(l.downArrow)return tt(1);if(l.return||c===" ")return ye();if(c==="p")return xe();if(c>="1"&&c<="9"){let M=n[parseInt(c,10)-1];if(M)return Se(M)}},{isActive:h!=="newName"});let ve=async c=>{let l=c.trim();if(!l){S("dashboard");return}let M=t.catalog[F(f,t.catalog.length)]?.org??t.catalog[0]?.org;D(!0),S("dashboard"),await t.createNewSite(l,M),D(!1)},be=Math.max(5,(e?.rows??30)-18);return g(m,{flexDirection:"column",children:[g(m,{justifyContent:"space-between",paddingX:1,children:[g(m,{children:[o(u,{color:"green",bold:!0,children:"\u25C6 SleekCMS"}),o(u,{color:"gray",children:" \u2502 "}),o(u,{children:t.email??"signed in"})]}),g(m,{children:[g(u,{color:"gray",children:[I?`${C.length}/${R.length}`:R.length," sites \xB7 "]}),g(u,{color:"green",children:[t.watchingCount()," watching"]}),o(u,{color:"gray",children:` \xB7 ${i.toTimeString().slice(0,8)}`})]})]}),g(m,{children:[g(m,{flexDirection:"column",borderStyle:"round",borderColor:"green",width:38,paddingX:1,children:[g(m,{justifyContent:"space-between",children:[o(u,{color:"green",bold:!0,children:"SITE WORKSPACES"}),R.length>N&&!U&&!I?o(u,{color:"gray",children:"/ filter"}):null]}),U?g(m,{children:[o(u,{color:"cyan",children:"/ "}),o(ue,{value:T,onChange:E,onSubmit:()=>Q(!1),placeholder:"type to filter\u2026"})]}):I?o(u,{color:"gray",children:`/ ${T} \xB7 ${C.length} match${C.length===1?"":"es"}`}):null,o(m,{flexDirection:"column",marginTop:1,minHeight:Z?N+3:void 0,children:C.length===0?o(u,{color:"gray",children:I?`no matches for \u201C${T}\u201D`:"no sites \u2014 press n to create one"}):g(ct,{children:[Z?o(u,{color:"gray",children:X>0?` \u2191 ${X} more`:" "}):null,fe.map((c,l)=>{let M=X+l,At=c.entry?at(c.entry.status):{dot:"\xB7",color:"gray",label:"idle"},Rt=ot(c.entry?.progress),Te=c.entry?c.entry.status==="syncing"&&Rt!=null?`${Rt}%`:c.entry.status==="paused"?"paused":"--":"",et=M===j,ke=M===$t&&$t>0;return g(en,{children:[ke?o(u,{color:"gray",children:"\u2500".repeat(34)}):null,g(m,{justifyContent:"space-between",children:[g(m,{children:[o(u,{color:et?"green":"gray",children:et?"\u276F ":" "}),g(u,{color:At.color,children:[At.dot," "]}),o(u,{color:et?"white":c.entry?void 0:"gray",bold:et,children:c.site.name})]}),o(u,{color:c.entry?.status==="syncing"?"green":"gray",children:Te})]})]},c.site.id)}),Z?o(u,{color:"gray",children:lt<H?` \u2193 ${H-lt} more`:" "}):null]})}),o(m,{marginTop:1,children:o(u,{color:"gray",children:"+ new site \xB7 n"})})]}),g(m,{flexDirection:"column",flexGrow:1,marginLeft:1,children:[o(m,{flexDirection:"column",borderStyle:"round",borderColor:"gray",paddingX:1,children:k?g(ct,{children:[g(u,{children:[k.org.name||k.org.subdomain?g(u,{color:"gray",children:[k.org.name||k.org.subdomain," / "]}):null,o(u,{color:"cyan",bold:!0,children:k.site.name})]}),g(m,{marginTop:1,children:[o(u,{color:"gray",children:"status "}),x?o(u,{color:at(x.status).color,children:`: ${at(x.status).label}`}):o(u,{color:"gray",children:": idle \u2014 not syncing (press \u23CE to start)"})]}),g(m,{children:[o(u,{color:"gray",children:"path "}),o(u,{children:x?.viewsDir?Mt(x.viewsDir):"\u2014"})]}),g(m,{children:[o(u,{color:"gray",children:"synced "}),o(u,{children:ce(x?.lastSyncedAt)}),o(u,{color:"gray",children:" progress "}),o(u,{color:x?.status==="syncing"?"green":"gray",children:x?.status==="syncing"?`${ot(x.progress)??0}%`:"\u2014"})]}),o(m,{marginTop:1,children:o(un,{percent:x?x.status==="syncing"?ot(x.progress)??0:x.status==="watching"?100:0:0})})]}):o(u,{color:"gray",children:"No site selected."})}),h==="dashboard"?g(m,{flexDirection:"column",borderStyle:"round",borderColor:"gray",paddingX:1,marginTop:1,flexGrow:1,children:[g(u,{children:[o(u,{color:"cyan",bold:!0,children:"LOGS \xB7 "}),o(u,{color:"white",children:k?.site.name??"\u2014"})]}),o(m,{flexDirection:"column",marginTop:1,children:!x||x.logs.length===0?o(u,{color:"gray",children:"no activity yet"}):x.logs.slice(-be).map((c,l)=>g(m,{children:[g(u,{color:"gray",children:[c.time," "]}),o(u,{children:c.text})]},l))})]}):g(m,{flexDirection:"column",borderStyle:"round",borderColor:"blue",paddingX:1,marginTop:1,flexGrow:1,children:[h==="newOrg"&&g(ct,{children:[o(u,{color:"cyan",bold:!0,children:"CREATE SITE \u2014 PICK ORG"}),o(u,{color:"gray",children:"\u2191/\u2193 select \xB7 \u23CE next \xB7 esc cancel"}),o(m,{flexDirection:"column",marginTop:1,children:t.catalog.map((c,l)=>g(u,{color:l===F(f,t.catalog.length)?"blue":void 0,bold:l===F(f,t.catalog.length),children:[l===F(f,t.catalog.length)?"\u276F ":" ",c.org.name||c.org.subdomain]},c.org.id))})]}),h==="newName"&&g(ct,{children:[o(u,{color:"cyan",bold:!0,children:"CREATE SITE"}),g(m,{marginTop:1,children:[o(u,{children:"New site name: "}),o(ue,{value:y,onChange:b,onSubmit:ve})]}),o(u,{color:"gray",children:"\u23CE create \xB7 empty + \u23CE to cancel"})]})]})]})]}),t.message?o(m,{paddingX:1,children:g(u,{color:t.messageIsError?"red":"gray",children:[t.messageIsError?"\u2717 ":"",t.message]})}):null,g(m,{flexDirection:"column",marginTop:1,children:[g(m,{paddingX:1,flexWrap:"wrap",children:[o(A,{k:"\u2191\u2193",label:"select"}),o(A,{k:"/",label:"search"}),o(A,{k:"\u23CE",label:"start/stop"}),o(A,{k:"p",label:"pause"}),o(A,{k:"n",label:"new site"}),o(A,{k:"q",label:"logout"}),o(A,{k:"ctrl+c",label:"quit",width:14})]}),n.length>0?o(m,{paddingX:1,marginTop:1,flexWrap:"wrap",children:n.map((c,l)=>o(A,{k:String(l+1),label:c.label},c.key))}):null]})]})}import{jsx as ut}from"react/jsx-runtime";function de(t){let e=t;return e?.body||e?.message||String(t)}function he({controller:t,env:e,devSecret:n,savedToken:r,savedEmail:i}){let[s,a]=ge(r?"loading":"auth"),[d,h]=ge();hn((f,p)=>{p.ctrl&&f==="c"&&t.exit()}),ln(()=>{r&&t.init(e,r,i).then(()=>a("manage")).catch(f=>{f instanceof P&&f.status===401?h("Saved session expired \u2014 please sign in again."):h(de(f)),a("auth")})},[]);let S=async(f,p)=>{h(void 0),await zt({env:e,userToken:f,devSecret:n,email:p}),a("loading");try{await t.init(e,f,p),a("manage")}catch(y){h(de(y)),a("auth")}};return s==="auth"?ut(ae,{env:e,initialError:d,onAuthed:S}):s==="loading"?ut(gn,{children:ut(dn,{children:"Loading your sites\u2026 "})}):ut(le,{controller:t})}import{jsx as bn}from"react/jsx-runtime";var wn=fn(import.meta.url),Pt=Ft();if(Pt.version){let t=wn("../package.json");console.log(t.version),process.exit(0)}var Sn=pe.dirname(mn(import.meta.url)),xn=pn(pe.join(Sn,"AGENT.md"),"utf-8");async function vn(){let t=new st(xn),e=await Xt(),n=(Pt.env??e?.env??"production").toLowerCase(),r=Pt.dev??process.env.SLEEKCMS_DEV_SECRET??e?.devSecret;Ht(r),process.stdout.isTTY&&process.stdout.write("\x1B[2J\x1B[3J\x1B[H");let i=yn(bn(he,{controller:t,env:n,devSecret:r,savedToken:e?.userToken,savedEmail:e?.email}),{exitOnCtrlC:!1}),s=a=>{i.unmount(),console.log(a),process.exit(0)};t.once("exit",()=>s("\u2713 Stopped. Local site files were cleaned up.")),t.once("logout",()=>s("\u{1F44B} Logged out. Run `sleekcms` to sign in again.")),process.on("SIGINT",()=>{t.exit()}),process.on("SIGTERM",()=>{t.exit()}),await i.waitUntilExit()}vn().catch(t=>{let e=t;console.error("\u274C",e.body||e.message),process.exit(1)});
|
|
9
|
+
`)}async function Ve(t,e,r,n,i){wt.delete(v.resolve(t)),await f.remove(v.join(t,vt)),n("\u{1F4E5} Fetching files...");let s=await bt(e,r,"GET","/get_files"),c={},d=0;for(let h of s){let S=v.join(t,h.path);await f.outputFile(S,h.content);let y=(await f.stat(S)).mtimeMs;d++,i?.(d,s.length),c[h.path]={mtimeMs:y}}return n(`\u2714\uFE0F Synced ${s.length} file(s).`),{fileMap:c,pulled:d}}import Ye from"path";import Qe from"chokidar";var Ze=5e3,tr=/(^|[/\\])(\.DS_Store|Thumbs\.db|desktop\.ini)$/,z=class{viewsDir;onSync;debounceMs;watcher=null;debounceTimer=null;isShuttingDown=!1;dirty=!1;syncInFlight=!1;constructor(e){this.viewsDir=e.viewsDir,this.onSync=e.onSync,this.debounceMs=e.debounceMs??Ze}start(){if(this.watcher)return;let e=Ye.join(this.viewsDir,"src");this.watcher=Qe.watch([e],{persistent:!0,ignoreInitial:!0,ignored:tr}).on("change",()=>this.scheduleSync()).on("add",()=>this.scheduleSync()).on("unlink",()=>this.scheduleSync())}setShuttingDown(e){this.isShuttingDown=e}async stop(){this.debounceTimer&&(clearTimeout(this.debounceTimer),this.debounceTimer=null),this.watcher&&(await this.watcher.close(),this.watcher=null)}scheduleSync(){this.isShuttingDown||(this.dirty=!0,this.debounceTimer&&clearTimeout(this.debounceTimer),this.debounceTimer=setTimeout(()=>this.flush(),this.debounceMs))}async flush(){if(this.debounceTimer=null,!(!this.dirty||this.isShuttingDown)){if(this.syncInFlight){this.debounceTimer=setTimeout(()=>this.flush(),this.debounceMs);return}this.dirty=!1,this.syncInFlight=!0;try{await this.onSync()}catch(e){let r=e;console.error("\u274C Sync failed:",r.body||r.message)}finally{this.syncInFlight=!1}}}};import it from"fs-extra";import er from"os";import zt from"path";var Xt={localhost:"http://app.sleekcms.test/api",development:"https://app.sleekcms.dev/api",production:"https://app.sleekcms.com/api"};function Tt(t){return Xt[t]||Xt.production}var rr=zt.join(er.homedir(),"SleekCMS"),st=zt.join(rr,"auth.json"),xt;function Kt(t){xt=t||void 0}function kt(t){let e=[];return t&&e.push(`_auth=${t}`),xt&&e.push(`_dev=${xt}`),e.length?e.join("; "):void 0}var O=class extends Error{status;body;constructor(e,r,n=""){super(e),this.name="UserAuthError",this.status=r,this.body=n}};async function Vt(){try{let t=await it.readJson(st);return t&&t.userToken?t:null}catch{return null}}async function Yt(t){await it.outputJson(st,t,{spaces:2});try{await it.chmod(st,384)}catch{}}async function Qt(){await it.remove(st)}async function G(t){let e={},r=kt(t.userToken);r&&(e.Cookie=r),t.subdomain&&(e["x-subdomain"]=t.subdomain),t.slug&&(e.site=t.slug),t.body!==void 0&&(e["Content-Type"]="application/json");let n=await fetch(Tt(t.env)+t.path,{method:t.method,headers:e,body:t.body!==void 0?JSON.stringify(t.body):void 0});if(!n.ok){let i=await n.text().catch(()=>"");throw new O(`${t.method} ${t.path} \u2192 ${n.status}: ${i}`,n.status,i)}return n.json()}async function Zt(t,e){let r={"Content-Type":"application/json"},n=kt();n&&(r.Cookie=n);let i=await fetch(Tt(t)+"/auth/otp",{method:"PUT",headers:r,body:JSON.stringify({email:e})});if(!i.ok){let c=await i.text().catch(()=>"");throw new O(`Failed to send OTP \u2192 ${i.status}: ${c}`,i.status,c)}return{otp:(await i.json().catch(()=>({}))).do_not_use_in_code}}async function Ct(t,e,r){let n={"Content-Type":"application/json"},i=kt();i&&(n.Cookie=i);let s=await fetch(Tt(t)+"/auth/login",{method:"POST",headers:n,body:JSON.stringify({email:e,otp:r})});if(!s.ok){let h=await s.text().catch(()=>"");throw new O(`Login failed \u2192 ${s.status}: ${h}`,s.status,h)}let c=s.headers.getSetCookie?.()??[],d=nr(c);if(!d)throw new O("Login succeeded but no _auth cookie was returned.",500);return d}function nr(t){for(let e of t){let r=e.match(/(?:^|;\s*)_auth=([^;]+)/);if(r&&r[1])return decodeURIComponent(r[1])}return null}async function Et(t,e){return G({env:t,userToken:e,method:"GET",path:"/org"})}async function te(t,e,r){return G({env:t,userToken:e,method:"GET",path:"/site",subdomain:r})}async function ee(t,e,r,n){let i;try{i=await G({env:t,userToken:e,method:"GET",path:"/site_build",subdomain:r,slug:n})}catch{return null}if(!Array.isArray(i)||i.length===0)return null;let s=i[0],c=i.find(h=>typeof h.url=="string"&&h.url.length>0),d=Array.isArray(s.errors)?s.errors.map(h=>String(h)):[];return d.length>0?{status:"error",mode:"preview",errors:d,url:c?.url||void 0,at:s.updated_at?Date.parse(s.updated_at):void 0}:c?{status:"success",mode:"preview",url:c.url||void 0,at:c.updated_at?Date.parse(c.updated_at):void 0}:null}async function re(t,e,r,n){let{token:i}=await G({env:t,userToken:e,method:"POST",path:"/site_user/api_token",subdomain:r,slug:n,body:{refresh:!1}});return i}async function ne(t,e,r){let n=await G({env:t,userToken:e,method:"POST",path:"/org",body:r?{name:r}:{}});return{id:n.id,name:n.name,subdomain:n.subdomain}}async function ie(t,e,r,n){let i=await G({env:t,userToken:e,method:"POST",path:"/site",subdomain:r,body:{name:n}});return{id:i.id,name:i.name,slug:i.slug}}var se=500;function ar(){return new Date().toTimeString().slice(0,8)}function ot(t){let e=t;if(e.body){try{let r=JSON.parse(e.body);if(r&&typeof r.message=="string")return r.message;if(r&&typeof r.error=="string")return r.error}catch{}return e.body}return e.message}async function oe(t){t&&(await K.remove(Bt.join(t,"src")),await K.remove(Bt.join(t,".cache","state.json")))}var at=class extends ir{env="production";userToken="";email;catalog=[];synced=new Map;message;messageIsError=!1;agentMd;buildScript;isShuttingDown=!1;constructor(e,r){super(),this.agentMd=e,this.buildScript=r}changed(){this.emit("change")}flash(e,r=!1){this.message=e,this.messageIsError=r,this.changed()}appendLog(e,r){let n=this.synced.get(e);n&&(n.logs.push({time:ar(),text:r}),n.logs.length>se&&n.logs.splice(0,n.logs.length-se),this.changed())}siteCount(){return this.synced.size}syncingCount(){let e=0;for(let r of this.synced.values())r.status==="syncing"&&e++;return e}watchingCount(){let e=0;for(let r of this.synced.values())(r.status==="watching"||r.status==="syncing")&&e++;return e}async init(e,r,n){this.env=e,this.userToken=r,n&&(this.email=n);let i=await Et(e,r);await this.buildCatalog(i)}async buildCatalog(e){let r=[];for(let n of e)try{r.push({org:n,sites:await te(this.env,this.userToken,n.subdomain)})}catch(i){if(i instanceof O&&i.status===401)throw i;r.push({org:n,sites:[]})}this.catalog=r,this.changed()}isSynced(e){return this.synced.has(e)}availableSites(){let e=[];for(let r of this.catalog)for(let n of r.sites)this.synced.has(n.id)||e.push({org:r.org,site:n});return e}async addSite(e,r){if(this.synced.has(r.id))return;let n;try{n=await re(this.env,this.userToken,e.subdomain,r.slug)}catch(s){this.flash(`Failed to authorize "${r.name}": ${ot(s)}`,!0);return}let i={org:e,site:r,siteToken:n,viewsDir:"",status:"syncing",logs:[]};this.synced.set(r.id,i),this.appendLog(r.id,"\u2795 added \u2014 pulling files"),this.changed();try{let s=await St({token:n,env:this.env,agentMd:this.agentMd,buildScript:this.buildScript,log:c=>this.appendLog(r.id,c),onProgress:(c,d)=>{i.progress={done:c,total:d},this.changed()}});i.viewsDir=s.viewsDir,i.progress=void 0,i.status="watching",i.lastSyncedAt=Date.now(),i.lastMsg=`pulled ${s.pulled} file(s)`,i.watcher=new z({viewsDir:s.viewsDir,onSync:()=>this.runSync(r.id)}),i.watcher.start(),this.watchBuildStatus(r.id),this.seedLastBuild(r.id),this.appendLog(r.id,"\u{1F440} watching for changes")}catch(s){let c=s;i.status="error",i.progress=void 0,i.lastMsg=c.body||c.message,this.appendLog(r.id,`\u274C ${c.body||c.message}`)}this.changed()}async removeSite(e){let r=this.synced.get(e);r&&(r.watcher?.setShuttingDown(!0),await r.watcher?.stop(),await r.buildWatcher?.close(),await oe(r.viewsDir),this.synced.delete(e),this.changed())}buildSite(e,r=!1){let n=this.synced.get(e);if(!n||!n.viewsDir)return;let i=r?"live":"preview";this.writeBuildFile(n.viewsDir,{status:"building",mode:i,stage:"queued",url:n.lastBuild?.url,at:Date.now()}),sr("node",r?["build.mjs","--deploy"]:["build.mjs"],{cwd:n.viewsDir,stdio:"ignore"}).on("error",d=>{this.writeBuildFile(n.viewsDir,{status:"error",mode:i,errors:[d.message],url:n.lastBuild?.url,at:Date.now()}),this.appendLog(e,`\u274C build: ${d.message}`)}),this.appendLog(e,r?"\u{1F680} deploying\u2026":"\u{1F528} building\u2026")}buildFile(e){return Bt.join(e,".cache","build.json")}async readBuildFile(e){try{let r=await K.readJson(this.buildFile(e));return r&&r.status?r:null}catch{return null}}async writeBuildFile(e,r){let n=this.buildFile(e),i=`${n}.tmp`;try{await K.outputJson(i,r),await K.rename(i,n)}catch{}}watchBuildStatus(e){let r=this.synced.get(e);!r||!r.viewsDir||(r.buildWatcher=or.watch(this.buildFile(r.viewsDir),{ignoreInitial:!1}).on("add",()=>{this.readBuildStatus(e)}).on("change",()=>{this.readBuildStatus(e)}))}async readBuildStatus(e){let r=this.synced.get(e);if(!r)return;let n=await this.readBuildFile(r.viewsDir);if(!n)return;let i=r.lastBuild?.url;r.lastBuild={status:n.status,mode:n.mode??"preview",url:n.url??(n.status==="success"?void 0:i),errors:n.errors,stage:n.stage,at:n.at},this.changed()}async seedLastBuild(e){let r=this.synced.get(e);if(!r)return;let n=await ee(this.env,this.userToken,r.org.subdomain,r.site.slug);if(!n)return;let i=this.synced.get(e);if(!i)return;let s=await this.readBuildFile(i.viewsDir);s?.at!=null&&(n.at==null||s.at>=n.at)||await this.writeBuildFile(i.viewsDir,n)}async ensureOrg(e){if(e)return e;if(this.catalog[0])return this.catalog[0].org;let r;try{r=await Et(this.env,this.userToken)}catch(n){return this.flash(`Couldn't check your organizations: ${ot(n)}`,!0),null}if(r.length>0){try{await this.buildCatalog(r)}catch{}return r[0]}try{let n=this.email?`${this.email.split("@")[0]}'s Sites`:"My Sites";this.flash("No organization yet \u2014 creating one\u2026");let i=await ne(this.env,this.userToken,n);return this.catalog=[...this.catalog,{org:i,sites:[]}],this.changed(),i}catch(n){return this.flash(`Could not create an organization: ${ot(n)}`,!0),null}}async createNewSite(e,r){let n=await this.ensureOrg(r);if(!n)return;let i;try{i=await ie(this.env,this.userToken,n.subdomain,e)}catch(c){this.flash(`Could not create "${e}": ${ot(c)}`,!0);return}let s=this.catalog.find(c=>c.org.id===n.id);s&&!s.sites.some(c=>c.id===i.id)&&(s.sites=[...s.sites,i]),this.flash(`Created "${i.name}".`),await this.addSite(n,i)}async refetch(e){await this.runSync(e,{flush:!0})}async runSync(e,r={}){let n=this.synced.get(e);if(!(!n||!n.viewsDir||n.status==="paused")){n.status="syncing",this.changed();try{let i=await St({token:n.siteToken,env:this.env,viewsDir:n.viewsDir,agentMd:this.agentMd,buildScript:this.buildScript,flush:r.flush,log:s=>this.appendLog(e,s),onProgress:(s,c)=>{n.progress={done:s,total:c},this.changed()}});n.progress=void 0,n.status="watching",n.lastSyncedAt=Date.now(),r.flush?n.lastMsg=`re-fetched ${i.pulled} file(s)`:n.lastMsg=i.pushed>0?`pushed ${i.pushed} file(s)`:"up to date"}catch(i){let s=i;n.status="error",n.progress=void 0,n.lastMsg=s.body||s.message,this.appendLog(e,`\u274C ${s.body||s.message}`)}this.changed()}}async togglePause(e){let r=this.synced.get(e);!r||!r.viewsDir||(r.status==="paused"?(r.watcher=new z({viewsDir:r.viewsDir,onSync:()=>this.runSync(e)}),r.watcher.start(),r.status="watching",this.appendLog(e,"\u25B6 resumed \u2014 watching for changes")):(r.watcher?.setShuttingDown(!0),await r.watcher?.stop(),r.watcher=void 0,r.progress=void 0,r.status="paused",this.appendLog(e,"\u23F8 paused")),this.changed())}async cleanupAll(){for(let e of this.synced.values())e.watcher?.setShuttingDown(!0),await e.watcher?.stop(),await e.buildWatcher?.close(),await oe(e.viewsDir)}async exit(){this.isShuttingDown||(this.isShuttingDown=!0,await this.cleanupAll(),this.emit("exit"))}async logout(){this.isShuttingDown||(this.isShuttingDown=!0,await Qt(),await this.cleanupAll(),this.emit("logout"))}};import{useEffect as wr,useState as pe}from"react";import{Box as br,Text as Sr,useInput as vr}from"ink";import{useState as V}from"react";import{Box as Y,Text as U}from"ink";import ae from"ink-text-input";import{jsx as I,jsxs as q}from"react/jsx-runtime";function ce(t){let e=t;return e?.body||e?.message||String(t)}function ue({env:t,initialError:e,onAuthed:r}){let[n,i]=V("email"),[s,c]=V(""),[d,h]=V(""),[S,y]=V(e),[m,w]=V(),x=async L=>{let T=L.trim();if(T){y(void 0),i("busy");try{let{otp:C}=await Zt(t,T);if(c(T),C){let J=await Ct(t,T,C);await r(J,T);return}w(`OTP sent to ${T}`),i("otp")}catch(C){y(ce(C)),i("email")}}},P=async L=>{let T=L.trim();if(T){y(void 0),i("busy");try{let C=await Ct(t,s,T);await r(C,s)}catch(C){y(ce(C)),h(""),i("otp")}}};return q(Y,{flexDirection:"column",children:[q(U,{bold:!0,children:["\u25C6 SleekCMS \u2014 sign in",t!=="production"?` (${t})`:""]}),S?q(U,{color:"red",children:["\u2717 ",S]}):null,n==="email"&&q(Y,{marginTop:1,children:[I(U,{children:"Email: "}),I(ae,{value:s,onChange:c,onSubmit:x,placeholder:"you@example.com"})]}),n==="otp"&&q(Y,{flexDirection:"column",marginTop:1,children:[m?I(U,{color:"green",children:m}):null,q(Y,{children:[I(U,{children:"Enter OTP: "}),I(ae,{value:d,onChange:h,onSubmit:P})]})]}),n==="busy"&&I(Y,{marginTop:1,children:I(U,{children:"Working\u2026"})})]})}import{Fragment as ur,useEffect as dt,useMemo as lr,useState as M}from"react";import{Box as p,Text as u,useInput as dr,useStdout as gr}from"ink";import ge from"ink-text-input";import cr from"node:os";function Pt(t){if(!t)return"\u2014";let e=cr.homedir();return t.startsWith(e)?"~"+t.slice(e.length):t}function Dt(t){if(!t)return"\u2014";let e=Math.floor((Date.now()-t)/1e3);if(e<5)return"just now";if(e<60)return`${e}s ago`;let r=Math.floor(e/60);if(r<60)return`${r}m ago`;let n=Math.floor(r/60);return n<24?`${n}h ago`:`${Math.floor(n/24)}d ago`}function ut(t){return!t||t.total<=0?null:Math.round(t.done/t.total*100)}var ct=["\u280B","\u2819","\u2839","\u2838","\u283C","\u2834","\u2826","\u2827","\u2807","\u280F"];function le(t){return ct[(t%ct.length+ct.length)%ct.length]}function lt(t){switch(t){case"syncing":return{dot:"\u25CF",color:"green",label:"syncing"};case"watching":return{dot:"\u25CB",color:"gray",label:"watching"};case"paused":return{dot:"\u2016",color:"yellow",label:"paused"};case"error":return{dot:"\u2717",color:"red",label:"error"}}}function de(t){if(!t)return{dot:"\xB7",color:"gray",label:"\u2014"};switch(t.status){case"building":return{dot:"\u2234",color:"yellow",label:"building"};case"error":return{dot:"\u2717",color:"red",label:"failed"};case"success":return{dot:"\u2713",color:"green",label:t.mode==="live"?"live":"preview"}}}import{Fragment as Q,jsx as o,jsxs as l}from"react/jsx-runtime";var _=(t,e)=>e<=0?0:Math.max(0,Math.min(t,e-1)),hr=16,pr=5;function mr({k:t}){return o(u,{color:"black",backgroundColor:"green",children:` ${t} `})}function $({k:t,label:e,width:r=16}){return l(p,{width:r,children:[o(mr,{k:t}),o(u,{color:"gray",children:` ${e}`})]})}function fr({percent:t,width:e=28}){let r=Math.round(Math.max(0,Math.min(100,t))/100*e);return l(u,{children:[o(u,{color:"green",children:"\u2588".repeat(r)}),o(u,{color:"gray",children:"\u2591".repeat(Math.max(0,e-r))})]})}function yr({frame:t,width:e=24,seg:r=6}){let n=e+r,i=t%n-r,s="";for(let c=0;c<e;c++)s+=c>=i&&c<i+r?"\u25AE":"\u25AF";return o(u,{color:"yellow",children:s})}function he({controller:t}){let{stdout:e}=gr(),r=lr(()=>Jt(),[]),[,n]=M(0),[i,s]=M(()=>new Date),[c,d]=M(null),[h,S]=M(()=>t.synced.size>0||t.catalog.some(g=>g.sites.length>0)?"dashboard":t.catalog.length<=1?"newName":"newOrg"),[y,m]=M(0),[w,x]=M(""),[P,L]=M(!1),[T,C]=M(""),[J,Z]=M(!1),[$t,be]=M(0),Lt=()=>n(a=>a+1);dt(()=>(t.on("change",Lt),()=>{t.off("change",Lt)}),[t]),dt(()=>{let a=setInterval(()=>s(new Date),1e3);return()=>clearInterval(a)},[]);let F=t.catalog.flatMap(a=>a.sites.map(g=>({org:a.org,site:g,entry:t.synced.get(g.id)}))).sort((a,g)=>(a.entry?0:1)-(g.entry?0:1)),A=T.trim().toLowerCase(),E=A?F.filter(a=>a.site.name.toLowerCase().includes(A)||(a.org.name||a.org.subdomain||"").toLowerCase().includes(A)):F,N=E.findIndex(a=>a.site.id===c);N<0&&(N=0);let k=E[N],b=k?.entry,R=b?.lastBuild,ht=R?.status==="building",pt=de(R),tt=ht;if(!tt){for(let a of t.synced.values())if(a.status==="watching"||a.status==="syncing"){tt=!0;break}}dt(()=>{if(!tt)return;let a=setInterval(()=>be(g=>g+1),120);return()=>clearInterval(a)},[tt]);let Rt=E.findIndex(a=>!a.entry),W=Math.max(pr,(e?.rows??30)-hr),H=E.length,et=H>W,Se=Math.floor(W/2),X=et?Math.max(0,Math.min(N-Se,H-W)):0,mt=Math.min(H,X+W),ve=E.slice(X,mt);dt(()=>{h!=="dashboard"||P||F.length>0||(x(""),t.catalog.length<=1?S("newName"):(m(0),S("newOrg")))},[F.length,h,P,t.catalog.length]);let rt=a=>{if(E.length===0)return;let g=_(N+a,E.length);d(E[g].site.id)},xe=()=>{k&&(k.entry?t.removeSite(k.site.id):t.addSite(k.org,k.site))},Te=()=>{x(""),t.catalog.length<=1?S("newName"):(m(0),S("newOrg"))},ke=a=>{if(!b){t.flash("Press \u23CE to start syncing this site first.",!0);return}let g=a.run(b.viewsDir);t.flash(g?`Opened ${a.label} at ${Pt(b.viewsDir)}`:`Couldn't open ${a.label}.`,!g)},Ce=()=>{if(!b){t.flash("Press \u23CE to start syncing this site first.",!0);return}t.togglePause(b.site.id)},Ft=a=>{if(!b){t.flash("Press \u23CE to start syncing this site first.",!0);return}t.buildSite(b.site.id,a),t.flash(a?"Deploying \u2014 building live\u2026":"Building preview\u2026")},Ee=()=>{let a=b?.lastBuild?.url;if(!a){t.flash("No build URL yet \u2014 press b to build.",!0);return}t.flash(qt(a)?`Opening ${a}`:"Couldn't open the browser.",!1)};dr((a,g)=>{if(h==="newOrg"){g.escape?S("dashboard"):g.upArrow?m(B=>_(B-1,t.catalog.length)):g.downArrow?m(B=>_(B+1,t.catalog.length)):g.return&&S("newName");return}if(J){g.escape?(Z(!1),C(""),d(null)):g.return?Z(!1):g.upArrow?rt(-1):g.downArrow&&rt(1);return}if(g.escape){C(""),d(null);return}if(a==="/")return void Z(!0);if(a==="n")return void Te();if(a==="q")return void t.logout();if(g.upArrow)return rt(-1);if(g.downArrow)return rt(1);if(g.return||a===" ")return xe();if(a==="p")return Ce();if(a==="b")return Ft(!1);if(a==="B")return Ft(!0);if(a==="o")return Ee();if(a>="1"&&a<="9"){let B=r[parseInt(a,10)-1];if(B)return ke(B)}},{isActive:h!=="newName"});let Be=async a=>{let g=a.trim();if(!g){S("dashboard");return}let B=t.catalog[_(y,t.catalog.length)]?.org??t.catalog[0]?.org;L(!0),S("dashboard"),await t.createNewSite(g,B),L(!1)},Pe=Math.max(5,(e?.rows??30)-18);return l(p,{flexDirection:"column",children:[l(p,{justifyContent:"space-between",paddingX:1,children:[l(p,{children:[o(u,{color:"green",bold:!0,children:"\u25C6 SleekCMS"}),o(u,{color:"gray",children:" \u2502 "}),o(u,{children:t.email??"signed in"})]}),l(p,{children:[l(u,{color:"gray",children:[A?`${E.length}/${F.length}`:F.length," sites \xB7 "]}),l(u,{color:"green",children:[t.watchingCount()," watching"]}),o(u,{color:"gray",children:` \xB7 ${i.toTimeString().slice(0,8)}`})]})]}),l(p,{children:[l(p,{flexDirection:"column",borderStyle:"round",borderColor:"green",width:38,paddingX:1,children:[l(p,{justifyContent:"space-between",children:[o(u,{color:"green",bold:!0,children:"SITE WORKSPACES"}),F.length>W&&!J&&!A?o(u,{color:"gray",children:"/ filter"}):null]}),J?l(p,{children:[o(u,{color:"cyan",children:"/ "}),o(ge,{value:T,onChange:C,onSubmit:()=>Z(!1),placeholder:"type to filter\u2026"})]}):A?o(u,{color:"gray",children:`/ ${T} \xB7 ${E.length} match${E.length===1?"":"es"}`}):null,o(p,{flexDirection:"column",marginTop:1,minHeight:et?W+3:void 0,children:E.length===0?o(u,{color:"gray",children:A?`no matches for \u201C${T}\u201D`:"no sites \u2014 press n to create one"}):l(Q,{children:[et?o(u,{color:"gray",children:X>0?` \u2191 ${X} more`:" "}):null,ve.map((a,g)=>{let B=X+g,At=a.entry?lt(a.entry.status):{dot:"\xB7",color:"gray",label:"idle"},jt=ut(a.entry?.progress),De=a.entry?a.entry.status==="syncing"&&jt!=null?`${jt}%`:a.entry.status==="paused"?"paused":"--":"",nt=B===N,Oe=B===Rt&&Rt>0;return l(ur,{children:[Oe?o(u,{color:"gray",children:"\u2500".repeat(34)}):null,l(p,{justifyContent:"space-between",children:[l(p,{children:[o(u,{color:nt?"green":"gray",children:nt?"\u276F ":" "}),l(u,{color:a.entry?.status==="watching"?"green":At.color,children:[a.entry?.status==="watching"?le($t):At.dot," "]}),o(u,{color:nt?"white":a.entry?void 0:"gray",bold:nt,children:a.site.name})]}),o(u,{color:a.entry?.status==="syncing"?"green":"gray",children:De})]})]},a.site.id)}),et?o(u,{color:"gray",children:mt<H?` \u2193 ${H-mt} more`:" "}):null]})}),o(p,{marginTop:1,children:o(u,{color:"gray",children:"+ new site \xB7 n"})})]}),l(p,{flexDirection:"column",flexGrow:1,marginLeft:1,children:[o(p,{flexDirection:"column",borderStyle:"round",borderColor:"gray",paddingX:1,children:k?l(Q,{children:[l(u,{children:[k.org.name||k.org.subdomain?l(u,{color:"gray",children:[k.org.name||k.org.subdomain," / "]}):null,o(u,{color:"cyan",bold:!0,children:k.site.name})]}),l(p,{marginTop:1,children:[o(u,{color:"gray",children:"status "}),b?o(u,{color:lt(b.status).color,children:`: ${lt(b.status).label}`}):o(u,{color:"gray",children:": idle \u2014 not syncing (press \u23CE to start)"})]}),l(p,{children:[o(u,{color:"gray",children:"path "}),o(u,{children:b?.viewsDir?Pt(b.viewsDir):"\u2014"})]}),l(p,{children:[o(u,{color:"gray",children:"synced "}),o(u,{children:Dt(b?.lastSyncedAt)}),o(u,{color:"gray",children:" progress "}),o(u,{color:b?.status==="syncing"?"green":"gray",children:b?.status==="syncing"?`${ut(b.progress)??0}%`:"\u2014"})]}),o(p,{marginTop:1,children:o(fr,{percent:b?b.status==="syncing"?ut(b.progress)??0:b.status==="watching"?100:0:0})}),b?l(Q,{children:[l(p,{marginTop:1,children:[o(u,{color:"gray",children:"build "}),o(u,{color:pt.color,children:`${pt.dot} ${pt.label}`}),o(u,{color:"gray",children:" last "}),o(u,{children:ht?"in progress":Dt(R?.at)})]}),l(p,{children:[o(u,{color:"gray",children:"url "}),R?.url?o(u,{color:"cyan",children:R.url}):o(u,{color:"gray",children:"\u2014"})]}),R?.status==="error"&&R.errors?.length?l(p,{children:[o(u,{color:"red",children:"\u258C "}),o(u,{color:"red",children:`\u2717 ${R.errors[0]}`})]}):null,ht?l(p,{children:[o(yr,{frame:$t}),o(u,{color:"gray",children:` ${R?.stage??"building"}`})]}):null]}):null]}):o(u,{color:"gray",children:"No site selected."})}),h==="dashboard"?l(p,{flexDirection:"column",borderStyle:"round",borderColor:"gray",paddingX:1,marginTop:1,flexGrow:1,children:[l(u,{children:[o(u,{color:"cyan",bold:!0,children:"LOGS \xB7 "}),o(u,{color:"white",children:k?.site.name??"\u2014"})]}),o(p,{flexDirection:"column",marginTop:1,children:!b||b.logs.length===0?o(u,{color:"gray",children:"no activity yet"}):b.logs.slice(-Pe).map((a,g)=>l(p,{children:[l(u,{color:"gray",children:[a.time," "]}),o(u,{children:a.text})]},g))})]}):l(p,{flexDirection:"column",borderStyle:"round",borderColor:"blue",paddingX:1,marginTop:1,flexGrow:1,children:[h==="newOrg"&&l(Q,{children:[o(u,{color:"cyan",bold:!0,children:"CREATE SITE \u2014 PICK ORG"}),o(u,{color:"gray",children:"\u2191/\u2193 select \xB7 \u23CE next \xB7 esc cancel"}),o(p,{flexDirection:"column",marginTop:1,children:t.catalog.map((a,g)=>l(u,{color:g===_(y,t.catalog.length)?"blue":void 0,bold:g===_(y,t.catalog.length),children:[g===_(y,t.catalog.length)?"\u276F ":" ",a.org.name||a.org.subdomain]},a.org.id))})]}),h==="newName"&&l(Q,{children:[o(u,{color:"cyan",bold:!0,children:"CREATE SITE"}),l(p,{marginTop:1,children:[o(u,{children:"New site name: "}),o(ge,{value:w,onChange:x,onSubmit:Be})]}),o(u,{color:"gray",children:"\u23CE create \xB7 empty + \u23CE to cancel"})]})]})]})]}),t.message?o(p,{paddingX:1,children:l(u,{color:t.messageIsError?"red":"gray",children:[t.messageIsError?"\u2717 ":"",t.message]})}):null,l(p,{flexDirection:"column",marginTop:1,children:[l(p,{paddingX:1,flexWrap:"wrap",children:[o($,{k:"\u2191\u2193",label:"select"}),o($,{k:"/",label:"search"}),o($,{k:"\u23CE",label:"start/stop"}),o($,{k:"p",label:"pause"}),o($,{k:"b",label:"build"}),o($,{k:"o",label:"open url"}),o($,{k:"n",label:"new site"}),o($,{k:"q",label:"logout"}),o($,{k:"ctrl+c",label:"quit",width:14})]}),r.length>0?o(p,{paddingX:1,marginTop:1,flexWrap:"wrap",children:r.map((a,g)=>o($,{k:String(g+1),label:a.label},a.key))}):null]})]})}import{jsx as gt}from"react/jsx-runtime";function me(t){let e=t;return e?.body||e?.message||String(t)}function fe({controller:t,env:e,devSecret:r,savedToken:n,savedEmail:i}){let[s,c]=pe(n?"loading":"auth"),[d,h]=pe();vr((y,m)=>{m.ctrl&&y==="c"&&t.exit()}),wr(()=>{n&&t.init(e,n,i).then(()=>c("manage")).catch(y=>{y instanceof O&&y.status===401?h("Saved session expired \u2014 please sign in again."):h(me(y)),c("auth")})},[]);let S=async(y,m)=>{h(void 0),await Yt({env:e,userToken:y,devSecret:r,email:m}),c("loading");try{await t.init(e,y,m),c("manage")}catch(w){h(me(w)),c("auth")}};return s==="auth"?gt(ue,{env:e,initialError:d,onAuthed:S}):s==="loading"?gt(br,{children:gt(Sr,{children:"Loading your sites\u2026 "})}):gt(he,{controller:t})}import{jsx as Dr}from"react/jsx-runtime";var Cr=Tr(import.meta.url),Ot=Gt();if(Ot.version){let t=Cr("../package.json");console.log(t.version),process.exit(0)}var we=Mt.dirname(xr(import.meta.url)),Er=ye(Mt.join(we,"AGENT.md"),"utf-8"),Br=ye(Mt.join(we,"build.mjs"),"utf-8");async function Pr(){let t=new at(Er,Br),e=await Vt(),r=(Ot.env??e?.env??"production").toLowerCase(),n=Ot.dev??process.env.SLEEKCMS_DEV_SECRET??e?.devSecret;Kt(n),process.stdout.isTTY&&process.stdout.write("\x1B[2J\x1B[3J\x1B[H");let i=kr(Dr(fe,{controller:t,env:r,devSecret:n,savedToken:e?.userToken,savedEmail:e?.email}),{exitOnCtrlC:!1}),s=c=>{i.unmount(),console.log(c),process.exit(0)};t.once("exit",()=>s("\u2713 Stopped. Local site files were cleaned up.")),t.once("logout",()=>s("\u{1F44B} Logged out. Run `sleekcms` to sign in again.")),process.on("SIGINT",()=>{t.exit()}),process.on("SIGTERM",()=>{t.exit()}),await i.waitUntilExit()}Pr().catch(t=>{let e=t;console.error("\u274C",e.body||e.message),process.exit(1)});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sleekcms/sync",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Edit SleekCMS sites locally — models, content, templates, images — with live two-way sync and AI agent support.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sleekcms",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"dist"
|
|
29
29
|
],
|
|
30
30
|
"scripts": {
|
|
31
|
-
"build": "npm run clean && node esbuild.config.mjs && cp src/AGENT.md dist/ && chmod +x dist/index.js",
|
|
31
|
+
"build": "npm run clean && node esbuild.config.mjs && cp src/AGENT.md dist/ && cp src/build.mjs dist/ && chmod +x dist/index.js",
|
|
32
32
|
"dev": "tsx src/index.tsx",
|
|
33
33
|
"typecheck": "tsc --noEmit",
|
|
34
34
|
"clean": "rm -rf dist",
|