@sleekcms/cli 1.4.0 → 2.0.1

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.
Files changed (3) hide show
  1. package/agent.js +136 -0
  2. package/index.js +115 -12
  3. package/package.json +2 -1
package/agent.js ADDED
@@ -0,0 +1,136 @@
1
+ const agentMd = `# EJS Template Editing Guide
2
+
3
+ ## Workspace Information
4
+
5
+ This is a **synced SleekCMS workspace**. Files are automatically synced with the SleekCMS server.
6
+
7
+ ### Important Notes:
8
+ - **Standard VS Code tools work** - use \`file_search\`, \`grep_search\`, \`read_file\`, etc.
9
+ - **File edits are auto-synced** - changes are automatically saved to the server
10
+ - **DO NOT create new files** - new templates must be created via the SleekCMS dashboard
11
+ - **Deleting files** will also delete them from the server
12
+
13
+ ---
14
+
15
+ Templates use [EJS](https://ejs.co/) syntax. The template receives a context object with site content and helper functions.
16
+
17
+ ## Directory structure
18
+ - css/tailwind.css - tailwind config v4
19
+ - css/*.css - other styles css
20
+ - js/*.js - script files
21
+ - views/blocks/*.ejs - ejs templates for each block. Blocks are groups of fields, used as a field type in entries or pages
22
+ - views/entries/*.ejs - ejs template corresponding to each entry. Entries are regular records and can be referenced by other models
23
+ - views/pages/*.ejs - ejs template corresponding to each page or page collections (path/[slug])
24
+ - All _index.ejs are for collection of pages and created for each [slug]
25
+
26
+ **Note:** Do not create any new files. You can suggest creating new models or ask for model schema but don't add any new files.
27
+
28
+ ## Available Data
29
+
30
+ - \`item\` — The current page, entry, or block being rendered. Fields depend on the schema (e.g. \`item.title\`, \`item.body\`, \`item.image\`).
31
+ - \`pages\` — Array of all pages. Each page has \`_path\`, \`_slug\`, and its own fields.
32
+ - \`entries\` — Object of entries keyed by handle (e.g. \`entries.header\`, \`entries.footer\`).
33
+ - \`images\` — Object of images keyed by name. Each has \`{ url, raw, alt }\`.
34
+ - \`options\` — Object of option sets keyed by name. Each is an array of \`{ label, value }\`.
35
+ - \`main\` — The rendered HTML from the previous template in the chain (used in base/layout templates).
36
+
37
+ Note: Although all data can be accessed directly, best to use Helper function instead of accessing it directly.
38
+
39
+ ## Helper Functions
40
+
41
+ ### Content Querying
42
+
43
+ | Function | Description |
44
+ |---|---|
45
+ | \`render(blocks:any)\` | Render a block or section (array of blocks) to HTML |
46
+ | \`getEntry(handle:string)\` | Get an entry by handle |
47
+ | \`getPage(path:string)\` | Get a page with the exact path |
48
+ | \`getPages(path:string, {collection?: boolean})\` | Get all pages where path begins with the string. |
49
+ | \`getSlugs(path:string)\` | Get slugs of pages under a path |
50
+ | \`getImage(name:string)\` | Get an image object by name |
51
+ | \`getOptions(name:string)\` | Get an option set by name |
52
+ | \`getContent(query?)\` | Get all content, or filter with a [JMESPath](https://jmespath.org/) query |
53
+ | \`path(page)\` | Get the URL path of a page |
54
+
55
+ ### Images
56
+
57
+ | Function | Description |
58
+ |---|---|
59
+ | \`src(image:{url: string}, "WxH")\` | Get a resized image URL. e.g. \`src(item.image, "800x600")\` |
60
+ | \`img(image:{url: string}, "WxH")\` | Get a full \`<img>\` tag |
61
+ | \`picture(image:{url: string}, "WxH")\` | Get a \`<picture>\` tag (supports dark/light variants) |
62
+ | \`svg(image:{url: string})\` | Render an SVG reference |
63
+
64
+ Size can be a string \`"WxH"\` or an object \`{ w, h, fit, class, style }\`.
65
+
66
+ ### Head Injection
67
+
68
+ Call these to add elements to \`<head>\`. Deduplicated automatically.
69
+
70
+ | Function | Description |
71
+ |---|---|
72
+ | \`meta(attrs)\` | Add a \`<meta>\` tag. e.g. \`meta({ name: "description", content: "..." })\` |
73
+ | \`link(attrs)\` | Add a \`<link>\` tag |
74
+ | \`style(css)\` | Add a \`<style>\` block |
75
+ | \`script(js)\` | Add a \`<script>\` block |
76
+ | \`title(text)\` | Set the page \`<title>\` |
77
+ | \`seo()\` | Auto-generate SEO meta tags from \`item.seo\` or \`item.title\`/\`item.description\`/\`item.image\` |
78
+
79
+ ## Template Types
80
+
81
+ - **main** — Renders the content for the current item.
82
+ - **base** — Layout wrapper. Use \`<%- main %>\` to output the rendered main content.
83
+
84
+ ## Examples
85
+
86
+ ### Simple page template (main)
87
+ \`\`\`ejs
88
+ <h1><%= item.title %></h1>
89
+ <div><%- item.body %></div>
90
+ \`\`\`
91
+
92
+ ### Render blocks
93
+ \`\`\`ejs
94
+ <%- render(item.blocks) %>
95
+ \`\`\`
96
+
97
+ ### List pages
98
+ \`\`\`ejs
99
+ <% for (let page of findPages('/blog')) { %>
100
+ <a href="<%= path(page) %>"><%= page.title %></a>
101
+ <% } %>
102
+ \`\`\`
103
+
104
+ ### Image
105
+ \`\`\`ejs
106
+ <%- img(item.image, "600x400") %>
107
+ \`\`\`
108
+
109
+ ### Base layout
110
+ \`\`\`ejs
111
+ <!DOCTYPE html>
112
+ <html>
113
+ <head>
114
+ <title><%= item.title %></title>
115
+ </head>
116
+ <body>
117
+ <%- render(entries.header) %>
118
+ <%- main %>
119
+ <%- render(entries.footer) %>
120
+ </body>
121
+ </html>
122
+ \`\`\`
123
+
124
+ ### SEO
125
+ \`\`\`ejs
126
+ <% seo() %>
127
+ \`\`\`
128
+
129
+ ## EJS Syntax Quick Reference
130
+
131
+ - \`<%= expr %>\` — Output escaped HTML
132
+ - \`<%- expr %>\` — Output raw/unescaped HTML (use for rendered blocks, images, HTML fields)
133
+ - \`<% code %>\` — Execute JS (loops, conditionals)
134
+ `;
135
+
136
+ module.exports = agentMd;
package/index.js CHANGED
@@ -5,6 +5,10 @@ const axios = require("axios");
5
5
  const chokidar = require("chokidar");
