@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.
- package/agent.js +136 -0
- package/index.js +115 -12
- 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
|
-
.
|
|
24
|
-
.
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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": "
|
|
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
|
}
|