@magentrix-corp/magentrix-cli 1.3.16 → 1.3.17

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 (68) hide show
  1. package/LICENSE +25 -25
  2. package/README.md +1166 -1166
  3. package/actions/autopublish.old.js +293 -293
  4. package/actions/config.js +182 -182
  5. package/actions/create.js +466 -466
  6. package/actions/help.js +164 -164
  7. package/actions/iris/buildStage.js +874 -874
  8. package/actions/iris/delete.js +256 -256
  9. package/actions/iris/dev.js +391 -391
  10. package/actions/iris/index.js +6 -6
  11. package/actions/iris/link.js +375 -375
  12. package/actions/iris/recover.js +268 -268
  13. package/actions/main.js +80 -80
  14. package/actions/publish.js +1420 -1420
  15. package/actions/pull.js +684 -684
  16. package/actions/setup.js +148 -148
  17. package/actions/status.js +17 -17
  18. package/actions/update.js +248 -248
  19. package/bin/magentrix.js +393 -393
  20. package/package.json +55 -55
  21. package/utils/assetPaths.js +158 -158
  22. package/utils/autopublishLock.js +77 -77
  23. package/utils/cacher.js +206 -206
  24. package/utils/cli/checkInstanceUrl.js +76 -74
  25. package/utils/cli/helpers/compare.js +282 -282
  26. package/utils/cli/helpers/ensureApiKey.js +63 -63
  27. package/utils/cli/helpers/ensureCredentials.js +68 -68
  28. package/utils/cli/helpers/ensureInstanceUrl.js +75 -75
  29. package/utils/cli/writeRecords.js +262 -262
  30. package/utils/compare.js +135 -135
  31. package/utils/compress.js +17 -17
  32. package/utils/config.js +527 -527
  33. package/utils/debug.js +144 -144
  34. package/utils/diagnostics/testPublishLogic.js +96 -96
  35. package/utils/diff.js +49 -49
  36. package/utils/downloadAssets.js +291 -291
  37. package/utils/filetag.js +115 -115
  38. package/utils/hash.js +14 -14
  39. package/utils/iris/backup.js +411 -411
  40. package/utils/iris/builder.js +541 -541
  41. package/utils/iris/config-reader.js +664 -664
  42. package/utils/iris/deleteHelper.js +150 -150
  43. package/utils/iris/errors.js +537 -537
  44. package/utils/iris/linker.js +601 -601
  45. package/utils/iris/lock.js +360 -360
  46. package/utils/iris/validation.js +360 -360
  47. package/utils/iris/validator.js +281 -281
  48. package/utils/iris/zipper.js +248 -248
  49. package/utils/logger.js +291 -291
  50. package/utils/magentrix/api/assets.js +220 -220
  51. package/utils/magentrix/api/auth.js +107 -107
  52. package/utils/magentrix/api/createEntity.js +61 -61
  53. package/utils/magentrix/api/deleteEntity.js +55 -55
  54. package/utils/magentrix/api/iris.js +251 -251
  55. package/utils/magentrix/api/meqlQuery.js +36 -36
  56. package/utils/magentrix/api/retrieveEntity.js +86 -86
  57. package/utils/magentrix/api/updateEntity.js +66 -66
  58. package/utils/magentrix/fetch.js +168 -168
  59. package/utils/merge.js +22 -22
  60. package/utils/permissionError.js +70 -70
  61. package/utils/preferences.js +40 -40
  62. package/utils/progress.js +469 -469
  63. package/utils/spinner.js +43 -43
  64. package/utils/template.js +52 -52
  65. package/utils/updateFileBase.js +121 -121
  66. package/utils/workspaces.js +108 -108
  67. package/vars/config.js +11 -11
  68. package/vars/global.js +50 -50
