@magentrix-corp/magentrix-cli 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.
Files changed (43) hide show
  1. package/LICENSE +25 -0
  2. package/README.md +471 -0
  3. package/actions/autopublish.js +283 -0
  4. package/actions/autopublish.old.js +293 -0
  5. package/actions/autopublish.v2.js +447 -0
  6. package/actions/create.js +329 -0
  7. package/actions/help.js +165 -0
  8. package/actions/main.js +81 -0
  9. package/actions/publish.js +567 -0
  10. package/actions/pull.js +139 -0
  11. package/actions/setup.js +61 -0
  12. package/actions/status.js +17 -0
  13. package/bin/magentrix.js +159 -0
  14. package/package.json +61 -0
  15. package/utils/cacher.js +112 -0
  16. package/utils/cli/checkInstanceUrl.js +29 -0
  17. package/utils/cli/helpers/compare.js +281 -0
  18. package/utils/cli/helpers/ensureApiKey.js +57 -0
  19. package/utils/cli/helpers/ensureCredentials.js +60 -0
  20. package/utils/cli/helpers/ensureInstanceUrl.js +63 -0
  21. package/utils/cli/writeRecords.js +223 -0
  22. package/utils/compare.js +135 -0
  23. package/utils/compress.js +18 -0
  24. package/utils/config.js +451 -0
  25. package/utils/diff.js +49 -0
  26. package/utils/downloadAssets.js +75 -0
  27. package/utils/filetag.js +115 -0
  28. package/utils/hash.js +14 -0
  29. package/utils/magentrix/api/assets.js +145 -0
  30. package/utils/magentrix/api/auth.js +56 -0
  31. package/utils/magentrix/api/createEntity.js +61 -0
  32. package/utils/magentrix/api/deleteEntity.js +55 -0
  33. package/utils/magentrix/api/meqlQuery.js +31 -0
  34. package/utils/magentrix/api/retrieveEntity.js +32 -0
  35. package/utils/magentrix/api/updateEntity.js +66 -0
  36. package/utils/magentrix/fetch.js +154 -0
  37. package/utils/merge.js +22 -0
  38. package/utils/preferences.js +40 -0
  39. package/utils/spinner.js +43 -0
  40. package/utils/template.js +52 -0
  41. package/utils/updateFileBase.js +103 -0
  42. package/vars/config.js +1 -0
  43. package/vars/global.js +33 -0
