@magentrix-corp/magentrix-cli 1.3.12 → 1.3.14
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/actions/iris/buildStage.js +22 -3
- package/actions/publish.js +32 -14
- package/actions/pull.js +41 -22
- package/bin/magentrix.js +18 -4
- package/package.json +1 -1
- package/utils/cacher.js +8 -0
- package/utils/cli/helpers/ensureCredentials.js +8 -0
- package/utils/debug.js +144 -0
- package/utils/iris/zipper.js +10 -1
- package/utils/magentrix/api/auth.js +12 -0
- package/utils/magentrix/api/iris.js +7 -5
- package/utils/magentrix/api/meqlQuery.js +5 -0
- package/utils/magentrix/fetch.js +11 -1
|
@@ -435,11 +435,23 @@ async function buildFromVueProject(options) {
|
|
|
435
435
|
// This ensures pull doesn't overwrite our staged files
|
|
436
436
|
console.log();
|
|
437
437
|
console.log(chalk.gray('Checking workspace sync status...'));
|
|
438
|
+
|
|
439
|
+
// Check for a previous incomplete pull first — `magentrix status` only checks
|
|
440
|
+
// code entities, so a partial pull that synced code but not assets would falsely
|
|
441
|
+
// report "in sync". The marker file catches this case.
|
|
442
|
+
const workspaceConfig = new Config({ projectDir: workspacePath });
|
|
443
|
+
const hadIncompletePull = workspaceConfig.read('pullIncomplete', { global: false, filename: 'config.json' });
|
|
444
|
+
|
|
438
445
|
const syncStatus = await checkWorkspaceSyncStatus(workspacePath);
|
|
446
|
+
const needsPull = syncStatus.needsPull || !!hadIncompletePull;
|
|
439
447
|
|
|
440
|
-
if (
|
|
448
|
+
if (needsPull) {
|
|
441
449
|
console.log();
|
|
442
|
-
|
|
450
|
+
if (hadIncompletePull && !syncStatus.needsPull) {
|
|
451
|
+
console.log(chalk.yellow('⚠ A previous pull did not complete. Your workspace may be out of sync.'));
|
|
452
|
+
} else {
|
|
453
|
+
console.log(chalk.yellow('⚠ Your workspace may be out of sync with the server.'));
|
|
454
|
+
}
|
|
443
455
|
|
|
444
456
|
const shouldPull = await confirm({
|
|
445
457
|
message: 'Would you like to pull latest changes first?',
|
|
@@ -451,9 +463,16 @@ async function buildFromVueProject(options) {
|
|
|
451
463
|
console.log(chalk.blue('Running pull from workspace...'));
|
|
452
464
|
console.log();
|
|
453
465
|
|
|
466
|
+
// Mark pull as in-progress before starting
|
|
467
|
+
workspaceConfig.save('pullIncomplete', true, { global: false, filename: 'config.json' });
|
|
468
|
+
|
|
454
469
|
const pullSuccess = await runCommandFromWorkspace(workspacePath, 'pull');
|
|
455
470
|
|
|
456
|
-
if (
|
|
471
|
+
if (pullSuccess) {
|
|
472
|
+
// Pull completed successfully — clear the marker
|
|
473
|
+
workspaceConfig.removeKey('pullIncomplete', { filename: 'config.json' });
|
|
474
|
+
} else {
|
|
475
|
+
// Pull failed or was cancelled — marker stays for next run
|
|
457
476
|
console.log();
|
|
458
477
|
console.log(chalk.yellow('Pull encountered issues. You may want to resolve them manually.'));
|
|
459
478
|
|
package/actions/publish.js
CHANGED
|
@@ -199,9 +199,14 @@ const handleDeleteStaticAssetAction = async (instanceUrl, apiKey, action) => {
|
|
|
199
199
|
|
|
200
200
|
if (isNotFound) {
|
|
201
201
|
// Clean up base.json since file doesn't exist on server
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
removeFromBase(
|
|
202
|
+
// Use the original base.json key if available (avoids path format mismatches)
|
|
203
|
+
if (action.baseKey) {
|
|
204
|
+
removeFromBase(action.baseKey);
|
|
205
|
+
} else {
|
|
206
|
+
for (const name of action.names) {
|
|
207
|
+
const filePath = action.filePath || `${EXPORT_ROOT}/${action.folder}/${name}`;
|
|
208
|
+
removeFromBase(filePath);
|
|
209
|
+
}
|
|
205
210
|
}
|
|
206
211
|
return { cleanedFromCache: true };
|
|
207
212
|
}
|
|
@@ -253,7 +258,8 @@ const handleDeleteFolderAction = async (instanceUrl, apiKey, action) => {
|
|
|
253
258
|
|
|
254
259
|
if (isNotFound) {
|
|
255
260
|
// Clean up base.json since folder doesn't exist on server
|
|
256
|
-
|
|
261
|
+
// Use original base.json key if available (avoids path format mismatches)
|
|
262
|
+
removeFromBase(action.baseKey || action.folderPath);
|
|
257
263
|
|
|
258
264
|
// Also remove all files and subfolders inside this folder from base
|
|
259
265
|
const hits = await config.searchObject({}, { filename: "base.json", global: false });
|
|
@@ -289,14 +295,19 @@ const handlePublishIrisAppAction = async (instanceUrl, apiKey, action) => {
|
|
|
289
295
|
// Create zip from the app folder
|
|
290
296
|
const zipBuffer = await createIrisZip(action.appPath, action.slug);
|
|
291
297
|
|
|
292
|
-
//
|
|
298
|
+
// For updates, don't send app-name/description/icon to avoid triggering
|
|
299
|
+
// metadata modification that may require permissions the user doesn't have.
|
|
300
|
+
// The server already has the correct metadata from the original create.
|
|
301
|
+
// For creates, send all metadata so the app is properly registered.
|
|
302
|
+
const isUpdate = action.action === 'update_iris_app';
|
|
303
|
+
|
|
293
304
|
const response = await publishApp(
|
|
294
305
|
instanceUrl,
|
|
295
306
|
apiKey,
|
|
296
307
|
zipBuffer,
|
|
297
308
|
`${action.slug}.zip`,
|
|
298
|
-
action.appName,
|
|
299
|
-
{
|
|
309
|
+
isUpdate ? null : action.appName,
|
|
310
|
+
isUpdate ? {} : {
|
|
300
311
|
appDescription: action.appDescription,
|
|
301
312
|
appIconId: action.appIconId
|
|
302
313
|
}
|
|
@@ -425,9 +436,14 @@ const updateCacheAfterSuccess = async (action, operationResult) => {
|
|
|
425
436
|
case "delete_static_asset":
|
|
426
437
|
// Skip if already cleaned from cache during 404 handling
|
|
427
438
|
if (!operationResult?.cleanedFromCache) {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
removeFromBase(
|
|
439
|
+
// Use the original base.json key if available (avoids path format mismatches)
|
|
440
|
+
if (action.baseKey) {
|
|
441
|
+
removeFromBase(action.baseKey);
|
|
442
|
+
} else {
|
|
443
|
+
for (const name of action.names) {
|
|
444
|
+
const filePath = action.filePath || `${EXPORT_ROOT}/${action.folder}/${name}`;
|
|
445
|
+
removeFromBase(filePath);
|
|
446
|
+
}
|
|
431
447
|
}
|
|
432
448
|
}
|
|
433
449
|
break;
|
|
@@ -443,8 +459,8 @@ const updateCacheAfterSuccess = async (action, operationResult) => {
|
|
|
443
459
|
case "delete_folder": {
|
|
444
460
|
// Skip if already cleaned from cache during 404 handling
|
|
445
461
|
if (!operationResult?.cleanedFromCache) {
|
|
446
|
-
// Remove the folder itself from base using
|
|
447
|
-
removeFromBase(action.folderPath);
|
|
462
|
+
// Remove the folder itself from base using the original base.json key
|
|
463
|
+
removeFromBase(action.baseKey || action.folderPath);
|
|
448
464
|
|
|
449
465
|
// Also remove all files and subfolders inside this folder from base
|
|
450
466
|
const hits = await config.searchObject({}, { filename: "base.json", global: false });
|
|
@@ -1029,7 +1045,8 @@ export const runPublish = async (options = {}) => {
|
|
|
1029
1045
|
action: "delete_folder",
|
|
1030
1046
|
folderPath: cachedPath,
|
|
1031
1047
|
parentPath: toApiFolderPath(parentDir),
|
|
1032
|
-
folderName: path.basename(cachedPath)
|
|
1048
|
+
folderName: path.basename(cachedPath),
|
|
1049
|
+
baseKey: cachedFolder.tag // The original base.json key for correct cache cleanup
|
|
1033
1050
|
});
|
|
1034
1051
|
}
|
|
1035
1052
|
}
|
|
@@ -1067,7 +1084,8 @@ export const runPublish = async (options = {}) => {
|
|
|
1067
1084
|
action: 'delete_static_asset',
|
|
1068
1085
|
folder: toApiPath(actualPath),
|
|
1069
1086
|
names: [path.basename(actualPath)],
|
|
1070
|
-
filePath: actualPath // Store actual file path for filtering
|
|
1087
|
+
filePath: actualPath, // Store actual file path for filtering
|
|
1088
|
+
baseKey: id // The original base.json key for correct cache cleanup
|
|
1071
1089
|
});
|
|
1072
1090
|
continue;
|
|
1073
1091
|
}
|
package/actions/pull.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ensureValidCredentials } from "../utils/cli/helpers/ensureCredentials.js";
|
|
2
|
+
import debug from '../utils/debug.js';
|
|
2
3
|
import Config from "../utils/config.js";
|
|
3
4
|
import { meqlQuery } from "../utils/magentrix/api/meqlQuery.js";
|
|
4
5
|
import fs from "fs";
|
|
@@ -63,7 +64,9 @@ export const pull = async () => {
|
|
|
63
64
|
try {
|
|
64
65
|
// Step 1: Authenticate and retrieve instance URL and token
|
|
65
66
|
progress.startStep('auth');
|
|
67
|
+
debug.log('PULL', 'Step 1: Authenticating...');
|
|
66
68
|
const { instanceUrl, token } = await ensureValidCredentials();
|
|
69
|
+
debug.log('PULL', `Authenticated with instance: ${instanceUrl}`);
|
|
67
70
|
progress.completeStep('auth', '✓ Authenticated');
|
|
68
71
|
|
|
69
72
|
// Step 2: Check if instance URL has changed (credential switch detected)
|
|
@@ -128,13 +131,18 @@ export const pull = async () => {
|
|
|
128
131
|
progress.startStep('load');
|
|
129
132
|
const hits = await config.searchObject({}, { filename: "base.json", global: false });
|
|
130
133
|
const cachedResults = hits?.[0]?.value || {};
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
+
// Include the base.json key (_baseKey) alongside each entry so we can
|
|
135
|
+
// use the correct key for removeFromBaseBulk later. The keys may differ
|
|
136
|
+
// from lastKnownActualPath/filePath due to path format inconsistencies.
|
|
137
|
+
const cachedAssets = Object.entries(cachedResults)
|
|
138
|
+
.filter(([, c]) => c.type === 'File' || c.type === 'Folder')
|
|
139
|
+
.map(([key, c]) => ({ ...c, _baseKey: key }));
|
|
134
140
|
progress.completeStep('load', `✓ Loaded ${Object.keys(cachedResults).length} cached entries`);
|
|
135
141
|
|
|
136
142
|
// Step 4a: Download code entities (ActiveClass and ActivePage)
|
|
137
143
|
progress.startStep('download-code');
|
|
144
|
+
debug.log('PULL', 'Step 4a: Downloading code entities...');
|
|
145
|
+
debug.log('PULL', `Queries: ${queries.map(q => q.query).join(' | ')}`);
|
|
138
146
|
|
|
139
147
|
let activeClassResult, activePageResult;
|
|
140
148
|
const codeDownloadErrors = [];
|
|
@@ -185,6 +193,7 @@ export const pull = async () => {
|
|
|
185
193
|
let assets;
|
|
186
194
|
const assetDownloadErrors = [];
|
|
187
195
|
|
|
196
|
+
debug.log('PULL', 'Step 4b: Downloading static assets...');
|
|
188
197
|
try {
|
|
189
198
|
logger.info('Starting static asset downloads');
|
|
190
199
|
assets = await downloadAssets(instanceUrl, token.value, null, (current, total, message) => {
|
|
@@ -338,7 +347,7 @@ export const pull = async () => {
|
|
|
338
347
|
|
|
339
348
|
// If this asset was in base.json but not returned from server, it was deleted
|
|
340
349
|
if (!serverAssetPaths.has(cachedPath)) {
|
|
341
|
-
assetsToDelete.push(cachedPath);
|
|
350
|
+
assetsToDelete.push({ path: cachedPath, baseKey: cached._baseKey });
|
|
342
351
|
}
|
|
343
352
|
|
|
344
353
|
processedCount++;
|
|
@@ -402,33 +411,35 @@ export const pull = async () => {
|
|
|
402
411
|
// Delete local files/folders that were deleted on server
|
|
403
412
|
logger.info(`Starting deletion of ${assetsToDelete.length} assets`);
|
|
404
413
|
const deletionLogs = [];
|
|
405
|
-
for
|
|
414
|
+
// Track base.json keys for entries that need removal
|
|
415
|
+
const assetBaseKeysToRemove = [];
|
|
416
|
+
for (const asset of assetsToDelete) {
|
|
406
417
|
try {
|
|
407
|
-
if (fs.existsSync(
|
|
408
|
-
const stats = fs.statSync(
|
|
418
|
+
if (fs.existsSync(asset.path)) {
|
|
419
|
+
const stats = fs.statSync(asset.path);
|
|
409
420
|
if (stats.isDirectory()) {
|
|
410
|
-
fs.rmSync(
|
|
411
|
-
deletionLogs.push({ type: 'folder', path:
|
|
412
|
-
logger.info('Deleted folder', { path:
|
|
421
|
+
fs.rmSync(asset.path, { recursive: true, force: true });
|
|
422
|
+
deletionLogs.push({ type: 'folder', path: asset.path });
|
|
423
|
+
logger.info('Deleted folder', { path: asset.path });
|
|
413
424
|
} else {
|
|
414
|
-
fs.unlinkSync(
|
|
415
|
-
deletionLogs.push({ type: 'file', path:
|
|
416
|
-
logger.info('Deleted file', { path:
|
|
425
|
+
fs.unlinkSync(asset.path);
|
|
426
|
+
deletionLogs.push({ type: 'file', path: asset.path });
|
|
427
|
+
logger.info('Deleted file', { path: asset.path });
|
|
417
428
|
}
|
|
418
429
|
}
|
|
430
|
+
// Always remove from base.json regardless of whether the local file
|
|
431
|
+
// existed — the server confirmed deletion, so the cache entry is stale.
|
|
432
|
+
assetBaseKeysToRemove.push(asset.baseKey);
|
|
419
433
|
} catch (err) {
|
|
420
|
-
deletionLogs.push({ type: 'error', path:
|
|
421
|
-
logger.error(`Failed to delete asset: ${
|
|
434
|
+
deletionLogs.push({ type: 'error', path: asset.path, error: err.message });
|
|
435
|
+
logger.error(`Failed to delete asset: ${asset.path}`, err);
|
|
422
436
|
}
|
|
423
437
|
}
|
|
424
438
|
|
|
425
|
-
// Bulk remove from base.json
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
if (assetPathsToRemove.length > 0) {
|
|
431
|
-
removeFromBaseBulk(assetPathsToRemove);
|
|
439
|
+
// Bulk remove from base.json using the original base.json keys
|
|
440
|
+
// (not the normalized paths, which may have a different format)
|
|
441
|
+
if (assetBaseKeysToRemove.length > 0) {
|
|
442
|
+
removeFromBaseBulk(assetBaseKeysToRemove);
|
|
432
443
|
}
|
|
433
444
|
|
|
434
445
|
// Delete local code entity files that were deleted on server
|
|
@@ -637,8 +648,14 @@ export const pull = async () => {
|
|
|
637
648
|
// Step 7: Finish progress tracker
|
|
638
649
|
logger.info('Pull completed successfully');
|
|
639
650
|
logger.close();
|
|
651
|
+
debug.log('PULL', 'Pull completed successfully');
|
|
652
|
+
debug.close();
|
|
640
653
|
progress.finish('Pull completed successfully!');
|
|
641
654
|
|
|
655
|
+
// Clear any incomplete-pull marker (set by vue-run-build when a pull is
|
|
656
|
+
// started but cancelled/fails). A successful pull means we're fully synced.
|
|
657
|
+
config.removeKey('pullIncomplete', { filename: 'config.json' });
|
|
658
|
+
|
|
642
659
|
// Summary
|
|
643
660
|
console.log(chalk.bold(`Summary:`));
|
|
644
661
|
console.log(` • ${activeClassResult.Records.length} ActiveClass records`);
|
|
@@ -654,6 +671,8 @@ export const pull = async () => {
|
|
|
654
671
|
} catch (error) {
|
|
655
672
|
logger.error('Pull operation failed', error);
|
|
656
673
|
logger.close();
|
|
674
|
+
debug.log('PULL', `Pull failed: ${error.message}`, error.stack);
|
|
675
|
+
debug.close();
|
|
657
676
|
progress.abort(error.message);
|
|
658
677
|
|
|
659
678
|
// Display log summary even on failure
|
package/bin/magentrix.js
CHANGED
|
@@ -21,9 +21,17 @@ import { irisLink, irisDev, irisDelete, irisRecover, vueBuildStage } from '../ac
|
|
|
21
21
|
import Config from '../utils/config.js';
|
|
22
22
|
import { registerWorkspace, getRegisteredWorkspaces } from '../utils/workspaces.js';
|
|
23
23
|
import { ensureVSCodeFileAssociation } from '../utils/preferences.js';
|
|
24
|
+
import debug from '../utils/debug.js';
|
|
24
25
|
|
|
25
26
|
const config = new Config();
|
|
26
27
|
|
|
28
|
+
// ── Debug Mode ───────────────────────────────
|
|
29
|
+
// Check early (before Commander parses) so debug logging covers the full lifecycle
|
|
30
|
+
if (process.argv.includes('--debug') || process.env.DEBUG === 'true') {
|
|
31
|
+
debug.enable();
|
|
32
|
+
debug.env();
|
|
33
|
+
}
|
|
34
|
+
|
|
27
35
|
// ── Vue Project Detection ────────────────────────────────
|
|
28
36
|
/**
|
|
29
37
|
* Check if current directory is a Vue project (has config.ts)
|
|
@@ -134,6 +142,7 @@ program
|
|
|
134
142
|
.name('magentrix')
|
|
135
143
|
.description('Manage Magentrix assets and automation')
|
|
136
144
|
.version(VERSION)
|
|
145
|
+
.option('--debug', 'Enable debug logging')
|
|
137
146
|
.configureHelp({
|
|
138
147
|
formatHelp: (_cmd, _helper) => {
|
|
139
148
|
const divider = chalk.gray('━'.repeat(60));
|
|
@@ -173,6 +182,7 @@ program
|
|
|
173
182
|
|
|
174
183
|
help += `\n${chalk.bold.yellow('OPTIONS')}\n`;
|
|
175
184
|
help += ` ${chalk.cyan('-V, --version')} ${chalk.dim('Output the version number')}\n`;
|
|
185
|
+
help += ` ${chalk.cyan('--debug')} ${chalk.dim('Enable debug logging to .magentrix/logs/')}\n`;
|
|
176
186
|
help += ` ${chalk.cyan('-h, --help')} ${chalk.dim('Display this help message')}\n`;
|
|
177
187
|
|
|
178
188
|
help += `\n${chalk.bold.yellow('EXAMPLES')}\n`;
|
|
@@ -365,11 +375,15 @@ function handleFatal(err) {
|
|
|
365
375
|
console.error(`\n${divider}\n${header}`);
|
|
366
376
|
console.error(`${chalk.redBright(err?.message || 'An unexpected error occurred.')}\n`);
|
|
367
377
|
|
|
368
|
-
if (
|
|
369
|
-
|
|
370
|
-
|
|
378
|
+
if (debug.enabled) {
|
|
379
|
+
debug.log('FATAL', err?.message, err?.stack);
|
|
380
|
+
debug.close();
|
|
381
|
+
if (err?.stack) {
|
|
382
|
+
console.error(chalk.dim(err.stack));
|
|
383
|
+
console.error();
|
|
384
|
+
}
|
|
371
385
|
} else {
|
|
372
|
-
console.log(`${chalk.yellow('💡 Run with')} ${chalk.cyan('
|
|
386
|
+
console.log(`${chalk.yellow('💡 Run with')} ${chalk.cyan('--debug')} ${chalk.yellow('for full details.')}`);
|
|
373
387
|
}
|
|
374
388
|
|
|
375
389
|
console.log(divider + '\n');
|
package/package.json
CHANGED
package/utils/cacher.js
CHANGED
|
@@ -8,6 +8,9 @@ import { EXPORT_ROOT, ALLOWED_SRC_DIRS, IRIS_APPS_DIR } from '../vars/global.js'
|
|
|
8
8
|
|
|
9
9
|
const config = new Config();
|
|
10
10
|
|
|
11
|
+
// System/hidden files that should never be synced
|
|
12
|
+
const IGNORED_FILES = new Set(['.DS_Store', 'Thumbs.db', 'desktop.ini']);
|
|
13
|
+
|
|
11
14
|
/**
|
|
12
15
|
* Recursively caches all files in a directory with tagging and content snapshotting
|
|
13
16
|
* OPTIMIZED: Parallel processing + mtime checks for maximum speed
|
|
@@ -179,6 +182,11 @@ export async function walkFiles(dir, settings) {
|
|
|
179
182
|
for (const entry of entries) {
|
|
180
183
|
const fullPath = path.join(dir, entry.name);
|
|
181
184
|
|
|
185
|
+
// Skip OS-generated system files (.DS_Store, Thumbs.db, etc.)
|
|
186
|
+
if (IGNORED_FILES.has(entry.name)) {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
182
190
|
// Check if this path should be ignored
|
|
183
191
|
if (ignore.find(p => fullPath.startsWith(p) || fullPath === p)) {
|
|
184
192
|
continue;
|
|
@@ -2,6 +2,7 @@ import Config from "../../config.js";
|
|
|
2
2
|
import { setup } from "../../../actions/setup.js";
|
|
3
3
|
import { HASHED_CWD } from "../../../vars/global.js";
|
|
4
4
|
import { tryAuthenticate } from "../../magentrix/api/auth.js";
|
|
5
|
+
import debug from '../../debug.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Returns true if the token is present and not expired (60 seconds buffer).
|
|
@@ -31,16 +32,22 @@ export async function ensureValidCredentials() {
|
|
|
31
32
|
const instanceUrl = config.read('instanceUrl', { global: true, pathHash: HASHED_CWD });
|
|
32
33
|
const token = config.read('token', { global: true, pathHash: HASHED_CWD });
|
|
33
34
|
|
|
35
|
+
debug.auth(`Credential check: apiKey=${apiKey ? 'present' : 'missing'}, instanceUrl=${instanceUrl || '(missing)'}`);
|
|
36
|
+
|
|
34
37
|
// If missing API key or URL, prompt/setup immediately
|
|
35
38
|
if (!apiKey || !instanceUrl) {
|
|
39
|
+
debug.auth('Missing credentials, falling back to setup wizard');
|
|
36
40
|
return setup();
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
// If token is present and valid, return immediately
|
|
40
44
|
if (isTokenValid(token)) {
|
|
45
|
+
debug.auth(`Existing token is valid (expires: ${token.validUntil})`);
|
|
41
46
|
return { apiKey, instanceUrl, token };
|
|
42
47
|
}
|
|
43
48
|
|
|
49
|
+
debug.auth(`Token ${token ? `expired (was valid until: ${token.validUntil})` : 'missing'}, refreshing...`);
|
|
50
|
+
|
|
44
51
|
// If we have API key & URL but no valid token, try to refresh
|
|
45
52
|
try {
|
|
46
53
|
const result = await tryAuthenticate(apiKey, instanceUrl);
|
|
@@ -54,6 +61,7 @@ export async function ensureValidCredentials() {
|
|
|
54
61
|
config.save('token', newToken, { global: true, pathHash: HASHED_CWD });
|
|
55
62
|
return { apiKey, instanceUrl, token: newToken };
|
|
56
63
|
} catch (err) {
|
|
64
|
+
debug.auth(`Token refresh failed: ${err.message}, falling back to setup wizard`);
|
|
57
65
|
// Failed to refresh, fall back to prompting the user
|
|
58
66
|
return setup();
|
|
59
67
|
}
|
package/utils/debug.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { VERSION } from '../vars/config.js';
|
|
5
|
+
import { CWD, HASHED_CWD } from '../vars/global.js';
|
|
6
|
+
import Config from './config.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Masks a sensitive string, showing only the first 4 and last 4 characters.
|
|
10
|
+
* @param {string} value
|
|
11
|
+
* @returns {string}
|
|
12
|
+
*/
|
|
13
|
+
function mask(value) {
|
|
14
|
+
if (!value || typeof value !== 'string') return '(empty)';
|
|
15
|
+
if (value.length <= 12) return '****';
|
|
16
|
+
return `${value.slice(0, 4)}...${value.slice(-4)}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Centralized debug logger singleton.
|
|
21
|
+
* When enabled, writes timestamped lines to .magentrix/logs/debug-<timestamp>.log.
|
|
22
|
+
* All methods are no-ops when debug.enabled === false.
|
|
23
|
+
*/
|
|
24
|
+
const debug = {
|
|
25
|
+
enabled: false,
|
|
26
|
+
_logFile: null,
|
|
27
|
+
_startTime: null,
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Enable debug mode and open the log file.
|
|
31
|
+
*/
|
|
32
|
+
enable() {
|
|
33
|
+
this.enabled = true;
|
|
34
|
+
this._startTime = Date.now();
|
|
35
|
+
|
|
36
|
+
const logsDir = path.join(CWD, '.magentrix', 'logs');
|
|
37
|
+
fs.mkdirSync(logsDir, { recursive: true, mode: 0o700 });
|
|
38
|
+
|
|
39
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
40
|
+
this._logFile = path.join(logsDir, `debug-${timestamp}.log`);
|
|
41
|
+
|
|
42
|
+
// Write header
|
|
43
|
+
fs.writeFileSync(this._logFile, `# MagentrixCLI Debug Log\n# Started: ${new Date().toISOString()}\n\n`, { mode: 0o600 });
|
|
44
|
+
console.log(`Debug log: ${path.relative(CWD, this._logFile)}`);
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Write a timestamped line to the debug log.
|
|
49
|
+
* @param {string} label
|
|
50
|
+
* @param {...any} args
|
|
51
|
+
*/
|
|
52
|
+
log(label, ...args) {
|
|
53
|
+
if (!this.enabled || !this._logFile) return;
|
|
54
|
+
const elapsed = Date.now() - this._startTime;
|
|
55
|
+
const ts = `[+${String(elapsed).padStart(6)}ms]`;
|
|
56
|
+
const detail = args.length
|
|
57
|
+
? ' ' + args.map(a => (typeof a === 'object' ? JSON.stringify(a, null, 2) : String(a))).join(' ')
|
|
58
|
+
: '';
|
|
59
|
+
fs.appendFileSync(this._logFile, `${ts} [${label}]${detail}\n`);
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Log an outgoing HTTP request (sanitizes auth tokens/API keys).
|
|
64
|
+
*/
|
|
65
|
+
request(method, url, headers, body) {
|
|
66
|
+
if (!this.enabled) return;
|
|
67
|
+
const safeHeaders = { ...headers };
|
|
68
|
+
if (safeHeaders.Authorization) {
|
|
69
|
+
safeHeaders.Authorization = `Bearer ${mask(safeHeaders.Authorization.replace('Bearer ', ''))}`;
|
|
70
|
+
}
|
|
71
|
+
this.log('HTTP-REQ', `${method} ${url}`);
|
|
72
|
+
this.log('HTTP-REQ', 'Headers:', JSON.stringify(safeHeaders, null, 2));
|
|
73
|
+
if (body !== undefined && body !== null) {
|
|
74
|
+
let bodyStr = typeof body === 'string' ? body : JSON.stringify(body, null, 2);
|
|
75
|
+
// Mask sensitive fields in body
|
|
76
|
+
bodyStr = bodyStr.replace(/"refresh_token"\s*:\s*"([^"]+)"/g, (_, val) => `"refresh_token": "${mask(val)}"`);
|
|
77
|
+
bodyStr = bodyStr.replace(/"apiKey"\s*:\s*"([^"]+)"/g, (_, val) => `"apiKey": "${mask(val)}"`);
|
|
78
|
+
this.log('HTTP-REQ', 'Body:', bodyStr);
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Log an HTTP response including status, headers, and body.
|
|
84
|
+
*/
|
|
85
|
+
response(status, statusText, headers, body) {
|
|
86
|
+
if (!this.enabled) return;
|
|
87
|
+
this.log('HTTP-RES', `${status} ${statusText}`);
|
|
88
|
+
if (headers) {
|
|
89
|
+
const headerObj = {};
|
|
90
|
+
if (typeof headers.forEach === 'function') {
|
|
91
|
+
headers.forEach((value, key) => { headerObj[key] = value; });
|
|
92
|
+
} else if (typeof headers === 'object') {
|
|
93
|
+
Object.assign(headerObj, headers);
|
|
94
|
+
}
|
|
95
|
+
this.log('HTTP-RES', 'Headers:', JSON.stringify(headerObj, null, 2));
|
|
96
|
+
}
|
|
97
|
+
if (body !== undefined && body !== null) {
|
|
98
|
+
let bodyStr = typeof body === 'string' ? body : JSON.stringify(body, null, 2);
|
|
99
|
+
// Mask tokens in response
|
|
100
|
+
bodyStr = bodyStr.replace(/"token"\s*:\s*"([^"]+)"/g, (_, val) => `"token": "${mask(val)}"`);
|
|
101
|
+
this.log('HTTP-RES', 'Body:', bodyStr);
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Log environment information.
|
|
107
|
+
*/
|
|
108
|
+
env() {
|
|
109
|
+
if (!this.enabled) return;
|
|
110
|
+
const config = new Config();
|
|
111
|
+
const instanceUrl = config.read('instanceUrl', { global: true, pathHash: HASHED_CWD }) || '(not set)';
|
|
112
|
+
|
|
113
|
+
this.log('ENV', 'Node version:', process.version);
|
|
114
|
+
this.log('ENV', 'OS:', `${os.type()} ${os.release()} (${os.arch()})`);
|
|
115
|
+
this.log('ENV', 'CLI version:', VERSION);
|
|
116
|
+
this.log('ENV', 'CWD:', CWD);
|
|
117
|
+
this.log('ENV', 'Hashed CWD:', HASHED_CWD);
|
|
118
|
+
this.log('ENV', 'Instance URL:', instanceUrl);
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Auth-specific debug logging.
|
|
123
|
+
*/
|
|
124
|
+
auth(message, details) {
|
|
125
|
+
if (!this.enabled) return;
|
|
126
|
+
if (details) {
|
|
127
|
+
this.log('AUTH', message, details);
|
|
128
|
+
} else {
|
|
129
|
+
this.log('AUTH', message);
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Finalize the debug log and print its path.
|
|
135
|
+
*/
|
|
136
|
+
close() {
|
|
137
|
+
if (!this.enabled || !this._logFile) return;
|
|
138
|
+
const elapsed = Date.now() - this._startTime;
|
|
139
|
+
this.log('END', `Debug session ended. Total time: ${elapsed}ms`);
|
|
140
|
+
console.log(`\nDebug log saved: ${path.relative(CWD, this._logFile)}`);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export default debug;
|
package/utils/iris/zipper.js
CHANGED
|
@@ -5,6 +5,9 @@ import { randomUUID, createHash } from 'node:crypto';
|
|
|
5
5
|
import archiver from 'archiver';
|
|
6
6
|
import extractZip from 'extract-zip';
|
|
7
7
|
|
|
8
|
+
// System/hidden files that should never be included in IRIS app zips or hashes
|
|
9
|
+
const IGNORED_FILES = new Set(['.DS_Store', 'Thumbs.db', 'desktop.ini']);
|
|
10
|
+
|
|
8
11
|
/**
|
|
9
12
|
* Recursively fix permissions on extracted files.
|
|
10
13
|
* Sets directories to 0o755 and files to 0o644.
|
|
@@ -61,7 +64,10 @@ export async function createIrisZip(distPath, appSlug) {
|
|
|
61
64
|
|
|
62
65
|
// Add the dist directory contents under the app slug folder
|
|
63
66
|
// This creates the structure: appSlug/remoteEntry.js, appSlug/assets/...
|
|
64
|
-
|
|
67
|
+
// Filter out OS-generated system files (.DS_Store, Thumbs.db, etc.)
|
|
68
|
+
archive.directory(distPath, appSlug, (entry) =>
|
|
69
|
+
IGNORED_FILES.has(basename(entry.name)) ? false : entry
|
|
70
|
+
);
|
|
65
71
|
|
|
66
72
|
archive.finalize();
|
|
67
73
|
});
|
|
@@ -195,6 +201,9 @@ export function getFilesRecursive(dir, basePath = dir) {
|
|
|
195
201
|
function walk(currentDir) {
|
|
196
202
|
const entries = readdirSync(currentDir);
|
|
197
203
|
for (const entry of entries) {
|
|
204
|
+
// Skip OS-generated system files
|
|
205
|
+
if (IGNORED_FILES.has(entry)) continue;
|
|
206
|
+
|
|
198
207
|
const fullPath = join(currentDir, entry);
|
|
199
208
|
const stat = statSync(fullPath);
|
|
200
209
|
if (stat.isDirectory()) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { fetchMagentrix } from "../fetch.js";
|
|
2
2
|
import chalk from "chalk";
|
|
3
|
+
import debug from '../../debug.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Authenticates with Magentrix and retrieves an access token using the API key as a refresh token.
|
|
@@ -11,6 +12,7 @@ import chalk from "chalk";
|
|
|
11
12
|
* @returns {Promise<string>} Resolves to the Magentrix access token string.
|
|
12
13
|
*/
|
|
13
14
|
export const getAccessToken = async (apiKey, instanceUrl) => {
|
|
15
|
+
debug.auth(`Requesting access token from ${instanceUrl}/api/3.0/token`);
|
|
14
16
|
try {
|
|
15
17
|
const data = await fetchMagentrix({
|
|
16
18
|
instanceUrl,
|
|
@@ -22,12 +24,14 @@ export const getAccessToken = async (apiKey, instanceUrl) => {
|
|
|
22
24
|
method: "POST"
|
|
23
25
|
})
|
|
24
26
|
|
|
27
|
+
debug.auth('Token received successfully', { validUntil: data.validUntil });
|
|
25
28
|
// Success
|
|
26
29
|
return {
|
|
27
30
|
token: data.token,
|
|
28
31
|
validUntil: data.validUntil
|
|
29
32
|
};
|
|
30
33
|
} catch (error) {
|
|
34
|
+
debug.auth(`Token request failed: ${error.message}`);
|
|
31
35
|
throw new Error(`Error retrieving Magentrix access token: ${error.message}`);
|
|
32
36
|
}
|
|
33
37
|
};
|
|
@@ -52,35 +56,43 @@ export const tryAuthenticate = async (apiKey, instanceUrl) => {
|
|
|
52
56
|
let formattedMessage = '\n' + chalk.red.bold('✖ Authentication Failed') + '\n';
|
|
53
57
|
formattedMessage += chalk.dim('─'.repeat(50)) + '\n\n';
|
|
54
58
|
|
|
59
|
+
debug.auth(`Authentication failed, categorizing error: ${errorMessage.substring(0, 100)}`);
|
|
60
|
+
|
|
55
61
|
if (errorMessage.includes('Network error')) {
|
|
62
|
+
debug.auth('Error category: Network error (unable to reach instance)');
|
|
56
63
|
formattedMessage += chalk.cyan.bold('🌐 Unable to reach the Magentrix instance') + '\n\n';
|
|
57
64
|
formattedMessage += chalk.yellow(' Possible causes:') + '\n';
|
|
58
65
|
formattedMessage += chalk.gray(' • Check your internet connection') + '\n';
|
|
59
66
|
formattedMessage += chalk.gray(' • Verify the instance URL is correct') + '\n';
|
|
60
67
|
formattedMessage += chalk.gray(' • Ensure the server is online and accessible') + '\n';
|
|
61
68
|
} else if (errorMessage.includes('HTTP 401') || errorMessage.includes('HTTP 403') || errorMessage.includes('Unauthorized')) {
|
|
69
|
+
debug.auth('Error category: Invalid API key (401/403)');
|
|
62
70
|
formattedMessage += chalk.cyan.bold('🔑 Invalid API Key') + '\n\n';
|
|
63
71
|
formattedMessage += chalk.yellow(' What to do:') + '\n';
|
|
64
72
|
formattedMessage += chalk.gray(' • The API key you entered is incorrect') + '\n';
|
|
65
73
|
formattedMessage += chalk.gray(' • Verify your API key from the Magentrix admin panel') + '\n';
|
|
66
74
|
} else if (errorMessage.includes('HTTP 404')) {
|
|
75
|
+
debug.auth('Error category: Invalid instance URL (404)');
|
|
67
76
|
formattedMessage += chalk.cyan.bold('🔍 Invalid Magentrix Instance URL') + '\n\n';
|
|
68
77
|
formattedMessage += chalk.yellow(' What to do:') + '\n';
|
|
69
78
|
formattedMessage += chalk.gray(' • The URL does not appear to be a valid Magentrix server') + '\n';
|
|
70
79
|
formattedMessage += chalk.gray(' • Verify the URL matches your Magentrix instance') + '\n';
|
|
71
80
|
} else if (errorMessage.includes('HTTP 5')) {
|
|
81
|
+
debug.auth('Error category: Server error (5xx)');
|
|
72
82
|
formattedMessage += chalk.cyan.bold('⚠️ Magentrix Server Error') + '\n\n';
|
|
73
83
|
formattedMessage += chalk.yellow(' What to do:') + '\n';
|
|
74
84
|
formattedMessage += chalk.gray(' • The server is experiencing issues') + '\n';
|
|
75
85
|
formattedMessage += chalk.gray(' • Please try again in a few moments') + '\n';
|
|
76
86
|
formattedMessage += chalk.gray(' • Contact support if the issue persists') + '\n';
|
|
77
87
|
} else if (errorMessage.includes('timeout') || errorMessage.includes('ETIMEDOUT')) {
|
|
88
|
+
debug.auth('Error category: Connection timeout');
|
|
78
89
|
formattedMessage += chalk.cyan.bold('⏱️ Connection Timeout') + '\n\n';
|
|
79
90
|
formattedMessage += chalk.yellow(' What to do:') + '\n';
|
|
80
91
|
formattedMessage += chalk.gray(' • The server took too long to respond') + '\n';
|
|
81
92
|
formattedMessage += chalk.gray(' • Check your internet connection') + '\n';
|
|
82
93
|
formattedMessage += chalk.gray(' • Try again in a moment') + '\n';
|
|
83
94
|
} else {
|
|
95
|
+
debug.auth('Error category: Unknown error');
|
|
84
96
|
formattedMessage += chalk.cyan.bold('❓ Unable to Authenticate') + '\n\n';
|
|
85
97
|
formattedMessage += chalk.yellow(' What to do:') + '\n';
|
|
86
98
|
formattedMessage += chalk.gray(' • Verify both your API key and instance URL are correct') + '\n';
|
|
@@ -48,10 +48,6 @@ export const publishApp = async (instanceUrl, token, zipBuffer, filename, appNam
|
|
|
48
48
|
throw new Error('filename is required');
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
if (!appName) {
|
|
52
|
-
throw new Error('appName is required for navigation menu updates');
|
|
53
|
-
}
|
|
54
|
-
|
|
55
51
|
// Create a File object from the buffer for FormData
|
|
56
52
|
const file = new File([zipBuffer], filename, { type: 'application/zip' });
|
|
57
53
|
|
|
@@ -59,8 +55,14 @@ export const publishApp = async (instanceUrl, token, zipBuffer, filename, appNam
|
|
|
59
55
|
formData.append('file', file);
|
|
60
56
|
|
|
61
57
|
// Build query parameters
|
|
58
|
+
// Note: app-name is only sent when provided (required for creates, optional for updates)
|
|
59
|
+
// Sending app-name on updates can trigger a rename attempt that fails if the user
|
|
60
|
+
// lacks metadata-edit permissions on the server
|
|
62
61
|
const params = new URLSearchParams();
|
|
63
|
-
|
|
62
|
+
|
|
63
|
+
if (appName) {
|
|
64
|
+
params.append('app-name', appName);
|
|
65
|
+
}
|
|
64
66
|
|
|
65
67
|
if (options.appDescription) {
|
|
66
68
|
params.append('app-description', options.appDescription);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { fetchMagentrix } from "../fetch.js";
|
|
2
|
+
import debug from '../../debug.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Executes a Magentrix Entity Query Language (MEQL) query via the REST API v3.
|
|
@@ -18,6 +19,8 @@ export const meqlQuery = async (instanceUrl, token, query = '') => {
|
|
|
18
19
|
throw new Error('MEQL query string is required');
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
debug.log('MEQL', `Query: ${query}`);
|
|
23
|
+
|
|
21
24
|
const data = await fetchMagentrix({
|
|
22
25
|
instanceUrl,
|
|
23
26
|
token,
|
|
@@ -26,6 +29,8 @@ export const meqlQuery = async (instanceUrl, token, query = '') => {
|
|
|
26
29
|
body: query
|
|
27
30
|
})
|
|
28
31
|
|
|
32
|
+
debug.log('MEQL', `Result: ${data?.Records?.length ?? 0} records returned`);
|
|
33
|
+
|
|
29
34
|
// --- Success ---
|
|
30
35
|
return data;
|
|
31
36
|
};
|
package/utils/magentrix/fetch.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import debug from '../debug.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Checks if a request body should be JSON-stringified.
|
|
3
5
|
* Excludes FormData, Blob, ArrayBuffer, URLSearchParams, and typed arrays.
|
|
@@ -71,14 +73,18 @@ export const fetchMagentrix = async ({
|
|
|
71
73
|
}
|
|
72
74
|
if (!finalHeaders['Content-Type'] && !ignoreContentType) finalHeaders['Content-Type'] = 'application/json';
|
|
73
75
|
|
|
76
|
+
const fullUrl = `${instanceUrl.replace(/\/$/, '')}${path}`;
|
|
77
|
+
debug.request(method, fullUrl, finalHeaders, body);
|
|
78
|
+
|
|
74
79
|
let response, responseData;
|
|
75
80
|
try {
|
|
76
|
-
response = await fetch(
|
|
81
|
+
response = await fetch(fullUrl, {
|
|
77
82
|
method,
|
|
78
83
|
headers: finalHeaders,
|
|
79
84
|
body: requestBody
|
|
80
85
|
});
|
|
81
86
|
} catch (err) {
|
|
87
|
+
debug.log('HTTP-ERR', `Network error: ${err.message}`, err.stack);
|
|
82
88
|
const errorObj = {
|
|
83
89
|
type: 'network',
|
|
84
90
|
message: `Network error contacting Magentrix API: ${err.message}`,
|
|
@@ -94,6 +100,8 @@ export const fetchMagentrix = async ({
|
|
|
94
100
|
responseData = null;
|
|
95
101
|
}
|
|
96
102
|
|
|
103
|
+
debug.response(response.status, response.statusText, response.headers, responseData);
|
|
104
|
+
|
|
97
105
|
if (!response.ok) {
|
|
98
106
|
const errorObj = {
|
|
99
107
|
type: 'http',
|
|
@@ -117,6 +125,7 @@ export const fetchMagentrix = async ({
|
|
|
117
125
|
}
|
|
118
126
|
if (errorConfig?.includeURL) msg += `\nURL: ${response.url}`;
|
|
119
127
|
errorObj.message = msg;
|
|
128
|
+
debug.log('HTTP-ERR', `HTTP ${response.status}: ${msg}`);
|
|
120
129
|
if (returnErrorObject) throw errorObj;
|
|
121
130
|
throw new Error(msg);
|
|
122
131
|
}
|
|
@@ -146,6 +155,7 @@ export const fetchMagentrix = async ({
|
|
|
146
155
|
details = String(responseData);
|
|
147
156
|
}
|
|
148
157
|
errorObj.message = `Magentrix API error:\n${details}`;
|
|
158
|
+
debug.log('API-ERR', errorObj.message);
|
|
149
159
|
if (returnErrorObject) throw errorObj;
|
|
150
160
|
throw new Error(errorObj.message);
|
|
151
161
|
}
|