package/utils/spinner.js CHANGED
@@ -1,43 +1,43 @@
1
- export async function withSpinner(message, fn, config = { showCompletion: true }) {
2
- const spinnerChars = ['|', '/', '-', '\\'];
3
- let i = 0;
4
- let spinnerActive = true;
5
- let spinner;
6
-
7
- // Patch both log and error for full coverage
8
- const originalLog = console.log;
9
- const originalError = console.error;
10
- function clearSpinnerLine() {
11
- process.stdout.write('\r' + ' '.repeat(message.length + 8) + '\r');
12
- if (spinnerActive) {
13
- clearInterval(spinner);
14
- spinnerActive = false;
15
- }
16
- }
17
- console.log = (...args) => { clearSpinnerLine(); originalLog(...args); };
18
- console.error = (...args) => { clearSpinnerLine(); originalError(...args); };
19
-
20
- process.stdout.write(`${message} `);
21
-
22
- spinner = setInterval(() => {
23
- process.stdout.write(`\r${message} ${spinnerChars[i++ % spinnerChars.length]}`);
24
- }, 80);
25
-
26
- try {
27
- const result = await fn();
28
- const treatAsError = result?.hasErrors;
29
- spinnerActive = false;
30
- clearInterval(spinner);
31
- if (config?.showCompletion) process.stdout.write(`\r${message} ${treatAsError ? '❌' : '✅'}\n`);
32
- return result;
33
- } catch (err) {
34
- spinnerActive = false;
35
- clearInterval(spinner);
36
- if (config?.showCompletion) process.stdout.write(`\r${message} ❌\n`);
37
- throw err;
38
- } finally {
39
- // Restore logs always
40
- console.log = originalLog;
41
- console.error = originalError;
42
- }
43
- }
1
+ export async function withSpinner(message, fn, config = { showCompletion: true }) {
2
+ const spinnerChars = ['|', '/', '-', '\\'];
3
+ let i = 0;
4
+ let spinnerActive = true;
5
+ let spinner;
6
+
7
+ // Patch both log and error for full coverage
8
+ const originalLog = console.log;
9
+ const originalError = console.error;
10
+ function clearSpinnerLine() {
11
+ process.stdout.write('\r' + ' '.repeat(message.length + 8) + '\r');
12
+ if (spinnerActive) {
13
+ clearInterval(spinner);
14
+ spinnerActive = false;
15
+ }
16
+ }
17
+ console.log = (...args) => { clearSpinnerLine(); originalLog(...args); };
18
+ console.error = (...args) => { clearSpinnerLine(); originalError(...args); };
19
+
20
+ process.stdout.write(`${message} `);
21
+
22
+ spinner = setInterval(() => {
23
+ process.stdout.write(`\r${message} ${spinnerChars[i++ % spinnerChars.length]}`);
24
+ }, 80);
25
+
26
+ try {
27
+ const result = await fn();
28
+ const treatAsError = result?.hasErrors;
29
+ spinnerActive = false;
30
+ clearInterval(spinner);
31
+ if (config?.showCompletion) process.stdout.write(`\r${message} ${treatAsError ? '❌' : '✅'}\n`);
32
+ return result;
33
+ } catch (err) {
34
+ spinnerActive = false;
35
+ clearInterval(spinner);
36
+ if (config?.showCompletion) process.stdout.write(`\r${message} ❌\n`);
37
+ throw err;
38
+ } finally {
39
+ // Restore logs always
40
+ console.log = originalLog;
41
+ console.error = originalError;
42
+ }
43
+ }
package/utils/template.js CHANGED
@@ -1,52 +1,52 @@
1
- /**
2
- * Returns the default template for an ActiveClass Controller.
3
- * @param {string} className
4
- * @returns {string}
5
- */
6
- export const getControllerTemplate = (className) => `
7
- public class ${className} : AspxController {
8
- public override ActionResponse Index()
9
- {
10
- return View();
11
- }
12
- }
13
- `.trim();
14
-
15
- /**
16
- * Returns the default template for an ActiveClass Trigger.
17
- * @param {string} className - For instance ContactTrigger
18
- * @param {string} entityName - For instance Force__Contact
19
- * @returns {string}
20
- */
21
- export const getTriggerTemplate = (className, entityName = '<ENTITYNAME>') => `
22
- public class ${className} : ActiveTrigger<${entityName}>
23
- {
24
- public override void Execute(TransactionContext<${entityName}> trigger) {
25
-
26
- }
27
- }
28
- `.trim();
29
-
30
- /**
31
- * Returns the default template for a general ActiveClass.
32
- * @param {string} className
33
- * @returns {string}
34
- */
35
- export const getClassTemplate = (className) => `
36
- public class ${className} {
37
-
38
- }
39
- `.trim();
40
-
41
- /**
42
- * Returns the default template for an Active Page.
43
- * @param {string} pageName
44
- * @param {string} pageLabel
45
- * @returns {string}
46
- */
47
- export const getPageTemplate = (pageName, pageLabel) => `
48
- <aspx:AspxPage runat="server" Id="${pageName}" title="${pageLabel}">
49
- <body>
50
- </body>
51
- </aspx:AspxPage>
52
- `.trim();
1
+ /**
2
+ * Returns the default template for an ActiveClass Controller.
3
+ * @param {string} className
4
+ * @returns {string}
5
+ */
6
+ export const getControllerTemplate = (className) => `
7
+ public class ${className} : AspxController {
8
+ public override ActionResponse Index()
9
+ {
10
+ return View();
11
+ }
12
+ }
13
+ `.trim();
14
+
15
+ /**
16
+ * Returns the default template for an ActiveClass Trigger.
17
+ * @param {string} className - For instance ContactTrigger
18
+ * @param {string} entityName - For instance Force__Contact
19
+ * @returns {string}
20
+ */
21
+ export const getTriggerTemplate = (className, entityName = '<ENTITYNAME>') => `
22
+ public class ${className} : ActiveTrigger<${entityName}>
23
+ {
24
+ public override void Execute(TransactionContext<${entityName}> trigger) {
25
+
26
+ }
27
+ }
28
+ `.trim();
29
+
30
+ /**
31
+ * Returns the default template for a general ActiveClass.
32
+ * @param {string} className
33
+ * @returns {string}
34
+ */
35
+ export const getClassTemplate = (className) => `
36
+ public class ${className} {
37
+
38
+ }
39
+ `.trim();
40
+
41
+ /**
42
+ * Returns the default template for an Active Page.
43
+ * @param {string} pageName
44
+ * @param {string} pageLabel
45
+ * @returns {string}
46
+ */
47
+ export const getPageTemplate = (pageName, pageLabel) => `
48
+ <aspx:AspxPage runat="server" Id="${pageName}" title="${pageLabel}">
49
+ <body>
50
+ </body>
51
+ </aspx:AspxPage>
52
+ `.trim();
@@ -1,121 +1,121 @@
1
- import { EXPORT_ROOT } from "../vars/global.js";
2
- import fs from "fs";
3
- import path from "path";
4
- import { sha256 } from "./hash.js";
5
- import Config from "./config.js";
6
- import { compressString } from "./compress.js";
7
-
8
- const config = new Config();
9
-
10
- /**
11
- * Recursively collects all file paths under a directory, returning relative paths from base.
12
- *
13
- * @param {string} dirPath - Absolute or relative path to the root directory to scan.
14
- * @param {string} [basePath=''] - Internal use. The base path for recursion, relative to dirPath.
15
- * @returns {string[]} Array of relative file paths found under dirPath.
16
- */
17
- export function getAllFiles(dirPath, basePath = "") {
18
- let results = [];
19
- const fullDirPath = path.join(dirPath, basePath);
20
- const entries = fs.readdirSync(fullDirPath, { withFileTypes: true });
21
-
22
- for (const entry of entries) {
23
- const relPath = path.join(basePath, entry.name);
24
- const entryPath = path.join(dirPath, relPath);
25
-
26
- if (entry.isDirectory()) {
27
- // Recurse into subdirectory
28
- results = results.concat(getAllFiles(dirPath, relPath));
29
- } else if (entry.isFile()) {
30
- // Add file's relative path to results
31
- results.push(relPath);
32
- }
33
- }
34
- return results;
35
- }
36
-
37
- /**
38
- * Updates (or creates) the base sync state for a specific file.
39
- *
40
- * Saves the current file's lastModified time, content hash, and contents
41
- * to base.json using config.save(), under the key of the file's relative path.
42
- *
43
- * This should be called only when the local and remote file are confirmed to be in sync
44
- * (i.e., after a successful pull, push, or conflict resolution).
45
- *
46
- * @param {string} filePath - The expected path to the file (relative to project root or EXPORT_ROOT).
47
- * @param {object} record - The Magentrix record
48
- * @param {string} actualPath - Should only be provided if the file has been renamed and the base has not been updated
49
- * @param {object} contentSnapshot - Optional { content, hash } snapshot of what was actually published (prevents race conditions)
50
- */
51
- export const updateBase = (filePath, record, actualPath = '', contentSnapshot = null) => {
52
- // This is the true location of the file
53
- const fileSystemLocation = actualPath || path.resolve(filePath);
54
-
55
- if (!fs.existsSync(fileSystemLocation)) {
56
- // Silently skip files that don't exist - they may have failed to download
57
- // This is expected behavior for files that returned 404 during download
58
- return;
59
- }
60
-
61
- // Get file stats for mtime
62
- const fileStats = fs.statSync(fileSystemLocation);
63
- const isDirectory = fileStats.isDirectory();
64
-
65
- // Use snapshot if provided (to avoid race conditions), otherwise read from disk
66
- let fileContent, contentHash;
67
- if (isDirectory) {
68
- // Folders don't have content - use provided contentHash if any (e.g., for Iris apps)
69
- fileContent = '';
70
- contentHash = record.contentHash || '';
71
- } else if (contentSnapshot && contentSnapshot.content) {
72
- // Use the snapshot of what was actually published
73
- fileContent = contentSnapshot.content;
74
- contentHash = contentSnapshot.hash;
75
- } else {
76
- // Read from disk (normal behavior)
77
- fileContent = fs.readFileSync(fileSystemLocation, "utf-8");
78
- contentHash = sha256(fileContent);
79
- }
80
-
81
- // Save sync metadata and content to base.json via config manager.
82
- // - key: the relative path to the file
83
- // - value: lastModified, contentHash, and full content
84
- // - options: ensure writing to base.json
85
- const saveData = {
86
- lastModified: fileStats.mtimeMs,
87
- contentHash,
88
- compressedContent: isDirectory ? '' : compressString(fileContent),
89
- recordId: record.Id,
90
- type: record.Type,
91
- filePath,
92
- lastKnownActualPath: fileSystemLocation,
93
- lastKnownPath: path.resolve(filePath)
94
- };
95
-
96
- // Preserve custom fields from record (e.g., for Iris apps: folderName, appName, modifiedOn)
97
- const customFields = ['folderName', 'appName', 'modifiedOn', 'uploadedOn', 'size'];
98
- for (const field of customFields) {
99
- if (record[field] !== undefined) {
100
- saveData[field] = record[field];
101
- }
102
- }
103
-
104
- if (saveData.type === 'File' || saveData.type === 'Folder') delete saveData.compressedContent;
105
-
106
- config.save(
107
- record.Id,
108
- saveData,
109
- {
110
- filename: "base.json"
111
- }
112
- );
113
- };
114
-
115
- export const removeFromBase = (recordId) => {
116
- config.removeKey(recordId, { filename: "base.json" });
117
- }
118
-
119
- export const removeFromBaseBulk = (recordIds) => {
120
- config.removeKeys(recordIds, { filename: "base.json" });
121
- }
1
+ import { EXPORT_ROOT } from "../vars/global.js";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { sha256 } from "./hash.js";
5
+ import Config from "./config.js";
6
+ import { compressString } from "./compress.js";
7
+
8
+ const config = new Config();
9
+
10
+ /**
11
+ * Recursively collects all file paths under a directory, returning relative paths from base.
12
+ *
13
+ * @param {string} dirPath - Absolute or relative path to the root directory to scan.
14
+ * @param {string} [basePath=''] - Internal use. The base path for recursion, relative to dirPath.
15
+ * @returns {string[]} Array of relative file paths found under dirPath.
16
+ */
17
+ export function getAllFiles(dirPath, basePath = "") {
18
+ let results = [];
19
+ const fullDirPath = path.join(dirPath, basePath);
20
+ const entries = fs.readdirSync(fullDirPath, { withFileTypes: true });
21
+
22
+ for (const entry of entries) {
23
+ const relPath = path.join(basePath, entry.name);
24
+ const entryPath = path.join(dirPath, relPath);
25
+
26
+ if (entry.isDirectory()) {
27
+ // Recurse into subdirectory
28
+ results = results.concat(getAllFiles(dirPath, relPath));
29
+ } else if (entry.isFile()) {
30
+ // Add file's relative path to results
31
+ results.push(relPath);
32
+ }
33
+ }
34
+ return results;
35
+ }
36
+
37
+ /**
38
+ * Updates (or creates) the base sync state for a specific file.
39
+ *
40
+ * Saves the current file's lastModified time, content hash, and contents
41
+ * to base.json using config.save(), under the key of the file's relative path.
42
+ *
43
+ * This should be called only when the local and remote file are confirmed to be in sync
44
+ * (i.e., after a successful pull, push, or conflict resolution).
45
+ *
46
+ * @param {string} filePath - The expected path to the file (relative to project root or EXPORT_ROOT).
47
+ * @param {object} record - The Magentrix record
48
+ * @param {string} actualPath - Should only be provided if the file has been renamed and the base has not been updated
49
+ * @param {object} contentSnapshot - Optional { content, hash } snapshot of what was actually published (prevents race conditions)
50
+ */
51
+ export const updateBase = (filePath, record, actualPath = '', contentSnapshot = null) => {
52
+ // This is the true location of the file
53
+ const fileSystemLocation = actualPath || path.resolve(filePath);
54
+
55
+ if (!fs.existsSync(fileSystemLocation)) {
56
+ // Silently skip files that don't exist - they may have failed to download
57
+ // This is expected behavior for files that returned 404 during download
58
+ return;
59
+ }
60
+
61
+ // Get file stats for mtime
62
+ const fileStats = fs.statSync(fileSystemLocation);
63
+ const isDirectory = fileStats.isDirectory();
64
+
65
+ // Use snapshot if provided (to avoid race conditions), otherwise read from disk
66
+ let fileContent, contentHash;
67
+ if (isDirectory) {
68
+ // Folders don't have content - use provided contentHash if any (e.g., for Iris apps)
69
+ fileContent = '';
70
+ contentHash = record.contentHash || '';
71
+ } else if (contentSnapshot && contentSnapshot.content) {
72
+ // Use the snapshot of what was actually published
73
+ fileContent = contentSnapshot.content;
74
+ contentHash = contentSnapshot.hash;
75
+ } else {
76
+ // Read from disk (normal behavior)
77
+ fileContent = fs.readFileSync(fileSystemLocation, "utf-8");
78
+ contentHash = sha256(fileContent);
79
+ }
80
+
81
+ // Save sync metadata and content to base.json via config manager.
82
+ // - key: the relative path to the file
83
+ // - value: lastModified, contentHash, and full content
84
+ // - options: ensure writing to base.json
85
+ const saveData = {
86
+ lastModified: fileStats.mtimeMs,
87
+ contentHash,
88
+ compressedContent: isDirectory ? '' : compressString(fileContent),
89
+ recordId: record.Id,
90
+ type: record.Type,
91
+ filePath,
92
+ lastKnownActualPath: fileSystemLocation,
93
+ lastKnownPath: path.resolve(filePath)
94
+ };
95
+
96
+ // Preserve custom fields from record (e.g., for Iris apps: folderName, appName, modifiedOn)
97
+ const customFields = ['folderName', 'appName', 'modifiedOn', 'uploadedOn', 'size'];
98
+ for (const field of customFields) {
99
+ if (record[field] !== undefined) {
100
+ saveData[field] = record[field];
101
+ }
102
+ }
103
+
104
+ if (saveData.type === 'File' || saveData.type === 'Folder') delete saveData.compressedContent;
105
+
106
+ config.save(
107
+ record.Id,
108
+ saveData,
109
+ {
110
+ filename: "base.json"
111
+ }
112
+ );
113
+ };
114
+
115
+ export const removeFromBase = (recordId) => {
116
+ config.removeKey(recordId, { filename: "base.json" });
117
+ }
118
+
119
+ export const removeFromBaseBulk = (recordIds) => {
120
+ config.removeKeys(recordIds, { filename: "base.json" });
121
+ }
@@ -1,108 +1,108 @@
1
- import { existsSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import Config from './config.js';
4
-
5
- const config = new Config();
6
-
7
- /**
8
- * Register a workspace in the global registry.
9
- * This stores the workspace path so it can be discovered later.
10
- *
11
- * @param {string} workspacePath - Full path to the workspace
12
- * @param {string} instanceUrl - The Magentrix instance URL for this workspace
13
- */
14
- export function registerWorkspace(workspacePath, instanceUrl) {
15
- const workspaces = config.read('workspaces', { global: true }) || [];
16
-
17
- // Check if already registered
18
- const existingIndex = workspaces.findIndex(w => w.path === workspacePath);
19
-
20
- if (existingIndex >= 0) {
21
- // Update existing entry
22
- workspaces[existingIndex] = {
23
- path: workspacePath,
24
- instanceUrl,
25
- lastUsed: new Date().toISOString()
26
- };
27
- } else {
28
- // Add new entry
29
- workspaces.push({
30
- path: workspacePath,
31
- instanceUrl,
32
- lastUsed: new Date().toISOString()
33
- });
34
- }
35
-
36
- config.save('workspaces', workspaces, { global: true });
37
- }
38
-
39
- /**
40
- * Get all registered workspaces from the global registry.
41
- * Validates that each workspace still exists on disk.
42
- *
43
- * @returns {Array<{path: string, instanceUrl: string, lastUsed: string, valid: boolean}>}
44
- */
45
- export function getRegisteredWorkspaces() {
46
- const workspaces = config.read('workspaces', { global: true }) || [];
47
-
48
- return workspaces.map(workspace => {
49
- // Check if the workspace still exists and is valid
50
- const magentrixFolder = join(workspace.path, '.magentrix');
51
- const srcFolder = join(workspace.path, 'src');
52
-
53
- const valid = existsSync(magentrixFolder) && existsSync(srcFolder);
54
-
55
- return {
56
- ...workspace,
57
- valid
58
- };
59
- });
60
- }
61
-
62
- /**
63
- * Get all valid registered workspaces (those that still exist on disk).
64
- *
65
- * @returns {Array<{path: string, instanceUrl: string, lastUsed: string, valid: boolean}>}
66
- */
67
- export function getValidWorkspaces() {
68
- return getRegisteredWorkspaces().filter(w => w.valid);
69
- }
70
-
71
- /**
72
- * Remove a workspace from the global registry.
73
- *
74
- * @param {string} workspacePath - Path to the workspace to remove
75
- * @returns {boolean} True if removed, false if not found
76
- */
77
- export function unregisterWorkspace(workspacePath) {
78
- const workspaces = config.read('workspaces', { global: true }) || [];
79
- const initialLength = workspaces.length;
80
-
81
- const filtered = workspaces.filter(w => w.path !== workspacePath);
82
-
83
- if (filtered.length < initialLength) {
84
- config.save('workspaces', filtered, { global: true });
85
- return true;
86
- }
87
-
88
- return false;
89
- }
90
-
91
- /**
92
- * Clean up invalid workspaces (those that no longer exist on disk).
93
- *
94
- * @returns {number} Number of workspaces removed
95
- */
96
- export function cleanupInvalidWorkspaces() {
97
- const workspaces = getRegisteredWorkspaces();
98
- const validWorkspaces = workspaces.filter(w => w.valid);
99
- const removedCount = workspaces.length - validWorkspaces.length;
100
-
101
- if (removedCount > 0) {
102
- // Remove the 'valid' property before saving
103
- const toSave = validWorkspaces.map(({ valid, ...rest }) => rest);
104
- config.save('workspaces', toSave, { global: true });
105
- }
106
-
107
- return removedCount;
108
- }
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import Config from './config.js';
4
+
5
+ const config = new Config();
6
+
7
+ /**
8
+ * Register a workspace in the global registry.
9
+ * This stores the workspace path so it can be discovered later.
10
+ *
11
+ * @param {string} workspacePath - Full path to the workspace
12
+ * @param {string} instanceUrl - The Magentrix instance URL for this workspace
13
+ */
14
+ export function registerWorkspace(workspacePath, instanceUrl) {
15
+ const workspaces = config.read('workspaces', { global: true }) || [];
16
+
17
+ // Check if already registered
18
+ const existingIndex = workspaces.findIndex(w => w.path === workspacePath);
19
+
20
+ if (existingIndex >= 0) {
21
+ // Update existing entry
22
+ workspaces[existingIndex] = {
23
+ path: workspacePath,
24
+ instanceUrl,
25
+ lastUsed: new Date().toISOString()
26
+ };
27
+ } else {
28
+ // Add new entry
29
+ workspaces.push({
30
+ path: workspacePath,
31
+ instanceUrl,
32
+ lastUsed: new Date().toISOString()
33
+ });
34
+ }
35
+
36
+ config.save('workspaces', workspaces, { global: true });
37
+ }
38
+
39
+ /**
40
+ * Get all registered workspaces from the global registry.
41
+ * Validates that each workspace still exists on disk.
42
+ *
43
+ * @returns {Array<{path: string, instanceUrl: string, lastUsed: string, valid: boolean}>}
44
+ */
45
+ export function getRegisteredWorkspaces() {
46
+ const workspaces = config.read('workspaces', { global: true }) || [];
47
+
48
+ return workspaces.map(workspace => {
49
+ // Check if the workspace still exists and is valid
50
+ const magentrixFolder = join(workspace.path, '.magentrix');
51
+ const srcFolder = join(workspace.path, 'src');
52
+
53
+ const valid = existsSync(magentrixFolder) && existsSync(srcFolder);
54
+
55
+ return {
56
+ ...workspace,
57
+ valid
58
+ };
59
+ });
60
+ }
61
+
62
+ /**
63
+ * Get all valid registered workspaces (those that still exist on disk).
64
+ *
65
+ * @returns {Array<{path: string, instanceUrl: string, lastUsed: string, valid: boolean}>}
66
+ */
67
+ export function getValidWorkspaces() {
68
+ return getRegisteredWorkspaces().filter(w => w.valid);
69
+ }
70
+
71
+ /**
72
+ * Remove a workspace from the global registry.
73
+ *
74
+ * @param {string} workspacePath - Path to the workspace to remove
75
+ * @returns {boolean} True if removed, false if not found
76
+ */
77
+ export function unregisterWorkspace(workspacePath) {
78
+ const workspaces = config.read('workspaces', { global: true }) || [];
79
+ const initialLength = workspaces.length;
80
+
81
+ const filtered = workspaces.filter(w => w.path !== workspacePath);
82
+
83
+ if (filtered.length < initialLength) {
84
+ config.save('workspaces', filtered, { global: true });
85
+ return true;
86
+ }
87
+
88
+ return false;
89
+ }
90
+
91
+ /**
92
+ * Clean up invalid workspaces (those that no longer exist on disk).
93
+ *
94
+ * @returns {number} Number of workspaces removed
95
+ */
96
+ export function cleanupInvalidWorkspaces() {
97
+ const workspaces = getRegisteredWorkspaces();
98
+ const validWorkspaces = workspaces.filter(w => w.valid);
99
+ const removedCount = workspaces.length - validWorkspaces.length;
100
+
101
+ if (removedCount > 0) {
102
+ // Remove the 'valid' property before saving
103
+ const toSave = validWorkspaces.map(({ valid, ...rest }) => rest);
104
+ config.save('workspaces', toSave, { global: true });
105
+ }
106
+
107
+ return removedCount;
108
+ }