@@ -0,0 +1,447 @@
1
+ import chokidar from 'chokidar';
2
+ import { ensureValidCredentials } from '../utils/cli/helpers/ensureCredentials.js';
3
+ import { withSpinner } from '../utils/spinner.js';
4
+ import { ENTITY_FIELD_MAP, ENTITY_TYPE_MAP, EXPORT_ROOT, TYPE_DIR_MAP } from '../vars/global.js';
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ import { updateEntity } from '../utils/magentrix/api/updateEntity.js';
8
+ import { createEntity } from '../utils/magentrix/api/createEntity.js';
9
+ import { deleteEntity } from '../utils/magentrix/api/deleteEntity.js';
10
+ import chalk from 'chalk';
11
+ import { getFileTag, setFileTag } from '../utils/filetag.js';
12
+ import { sha256 } from '../utils/hash.js';
13
+ import { updateBase, removeFromBase } from '../utils/updateFileBase.js';
14
+ import { uploadAsset, deleteAsset } from '../utils/magentrix/api/assets.js';
15
+
16
+ const LOCK_FILE = path.join(process.cwd(), '.magentrix', 'autopublish.lock');
17
+
18
+ let credentials = {};
19
+ let isProcessing = false;
20
+ let pendingChanges = new Map(); // Map of file paths to their operation types
21
+
22
+ /**
23
+ * Creates a lock file to prevent multiple autopublish instances
24
+ * @returns {boolean} True if lock was acquired, false if already locked
25
+ */
26
+ const acquireLock = () => {
27
+ try {
28
+ if (fs.existsSync(LOCK_FILE)) {
29
+ // Check if the lock is stale (process might have crashed)
30
+ const lockData = JSON.parse(fs.readFileSync(LOCK_FILE, 'utf-8'));
31
+ const lockAge = Date.now() - lockData.timestamp;
32
+
33
+ // If lock is older than 1 hour, consider it stale
34
+ if (lockAge < 3600000) {
35
+ return false;
36
+ }
37
+ }
38
+
39
+ // Create lock file
40
+ fs.mkdirSync(path.dirname(LOCK_FILE), { recursive: true });
41
+ fs.writeFileSync(LOCK_FILE, JSON.stringify({
42
+ pid: process.pid,
43
+ timestamp: Date.now()
44
+ }));
45
+
46
+ return true;
47
+ } catch (err) {
48
+ console.error(chalk.red(`Failed to acquire lock: ${err.message}`));
49
+ return false;
50
+ }
51
+ };
52
+
53
+ /**
54
+ * Releases the lock file
55
+ */
56
+ const releaseLock = () => {
57
+ try {
58
+ if (fs.existsSync(LOCK_FILE)) {
59
+ fs.unlinkSync(LOCK_FILE);
60
+ }
61
+ } catch (err) {
62
+ console.error(chalk.red(`Failed to release lock: ${err.message}`));
63
+ }
64
+ };
65
+
66
+ /**
67
+ * Formats multi-line errors for display
68
+ */
69
+ const formatMultilineError = (err) => {
70
+ const lines = String(err).split(/\r?\n/);
71
+ return lines
72
+ .map((line, i) => {
73
+ const prefix = i === 0 ? `${chalk.redBright(' •')} ` : ' ';
74
+ return prefix + chalk.whiteBright(line);
75
+ })
76
+ .join('\n');
77
+ };
78
+
79
+ /**
80
+ * Gets the folder path from a file path relative to EXPORT_ROOT
81
+ */
82
+ const getFolderFromPath = (filePath) => {
83
+ const fileFolderPath = filePath
84
+ .split("/")
85
+ .filter((x, i) => !(x === EXPORT_ROOT && i === 0))
86
+ .slice(0, -1)
87
+ .join("/");
88
+
89
+ return fileFolderPath;
90
+ };
91
+
92
+ /**
93
+ * Infers metadata from file path
94
+ */
95
+ const inferMeta = (filePath) => {
96
+ const parentFolder = path.basename(path.dirname(filePath));
97
+ const type = Object.keys(TYPE_DIR_MAP).find(
98
+ (k) => TYPE_DIR_MAP[k].directory === parentFolder
99
+ );
100
+ return {
101
+ type,
102
+ entity: ENTITY_TYPE_MAP[type],
103
+ contentField: ENTITY_FIELD_MAP[type],
104
+ };
105
+ };
106
+
107
+ /**
108
+ * Safely reads file content and hash
109
+ */
110
+ const readFileSafe = (filePath) => {
111
+ try {
112
+ const content = fs.readFileSync(filePath, "utf-8");
113
+ return { content, hash: sha256(content) };
114
+ } catch (err) {
115
+ return null;
116
+ }
117
+ };
118
+
119
+ /**
120
+ * Checks if file is within Contents/Assets (static asset)
121
+ */
122
+ const isStaticAsset = (filePath) => {
123
+ const normalizedPath = path.normalize(filePath);
124
+ return normalizedPath.includes(path.join('Contents', 'Assets'));
125
+ };
126
+
127
+ /**
128
+ * Processes a batch of pending changes
129
+ */
130
+ const processPendingChanges = async () => {
131
+ if (isProcessing || pendingChanges.size === 0) {
132
+ return;
133
+ }
134
+
135
+ isProcessing = true;
136
+
137
+ // Copy pending changes and clear the queue
138
+ const changesToProcess = new Map(pendingChanges);
139
+ pendingChanges.clear();
140
+
141
+ // Clear console and show header
142
+ process.stdout.write('\x1Bc');
143
+ console.log(chalk.cyan.bold('🔄 Auto-Publishing Changes...'));
144
+ console.log(chalk.gray('─'.repeat(48)));
145
+ console.log();
146
+
147
+ const results = [];
148
+
149
+ for (const [filePath, operation] of changesToProcess.entries()) {
150
+ try {
151
+ let result;
152
+
153
+ switch (operation) {
154
+ case 'create':
155
+ result = await handleCreate(filePath);
156
+ break;
157
+ case 'update':
158
+ result = await handleUpdate(filePath);
159
+ break;
160
+ case 'delete':
161
+ result = await handleDelete(filePath);
162
+ break;
163
+ }
164
+
165
+ results.push({ filePath, operation, result, success: true });
166
+ } catch (error) {
167
+ results.push({ filePath, operation, error: error.message, success: false });
168
+ }
169
+ }
170
+
171
+ // Display results
172
+ let successCount = 0;
173
+ let errorCount = 0;
174
+
175
+ for (const result of results) {
176
+ if (result.success) {
177
+ successCount++;
178
+ console.log(
179
+ chalk.green('✓') +
180
+ ` ${chalk.yellow(result.operation.toUpperCase())} ${chalk.cyan(path.basename(result.filePath))}`
181
+ );
182
+ } else {
183
+ errorCount++;
184
+ console.log();
185
+ console.log(chalk.bgRed.bold.white(' ✖ Operation Failed '));
186
+ console.log(chalk.redBright('─'.repeat(48)));
187
+ console.log(chalk.red.bold(`${result.operation.toUpperCase()} ${result.filePath}:`));
188
+ console.log(formatMultilineError(result.error));
189
+ console.log(chalk.redBright('─'.repeat(48)));
190
+ }
191
+ }
192
+
193
+ // Summary
194
+ console.log();
195
+ console.log(chalk.gray('─'.repeat(48)));
196
+ if (errorCount === 0) {
197
+ console.log(chalk.green(`✓ ${successCount} change(s) published successfully`));
198
+ } else {
199
+ console.log(chalk.green(`✓ Successful: ${successCount}`) + ' ' + chalk.red(`✗ Failed: ${errorCount}`));
200
+ }
201
+ console.log(chalk.gray('─'.repeat(48)));
202
+ console.log(chalk.cyan('Watching for file changes...'));
203
+ console.log();
204
+
205
+ isProcessing = false;
206
+ };
207
+
208
+ /**
209
+ * Handles file creation
210
+ */
211
+ const handleCreate = async (filePath) => {
212
+ // Check if this file already has a tag (created via magentrix create)
213
+ const existingTag = await getFileTag(filePath);
214
+ if (existingTag) {
215
+ // File was already created via CLI, treat as update instead
216
+ return await handleUpdate(filePath);
217
+ }
218
+
219
+ const safe = readFileSafe(filePath);
220
+ if (!safe) throw new Error('Unable to read file');
221
+
222
+ const { content, hash } = safe;
223
+
224
+ // Check if it's a static asset
225
+ if (isStaticAsset(filePath)) {
226
+ const folder = getFolderFromPath(filePath);
227
+ await uploadAsset(
228
+ credentials.instanceUrl,
229
+ credentials.token.value,
230
+ `/${folder}`,
231
+ [filePath]
232
+ );
233
+
234
+ updateBase(filePath, { Id: filePath, Type: "File" });
235
+ return { recordId: filePath };
236
+ }
237
+
238
+ // It's a code file
239
+ const { type, entity, contentField } = inferMeta(filePath);
240
+
241
+ const data = {
242
+ Name: path.basename(filePath, path.extname(filePath)),
243
+ Type: type,
244
+ [contentField]: content
245
+ };
246
+
247
+ const result = await createEntity(credentials.instanceUrl, credentials.token.value, entity, data);
248
+ const recordId = result.recordId || result.id;
249
+
250
+ // Tag the file and update base
251
+ await setFileTag(filePath, recordId);
252
+ updateBase(filePath, { Id: recordId, Type: type });
253
+
254
+ return { recordId };
255
+ };
256
+
257
+ /**
258
+ * Handles file updates
259
+ */
260
+ const handleUpdate = async (filePath) => {
261
+ const safe = readFileSafe(filePath);
262
+ if (!safe) throw new Error('Unable to read file');
263
+
264
+ const { content, hash } = safe;
265
+
266
+ // Check if it's a static asset
267
+ if (isStaticAsset(filePath)) {
268
+ const folder = getFolderFromPath(filePath);
269
+ await uploadAsset(
270
+ credentials.instanceUrl,
271
+ credentials.token.value,
272
+ `/${folder}`,
273
+ [filePath]
274
+ );
275
+
276
+ updateBase(filePath, { Id: filePath, Type: "File" });
277
+ return { updated: true };
278
+ }
279
+
280
+ // Get file tag to find record ID
281
+ const recordId = await getFileTag(filePath);
282
+ if (!recordId) {
283
+ // No record ID found, treat as create
284
+ return await handleCreate(filePath);
285
+ }
286
+
287
+ const { type, entity, contentField } = inferMeta(filePath);
288
+
289
+ const data = {
290
+ [contentField]: content
291
+ };
292
+
293
+ await updateEntity(credentials.instanceUrl, credentials.token.value, entity, recordId, data);
294
+ updateBase(filePath, { Id: recordId, Type: type });
295
+
296
+ return { recordId };
297
+ };
298
+
299
+ /**
300
+ * Handles file deletion
301
+ */
302
+ const handleDelete = async (filePath) => {
303
+ // Check if it's a static asset
304
+ if (isStaticAsset(filePath)) {
305
+ const folder = getFolderFromPath(filePath);
306
+ const fileName = path.basename(filePath);
307
+
308
+ await deleteAsset(
309
+ credentials.instanceUrl,
310
+ credentials.token.value,
311
+ `/${folder}`,
312
+ [fileName]
313
+ );
314
+
315
+ removeFromBase(filePath);
316
+ return { deleted: true };
317
+ }
318
+
319
+ // Get file tag to find record ID
320
+ const recordId = await getFileTag(filePath);
321
+ if (!recordId) {
322
+ // No record ID found, nothing to delete remotely
323
+ return { skipped: true };
324
+ }
325
+
326
+ const { entity } = inferMeta(filePath);
327
+
328
+ await deleteEntity(credentials.instanceUrl, credentials.token.value, entity, recordId);
329
+ removeFromBase(recordId);
330
+
331
+ return { recordId };
332
+ };
333
+
334
+ /**
335
+ * Debounce timer for batching changes
336
+ */
337
+ let debounceTimer = null;
338
+ const DEBOUNCE_DELAY = 1000; // 1 second
339
+
340
+ /**
341
+ * Queue a file change for processing
342
+ */
343
+ const queueChange = (filePath, operation) => {
344
+ // Normalize path
345
+ const normalizedPath = path.normalize(filePath);
346
+
347
+ // Skip dotfiles and the .magentrix directory
348
+ if (normalizedPath.includes(path.sep + '.') || normalizedPath.includes('.magentrix')) {
349
+ return;
350
+ }
351
+
352
+ // Add to pending changes
353
+ pendingChanges.set(normalizedPath, operation);
354
+
355
+ // Reset debounce timer
356
+ if (debounceTimer) {
357
+ clearTimeout(debounceTimer);
358
+ }
359
+
360
+ debounceTimer = setTimeout(() => {
361
+ processPendingChanges();
362
+ }, DEBOUNCE_DELAY);
363
+ };
364
+
365
+ /**
366
+ * File watcher event handlers
367
+ */
368
+ const onAdd = (filePath) => {
369
+ queueChange(filePath, 'create');
370
+ };
371
+
372
+ const onChange = (filePath) => {
373
+ queueChange(filePath, 'update');
374
+ };
375
+
376
+ const onUnlink = (filePath) => {
377
+ queueChange(filePath, 'delete');
378
+ };
379
+
380
+ /**
381
+ * Starts the file watcher
382
+ */
383
+ const startWatcher = () => {
384
+ console.log(chalk.cyan.bold('🔄 Auto-Publish Mode Activated'));
385
+ console.log(chalk.gray('─'.repeat(48)));
386
+ console.log(chalk.green('Watching for file changes...'));
387
+ console.log(chalk.gray('Press Ctrl+C to stop'));
388
+ console.log();
389
+
390
+ const watchPath = path.join(process.cwd(), EXPORT_ROOT);
391
+
392
+ const watcher = chokidar.watch(watchPath, {
393
+ ignored: /(^|[\/\\])\../, // Ignore dotfiles
394
+ persistent: true,
395
+ ignoreInitial: true, // Don't fire events for existing files
396
+ awaitWriteFinish: {
397
+ stabilityThreshold: 300,
398
+ pollInterval: 100
399
+ }
400
+ });
401
+
402
+ watcher
403
+ .on('add', onAdd)
404
+ .on('change', onChange)
405
+ .on('unlink', onUnlink);
406
+
407
+ // Handle cleanup on exit
408
+ const cleanup = () => {
409
+ console.log(chalk.yellow('\n\nStopping auto-publish...'));
410
+ watcher.close();
411
+ releaseLock();
412
+ process.exit(0);
413
+ };
414
+
415
+ process.on('SIGINT', cleanup);
416
+ process.on('SIGTERM', cleanup);
417
+ };
418
+
419
+ /**
420
+ * Main autopublish function
421
+ */
422
+ export const autoPublish = async () => {
423
+ process.stdout.write('\x1Bc');
424
+
425
+ // Check for existing lock
426
+ if (!acquireLock()) {
427
+ console.log(chalk.bgRed.bold.white(' ✖ Already Running '));
428
+ console.log(chalk.redBright('─'.repeat(48)));
429
+ console.log(chalk.red('Another instance of autopublish is already running.'));
430
+ console.log(chalk.gray('Only one autopublish instance can run at a time.'));
431
+ console.log(chalk.redBright('─'.repeat(48)));
432
+ console.log();
433
+ console.log(chalk.yellow('If you believe this is an error, delete the lock file:'));
434
+ console.log(chalk.cyan(LOCK_FILE));
435
+ process.exit(1);
436
+ }
437
+
438
+ // Authenticate
439
+ credentials = await withSpinner('Authenticating...', async () => {
440
+ return await ensureValidCredentials();
441
+ });
442
+
443
+ console.log();
444
+
445
+ // Start watching
446
+ startWatcher();
447
+ };