@sleekcms/sync 1.4.4 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -48,12 +48,15 @@ sleekcms --token <YOUR_AUTH_TOKEN>
48
48
  ## Usage
49
49
 
50
50
  ```bash
51
- # Basic
51
+ # Basic — auto-sync on every save
52
52
  npx @sleekcms/sync --token abc123-xxxx
53
53
 
54
+ # Manual sync — push changes only when you press [s]
55
+ npx @sleekcms/sync --token abc123-xxxx --manual
56
+
54
57
  ```
55
58
 
56
- Once running, press `r` to re-fetch all files or `x` / `Ctrl+C` to exit.
59
+ Once running, press `r` to re-fetch all files or `x` / `Ctrl+C` to exit. In `--manual` mode, press `s` to push your changes on demand.
57
60
 
58
61
  ---
59
62
 
@@ -136,12 +139,24 @@ On first run, the CLI pulls the full site state from the server. After that, a l
136
139
  Your editor → file save → watcher → SleekCMS API → rebuild → live site
137
140
  ```
138
141
 
139
- ### Watch mode commands
142
+ ### Manual sync mode (`--manual`)
143
+
144
+ Pass `--manual` (`-m`) to skip the live watcher. The CLI still does the initial fetch, but after that nothing is pushed until you press `s`. On each `s`, it scans the workspace, diffs against the cached state in `.cache/`, and pushes only the files that changed since your last fetch or sync — the same diff logic, run on demand instead of on every save.
145
+
146
+ This is useful when you want to batch a set of edits (or let an AI agent finish writing several files) and push them as one deliberate action.
147
+
148
+ ```
149
+ Your editor → edit freely → press [s] → diff vs last fetch → SleekCMS API → live site
150
+ ```
151
+
152
+ ### Session commands
140
153
 
141
154
  | Key | Action |
142
155
  |---|---|
156
+ | `s` | Push changed files now (`--manual` mode only) |
143
157
  | `r` | Re-fetch all files from server |
144
158
  | `x` or `Ctrl+C` | Exit and clean up local workspace |
159
+ | `q` | Quit, keeping local files |
145
160
 
146
161
  ---
147
162
 
@@ -266,7 +281,7 @@ Models describe the shape of your content. They're JSON-like files with unquoted
266
281
  | `json` | Object or array |
267
282
  | `block(key)` | Embedded block object |
268
283
  | `stack` | Array of heterogeneous block objects; each item carries `_block: "<block_key>"` to name its block |
269
- | `entry(key)` | Entry object or slug reference |
284
+ | `entry(key)` | Reference token `"<index>\|<text>"` — `index` is the entry's 0-based position in `content/entries/<key>+.json` (what binds); the optional `<text>` is the entry's opening content copied verbatim (a substring check, not a summary you write). Resolves to the entry object in templates |
270
285
 
271
286
  ---
272
287
 
package/dist/AGENT.md CHANGED
@@ -279,7 +279,7 @@ Model fields declare both the editor type and the shape expected in content JSON
279
279
  | `block(key)` | Object matching that block's model; stored inline in the parent page/entry content and rendered with `blocks/<key>.ejs` |
280
280
  | `[block(key)]` | Array of block objects; each item matches the block model and renders with `blocks/<key>.ejs` |
281
281
  | `stack` | Array of heterogeneous block-shaped objects. Each item is `{ "_block": "<block_key>", ...fields_of_that_block }`. `_block` is the target block's key as a plain string; the remaining keys must match that block's model. Different items in the same stack may use different blocks. Stored inline; each item renders through its own `blocks/<key>.ejs`. The CMS resolves block keys to ids server-side — never write numeric ids. |
282
- | `entry(key)` / `[entry(key)]` | Slug string / array of slug strings in content JSON; entry object / array of entry objects in templates |
282
+ | `entry(key)` / `[entry(key)]` | Reference token `"<index>\|<text>"` (or an array of them) in content JSON; entry object / array of entry objects in templates. `index` is the 0-based position of the target entry in its `content/entries/<key>+.json` list — that is what binds. `\|<text>` is optional: the **opening characters of that entry's content, copied verbatim** (matched as a substring — not a description you compose), used only to relocate the entry if the list shifted. You may write just the index (`"2"` or `2`). |
283
283
  | Group `{ ... }` | Nested object |
284
284
  | Collection `[{ ... }]` | Array of nested objects |
285
285
 
@@ -324,11 +324,11 @@ The content file at `content/pages/about.json`:
324
324
  "cta_label": "Contact us",
325
325
  "cta_link": "/contact"
326
326
  },
327
- "tags": ["engineering", "design"]
327
+ "tags": ["0|Engineering", "2|Design"]
328
328
  }
329
329
  ```
330
330
 
