@sleekcms/sync 1.7.0 → 2.1.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.
@@ -1,299 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
- /**
4
- * SleekCMS site sync — standalone, self-contained.
5
- *
6
- * Bi-directional sync between a local workspace and the SleekCMS server.
7
- * Safe to invoke repeatedly: a `.cache/state.json` inside the workspace
8
- * tracks server-known state so only real diffs are pushed.
9
- */
10
- var __importDefault = (this && this.__importDefault) || function (mod) {
11
- return (mod && mod.__esModule) ? mod : { "default": mod };
12
- };
13
- Object.defineProperty(exports, "__esModule", { value: true });
14
- exports.resolveViewsDir = resolveViewsDir;
15
- exports.syncSite = syncSite;
16
- const fs_extra_1 = __importDefault(require("fs-extra"));
17
- const os_1 = __importDefault(require("os"));
18
- const path_1 = __importDefault(require("path"));
19
- const commander_1 = require("commander");
20
- const API_BASE_URLS = {
21
- localhost: "http://app.sleekcms.test/api/mcp",
22
- development: "https://app.sleekcms.dev/api/mcp",
23
- production: "https://app.sleekcms.com/api/mcp",
24
- };
25
- const SRC_DIRS = [
26
- "src/views/blocks",
27
- "src/views/entries",
28
- "src/views/pages",
29
- "src/views/layouts",
30
- "src/models/blocks",
31
- "src/models/entries",
32
- "src/models/pages",
33
- "src/content/pages",
34
- "src/content/entries",
35
- "src/public/js",
36
- "src/public/css",
37
- ];
38
- class RequestError extends Error {
39
- status;
40
- body;
41
- constructor(message, status, body) {
42
- super(message);
43
- this.status = status;
44
- this.body = body;
45
- }
46
- }
47
- async function request(baseUrl, token, method, p, body) {
48
- const res = await fetch(baseUrl + p, {
49
- method,
50
- headers: {
51
- Authorization: `Bearer ${token}`,
52
- ...(body !== undefined ? { "Content-Type": "application/json" } : {}),
53
- },
54
- body: body !== undefined ? JSON.stringify(body) : undefined,
55
- });
56
- if (!res.ok) {
57
- const text = await res.text();
58
- throw new RequestError(`${method} ${p} → ${res.status}: ${text}`, res.status, text);
59
- }
60
- return res.json();
61
- }
62
- async function writeAuxFiles(viewsDir, agentMdContent) {
63
- if (agentMdContent) {
64
- await fs_extra_1.default.outputFile(path_1.default.join(viewsDir, "AGENT.md"), agentMdContent);
65
- await fs_extra_1.default.outputFile(path_1.default.join(viewsDir, "CLAUDE.md"), agentMdContent);
66
- await fs_extra_1.default.outputFile(path_1.default.join(viewsDir, ".vscode", "copilot-instructions.md"), agentMdContent);
67
- }
68
- await fs_extra_1.default.outputFile(path_1.default.join(viewsDir, ".vscode", "settings.json"), JSON.stringify({
69
- "files.associations": { "*.model": "javascript" },
70
- "[javascript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
71
- "js/ts.validate.enabled": false,
72
- }, null, 2));
73
- }
74
- async function checkAndWriteToken(viewsDir, token) {
75
- const tokenPath = path_1.default.join(viewsDir, ".cache", "token");
76
- if (await fs_extra_1.default.pathExists(tokenPath)) {
77
- const existing = (await fs_extra_1.default.readFile(tokenPath, "utf-8")).trim();
78
- if (existing !== token) {
79
- throw new Error(`Workspace at ${viewsDir} is tied to a different token. ` +
80
- `Remove ${tokenPath} or use a different workspace.`);
81
- }
82
- return;
83
- }
84
- await fs_extra_1.default.outputFile(tokenPath, token);
85
- }
86
- function resolveViewsDir(basePath, site) {
87
- const slug = `${site.name.substr(0, 20)} ${site.id}`
88
- .replace(/[\s_]+/g, "-")
89
- .toLowerCase();
90
- const base = basePath || path_1.default.join(os_1.default.homedir(), "SleekCMS");
91
- return path_1.default.resolve(base, slug);
92
- }
93
- async function syncSite(opts) {
94
- const token = (opts.token || "").trim();
95
- if (!token)
96
- throw new Error("syncSite: token is required");
97
- const env = (opts.env || token.split("-")[2] || "production").toLowerCase();
98
- const apiBase = API_BASE_URLS[env] || API_BASE_URLS.production;
99
- const site = await request(apiBase, token, "GET", "/get_site");
100
- const viewsDir = opts.viewsDir
101
- ? path_1.default.resolve(opts.viewsDir)
102
- : resolveViewsDir(opts.path, site);
103
- await fs_extra_1.default.ensureDir(viewsDir);
104
- await Promise.all(SRC_DIRS.map((dir) => fs_extra_1.default.ensureDir(path_1.default.join(viewsDir, dir))));
105
- await checkAndWriteToken(viewsDir, token);
106
- const statePath = path_1.default.join(viewsDir, ".cache", "state.json");
107
- if (opts.flush)
108
- await fs_extra_1.default.remove(statePath);
109
- const isFirstRun = !(await fs_extra_1.default.pathExists(statePath));
110
- let fileMap = isFirstRun ? {} : (await fs_extra_1.default.readJson(statePath)).fileMap || {};
111
- let pushed = 0;
112
- let pulled = 0;
113
- if (isFirstRun) {
114
- ({ fileMap, pulled } = await pullServerState(viewsDir, apiBase, token));
115
- await writeAuxFiles(viewsDir, opts.agentMd);
116
- }
117
- else {
118
- pushed = await pushLocalChanges(viewsDir, fileMap, apiBase, token);
119
- }
120
- await fs_extra_1.default.outputJson(statePath, { fileMap }, { spaces: 2 });
121
- return { viewsDir, site, isFirstRun, pushed, pulled };
122
- }
123
- const IGNORED_FILENAMES = new Set([".DS_Store", "Thumbs.db", "desktop.ini"]);
124
- async function walkFiles(viewsDir) {
125
- const sourceRoot = path_1.default.join(viewsDir, "src");
126
- if (!(await fs_extra_1.default.pathExists(sourceRoot)))
127
- return [];
128
- const out = [];
129
- async function walk(dir) {
130
- const entries = await fs_extra_1.default.readdir(dir, { withFileTypes: true });
131
- for (const entry of entries) {
132
- if (IGNORED_FILENAMES.has(entry.name))
133
- continue;
134
- const full = path_1.default.join(dir, entry.name);
135
- if (entry.isDirectory())
136
- await walk(full);
137
- else if (entry.isFile())
138
- out.push(path_1.default.relative(viewsDir, full).replace(/\\/g, "/"));
139
- }
140
- }
141
- await walk(sourceRoot);
142
- return out;
143
- }
144
- /**
145
- * Push local edits via /save_files. Server enforces save order.
146
- *
147
- * Skip: file mtime matches cache → don't read, don't push.
148
- * The server replies with success/error per file only — it never echoes back a
149
- * normalized copy, so local files are never rewritten on save. A failed save is
150
- * recorded in sync-errors.log; a successful save clears that file's entry.
151
- * Server-side normalization (resolved image objects, JSON frontmatter, entry
152
- * refs, etc.) is reflected locally only on a full re-fetch.
153
- */
154
- async function pushLocalChanges(viewsDir, fileMap, apiBase, token) {
155
- const changes = [];
156
- for (const rel of await walkFiles(viewsDir)) {
157
- const full = path_1.default.join(viewsDir, rel);
158
- const prior = fileMap[rel];
159
- const stat = await fs_extra_1.default.stat(full);
160
- if (prior && prior.mtimeMs === stat.mtimeMs)
161
- continue;
162
- const content = await fs_extra_1.default.readFile(full, "utf-8");
163
- if (!content.trim())
164
- continue;
165
- changes.push({ rel, full, stat, content, prior });
166
- }
167
- if (changes.length === 0)
168
- return 0;
169
- let results;
170
- try {
171
- results = await request(apiBase, token, "POST", "/save_files", changes.map((c) => ({ path: c.rel, content: c.content })));
172
- }
173
- catch (err) {
174
- const e = err;
175
- console.error("❌ Error saving files:", e.body || e.message);
176
- return 0;
177
- }
178
- const errors = await getSessionErrors(viewsDir);
179
- let pushed = 0;
180
- const resultsByPath = indexSaveResultsByPath(results);
181
- for (let i = 0; i < changes.length; i++) {
182
- const c = changes[i];
183
- const r = getSaveResult(c, i, results, resultsByPath);
184
- if (!r) {
185
- const msg = "No save response returned";
186
- errors[c.rel] = msg;
187
- console.error(`❌ Error saving ${c.rel}: ${msg}`);
188
- continue;
189
- }
190
- if (r.error) {
191
- errors[c.rel] = r.error;
192
- console.error(`❌ Error saving ${c.rel}: ${r.error}`);
193
- continue;
194
- }
195
- delete errors[c.rel];
196
- // Record the mtime we pushed. The local file is left exactly as the
197
- // user wrote it — we don't rewrite it with a server-normalized copy.
198
- fileMap[c.rel] = { mtimeMs: c.stat.mtimeMs };
199
- console.log(`✅ ${c.prior ? "Updated" : "Created"} ${c.rel}`);
200
- pushed++;
201
- }
202
- await dumpErrors(viewsDir, errors);
203
- return pushed;
204
- }
205
- function indexSaveResultsByPath(results) {
206
- const byPath = new Map();
207
- for (const result of results) {
208
- if (result.path)
209
- byPath.set(result.path, result);
210
- }
211
- return byPath;
212
- }
213
- function getSaveResult(change, index, results, resultsByPath) {
214
- const resultByPath = resultsByPath.get(change.rel);
215
- if (resultByPath)
216
- return resultByPath;
217
- const resultByIndex = results[index];
218
- if (!resultByIndex?.path || resultByIndex.path === change.rel)
219
- return resultByIndex;
220
- return undefined;
221
- }
222
- const ERROR_LOG = "sync-errors.log";
223
- const syncErrorsByWorkspace = new Map();
224
- async function getSessionErrors(viewsDir) {
225
- const workspace = path_1.default.resolve(viewsDir);
226
- const cached = syncErrorsByWorkspace.get(workspace);
227
- if (cached)
228
- return cached;
229
- const errors = await loadErrors(workspace);
230
- syncErrorsByWorkspace.set(workspace, errors);
231
- return errors;
232
- }
233
- async function loadErrors(viewsDir) {
234
- const file = path_1.default.join(viewsDir, ERROR_LOG);
235
- if (!(await fs_extra_1.default.pathExists(file)))
236
- return {};
237
- const text = await fs_extra_1.default.readFile(file, "utf-8");
238
- const errors = {};
239
- for (const line of text.split("\n")) {
240
- const idx = line.indexOf(": ");
241
- if (idx > 0)
242
- errors[line.slice(0, idx)] = line.slice(idx + 2);
243
- }
244
- return errors;
245
- }
246
- async function dumpErrors(viewsDir, errors) {
247
- const file = path_1.default.join(viewsDir, ERROR_LOG);
248
- const entries = Object.entries(errors);
249
- if (entries.length === 0) {
250
- await fs_extra_1.default.remove(file);
251
- return;
252
- }
253
- await fs_extra_1.default.outputFile(file, entries.map(([p, msg]) => `${p}: ${msg}`).join("\n") + "\n");
254
- }
255
- async function pullServerState(viewsDir, apiBase, token) {
256
- syncErrorsByWorkspace.delete(path_1.default.resolve(viewsDir));
257
- await fs_extra_1.default.remove(path_1.default.join(viewsDir, ERROR_LOG));
258
- console.log("📥 Fetching files...");
259
- const files = await request(apiBase, token, "GET", "/get_files");
260
- const fileMap = {};
261
- let pulled = 0;
262
- for (const file of files) {
263
- const full = path_1.default.join(viewsDir, file.path);
264
- await fs_extra_1.default.outputFile(full, file.content);
265
- const mtimeMs = (await fs_extra_1.default.stat(full)).mtimeMs;
266
- pulled++;
267
- fileMap[file.path] = { mtimeMs };
268
- }
269
- console.log(`✔️ Synced ${files.length} file(s).`);
270
- return { fileMap, pulled };
271
- }
272
- if (require.main === module) {
273
- commander_1.program
274
- .name("setup-site")
275
- .description("Initialize a SleekCMS workspace: pull all files and persist the auth token for future syncs.")
276
- .requiredOption("-t, --token <token>", "SleekCMS CLI auth token")
277
- .option("-d, --dir <dir>", "Parent directory; workspace is created as a slug-named subfolder (default: current directory)")
278
- .option("-e, --env <env>", "Environment override (localhost, development, production)")
279
- .parse(process.argv);
280
- const opts = commander_1.program.opts();
281
- syncSite({
282
- token: opts.token,
283
- path: opts.dir || path_1.default.join(os_1.default.homedir(), "SleekCMS"),
284
- env: opts.env,
285
- })
286
- .then(({ viewsDir, site, isFirstRun, pulled }) => {
287
- if (isFirstRun) {
288
- console.log(`\n✅ Workspace initialized for "${site.name}" at ${viewsDir} (pulled ${pulled} file(s)).`);
289
- }
290
- else {
291
- console.log(`\n✅ Workspace already initialized for "${site.name}" at ${viewsDir}.`);
292
- }
293
- console.log(`\nNext: cd ${viewsDir} → edit files → run sync-site`);
294
- })
295
- .catch((err) => {
296
- console.error("❌", err.body || err.message);
297
- process.exit(1);
298
- });
299
- }
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * SleekCMS site sync — thin wrapper.
4
- *
5
- * Reads the auth token from <workspace>/.cache/token (written by setup-site)
6
- * and pushes local changes to the server.
7
- *
8
- * Usage: sync-site [-d <workspace-dir>]
9
- * -d defaults to the current directory.
10
- *
11
- * To initialize a new workspace for the first time, run setup-site instead:
12
- * setup-site -t <token> [-d <parent-dir>]
13
- */
14
- export {};
package/dist/sync-site.js DELETED
@@ -1,49 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
- /**
4
- * SleekCMS site sync — thin wrapper.
5
- *
6
- * Reads the auth token from <workspace>/.cache/token (written by setup-site)
7
- * and pushes local changes to the server.
8
- *
9
- * Usage: sync-site [-d <workspace-dir>]
10
- * -d defaults to the current directory.
11
- *
12
- * To initialize a new workspace for the first time, run setup-site instead:
13
- * setup-site -t <token> [-d <parent-dir>]
14
- */
15
- var __importDefault = (this && this.__importDefault) || function (mod) {
16
- return (mod && mod.__esModule) ? mod : { "default": mod };
17
- };
18
- Object.defineProperty(exports, "__esModule", { value: true });
19
- const fs_extra_1 = __importDefault(require("fs-extra"));
20
- const path_1 = __importDefault(require("path"));
21
- const commander_1 = require("commander");
22
- const setup_site_1 = require("./setup-site");
23
- commander_1.program
24
- .name("sync-site")
25
- .description("Push local changes to SleekCMS. Reads auth token from <workspace>/.cache/token.")
26
- .option("-d, --dir <dir>", "Workspace directory (default: current directory)")
27
- .parse(process.argv);
28
- const opts = commander_1.program.opts();
29
- const workspaceDir = path_1.default.resolve(opts.dir || ".");
30
- const tokenPath = path_1.default.join(workspaceDir, ".cache", "token");
31
- fs_extra_1.default.readFile(tokenPath, "utf-8")
32
- .then(raw => {
33
- const token = raw.trim();
34
- if (!token)
35
- throw new Error(`Token file is empty: ${tokenPath}`);
36
- return (0, setup_site_1.syncSite)({ token, viewsDir: workspaceDir });
37
- })
38
- .then(({ viewsDir, site, pushed }) => {
39
- console.log(`\n✅ Sync complete for "${site.name}" at ${viewsDir} (pushed ${pushed} file(s)).`);
40
- })
41
- .catch((err) => {
42
- if (err.code === "ENOENT") {
43
- console.error(`❌ Workspace not initialized — run: setup-site -t <token>`);
44
- }
45
- else {
46
- console.error("❌", err.body || err.message);
47
- }
48
- process.exit(1);
49
- });
package/dist/watcher.d.ts DELETED
@@ -1,18 +0,0 @@
1
- /**
2
- * File watching + debounced sync trigger.
3
- *
4
- * All push/pull logic lives in setup-site.ts. This module only:
5
- * - watches the workspace with chokidar
6
- * - debounces change events
7
- * - calls back into a provided `onSync` handler that invokes syncSite()
8
- */
9
- interface WatcherOptions {
10
- viewsDir: string;
11
- onSync: () => Promise<unknown>;
12
- onIdle?: () => void;
13
- }
14
- export declare function init(options: WatcherOptions): void;
15
- export declare function setShuttingDown(value: boolean): void;
16
- export declare function monitorFiles(): void;
17
- export declare function stopWatching(): Promise<void>;
18
- export {};
package/dist/watcher.js DELETED
@@ -1,122 +0,0 @@
1
- "use strict";
2
- /**
3
- * File watching + debounced sync trigger.
4
- *
5
- * All push/pull logic lives in setup-site.ts. This module only:
6
- * - watches the workspace with chokidar
7
- * - debounces change events
8
- * - calls back into a provided `onSync` handler that invokes syncSite()
9
- */
10
- var __importDefault = (this && this.__importDefault) || function (mod) {
11
- return (mod && mod.__esModule) ? mod : { "default": mod };
12
- };
13
- Object.defineProperty(exports, "__esModule", { value: true });
14
- exports.init = init;
15
- exports.setShuttingDown = setShuttingDown;
16
- exports.monitorFiles = monitorFiles;
17
- exports.stopWatching = stopWatching;
18
- const path_1 = __importDefault(require("path"));
19
- const chokidar_1 = __importDefault(require("chokidar"));
20
- const DEBOUNCE_DELAY = 5000;
21
- const IDLE_TIMEOUT_MS = 60 * 60 * 1000;
22
- // Poll on a short interval and compare wall-clock time so the timeout still
23
- // fires correctly after the system has been asleep (Node's setTimeout runs on
24
- // a monotonic clock that pauses during sleep).
25
- const IDLE_CHECK_INTERVAL_MS = 60 * 1000;
26
- let watcher = null;
27
- let isShuttingDown = false;
28
- let debounceTimer = null;
29
- let idleCheckTimer = null;
30
- let lastActivityAt = 0;
31
- let dirty = false;
32
- let syncInFlight = false;
33
- let viewsDir = null;
34
- let onSync = null;
35
- let onIdle = null;
36
- function init(options) {
37
- viewsDir = options.viewsDir;
38
- onSync = options.onSync;
39
- onIdle = options.onIdle ?? null;
40
- }
41
- function resetIdleTimer() {
42
- lastActivityAt = Date.now();
43
- if (isShuttingDown || !onIdle)
44
- return;
45
- if (idleCheckTimer)
46
- return;
47
- idleCheckTimer = setInterval(() => {
48
- if (isShuttingDown)
49
- return;
50
- if (Date.now() - lastActivityAt < IDLE_TIMEOUT_MS)
51
- return;
52
- if (idleCheckTimer) {
53
- clearInterval(idleCheckTimer);
54
- idleCheckTimer = null;
55
- }
56
- console.log(`\n💤 No changes for ${IDLE_TIMEOUT_MS / 60000} minutes. Terminating.`);
57
- onIdle?.();
58
- }, IDLE_CHECK_INTERVAL_MS);
59
- }
60
- function setShuttingDown(value) {
61
- isShuttingDown = value;
62
- }
63
- async function flush() {
64
- debounceTimer = null;
65
- if (!dirty || isShuttingDown)
66
- return;
67
- if (syncInFlight) {
68
- // Re-arm to retry after the current sync finishes.
69
- debounceTimer = setTimeout(flush, DEBOUNCE_DELAY);
70
- return;
71
- }
72
- dirty = false;
73
- syncInFlight = true;
74
- try {
75
- if (onSync)
76
- await onSync();
77
- }
78
- catch (err) {
79
- const e = err;
80
- console.error("❌ Sync failed:", e.body || e.message);
81
- }
82
- finally {
83
- syncInFlight = false;
84
- }
85
- }
86
- function scheduleSync() {
87
- if (isShuttingDown)
88
- return;
89
- dirty = true;
90
- if (debounceTimer)
91
- clearTimeout(debounceTimer);
92
- debounceTimer = setTimeout(flush, DEBOUNCE_DELAY);
93
- resetIdleTimer();
94
- }
95
- function watchTargets(rootDir) {
96
- return [path_1.default.join(rootDir, "src")];
97
- }
98
- function monitorFiles() {
99
- if (!viewsDir)
100
- throw new Error("watcher.init must be called before monitorFiles");
101
- const watchedFolder = path_1.default.join(viewsDir, "src");
102
- console.log(`👀 Watching folder: ${watchedFolder}`);
103
- watcher = chokidar_1.default.watch(watchTargets(viewsDir), {
104
- persistent: true,
105
- ignoreInitial: true,
106
- ignored: /(^|[/\\])(\.DS_Store|Thumbs\.db|desktop\.ini)$/,
107
- })
108
- .on("change", () => { scheduleSync(); })
109
- .on("add", () => { scheduleSync(); })
110
- .on("unlink", () => { scheduleSync(); });
111
- resetIdleTimer();
112
- }
113
- async function stopWatching() {
114
- if (idleCheckTimer) {
115
- clearInterval(idleCheckTimer);
116
- idleCheckTimer = null;
117
- }
118
- if (watcher) {
119
- await watcher.close();
120
- watcher = null;
121
- }
122
- }