6
6
  const { program } = require("commander");
7
7
  const path = require("path");
8
+ const express = require("express");
9
+ const { execSync, spawn } = require("child_process");
10
+ const readline = require("readline");
11
+ const agentMdContent = require("./agent.js");
8
12
 
9
13
  const API_BASE_URLS = {
10
14
  localhost: "http://localhost:9000/api/template",
@@ -20,22 +24,36 @@ let watcher;
20
24
 
21
25
  // CLI Setup to take `--token=<token>`
22
26
  program
23
- .option("--token <token>", "API authentication token")
24
- .option("--env <env>", "Environment (localhost, development, production)", "production")
27
+ .name("cms-cli")
28
+ .description("SleekCMS CLI tool to sync and edit CMS templates locally. Downloads templates, watches for changes, and syncs updates back to the API.")
29
+ .version("1.0.0", "-v, --version", "output the version number")
30
+ .option("-t, --token <token>", "API authentication token (required)")
31
+ .option("-e, --env <env>", "Environment (localhost, development, production)", "production")
32
+ .option("-p, --path <path>", "Directory path for files (default: <token-prefix>-views)")
33
+ .addHelpText("after", `
34
+ Examples:
35
+ $ cms-cli --token abc123-xxxx
36
+ $ cms-cli -t abc123-xxxx -e development
37
+ $ cms-cli -t abc123-xxxx -p ./my-templates
38
+ `)
25
39
  .parse(process.argv);
26
40
 
27
41
  const options = program.opts();
28
42
  const AUTH_TOKEN = options.token;
29
- const ENV = options.env.toLowerCase();
43
+ const tokenParts = AUTH_TOKEN ? AUTH_TOKEN.split('-') : [];
44
+ const ENV = (tokenParts[2] || options.env || "production").toLowerCase();
30
45
 
31
46
  if (!AUTH_TOKEN) {
32
- console.error("❌ Missing required --token parameter.");
47
+ console.error("❌ Missing required --token (-t) parameter. Use -h for help.");
33
48
  process.exit(1);
34
49
  }
35
50
 
36
51
  const API_BASE_URL = API_BASE_URLS[ENV] || API_BASE_URLS.production;
37
52
 
38
- const VIEWS_DIR = path.resolve(AUTH_TOKEN.split('-')[0] + "-views");
53
+ const viewsFolder = tokenParts[0] + "-views";
54
+ const VIEWS_DIR = options.path
55
+ ? path.resolve(options.path, viewsFolder)
56
+ : path.resolve(viewsFolder);
39
57
 
40
58
  // Axios instance with authorization
41
59
  const apiClient = axios.create({
@@ -61,7 +79,7 @@ async function refreshFile(filePath) {
61
79
  // Function to fetch and save files
62
80
  async function fetchFiles() {
63
81
  try {
64
- console.log("📥 Fetching files from API...");
82
+ console.log("📥 Fetching source code...");
65
83
  const response = await apiClient.get("/");
66
84
 
67
85
  await fs.ensureDir(VIEWS_DIR);
@@ -71,11 +89,14 @@ async function fetchFiles() {
71
89
  const filePath = path.join(VIEWS_DIR, file.file_path);
72
90
  await fs.outputFile(filePath, file.code);
73
91
  fileMap[file.file_path.replace(/\\/g,"/")] = file;
74
- console.log(`✅ Created: ${filePath}`);
92
+ //console.log(`✅ Created: ${filePath}`);
75
93
  }
76
94
  }
77
95
 
78
- console.log("✔️ All files downloaded. They will be deleted on exit.");
96
+ console.log(`✔️ Downloaded ${response.data.length} template(s).`);
97
+
98
+ // Create AGENT.md
99
+ await fs.outputFile(path.join(VIEWS_DIR, 'AGENT.md'), agentMdContent);
79
100
  } catch (error) {
80
101
  console.error("❌ Error fetching files:", error.response?.data || error.message);
81
102
  }
@@ -153,19 +174,101 @@ async function createSchema(filePath) {
153
174
  }
154
175
  }
155
176
 
177
+ // Start an Express server to serve files in VIEWS_DIR
178
+ function startServer() {
179
+ const app = express();
180
+
181
+ // Serve static files from VIEWS_DIR
182
+ app.use(express.static(VIEWS_DIR));
183
+
184
+ // Optional: Custom 404 for files not found
185
+ app.use((req, res) => {
186
+ res.status(404).send("File not found");
187
+ });
188
+
189
+ const port = process.env.PORT || 3000;
190
+ app.listen(port, () => {
191
+ console.log(`\n✅ Ready! Editing session started.`);
192
+ console.log(`\n📁 Templates directory: ${VIEWS_DIR}`);
193
+ console.log(`🌐 Environment: ${ENV}`);
194
+ console.log(`🔗 Static server: http://localhost:${port}`);
195
+ console.log(`\n⚠️ Files will be cleaned up on exit (Ctrl+C).`);
196
+ showEditorMenu();
197
+ });
198
+ }
199
+
156
200
  // Function to monitor file changes
157
201
  function monitorFiles() {
158
- console.log("👀 Watching for file changes...");
159
-
160
202
  watcher = chokidar.watch(VIEWS_DIR, {
161
203
  persistent: true,
162
204
  ignoreInitial: true,
163
- ignored: /\.vscode\//
205
+ ignored: [/\.vscode\//, /AGENT\.md$/]
164
206
  })
165
207
  .on("change", scheduleUpdate)
166
208
  .on("add", createSchema);
167
209
  }
168
210
 
211
+ // Check if a command exists in PATH
212
+ function commandExists(cmd) {
213
+ try {
214
+ execSync(`which ${cmd}`, { stdio: 'ignore' });
215
+ return true;
216
+ } catch {
217
+ return false;
218
+ }
219
+ }
220
+
221
+ // Show editor selection menu
222
+ function showEditorMenu() {
223
+ const editors = [];
224
+
225
+ if (commandExists('code')) {
226
+ editors.push({ key: '1', name: 'VS Code', cmd: 'code' });
227
+ }
228
+ if (commandExists('cursor')) {
229
+ editors.push({ key: '2', name: 'Cursor', cmd: 'cursor' });
230
+ }
231
+
232
+ if (editors.length === 0) {
233
+ console.log('\n👀 Watching for changes...\n');
234
+ return;
235
+ }
236
+
237
+ console.log('\n📂 Open in editor:');
238
+ editors.forEach(e => console.log(` [${e.key}] ${e.name}`));
239
+ console.log(' [Enter] Skip\n');
240
+
241
+ const rl = readline.createInterface({
242
+ input: process.stdin,
243
+ output: process.stdout
244
+ });
245
+
246
+ // Count lines to clear (menu header + editors + skip + empty + prompt)
247
+ const linesToClear = editors.length + 4;
248
+
249
+ rl.question('Select editor: ', (answer) => {
250
+ rl.close();
251
+
252
+ // Clear the menu lines
253
+ process.stdout.write(`\x1b[${linesToClear}A`); // Move cursor up
254
+ for (let i = 0; i < linesToClear; i++) {
255
+ process.stdout.write('\x1b[2K\n'); // Clear each line
256
+ }
257
+ process.stdout.write(`\x1b[${linesToClear}A`); // Move back up
258
+
259
+ const selected = editors.find(e => e.key === answer.trim());
260
+ if (selected) {
261
+ console.log(`👀 Watching for changes... (opened ${selected.name})\n`);
262
+ spawn(selected.cmd, [VIEWS_DIR], {
263
+ detached: true,
264
+ stdio: 'ignore'
265
+ }).unref();
266
+ } else {
267
+ console.log('👀 Watching for changes...\n');
268
+ }
269
+ });
270
+ }
271
+
169
272
  // Graceful shutdown handler
170
273
  async function handleExit() {
171
274
  if (isShuttingDown) return;
@@ -182,7 +285,7 @@ async function handleExit() {
182
285
  async function main() {
183
286
  await fetchFiles();
184
287
  monitorFiles();
185
- //const server = startServer();
288
+ startServer();
186
289
 
187
290
  process.on("SIGINT", async () => {
188
291
  console.log("\n🛑 Caught interrupt signal (Ctrl+C)");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleekcms/cli",
3
- "version": "1.4.0",
3
+ "version": "2.0.1",
4
4
  "description": "SleekCMS CLI for locally editing SleekCMS site template code",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -15,6 +15,7 @@
15
15
  "axios": "^1.7.9",
16
16
  "chokidar": "^4.0.3",
17
17
  "commander": "^13.1.0",
18
+ "express": "^4.21.0",
18
19
  "fs-extra": "^11.3.0"
19
20
  }
20
21
  }