331
- Here `hero` is embedded block data stored directly in the page content file. `image` and `hero.background` use the shortcut form — on save, the sync engine replaces each shortcut with a real image object (`{ "url": "...", "alt": "..." }`). Write the object form directly when you have a specific asset URL.
331
+ Here `hero` is embedded block data stored directly in the page content file. `image` and `hero.background` use the shortcut form — on save, the sync engine replaces each shortcut with a real image object (`{ "url": "...", "alt": "..." }`). Write the object form directly when you have a specific asset URL. `tags` references entries by position — `"0|Engineering"` points at the first entry in `content/entries/tags+.json`, where `Engineering` is just the start of that entry's content copied verbatim (a substring check, not a description you write). The index is what binds on save; the text only helps relocate the entry if the list shifted, and a bare index (`"0"`) is also accepted.
332
332
 
333
333
  ### Markdown content files
334
334
 
package/dist/cli.d.ts CHANGED
@@ -6,14 +6,16 @@ export interface CliOptions {
6
6
  token?: string;
7
7
  env?: string;
8
8
  path?: string;
9
+ manual?: boolean;
9
10
  }
10
11
  export interface KeyboardHandlers {
11
12
  onExit?: () => void | Promise<unknown>;
12
13
  onQuit?: () => void | Promise<unknown>;
13
14
  onRefetch?: () => void | Promise<unknown>;
15
+ onSync?: () => void | Promise<unknown>;
14
16
  }
15
17
  export declare function parseArgs(): CliOptions;
16
18
  export declare function prompt(question: string): Promise<string>;
17
- export declare function showWatchHelp(): void;
18
- export declare function setupKeyboardInput(handlers: KeyboardHandlers): void;
19
- export declare function showEditorMenu(viewsDir: string, handlers: KeyboardHandlers): void;
19
+ export declare function showWatchHelp(manual?: boolean): void;
20
+ export declare function setupKeyboardInput(handlers: KeyboardHandlers, manual?: boolean): void;
21
+ export declare function showEditorMenu(viewsDir: string, handlers: KeyboardHandlers, manual?: boolean): void;
package/dist/cli.js CHANGED
@@ -23,11 +23,13 @@ function parseArgs() {
23
23
  .option("-t, --token <token>", "API authentication token (required)")
24
24
  .addOption(new commander_1.Option("-e, --env <env>", "Environment (localhost, development, production)").default("production").hideHelp())
25
25
  .option("-p, --path <path>", "Directory path for files (default: <token-prefix>-views)")
26
+ .option("-m, --manual", "Manual sync mode: don't auto-sync on file changes; press [s] to push diffs on demand")
26
27
  .addHelpText("after", `
27
28
  Examples:
28
29
  $ cms-sync --token abc123-xxxx
29
30
  $ cms-sync -t abc123-xxxx -e development
30
31
  $ cms-sync -t abc123-xxxx -p ./my-templates
32
+ $ cms-sync -t abc123-xxxx -m # manual sync — push only when you press [s]
31
33
  `)
32
34
  .parse(process.argv);
33
35
  return commander_1.program.opts();
@@ -66,10 +68,11 @@ function commandExists(cmd) {
66
68
  return false;
67
69
  }
68
70
  }
