@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.
- package/LICENSE +25 -0
- package/README.md +471 -0
- package/actions/autopublish.js +283 -0
- package/actions/autopublish.old.js +293 -0
- package/actions/autopublish.v2.js +447 -0
- package/actions/create.js +329 -0
- package/actions/help.js +165 -0
- package/actions/main.js +81 -0
- package/actions/publish.js +567 -0
- package/actions/pull.js +139 -0
- package/actions/setup.js +61 -0
- package/actions/status.js +17 -0
- package/bin/magentrix.js +159 -0
- package/package.json +61 -0
- package/utils/cacher.js +112 -0
- package/utils/cli/checkInstanceUrl.js +29 -0
- package/utils/cli/helpers/compare.js +281 -0
- package/utils/cli/helpers/ensureApiKey.js +57 -0
- package/utils/cli/helpers/ensureCredentials.js +60 -0
- package/utils/cli/helpers/ensureInstanceUrl.js +63 -0
- package/utils/cli/writeRecords.js +223 -0
- package/utils/compare.js +135 -0
- package/utils/compress.js +18 -0
- package/utils/config.js +451 -0
- package/utils/diff.js +49 -0
- package/utils/downloadAssets.js +75 -0
- package/utils/filetag.js +115 -0
- package/utils/hash.js +14 -0
- package/utils/magentrix/api/assets.js +145 -0
- package/utils/magentrix/api/auth.js +56 -0
- package/utils/magentrix/api/createEntity.js +61 -0
- package/utils/magentrix/api/deleteEntity.js +55 -0
- package/utils/magentrix/api/meqlQuery.js +31 -0
- package/utils/magentrix/api/retrieveEntity.js +32 -0
- package/utils/magentrix/api/updateEntity.js +66 -0
- package/utils/magentrix/fetch.js +154 -0
- package/utils/merge.js +22 -0
- package/utils/preferences.js +40 -0
- package/utils/spinner.js +43 -0
- package/utils/template.js +52 -0
- package/utils/updateFileBase.js +103 -0
- package/vars/config.js +1 -0
- 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
|
+
};
|