@sleekcms/sync 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ /**
3
+ * CLI setup, prompts, and UI for the CMS CLI
4
+ */
5
+ var __importDefault = (this && this.__importDefault) || function (mod) {
6
+ return (mod && mod.__esModule) ? mod : { "default": mod };
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.parseArgs = parseArgs;
10
+ exports.prompt = prompt;
11
+ exports.showWatchHelp = showWatchHelp;
12
+ exports.setupKeyboardInput = setupKeyboardInput;
13
+ exports.showEditorMenu = showEditorMenu;
14
+ const readline_1 = __importDefault(require("readline"));
15
+ const child_process_1 = require("child_process");
16
+ const commander_1 = require("commander");
17
+ let rawModeEnabled = false;
18
+ function parseArgs() {
19
+ commander_1.program
20
+ .name("cms-sync")
21
+ .description("SleekCMS CLI tool to sync and edit CMS templates locally. Downloads templates, watches for changes, and syncs updates back to the API.")
22
+ .addOption(new commander_1.Option("-v, --version", "output the version number").hideHelp())
23
+ .option("-t, --token <token>", "API authentication token (required)")
24
+ .addOption(new commander_1.Option("-e, --env <env>", "Environment (localhost, development, production)").default("production").hideHelp())
25
+ .option("-p, --path <path>", "Directory path for files (default: <token-prefix>-views)")
26
+ .addHelpText("after", `
27
+ Examples:
28
+ $ cms-sync --token abc123-xxxx
29
+ $ cms-sync -t abc123-xxxx -e development
30
+ $ cms-sync -t abc123-xxxx -p ./my-templates
31
+ `)
32
+ .parse(process.argv);
33
+ return commander_1.program.opts();
34
+ }
35
+ function suspendRawMode() {
36
+ if (process.stdin.isTTY && rawModeEnabled) {
37
+ process.stdin.setRawMode(false);
38
+ }
39
+ }
40
+ function resumeRawMode() {
41
+ if (process.stdin.isTTY && rawModeEnabled) {
42
+ process.stdin.setRawMode(true);
43
+ process.stdin.resume();
44
+ }
45
+ }
46
+ function prompt(question) {
47
+ suspendRawMode();
48
+ const rl = readline_1.default.createInterface({
49
+ input: process.stdin,
50
+ output: process.stdout,
51
+ });
52
+ return new Promise(resolve => {
53
+ rl.question(question, answer => {
54
+ rl.close();
55
+ resumeRawMode();
56
+ resolve(answer.trim());
57
+ });
58
+ });
59
+ }
60
+ function commandExists(cmd) {
61
+ try {
62
+ (0, child_process_1.execSync)(`which ${cmd}`, { stdio: "ignore" });
63
+ return true;
64
+ }
65
+ catch {
66
+ return false;
67
+ }
68
+ }
69
+ function showWatchHelp() {
70
+ console.log("šŸ“‹ Commands: [r] Re-fetch all files [x] Exit\n");
71
+ }
72
+ function setupKeyboardInput(handlers) {
73
+ if (process.stdin.isTTY) {
74
+ process.stdin.setRawMode(true);
75
+ rawModeEnabled = true;
76
+ }
77
+ process.stdin.resume();
78
+ process.stdin.setEncoding("utf8");
79
+ process.stdin.on("data", async (key) => {
80
+ // Handle Ctrl+C
81
+ if (key === "\u0003") {
82
+ if (handlers.onExit)
83
+ await handlers.onExit();
84
+ return;
85
+ }
86
+ const cmd = key.toLowerCase();
87
+ if (cmd === "r" && handlers.onRefetch) {
88
+ console.log("\nšŸ”„ Re-fetching all files...");
89
+ await handlers.onRefetch();
90
+ console.log("šŸ‘€ Watching for changes...");
91
+ showWatchHelp();
92
+ }
93
+ else if (cmd === "x" && handlers.onExit) {
94
+ await handlers.onExit();
95
+ }
96
+ });
97
+ }
98
+ function showEditorMenu(viewsDir, handlers) {
99
+ const editors = [];
100
+ if (commandExists("code")) {
101
+ editors.push({ key: "1", name: "VS Code", cmd: "code" });
102
+ }
103
+ if (commandExists("cursor")) {
104
+ editors.push({ key: "2", name: "Cursor", cmd: "cursor" });
105
+ }
106
+ if (editors.length === 0) {
107
+ console.log("\nšŸ‘€ Watching for changes...");
108
+ showWatchHelp();
109
+ setupKeyboardInput(handlers);
110
+ return;
111
+ }
112
+ console.log("\nšŸ“‚ Open in editor:");
113
+ editors.forEach(e => console.log(` [${e.key}] ${e.name}`));
114
+ console.log(" [Enter] Skip");
115
+ console.log(" [x] Quit\n");
116
+ const rl = readline_1.default.createInterface({
117
+ input: process.stdin,
118
+ output: process.stdout,
119
+ });
120
+ // Count lines to clear (menu header + editors + skip + quit + empty + prompt)
121
+ const linesToClear = editors.length + 5;
122
+ rl.question("Select editor: ", async (answer) => {
123
+ rl.close();
124
+ // Clear the menu lines
125
+ process.stdout.write(`\x1b[${linesToClear}A`); // Move cursor up
126
+ for (let i = 0; i < linesToClear; i++) {
127
+ process.stdout.write("\x1b[2K\n"); // Clear each line
128
+ }
129
+ process.stdout.write(`\x1b[${linesToClear}A`); // Move back up
130
+ if (answer.trim().toLowerCase() === "x") {
131
+ if (handlers.onExit)
132
+ await handlers.onExit();
133
+ return;
134
+ }
135
+ const selected = editors.find(e => e.key === answer.trim());
136
+ if (selected) {
137
+ console.log(`šŸ‘€ Watching for changes... (opened ${selected.name})`);
138
+ (0, child_process_1.spawn)(selected.cmd, ["-n", viewsDir], {
139
+ detached: true,
140
+ stdio: "ignore",
141
+ }).unref();
142
+ }
143
+ else {
144
+ console.log("šŸ‘€ Watching for changes...");
145
+ }
146
+ showWatchHelp();
147
+ setupKeyboardInput(handlers);
148
+ });
149
+ }
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * SleekCMS CLI — interactive entry point.
4
+ *
5
+ * Handles CLI prompts, editor launch, and file watching. All sync work
6
+ * (fetch, push, pull, cache) is delegated to setup-site.ts so the same
7
+ * logic can be invoked standalone (e.g. as a skill for managed agents).
8
+ */
9
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * SleekCMS CLI — interactive entry point.
5
+ *
6
+ * Handles CLI prompts, editor launch, and file watching. All sync work
7
+ * (fetch, push, pull, cache) is delegated to setup-site.ts so the same
8
+ * logic can be invoked standalone (e.g. as a skill for managed agents).
9
+ */
10
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
22
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
23
+ }) : function(o, v) {
24
+ o["default"] = v;
25
+ });
26
+ var __importStar = (this && this.__importStar) || (function () {
27
+ var ownKeys = function(o) {
28
+ ownKeys = Object.getOwnPropertyNames || function (o) {
29
+ var ar = [];
30
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
31
+ return ar;
32
+ };
33
+ return ownKeys(o);
34
+ };
35
+ return function (mod) {
36
+ if (mod && mod.__esModule) return mod;
37
+ var result = {};
38
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
39
+ __setModuleDefault(result, mod);
40
+ return result;
41
+ };
42
+ })();
43
+ var __importDefault = (this && this.__importDefault) || function (mod) {
44
+ return (mod && mod.__esModule) ? mod : { "default": mod };
45
+ };
46
+ Object.defineProperty(exports, "__esModule", { value: true });
47
+ const fs_extra_1 = __importDefault(require("fs-extra"));
48
+ const os_1 = __importDefault(require("os"));
49
+ const path_1 = __importDefault(require("path"));
50
+ const cli = __importStar(require("./cli"));
51
+ const watcher = __importStar(require("./watcher"));
52
+ const setup_site_1 = require("./setup-site");
53
+ const agentMdContent = fs_extra_1.default.readFileSync(path_1.default.join(__dirname, "AGENT.md"), "utf-8");
54
+ const options = cli.parseArgs();
55
+ if (options.version) {
56
+ // package.json sits one directory above the compiled output
57
+ const pkg = require("../package.json");
58
+ console.log(pkg.version);
59
+ process.exit(0);
60
+ }
61
+ let VIEWS_DIR = null;
62
+ let ENV = null;
63
+ let TOKEN = null;
64
+ let site = null;
65
+ let isShuttingDown = false;
66
+ async function cleanupFiles(dir) {
67
+ if (!dir)
68
+ return;
69
+ console.log("🧹 Cleaning up files...");
70
+ try {
71
+ await fs_extra_1.default.remove(dir);
72
+ console.log(`āœ… Cleanup complete. Deleted workspace at ${dir}.`);
73
+ }
74
+ catch (err) {
75
+ const e = err;
76
+ console.error("āŒ Error during cleanup:", e.message);
77
+ }
78
+ }
79
+ async function runSync({ flush = false } = {}) {
80
+ const result = await (0, setup_site_1.syncSite)({
81
+ token: TOKEN,
82
+ viewsDir: VIEWS_DIR ?? undefined,
83
+ path: VIEWS_DIR ? undefined : options.path,
84
+ env: ENV ?? undefined,
85
+ agentMd: agentMdContent,
86
+ flush,
87
+ });
88
+ VIEWS_DIR = result.viewsDir;
89
+ site = result.site;
90
+ return result;
91
+ }
92
+ async function initConfig() {
93
+ TOKEN = options.token ?? null;
94
+ if (!TOKEN) {
95
+ TOKEN = await cli.prompt("Enter SleekCMS CLI auth token: ");
96
+ if (!TOKEN) {
97
+ console.error("āŒ Token is required.");
98
+ process.exit(1);
99
+ }
100
+ }
101
+ const tokenParts = TOKEN.trim().split("-");
102
+ ENV = (tokenParts[2] || options.env || "production").toLowerCase();
103
+ let customPath = options.path;
104
+ if (customPath && customPath.startsWith("~")) {
105
+ customPath = path_1.default.join(os_1.default.homedir(), customPath.slice(1));
106
+ }
107
+ options.path = customPath;
108
+ }
109
+ async function handleExit() {
110
+ if (isShuttingDown)
111
+ return;
112
+ isShuttingDown = true;
113
+ watcher.setShuttingDown(true);
114
+ console.log("\nāš ļø Shutting down...");
115
+ await watcher.stopWatching();
116
+ await cleanupFiles(VIEWS_DIR);
117
+ process.exit(0);
118
+ }
119
+ async function main() {
120
+ await initConfig();
121
+ try {
122
+ await runSync();
123
+ }
124
+ catch (err) {
125
+ const e = err;
126
+ console.error("āŒ Sync failed:", e.body || e.message);
127
+ process.exit(1);
128
+ }
129
+ watcher.init({ viewsDir: VIEWS_DIR, onSync: runSync });
130
+ watcher.monitorFiles();
131
+ console.log(`\nāœ… Ready! Editing session started for site - ${site.name}.`);
132
+ console.log(`\nšŸ“ Workspace created at: ${VIEWS_DIR}`);
133
+ if (ENV !== "production")
134
+ console.log(`🌐 Environment: ${ENV}`);
135
+ console.log(`\nāš ļø Files will be cleaned up on exit (Ctrl+C).`);
136
+ cli.showEditorMenu(VIEWS_DIR, {
137
+ onExit: handleExit,
138
+ onRefetch: () => runSync({ flush: true }),
139
+ });
140
+ process.on("SIGINT", async () => {
141
+ console.log("\nšŸ›‘ Caught interrupt signal (Ctrl+C)");
142
+ await handleExit();
143
+ });
144
+ process.on("SIGTERM", async () => {
145
+ console.log("\nšŸ›‘ Caught termination signal");
146
+ await handleExit();
147
+ });
148
+ }
149
+ main();
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * SleekCMS site sync — standalone, self-contained.
4
+ *
5
+ * Bi-directional sync between a local workspace and the SleekCMS server.
6
+ * Safe to invoke repeatedly: a `.cache/state.json` inside the workspace
7
+ * tracks server-known state so only real diffs are pushed.
8
+ */
9
+ export interface Site {
10
+ id: number;
11
+ name: string;
12
+ [key: string]: unknown;
13
+ }
14
+ export interface SyncSiteOptions {
15
+ token: string;
16
+ viewsDir?: string;
17
+ path?: string;
18
+ env?: string;
19
+ agentMd?: string;
20
+ flush?: boolean;
21
+ }
22
+ export interface SyncSiteResult {
23
+ viewsDir: string;
24
+ site: Site;
25
+ isFirstRun: boolean;
26
+ pushed: number;
27
+ pulled: number;
28
+ }
29
+ export declare function resolveViewsDir(basePath: string | undefined, site: Site): string;
30
+ export declare function syncSite(opts: SyncSiteOptions): Promise<SyncSiteResult>;
@@ -0,0 +1,262 @@
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
+ async function walkFiles(viewsDir) {
124
+ const sourceRoot = path_1.default.join(viewsDir, "src");
125
+ if (!(await fs_extra_1.default.pathExists(sourceRoot)))
126
+ return [];
127
+ const out = [];
128
+ async function walk(dir) {
129
+ const entries = await fs_extra_1.default.readdir(dir, { withFileTypes: true });
130
+ for (const entry of entries) {
131
+ const full = path_1.default.join(dir, entry.name);
132
+ if (entry.isDirectory())
133
+ await walk(full);
134
+ else if (entry.isFile())
135
+ out.push(path_1.default.relative(viewsDir, full).replace(/\\/g, "/"));
136
+ }
137
+ }
138
+ await walk(sourceRoot);
139
+ return out;
140
+ }
141
+ /**
142
+ * Push local edits via /save_files. Server enforces save order.
143
+ *
144
+ * Skip: file mtime matches cache → don't read, don't push.
145
+ * After save, only overwrite the local file if its mtime is unchanged from
146
+ * when we read it — otherwise a newer local edit is pending and we'd clobber it.
147
+ */
148
+ async function pushLocalChanges(viewsDir, fileMap, apiBase, token) {
149
+ const changes = [];
150
+ for (const rel of await walkFiles(viewsDir)) {
151
+ const full = path_1.default.join(viewsDir, rel);
152
+ const prior = fileMap[rel];
153
+ const stat = await fs_extra_1.default.stat(full);
154
+ if (prior && prior.mtimeMs === stat.mtimeMs)
155
+ continue;
156
+ const content = await fs_extra_1.default.readFile(full, "utf-8");
157
+ if (!content.trim())
158
+ continue;
159
+ changes.push({ rel, full, stat, content, prior });
160
+ }
161
+ if (changes.length === 0)
162
+ return 0;
163
+ let results;
164
+ try {
165
+ results = await request(apiBase, token, "POST", "/save_files", changes.map((c) => ({ path: c.rel, content: c.content })));
166
+ }
167
+ catch (err) {
168
+ const e = err;
169
+ console.error("āŒ Error saving files:", e.body || e.message);
170
+ return 0;
171
+ }
172
+ const errors = await loadErrors(viewsDir);
173
+ let pushed = 0;
174
+ for (let i = 0; i < changes.length; i++) {
175
+ const c = changes[i];
176
+ const r = results[i] || {};
177
+ if (r.error) {
178
+ errors[c.rel] = r.error;
179
+ console.error(`āŒ Error saving ${c.rel}: ${r.error}`);
180
+ continue;
181
+ }
182
+ delete errors[c.rel];
183
+ const finalContent = r.content ?? c.content;
184
+ let finalMtime = c.stat.mtimeMs;
185
+ const currentStat = await fs_extra_1.default.stat(c.full);
186
+ if (currentStat.mtimeMs === c.stat.mtimeMs && finalContent !== c.content) {
187
+ await fs_extra_1.default.outputFile(c.full, finalContent);
188
+ finalMtime = (await fs_extra_1.default.stat(c.full)).mtimeMs;
189
+ }
190
+ fileMap[c.rel] = { mtimeMs: finalMtime };
191
+ console.log(`āœ… ${c.prior ? "Updated" : "Created"} ${c.rel}`);
192
+ pushed++;
193
+ }
194
+ await saveErrors(viewsDir, errors);
195
+ return pushed;
196
+ }
197
+ const ERROR_LOG = "sync-errors.log";
198
+ async function loadErrors(viewsDir) {
199
+ const file = path_1.default.join(viewsDir, ERROR_LOG);
200
+ if (!(await fs_extra_1.default.pathExists(file)))
201
+ return {};
202
+ const text = await fs_extra_1.default.readFile(file, "utf-8");
203
+ const errors = {};
204
+ for (const line of text.split("\n")) {
205
+ const idx = line.indexOf(": ");
206
+ if (idx > 0)
207
+ errors[line.slice(0, idx)] = line.slice(idx + 2);
208
+ }
209
+ return errors;
210
+ }
211
+ async function saveErrors(viewsDir, errors) {
212
+ const file = path_1.default.join(viewsDir, ERROR_LOG);
213
+ const entries = Object.entries(errors);
214
+ if (entries.length === 0) {
215
+ await fs_extra_1.default.remove(file);
216
+ return;
217
+ }
218
+ await fs_extra_1.default.outputFile(file, entries.map(([p, msg]) => `${p}: ${msg}`).join("\n") + "\n");
219
+ }
220
+ async function pullServerState(viewsDir, apiBase, token) {
221
+ console.log("šŸ“„ Fetching files...");
222
+ const files = await request(apiBase, token, "GET", "/get_files");
223
+ const fileMap = {};
224
+ let pulled = 0;
225
+ for (const file of files) {
226
+ const full = path_1.default.join(viewsDir, file.path);
227
+ await fs_extra_1.default.outputFile(full, file.content);
228
+ const mtimeMs = (await fs_extra_1.default.stat(full)).mtimeMs;
229
+ pulled++;
230
+ fileMap[file.path] = { mtimeMs };
231
+ }
232
+ console.log(`āœ”ļø Synced ${files.length} file(s).`);
233
+ return { fileMap, pulled };
234
+ }
235
+ if (require.main === module) {
236
+ commander_1.program
237
+ .name("setup-site")
238
+ .description("Initialize a SleekCMS workspace: pull all files and persist the auth token for future syncs.")
239
+ .requiredOption("-t, --token <token>", "SleekCMS CLI auth token")
240
+ .option("-d, --dir <dir>", "Parent directory; workspace is created as a slug-named subfolder (default: current directory)")
241
+ .option("-e, --env <env>", "Environment override (localhost, development, production)")
242
+ .parse(process.argv);
243
+ const opts = commander_1.program.opts();
244
+ syncSite({
245
+ token: opts.token,
246
+ path: opts.dir || path_1.default.join(os_1.default.homedir(), ".sleekcms"),
247
+ env: opts.env,
248
+ })
249
+ .then(({ viewsDir, site, isFirstRun, pulled }) => {
250
+ if (isFirstRun) {
251
+ console.log(`\nāœ… Workspace initialized for "${site.name}" at ${viewsDir} (pulled ${pulled} file(s)).`);
252
+ }
253
+ else {
254
+ console.log(`\nāœ… Workspace already initialized for "${site.name}" at ${viewsDir}.`);
255
+ }
256
+ console.log(`\nNext: cd ${viewsDir} → edit files → run sync-site`);
257
+ })
258
+ .catch((err) => {
259
+ console.error("āŒ", err.body || err.message);
260
+ process.exit(1);
261
+ });
262
+ }
@@ -0,0 +1,14 @@
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 {};
@@ -0,0 +1,49 @@
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
+ });
@@ -0,0 +1,17 @@
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
+ }
13
+ export declare function init(options: WatcherOptions): void;
14
+ export declare function setShuttingDown(value: boolean): void;
15
+ export declare function monitorFiles(): void;
16
+ export declare function stopWatching(): Promise<void>;
17
+ export {};