69
- function showWatchHelp() {
70
- console.log("📋 Commands: [r] Re-fetch all files [x] Exit (cleanup) [q] Quit (keep files)\n");
71
+ function showWatchHelp(manual = false) {
72
+ const sync = manual ? "[s] Sync changes " : "";
73
+ console.log(`📋 Commands: ${sync}[r] Re-fetch all files [x] Exit (cleanup) [q] Quit (keep files)\n`);
71
74
  }
72
- function setupKeyboardInput(handlers) {
75
+ function setupKeyboardInput(handlers, manual = false) {
73
76
  if (process.stdin.isTTY) {
74
77
  process.stdin.setRawMode(true);
75
78
  rawModeEnabled = true;
@@ -84,11 +87,16 @@ function setupKeyboardInput(handlers) {
84
87
  return;
85
88
  }
86
89
  const cmd = key.toLowerCase();
87
- if (cmd === "r" && handlers.onRefetch) {
90
+ if (cmd === "s" && handlers.onSync) {
91
+ await handlers.onSync();
92
+ showWatchHelp(manual);
93
+ }
94
+ else if (cmd === "r" && handlers.onRefetch) {
88
95
  console.log("\n🔄 Re-fetching all files...");
89
96
  await handlers.onRefetch();
90
- console.log("👀 Watching for changes...");
91
- showWatchHelp();
97
+ if (!manual)
98
+ console.log("👀 Watching for changes...");
99
+ showWatchHelp(manual);
92
100
  }
93
101
  else if (cmd === "x" && handlers.onExit) {
94
102
  await handlers.onExit();
@@ -102,14 +110,19 @@ const EDITOR_CANDIDATES = [
102
110
  { name: "VS Code", cmd: "code", args: (dir) => ["-n", dir] },
103
111
  { name: "Cursor", cmd: "cursor", args: (dir) => ["-n", dir] },
104
112
  ];
105
- function showEditorMenu(viewsDir, handlers) {
113
+ function statusLine(manual, suffix = "") {
114
+ const base = manual ? "✋ Manual sync mode — press [s] to push changes." : "👀 Watching for changes...";
115
+ console.log(`${base}${suffix}`);
116
+ }
117
+ function showEditorMenu(viewsDir, handlers, manual = false) {
106
118
  const editors = EDITOR_CANDIDATES
107
119
  .filter(e => commandExists(e.cmd))
108
120
  .map((e, i) => ({ ...e, key: String(i + 1) }));
109
121
  if (editors.length === 0) {
110
- console.log("\n👀 Watching for changes...");
111
- showWatchHelp();
112
- setupKeyboardInput(handlers);
122
+ console.log("");
123
+ statusLine(manual);
124
+ showWatchHelp(manual);
125
+ setupKeyboardInput(handlers, manual);
113
126
  return;
114
127
  }
115
128
  console.log("\n📂 Open in editor:");
@@ -144,7 +157,7 @@ function showEditorMenu(viewsDir, handlers) {
144
157
  }
145
158
  const selected = editors.find(e => e.key === answer.trim());
146
159
  if (selected) {
147
- console.log(`👀 Watching for changes... (opened ${selected.name})`);
160
+ statusLine(manual, ` (opened ${selected.name})`);
148
161
  (0, child_process_1.spawn)(selected.cmd, selected.args(viewsDir), {
149
162
  detached: true,
150
163
  stdio: "ignore",
@@ -152,9 +165,9 @@ function showEditorMenu(viewsDir, handlers) {
152
165
  }).unref();
153
166
  }
154
167
  else {
155
- console.log("👀 Watching for changes...");
168
+ statusLine(manual);
156
169
  }
157
- showWatchHelp();
158
- setupKeyboardInput(handlers);
170
+ showWatchHelp(manual);
171
+ setupKeyboardInput(handlers, manual);
159
172
  });
160
173
  }
package/dist/index.js CHANGED
@@ -127,6 +127,21 @@ async function handleQuit() {
127
127
  console.log(`📁 Workspace left at: ${VIEWS_DIR}`);
128
128
  process.exit(0);
129
129
  }
130
+ /**
131
+ * Manual sync trigger ([s] key). Scans the workspace on demand, diffs against
132
+ * the cached state in .cache/state.json, and pushes only changed files.
133
+ */
134
+ async function handleManualSync() {
135
+ console.log("\n🔄 Syncing changes...");
136
+ try {
137
+ const { pushed } = await runSync();
138
+ console.log(pushed > 0 ? `✅ Synced ${pushed} change(s).` : "✔️ No changes to sync.");
139
+ }
140
+ catch (err) {
141
+ const e = err;
142
+ console.error("❌ Sync failed:", e.body || e.message);
143
+ }
144
+ }
130
145
  async function main() {
131
146
  await initConfig();
132
147
  try {
@@ -137,18 +152,24 @@ async function main() {
137
152
  console.error("❌ Sync failed:", e.body || e.message);
138
153
  process.exit(1);
139
154
  }
140
- watcher.init({ viewsDir: VIEWS_DIR, onSync: runSync, onIdle: handleExit });
141
- watcher.monitorFiles();
155
+ const manual = options.manual ?? false;
156
+ if (!manual) {
157
+ watcher.init({ viewsDir: VIEWS_DIR, onSync: runSync, onIdle: handleExit });
158
+ watcher.monitorFiles();
159
+ }
142
160
  console.log(`\n✅ Ready! Editing session started for site - ${site.name}.`);
143
161
  console.log(`\n📁 Workspace created at: ${VIEWS_DIR}`);
144
162
  if (ENV !== "production")
145
163
  console.log(`🌐 Environment: ${ENV}`);
164
+ if (manual)
165
+ console.log(`\n✋ Manual sync mode — changes are pushed only when you press [s].`);
146
166
  console.log(`\n⚠️ Files will be cleaned up on exit (Ctrl+C).`);
147
167
  cli.showEditorMenu(VIEWS_DIR, {
148
168
  onExit: handleExit,
149
169
  onQuit: handleQuit,
150
170
  onRefetch: () => runSync({ flush: true }),
151
- });
171
+ ...(manual ? { onSync: handleManualSync } : {}),
172
+ }, manual);
152
173
  process.on("SIGINT", async () => {
153
174
  console.log("\n🛑 Caught interrupt signal (Ctrl+C)");
154
175
  await handleExit();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleekcms/sync",
3
- "version": "1.4.4",
3
+ "version": "1.6.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",