cyber-elx 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/src/index.js ADDED
@@ -0,0 +1,314 @@
1
+ const { program } = require('commander');
2
+ const chalk = require('chalk');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { readConfig, writeConfig, validateConfig, configExists } = require('./config');
6
+ const { readCache, writeCache, getPageTimestamp, setPageTimestamp } = require('./cache');
7
+ const { createApiClient } = require('./api');
8
+ const { ensureDirectories, writePageFile, getLocalPages, DEFAULT_TEMPLATE_KEYS, fileExists, readPageFile, getFilePath, getFolder } = require('./files');
9
+ const { promptInitConfig, confirmOverwrite, confirmUpload } = require('./prompts');
10
+
11
+ program
12
+ .name('cyber-elx')
13
+ .description('CLI tool to upload/download ELX custom pages')
14
+ .version('1.0.0');
15
+
16
+ program
17
+ .command('init')
18
+ .description('Initialize configuration and download pages')
19
+ .action(async () => {
20
+ try {
21
+ const cwd = process.cwd();
22
+
23
+ if (configExists(cwd)) {
24
+ console.log(chalk.yellow('Config file already exists. Delete it first if you want to reinitialize.'));
25
+ return;
26
+ }
27
+
28
+ const config = await promptInitConfig();
29
+ writeConfig(config, cwd);
30
+ console.log(chalk.green('✓ Config file created: cyber-elx.jsonc'));
31
+
32
+ console.log(chalk.blue('Testing connection...'));
33
+ const api = createApiClient(config);
34
+
35
+ try {
36
+ await api.getPages();
37
+ console.log(chalk.green('✓ Connection successful!'));
38
+ } catch (err) {
39
+ console.log(chalk.red('✗ Connection failed: ' + (err.response?.data?.message || err.message)));
40
+ console.log(chalk.yellow('Config file was created. Fix the credentials and run "cyber-elx download".'));
41
+ return;
42
+ }
43
+
44
+ console.log(chalk.blue('Downloading pages...'));
45
+ await downloadPages(cwd, config, true);
46
+
47
+ console.log(chalk.green('\n✓ Initialization complete!'));
48
+ console.log(chalk.gray('Edit files in sections/ and templates/ folders, then run "cyber-elx upload" to publish.'));
49
+ } catch (err) {
50
+ console.error(chalk.red('Error: ' + err.message));
51
+ process.exit(1);
52
+ }
53
+ });
54
+
55
+ program
56
+ .command('download')
57
+ .description('Download pages from server')
58
+ .option('-f, --force', 'Force download without confirmation prompts')
59
+ .action(async (options) => {
60
+ try {
61
+ const cwd = process.cwd();
62
+ const config = readConfig(cwd);
63
+ const validation = validateConfig(config);
64
+
65
+ if (!validation.valid) {
66
+ console.error(chalk.red(validation.error));
67
+ process.exit(1);
68
+ }
69
+
70
+ await downloadPages(cwd, config, options.force);
71
+ } catch (err) {
72
+ console.error(chalk.red('Error: ' + err.message));
73
+ process.exit(1);
74
+ }
75
+ });
76
+
77
+ program
78
+ .command('upload')
79
+ .description('Upload pages to server')
80
+ .option('-f, --force', 'Force upload without confirmation prompts')
81
+ .action(async (options) => {
82
+ try {
83
+ const cwd = process.cwd();
84
+ const config = readConfig(cwd);
85
+ const validation = validateConfig(config);
86
+
87
+ if (!validation.valid) {
88
+ console.error(chalk.red(validation.error));
89
+ process.exit(1);
90
+ }
91
+
92
+ await uploadPages(cwd, config, options.force);
93
+ } catch (err) {
94
+ console.error(chalk.red('Error: ' + err.message));
95
+ process.exit(1);
96
+ }
97
+ });
98
+
99
+ async function downloadPages(cwd, config, force = false) {
100
+ const api = createApiClient(config);
101
+ const cache = readCache(cwd);
102
+
103
+ ensureDirectories(cwd);
104
+
105
+ console.log(chalk.blue('Fetching pages from server...'));
106
+ const [pagesResponse, defaultsResponse] = await Promise.all([
107
+ api.getPages(),
108
+ api.getDefaultPages()
109
+ ]);
110
+
111
+ if (!pagesResponse.success) {
112
+ throw new Error('Failed to fetch pages: ' + (pagesResponse.message || 'Unknown error'));
113
+ }
114
+ if (!defaultsResponse.success) {
115
+ throw new Error('Failed to fetch defaults: ' + (defaultsResponse.message || 'Unknown error'));
116
+ }
117
+
118
+ const remotePages = pagesResponse.pages || [];
119
+ const defaultPages = defaultsResponse.pages || [];
120
+
121
+ console.log(chalk.blue('Downloading default pages (read-only)...'));
122
+ // Clear existing defaults before downloading
123
+ const defaultsDirs = [
124
+ path.join(cwd, 'defaults', 'sections'),
125
+ path.join(cwd, 'defaults', 'templates'),
126
+ path.join(cwd, 'defaults', 'layouts')
127
+ ];
128
+ for (const dir of defaultsDirs) {
129
+ if (fs.existsSync(dir)) {
130
+ fs.rmSync(dir, { recursive: true });
131
+ }
132
+ }
133
+ ensureDirectories(cwd);
134
+
135
+ for (const page of defaultPages) {
136
+ writePageFile(page.type, page.key, page.content, cwd, true);
137
+ console.log(chalk.gray(` ✓ defaults/${getFolder(page.type)}/${page.key}.liquid`));
138
+ }
139
+
140
+ const remotePagesMap = new Map();
141
+ for (const page of remotePages) {
142
+ remotePagesMap.set(`${page.type}:${page.key}`, page);
143
+ }
144
+
145
+ const allKeys = new Set();
146
+
147
+ for (const page of remotePages) {
148
+ allKeys.add(`${page.type}:${page.key}`);
149
+ }
150
+
151
+ for (const key of DEFAULT_TEMPLATE_KEYS) {
152
+ allKeys.add(`template:${key}`);
153
+ }
154
+
155
+ for (const page of defaultPages) {
156
+ allKeys.add(`${page.type}:${page.key}`);
157
+ }
158
+
159
+ console.log(chalk.blue('\nDownloading custom pages...'));
160
+ let downloadedCount = 0;
161
+ let skippedCount = 0;
162
+
163
+ for (const fullKey of allKeys) {
164
+ const [type, key] = fullKey.split(':');
165
+ const remotePage = remotePagesMap.get(fullKey);
166
+ const filePath = `${getFolder(type)}/${key}.liquid`;
167
+
168
+ const localExists = fileExists(type, key, cwd);
169
+ const localContent = localExists ? readPageFile(type, key, cwd) : null;
170
+ const cachedTimestamp = getPageTimestamp(cache, type, key);
171
+ const remoteTimestamp = remotePage?.updated_at || null;
172
+ const remoteContent = remotePage?.content || '';
173
+
174
+ if (!localExists) {
175
+ writePageFile(type, key, remoteContent, cwd);
176
+ if (remoteTimestamp) {
177
+ setPageTimestamp(cache, type, key, remoteTimestamp);
178
+ }
179
+ console.log(chalk.green(` ✓ ${filePath} (created)`));
180
+ downloadedCount++;
181
+ continue;
182
+ }
183
+
184
+ if (remoteTimestamp && cachedTimestamp && remoteTimestamp > cachedTimestamp) {
185
+ if (!force) {
186
+ const shouldOverwrite = await confirmOverwrite(filePath, 'has been modified on server');
187
+ if (!shouldOverwrite) {
188
+ console.log(chalk.yellow(` ⊘ ${filePath} (skipped)`));
189
+ skippedCount++;
190
+ continue;
191
+ }
192
+ }
193
+ writePageFile(type, key, remoteContent, cwd);
194
+ setPageTimestamp(cache, type, key, remoteTimestamp);
195
+ console.log(chalk.green(` ✓ ${filePath} (updated)`));
196
+ downloadedCount++;
197
+ continue;
198
+ }
199
+
200
+ if (localContent !== remoteContent && !force) {
201
+ if (cachedTimestamp === null && localContent !== '') {
202
+ const shouldOverwrite = await confirmOverwrite(filePath, 'exists locally with different content');
203
+ if (!shouldOverwrite) {
204
+ console.log(chalk.yellow(` ⊘ ${filePath} (skipped)`));
205
+ skippedCount++;
206
+ continue;
207
+ }
208
+ }
209
+ }
210
+
211
+ if (localContent !== remoteContent) {
212
+ writePageFile(type, key, remoteContent, cwd);
213
+ if (remoteTimestamp) {
214
+ setPageTimestamp(cache, type, key, remoteTimestamp);
215
+ }
216
+ console.log(chalk.green(` ✓ ${filePath} (updated)`));
217
+ downloadedCount++;
218
+ } else {
219
+ if (remoteTimestamp) {
220
+ setPageTimestamp(cache, type, key, remoteTimestamp);
221
+ }
222
+ console.log(chalk.gray(` - ${filePath} (unchanged)`));
223
+ }
224
+ }
225
+
226
+ writeCache(cache, cwd);
227
+
228
+ console.log(chalk.blue(`\nDownload complete: ${downloadedCount} downloaded, ${skippedCount} skipped`));
229
+ }
230
+
231
+ async function uploadPages(cwd, config, force = false) {
232
+ const api = createApiClient(config);
233
+ const cache = readCache(cwd);
234
+
235
+ const localPages = getLocalPages(cwd);
236
+
237
+ if (localPages.length === 0) {
238
+ console.log(chalk.yellow('No pages found to upload. Create .liquid files in sections/, templates/ or layouts/ folders.'));
239
+ return;
240
+ }
241
+
242
+ console.log(chalk.blue('Checking for conflicts with server...'));
243
+ const pagesResponse = await api.getPages();
244
+
245
+ if (!pagesResponse.success) {
246
+ throw new Error('Failed to fetch pages: ' + (pagesResponse.message || 'Unknown error'));
247
+ }
248
+
249
+ const remotePages = pagesResponse.pages || [];
250
+ const remotePagesMap = new Map();
251
+ for (const page of remotePages) {
252
+ remotePagesMap.set(`${page.type}:${page.key}`, page);
253
+ }
254
+
255
+ const pagesToUpload = [];
256
+
257
+ for (const localPage of localPages) {
258
+ const fullKey = `${localPage.type}:${localPage.key}`;
259
+ const remotePage = remotePagesMap.get(fullKey);
260
+ const filePath = `${getFolder(localPage.type)}/${localPage.key}.liquid`;
261
+ const cachedTimestamp = getPageTimestamp(cache, localPage.type, localPage.key);
262
+
263
+ if (remotePage) {
264
+ const remoteTimestamp = remotePage.updated_at;
265
+
266
+ if (cachedTimestamp && remoteTimestamp > cachedTimestamp) {
267
+ if (!force) {
268
+ const shouldUpload = await confirmUpload(filePath, 'has been modified on server since last download');
269
+ if (!shouldUpload) {
270
+ console.log(chalk.yellow(` ⊘ ${filePath} (skipped)`));
271
+ continue;
272
+ }
273
+ }
274
+ }
275
+
276
+ if (localPage.content === remotePage.content) {
277
+ console.log(chalk.gray(` - ${filePath} (unchanged)`));
278
+ continue;
279
+ }
280
+ }
281
+
282
+ pagesToUpload.push(localPage);
283
+ console.log(chalk.cyan(` → ${filePath} (will upload)` + (localPage.content === '' ? ' [EMPTY]' : '')));
284
+ }
285
+
286
+ if (pagesToUpload.length === 0) {
287
+ console.log(chalk.yellow('\nNo changes to upload.'));
288
+ return;
289
+ }
290
+
291
+ console.log(chalk.blue(`\nUploading ${pagesToUpload.length} page(s)...`));
292
+
293
+ const response = await api.updatePages(pagesToUpload);
294
+
295
+ if (!response.success) {
296
+ throw new Error('Upload failed: ' + (response.message || 'Unknown error'));
297
+ }
298
+
299
+ const updatedPages = response.updatedpages || [];
300
+ for (const page of updatedPages) {
301
+ setPageTimestamp(cache, page.type, page.key, page.updated_at);
302
+ const filePath = `${getFolder(page.type)}/${page.key}.liquid`;
303
+ console.log(chalk.green(` ✓ ${filePath} (uploaded)`));
304
+ }
305
+
306
+ writeCache(cache, cwd);
307
+
308
+ console.log(chalk.green(`\n✓ Upload complete: ${updatedPages.length} page(s) updated [${updatedPages.map(p => p.key).join(', ')}]`));
309
+ if(response.debug) {
310
+ console.log(chalk.gray('Debug info:'), response.debug);
311
+ }
312
+ }
313
+
314
+ program.parse();
package/src/prompts.js ADDED
@@ -0,0 +1,70 @@
1
+ const inquirer = require('inquirer');
2
+ const chalk = require('chalk');
3
+
4
+ async function promptInitConfig() {
5
+ const answers = await inquirer.prompt([
6
+ {
7
+ type: 'input',
8
+ name: 'url',
9
+ message: 'Enter your website URL (e.g., https://my-website.net):',
10
+ validate: (input) => {
11
+ if (!input) return 'URL is required';
12
+ if (!input.startsWith('http://') && !input.startsWith('https://')) {
13
+ return 'URL must start with http:// or https://';
14
+ }
15
+ return true;
16
+ },
17
+ filter: (input) => input.replace(/\/$/, '')
18
+ },
19
+ {
20
+ type: 'input',
21
+ name: 'token',
22
+ message: 'Enter your authentication token:',
23
+ validate: (input) => input ? true : 'Token is required'
24
+ }
25
+ ]);
26
+ return answers;
27
+ }
28
+
29
+ async function confirmOverwrite(filePath, reason) {
30
+ const { confirm } = await inquirer.prompt([
31
+ {
32
+ type: 'confirm',
33
+ name: 'confirm',
34
+ message: `${chalk.yellow(filePath)} ${reason}. Overwrite?`,
35
+ default: false
36
+ }
37
+ ]);
38
+ return confirm;
39
+ }
40
+
41
+ async function confirmUpload(filePath, reason) {
42
+ const { confirm } = await inquirer.prompt([
43
+ {
44
+ type: 'confirm',
45
+ name: 'confirm',
46
+ message: `${chalk.cyan(filePath)} ${reason}. Upload anyway?`,
47
+ default: true
48
+ }
49
+ ]);
50
+ return confirm;
51
+ }
52
+
53
+ async function confirmAll(message) {
54
+ const { confirm } = await inquirer.prompt([
55
+ {
56
+ type: 'confirm',
57
+ name: 'confirm',
58
+ message,
59
+ default: false
60
+ }
61
+ ]);
62
+ return confirm;
63
+ }
64
+
65
+ module.exports = {
66
+ promptInitConfig,
67
+ confirmOverwrite,
68
+ confirmUpload,
69
+ confirmAll
70
+ };