@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/bin/magentrix.js CHANGED
@@ -1,393 +1,393 @@
1
- #!/usr/bin/env node
2
-
3
- // Imports
4
- import { Command } from 'commander';
5
- import chalk from 'chalk';
6
- import { existsSync } from 'node:fs';
7
- import { join } from 'node:path';
8
- import { VERSION } from '../vars/config.js';
9
- import { setup } from '../actions/setup.js';
10
- import { main } from '../actions/main.js';
11
- import { pull } from '../actions/pull.js';
12
- import { create } from '../actions/create.js';
13
- import { autoPublish } from '../actions/autopublish.js';
14
- import { status } from '../actions/status.js';
15
- import { cacheDir, recacheFileIdIndex } from '../utils/cacher.js';
16
- import { CWD, EXPORT_ROOT, HASHED_CWD } from '../vars/global.js';
17
- import { publish } from '../actions/publish.js';
18
- import { update } from '../actions/update.js';
19
- import { configWizard } from '../actions/config.js';
20
- import { irisLink, irisDev, irisDelete, irisRecover, vueBuildStage } from '../actions/iris/index.js';
21
- import Config from '../utils/config.js';
22
- import { registerWorkspace, getRegisteredWorkspaces } from '../utils/workspaces.js';
23
- import { ensureVSCodeFileAssociation } from '../utils/preferences.js';
24
- import debug from '../utils/debug.js';
25
-
26
- const config = new Config();
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
-
35
- // ── Vue Project Detection ────────────────────────────────
36
- /**
37
- * Check if current directory is a Vue project (has config.ts)
38
- */
39
- function isInVueProject() {
40
- const configLocations = [
41
- 'src/config.ts',
42
- 'config.ts',
43
- 'src/iris-config.ts',
44
- 'iris-config.ts'
45
- ];
46
- return configLocations.some(loc => existsSync(join(process.cwd(), loc)));
47
- }
48
-
49
- /**
50
- * Block non-iris commands when in a Vue project
51
- */
52
- function requireMagentrixWorkspace(fn) {
53
- return async (...args) => {
54
- if (isInVueProject()) {
55
- console.error(`\n${chalk.bgRed.white.bold(' ERROR ')} ${chalk.redBright('This command must be run in a Magentrix workspace')}\n`);
56
- console.error(chalk.yellow('It looks like you\'re in a Vue project directory.'));
57
- console.error(chalk.gray('This command requires a Magentrix workspace with global API key and instance URL.\n'));
58
- console.error(chalk.cyan('Available commands in Vue projects:'));
59
- console.error(chalk.gray(' • magentrix iris-app-link'));
60
- console.error(chalk.gray(' • magentrix vue-run-build'));
61
- console.error(chalk.gray(' • magentrix vue-run-dev'));
62
- console.error(chalk.gray(' • magentrix update\n'));
63
- process.exit(1);
64
- }
65
- // Execute the command
66
- await fn(...args);
67
- };
68
- }
69
-
70
- /**
71
- * Block commands that should not run in Vue projects (but don't require a workspace)
72
- */
73
- function blockInVueProject(fn) {
74
- return async (...args) => {
75
- if (isInVueProject()) {
76
- console.error(`\n${chalk.bgRed.white.bold(' ERROR ')} ${chalk.redBright('This command cannot be run in a Vue project')}\n`);
77
- console.error(chalk.yellow('It looks like you\'re in a Vue project directory.'));
78
- console.error(chalk.gray('Please run this command from a Magentrix workspace or an empty directory.\n'));
79
- process.exit(1);
80
- }
81
- // Execute the command
82
- await fn(...args);
83
- };
84
- }
85
-
86
- // ── Middleware ────────────────────────────────
87
-
88
- /**
89
- * Auto-register existing workspaces for backwards compatibility.
90
- * If the current directory has credentials configured but isn't in the
91
- * global workspace registry, register it automatically.
92
- */
93
- function ensureWorkspaceRegistered() {
94
- // Check if current directory has credentials configured
95
- const instanceUrl = config.read('instanceUrl', { global: true, pathHash: HASHED_CWD });
96
- const apiKey = config.read('apiKey', { global: true, pathHash: HASHED_CWD });
97
-
98
- if (!instanceUrl || !apiKey) {
99
- return; // Not a configured workspace
100
- }
101
-
102
- // Check if already registered
103
- const workspaces = getRegisteredWorkspaces();
104
- const alreadyRegistered = workspaces.some(w => w.path === CWD);
105
-
106
- if (!alreadyRegistered) {
107
- // Auto-register for backwards compatibility
108
- registerWorkspace(CWD, instanceUrl);
109
- }
110
- }
111
-
112
- async function preMiddleware() {
113
- ensureWorkspaceRegistered();
114
-
115
- // Ensure .vscode folder exists in project root (not in src/) for Magentrix projects
116
- const magentrixDir = join(CWD, '.magentrix');
117
- if (existsSync(magentrixDir)) {
118
- await ensureVSCodeFileAssociation(CWD);
119
- }
120
-
121
- await recacheFileIdIndex(EXPORT_ROOT);
122
- await cacheDir(EXPORT_ROOT);
123
- }
124
- async function postMiddleware() {
125
- await cacheDir(EXPORT_ROOT);
126
- }
127
-
128
- const withMiddleware = ({ pre, post }) => (fn) => async (...args) => {
129
- if (pre) await pre(...args);
130
- await fn(...args);
131
- if (post) await post(...args);
132
- };
133
-
134
- const withDefault = withMiddleware({ pre: preMiddleware, post: postMiddleware });
135
-
136
- // Combined wrapper: check for Vue project + run middleware
137
- const withWorkspaceCheck = (fn) => requireMagentrixWorkspace(withDefault(fn));
138
-
139
- // ── CLI Setup ────────────────────────────────
140
- const program = new Command();
141
- program
142
- .name('magentrix')
143
- .description('Manage Magentrix assets and automation')
144
- .version(VERSION)
145
- .option('--debug', 'Enable debug logging')
146
- .configureHelp({
147
- formatHelp: (_cmd, _helper) => {
148
- const divider = chalk.gray('━'.repeat(60));
149
- const titleBar = chalk.bold.bgBlue.white(' Magentrix CLI ');
150
- const version = chalk.dim(`v${VERSION}`);
151
-
152
- let help = `\n${divider}\n${titleBar} ${version}\n${divider}\n\n`;
153
- help += `${chalk.dim('Manage Magentrix assets and automation')}\n\n`;
154
-
155
- // Usage section
156
- help += `${chalk.bold.yellow('USAGE')}\n`;
157
- help += ` ${chalk.cyan('magentrix')} ${chalk.dim('<command> [options]')}\n\n`;
158
-
159
- // Commands section
160
- help += `${chalk.bold.yellow('COMMANDS')}\n`;
161
- const commands = [
162
- { name: 'setup', desc: 'Configure your Magentrix API key', icon: '⚙️ ' },
163
- { name: 'config', desc: 'Manage CLI settings', icon: '🔧 ' },
164
- { name: 'pull', desc: 'Pull files from the remote server', icon: '📥 ' },
165
- { name: 'create', desc: 'Create files locally', icon: '✨ ' },
166
- { name: 'status', desc: 'Show file conflicts and sync status', icon: '📊 ' },
167
- { name: 'publish', desc: 'Publish pending changes to the remote server', icon: '📤 ' },
168
- { name: 'autopublish', desc: 'Watch & sync changes in real time', icon: '🔄 ' },
169
- { name: 'update', desc: 'Update MagentrixCLI to the latest version', icon: '⬆️ ' },
170
- { name: 'iris-app-link', desc: 'Link a Vue project to the CLI', icon: '🔗 ' },
171
- { name: 'vue-run-build', desc: 'Build Vue project and stage for publish', icon: '🏗️ ' },
172
- { name: 'vue-run-dev', desc: 'Start Vue dev server with platform assets', icon: '🌐 ' },
173
- { name: 'iris-app-delete', desc: 'Delete an Iris app with backup', icon: '🗑️ ' },
174
- { name: 'iris-app-recover', desc: 'Recover a deleted Iris app from backup', icon: '♻️ ' }
175
- ];
176
-
177
- const maxNameLen = Math.max(...commands.map(c => c.name.length));
178
- commands.forEach(cmd => {
179
- const padding = ' '.repeat(maxNameLen - cmd.name.length);
180
- help += ` ${cmd.icon}${chalk.cyan.bold(cmd.name)}${padding} ${chalk.dim(cmd.desc)}\n`;
181
- });
182
-
183
- help += `\n${chalk.bold.yellow('OPTIONS')}\n`;
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`;
186
- help += ` ${chalk.cyan('-h, --help')} ${chalk.dim('Display this help message')}\n`;
187
-
188
- help += `\n${chalk.bold.yellow('EXAMPLES')}\n`;
189
- help += ` ${chalk.dim('# Initial setup')}\n`;
190
- help += ` ${chalk.cyan('magentrix setup')}\n\n`;
191
- help += ` ${chalk.dim('# Pull remote files')}\n`;
192
- help += ` ${chalk.cyan('magentrix pull')}\n\n`;
193
- help += ` ${chalk.dim('# Auto-sync on file changes')}\n`;
194
- help += ` ${chalk.cyan('magentrix autopublish')}\n`;
195
-
196
- help += `\n${divider}\n`;
197
-
198
- return help;
199
- }
200
- });
201
-
202
- // ── Error Handlers ───────────────────────────
203
- program.showHelpAfterError(false);
204
- program.configureOutput({
205
- outputError: (str, _write) => {
206
- // Custom error message for unknown options
207
- if (str.includes('unknown option')) {
208
- const match = str.match(/'([^']+)'/);
209
- const option = match ? match[1] : str;
210
-
211
- console.error(`\n${chalk.bgRed.white.bold(' ERROR ')} ${chalk.redBright('Unknown option:')} ${chalk.bold(option)}`);
212
- console.error(`${chalk.yellow('💡 Tip:')} Run ${chalk.cyan('magentrix --help')} to see available options.\n`);
213
- }
214
- // Custom error message for unknown commands
215
- else if (str.includes('unknown command')) {
216
- const match = str.match(/'([^']+)'/);
217
- const command = match ? match[1] : str;
218
-
219
- console.error(`\n${chalk.bgRed.white.bold(' ERROR ')} ${chalk.redBright('Unknown command:')} ${chalk.bold(command)}`);
220
- console.error(`${chalk.yellow('💡 Tip:')} Run ${chalk.cyan('magentrix --help')} to see available commands.\n`);
221
- }
222
- // Generic errors
223
- else {
224
- console.error(`\n${chalk.bgRed.white.bold(' ERROR ')} ${chalk.redBright(str.trim())}\n`);
225
- }
226
- }
227
- });
228
-
229
- // ── Commands ─────────────────────────────────
230
- program
231
- .command('setup')
232
- .description('Configure your Magentrix API key')
233
- .option('--api-key <apiKey>', 'Magentrix API key')
234
- .option('--instance-url <instanceUrl>', 'Magentrix instance URL (e.g., https://example.magentrixcloud.com)')
235
- .action(blockInVueProject(setup));
236
- program.command('pull').description('Pull files from the remote server').action(withWorkspaceCheck(pull));
237
- const createCommand = program
238
- .command('create')
239
- .description('Create files locally')
240
- .option('--type <type>', 'Entity type: class, page, or template')
241
- .option('--class-type <classType>', 'Class type: controller, utility, or trigger (for --type class)')
242
- .option('--name <name>', 'Name of the file to create')
243
- .option('--description <description>', 'Optional description')
244
- .option('--entity-id <entityId>', 'Entity ID (required for triggers)')
245
- .action(withWorkspaceCheck(create));
246
-
247
- // Override help for create command to show options
248
- createCommand.configureHelp({
249
- formatHelp: () => {
250
- const divider = chalk.gray('━'.repeat(60));
251
- const titleBar = chalk.bold.bgBlue.white(' Magentrix CLI - Create Command ');
252
-
253
- let help = `\n${divider}\n${titleBar}\n${divider}\n\n`;
254
- help += `${chalk.dim('Create files locally with optional parameters to bypass interactive prompts')}\n\n`;
255
-
256
- help += `${chalk.bold.yellow('USAGE')}\n`;
257
- help += ` ${chalk.cyan('magentrix create')} ${chalk.dim('[options]')}\n\n`;
258
-
259
- help += `${chalk.bold.yellow('OPTIONS')}\n`;
260
- const options = [
261
- { name: '--type <type>', desc: 'Entity type: class, page, or template' },
262
- { name: '--class-type <classType>', desc: 'Class type: controller, utility, or trigger (for --type class)' },
263
- { name: '--name <name>', desc: 'Name of the file to create' },
264
- { name: '--description <description>', desc: 'Optional description' },
265
- { name: '--entity-id <entityId>', desc: 'Entity ID (required for triggers)' },
266
- { name: '-h, --help', desc: 'Display this help message' }
267
- ];
268
-
269
- const maxNameLen = Math.max(...options.map(o => o.name.length));
270
- options.forEach(opt => {
271
- const padding = ' '.repeat(maxNameLen - opt.name.length);
272
- help += ` ${chalk.cyan(opt.name)}${padding} ${chalk.dim(opt.desc)}\n`;
273
- });
274
-
275
- help += `\n${chalk.bold.yellow('EXAMPLES')}\n`;
276
- help += ` ${chalk.dim('# Create a controller non-interactively')}\n`;
277
- help += ` ${chalk.cyan('magentrix create --type class --class-type controller --name UserController')}\n\n`;
278
- help += ` ${chalk.dim('# Create a trigger')}\n`;
279
- help += ` ${chalk.cyan('magentrix create --type class --class-type trigger --entity-id abc123 --name AccountTrigger')}\n\n`;
280
- help += ` ${chalk.dim('# Create a page')}\n`;
281
- help += ` ${chalk.cyan('magentrix create --type page --name HomePage --description "Main landing page"')}\n\n`;
282
- help += ` ${chalk.dim('# Create a template')}\n`;
283
- help += ` ${chalk.cyan('magentrix create --type template --name EmailTemplate')}\n\n`;
284
- help += ` ${chalk.dim('# Mix interactive and non-interactive (will prompt for missing info)')}\n`;
285
- help += ` ${chalk.cyan('magentrix create --name MyClass')}\n`;
286
-
287
- help += `\n${divider}\n`;
288
-
289
- return help;
290
- }
291
- });
292
- program.command('status').description('Show file conflicts').action(withWorkspaceCheck(status));
293
- program.command('autopublish').description('Watch & sync changes in real time').action(withWorkspaceCheck(autoPublish));
294
- // Publish does its own comprehensive file scanning, so skip the pre-cache middleware
295
- program.command('publish').description('Publish pending changes to the remote server').action(withWorkspaceCheck(publish));
296
- program.command('update').description('Update MagentrixCLI to the latest version').action(update);
297
-
298
- // Config command - interactive wizard
299
- program
300
- .command('config')
301
- .description('Configure CLI settings')
302
- .action(requireMagentrixWorkspace(configWizard));
303
-
304
- // Iris commands for Vue.js app management
305
- program
306
- .command('iris-app-link')
307
- .description('Link a Vue project to the CLI for deployment')
308
- .option('--path <path>', 'Path to the Vue project')
309
- .option('--unlink', 'Unlink a project instead of linking')
310
- .option('--list', 'List all linked projects')
311
- .option('--cleanup', 'Remove invalid (non-existent) linked projects')
312
- .action(irisLink);
313
-
314
- program
315
- .command('vue-run-build')
316
- .description('Build a Vue project and stage it for publishing')
317
- .option('--path <path>', 'Path to the Vue project')
318
- .option('--skip-build', 'Skip build step and use existing dist/')
319
- .option('--workspace <workspace>', 'Path to Magentrix workspace to stage into')
320
- .action(vueBuildStage);
321
-
322
- program
323
- .command('vue-run-dev')
324
- .description('Start Vue dev server with platform assets injected')
325
- .option('--path <path>', 'Path to the Vue project')
326
- .option('--no-inject', 'Skip asset injection')
327
- .action(irisDev);
328
-
329
- program
330
- .command('iris-app-delete')
331
- .description('Delete a published Iris app with recovery backup')
332
- .action(irisDelete);
333
-
334
- program
335
- .command('iris-app-recover')
336
- .description('Recover a deleted Iris app from backup')
337
- .option('--list', 'List available recovery backups')
338
- .action(irisRecover);
339
-
340
- // ── Unknown Command Handler ──────────────────
341
- program.argument('[command]', 'command to run').action((cmd) => {
342
- const runMain = async () => {
343
- // Check if in Vue project
344
- if (isInVueProject()) {
345
- console.error(`\n${chalk.bgRed.white.bold(' ERROR ')} ${chalk.redBright('This command must be run in a Magentrix workspace')}\n`);
346
- console.error(chalk.yellow('It looks like you\'re in a Vue project directory.'));
347
- console.error(chalk.gray('This command requires a Magentrix workspace with global API key and instance URL.\n'));
348
- console.error(chalk.cyan('Available commands in Vue projects:'));
349
- console.error(chalk.gray(' • magentrix iris-app-link'));
350
- console.error(chalk.gray(' • magentrix vue-run-build'));
351
- console.error(chalk.gray(' • magentrix vue-run-dev'));
352
- console.error(chalk.gray(' • magentrix update\n'));
353
- process.exit(1);
354
- }
355
-
356
- await preMiddleware();
357
- await main();
358
- await postMiddleware();
359
- };
360
-
361
- if (cmd) {
362
- console.log(`\n${chalk.bgRed.white.bold(' ERROR ')} ${chalk.redBright(`Unknown command:`)} ${chalk.bold(cmd)}\n`);
363
- console.log(`${chalk.yellow('💡 Tip:')} Run ${chalk.cyan('magentrix --help')} to view available commands.\n`);
364
- process.exit(1);
365
- } else {
366
- runMain().catch(handleFatal);
367
- }
368
- });
369
-
370
- // ── Global Error Handler ─────────────────────
371
- function handleFatal(err) {
372
- const divider = chalk.gray('──────────────────────────────────────────────');
373
- const header = `${chalk.bgRed.white.bold(' FATAL ERROR ')}`;
374
-
375
- console.error(`\n${divider}\n${header}`);
376
- console.error(`${chalk.redBright(err?.message || 'An unexpected error occurred.')}\n`);
377
-
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
- }
385
- } else {
386
- console.log(`${chalk.yellow('💡 Run with')} ${chalk.cyan('--debug')} ${chalk.yellow('for full details.')}`);
387
- }
388
-
389
- console.log(divider + '\n');
390
- process.exit(1);
391
- }
392
-
393
- program.parseAsync(process.argv).catch(handleFatal);
1
+ #!/usr/bin/env node
2
+
3
+ // Imports
4
+ import { Command } from 'commander';
5
+ import chalk from 'chalk';
6
+ import { existsSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { VERSION } from '../vars/config.js';
9
+ import { setup } from '../actions/setup.js';
10
+ import { main } from '../actions/main.js';
11
+ import { pull } from '../actions/pull.js';
12
+ import { create } from '../actions/create.js';
13
+ import { autoPublish } from '../actions/autopublish.js';
14
+ import { status } from '../actions/status.js';
15
+ import { cacheDir, recacheFileIdIndex } from '../utils/cacher.js';
16
+ import { CWD, EXPORT_ROOT, HASHED_CWD } from '../vars/global.js';
17
+ import { publish } from '../actions/publish.js';
18
+ import { update } from '../actions/update.js';
19
+ import { configWizard } from '../actions/config.js';
20
+ import { irisLink, irisDev, irisDelete, irisRecover, vueBuildStage } from '../actions/iris/index.js';
21
+ import Config from '../utils/config.js';
22
+ import { registerWorkspace, getRegisteredWorkspaces } from '../utils/workspaces.js';
23
+ import { ensureVSCodeFileAssociation } from '../utils/preferences.js';
24
+ import debug from '../utils/debug.js';
25
+
26
+ const config = new Config();
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
+
35
+ // ── Vue Project Detection ────────────────────────────────
36
+ /**
37
+ * Check if current directory is a Vue project (has config.ts)
38
+ */
39
+ function isInVueProject() {
40
+ const configLocations = [
41
+ 'src/config.ts',
42
+ 'config.ts',
43
+ 'src/iris-config.ts',
44
+ 'iris-config.ts'
45
+ ];
46
+ return configLocations.some(loc => existsSync(join(process.cwd(), loc)));
47
+ }
48
+
49
+ /**
50
+ * Block non-iris commands when in a Vue project
51
+ */
52
+ function requireMagentrixWorkspace(fn) {
53
+ return async (...args) => {
54
+ if (isInVueProject()) {
55
+ console.error(`\n${chalk.bgRed.white.bold(' ERROR ')} ${chalk.redBright('This command must be run in a Magentrix workspace')}\n`);
56
+ console.error(chalk.yellow('It looks like you\'re in a Vue project directory.'));
57
+ console.error(chalk.gray('This command requires a Magentrix workspace with global API key and instance URL.\n'));
58
+ console.error(chalk.cyan('Available commands in Vue projects:'));
59
+ console.error(chalk.gray(' • magentrix iris-app-link'));
60
+ console.error(chalk.gray(' • magentrix vue-run-build'));
61
+ console.error(chalk.gray(' • magentrix vue-run-dev'));
62
+ console.error(chalk.gray(' • magentrix update\n'));
63
+ process.exit(1);
64
+ }
65
+ // Execute the command
66
+ await fn(...args);
67
+ };
68
+ }
69
+
70
+ /**
71
+ * Block commands that should not run in Vue projects (but don't require a workspace)
72
+ */
73
+ function blockInVueProject(fn) {
74
+ return async (...args) => {
75
+ if (isInVueProject()) {
76
+ console.error(`\n${chalk.bgRed.white.bold(' ERROR ')} ${chalk.redBright('This command cannot be run in a Vue project')}\n`);
77
+ console.error(chalk.yellow('It looks like you\'re in a Vue project directory.'));
78
+ console.error(chalk.gray('Please run this command from a Magentrix workspace or an empty directory.\n'));
79
+ process.exit(1);
80
+ }
81
+ // Execute the command
82
+ await fn(...args);
83
+ };
84
+ }
85
+
86
+ // ── Middleware ────────────────────────────────
87
+
88
+ /**
89
+ * Auto-register existing workspaces for backwards compatibility.
90
+ * If the current directory has credentials configured but isn't in the
91
+ * global workspace registry, register it automatically.
92
+ */
93
+ function ensureWorkspaceRegistered() {
94
+ // Check if current directory has credentials configured
95
+ const instanceUrl = config.read('instanceUrl', { global: true, pathHash: HASHED_CWD });
96
+ const apiKey = config.read('apiKey', { global: true, pathHash: HASHED_CWD });
97
+
98
+ if (!instanceUrl || !apiKey) {
99
+ return; // Not a configured workspace
100
+ }
101
+
102
+ // Check if already registered
103
+ const workspaces = getRegisteredWorkspaces();
104
+ const alreadyRegistered = workspaces.some(w => w.path === CWD);
105
+
106
+ if (!alreadyRegistered) {
107
+ // Auto-register for backwards compatibility
108
+ registerWorkspace(CWD, instanceUrl);
109
+ }
110
+ }
111
+
112
+ async function preMiddleware() {
113
+ ensureWorkspaceRegistered();
114
+
115
+ // Ensure .vscode folder exists in project root (not in src/) for Magentrix projects
116
+ const magentrixDir = join(CWD, '.magentrix');
117
+ if (existsSync(magentrixDir)) {
118
+ await ensureVSCodeFileAssociation(CWD);
119
+ }
120
+
121
+ await recacheFileIdIndex(EXPORT_ROOT);
122
+ await cacheDir(EXPORT_ROOT);
123
+ }
124
+ async function postMiddleware() {
125
+ await cacheDir(EXPORT_ROOT);
126
+ }
127
+
128
+ const withMiddleware = ({ pre, post }) => (fn) => async (...args) => {
129
+ if (pre) await pre(...args);
130
+ await fn(...args);
131
+ if (post) await post(...args);
132
+ };
133
+
134
+ const withDefault = withMiddleware({ pre: preMiddleware, post: postMiddleware });
135
+
136
+ // Combined wrapper: check for Vue project + run middleware
137
+ const withWorkspaceCheck = (fn) => requireMagentrixWorkspace(withDefault(fn));
138
+
139
+ // ── CLI Setup ────────────────────────────────
140
+ const program = new Command();
141
+ program
142
+ .name('magentrix')
143
+ .description('Manage Magentrix assets and automation')
144
+ .version(VERSION)
145
+ .option('--debug', 'Enable debug logging')
146
+ .configureHelp({
147
+ formatHelp: (_cmd, _helper) => {
148
+ const divider = chalk.gray('━'.repeat(60));
149
+ const titleBar = chalk.bold.bgBlue.white(' Magentrix CLI ');
150
+ const version = chalk.dim(`v${VERSION}`);
151
+
152
+ let help = `\n${divider}\n${titleBar} ${version}\n${divider}\n\n`;
153
+ help += `${chalk.dim('Manage Magentrix assets and automation')}\n\n`;
154
+
155
+ // Usage section
156
+ help += `${chalk.bold.yellow('USAGE')}\n`;
157
+ help += ` ${chalk.cyan('magentrix')} ${chalk.dim('<command> [options]')}\n\n`;
158
+
159
+ // Commands section
160
+ help += `${chalk.bold.yellow('COMMANDS')}\n`;
161
+ const commands = [
162
+ { name: 'setup', desc: 'Configure your Magentrix API key', icon: '⚙️ ' },
163
+ { name: 'config', desc: 'Manage CLI settings', icon: '🔧 ' },
164
+ { name: 'pull', desc: 'Pull files from the remote server', icon: '📥 ' },
165
+ { name: 'create', desc: 'Create files locally', icon: '✨ ' },
166
+ { name: 'status', desc: 'Show file conflicts and sync status', icon: '📊 ' },
167
+ { name: 'publish', desc: 'Publish pending changes to the remote server', icon: '📤 ' },
168
+ { name: 'autopublish', desc: 'Watch & sync changes in real time', icon: '🔄 ' },
169
+ { name: 'update', desc: 'Update MagentrixCLI to the latest version', icon: '⬆️ ' },
170
+ { name: 'iris-app-link', desc: 'Link a Vue project to the CLI', icon: '🔗 ' },
171
+ { name: 'vue-run-build', desc: 'Build Vue project and stage for publish', icon: '🏗️ ' },
172
+ { name: 'vue-run-dev', desc: 'Start Vue dev server with platform assets', icon: '🌐 ' },
173
+ { name: 'iris-app-delete', desc: 'Delete an Iris app with backup', icon: '🗑️ ' },
174
+ { name: 'iris-app-recover', desc: 'Recover a deleted Iris app from backup', icon: '♻️ ' }
175
+ ];
176
+
177
+ const maxNameLen = Math.max(...commands.map(c => c.name.length));
178
+ commands.forEach(cmd => {
179
+ const padding = ' '.repeat(maxNameLen - cmd.name.length);
180
+ help += ` ${cmd.icon}${chalk.cyan.bold(cmd.name)}${padding} ${chalk.dim(cmd.desc)}\n`;
181
+ });
182
+
183
+ help += `\n${chalk.bold.yellow('OPTIONS')}\n`;
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`;
186
+ help += ` ${chalk.cyan('-h, --help')} ${chalk.dim('Display this help message')}\n`;
187
+
188
+ help += `\n${chalk.bold.yellow('EXAMPLES')}\n`;
189
+ help += ` ${chalk.dim('# Initial setup')}\n`;
190
+ help += ` ${chalk.cyan('magentrix setup')}\n\n`;
191
+ help += ` ${chalk.dim('# Pull remote files')}\n`;
192
+ help += ` ${chalk.cyan('magentrix pull')}\n\n`;
193
+ help += ` ${chalk.dim('# Auto-sync on file changes')}\n`;
194
+ help += ` ${chalk.cyan('magentrix autopublish')}\n`;
195
+
196
+ help += `\n${divider}\n`;
197
+
198
+ return help;
199
+ }
200
+ });
201
+
202
+ // ── Error Handlers ───────────────────────────
203
+ program.showHelpAfterError(false);
204
+ program.configureOutput({
205
+ outputError: (str, _write) => {
206
+ // Custom error message for unknown options
207
+ if (str.includes('unknown option')) {
208
+ const match = str.match(/'([^']+)'/);
209
+ const option = match ? match[1] : str;
210
+
211
+ console.error(`\n${chalk.bgRed.white.bold(' ERROR ')} ${chalk.redBright('Unknown option:')} ${chalk.bold(option)}`);
212
+ console.error(`${chalk.yellow('💡 Tip:')} Run ${chalk.cyan('magentrix --help')} to see available options.\n`);
213
+ }
214
+ // Custom error message for unknown commands
215
+ else if (str.includes('unknown command')) {
216
+ const match = str.match(/'([^']+)'/);
217
+ const command = match ? match[1] : str;
218
+
219
+ console.error(`\n${chalk.bgRed.white.bold(' ERROR ')} ${chalk.redBright('Unknown command:')} ${chalk.bold(command)}`);
220
+ console.error(`${chalk.yellow('💡 Tip:')} Run ${chalk.cyan('magentrix --help')} to see available commands.\n`);
221
+ }
222
+ // Generic errors
223
+ else {
224
+ console.error(`\n${chalk.bgRed.white.bold(' ERROR ')} ${chalk.redBright(str.trim())}\n`);
225
+ }
226
+ }
227
+ });
228
+
229
+ // ── Commands ─────────────────────────────────
230
+ program
231
+ .command('setup')
232
+ .description('Configure your Magentrix API key')
233
+ .option('--api-key <apiKey>', 'Magentrix API key')
234
+ .option('--instance-url <instanceUrl>', 'Magentrix instance URL (e.g., https://example.magentrixcloud.com)')
235
+ .action(blockInVueProject(setup));
236
+ program.command('pull').description('Pull files from the remote server').action(withWorkspaceCheck(pull));
237
+ const createCommand = program
238
+ .command('create')
239
+ .description('Create files locally')
240
+ .option('--type <type>', 'Entity type: class, page, or template')
241
+ .option('--class-type <classType>', 'Class type: controller, utility, or trigger (for --type class)')
242
+ .option('--name <name>', 'Name of the file to create')
243
+ .option('--description <description>', 'Optional description')
244
+ .option('--entity-id <entityId>', 'Entity ID (required for triggers)')
245
+ .action(withWorkspaceCheck(create));
246
+
247
+ // Override help for create command to show options
248
+ createCommand.configureHelp({
249
+ formatHelp: () => {
250
+ const divider = chalk.gray('━'.repeat(60));
251
+ const titleBar = chalk.bold.bgBlue.white(' Magentrix CLI - Create Command ');
252
+
253
+ let help = `\n${divider}\n${titleBar}\n${divider}\n\n`;
254
+ help += `${chalk.dim('Create files locally with optional parameters to bypass interactive prompts')}\n\n`;
255
+
256
+ help += `${chalk.bold.yellow('USAGE')}\n`;
257
+ help += ` ${chalk.cyan('magentrix create')} ${chalk.dim('[options]')}\n\n`;
258
+
259
+ help += `${chalk.bold.yellow('OPTIONS')}\n`;
260
+ const options = [
261
+ { name: '--type <type>', desc: 'Entity type: class, page, or template' },
262
+ { name: '--class-type <classType>', desc: 'Class type: controller, utility, or trigger (for --type class)' },
263
+ { name: '--name <name>', desc: 'Name of the file to create' },
264
+ { name: '--description <description>', desc: 'Optional description' },
265
+ { name: '--entity-id <entityId>', desc: 'Entity ID (required for triggers)' },
266
+ { name: '-h, --help', desc: 'Display this help message' }
267
+ ];
268
+
269
+ const maxNameLen = Math.max(...options.map(o => o.name.length));
270
+ options.forEach(opt => {
271
+ const padding = ' '.repeat(maxNameLen - opt.name.length);
272
+ help += ` ${chalk.cyan(opt.name)}${padding} ${chalk.dim(opt.desc)}\n`;
273
+ });
274
+
275
+ help += `\n${chalk.bold.yellow('EXAMPLES')}\n`;
276
+ help += ` ${chalk.dim('# Create a controller non-interactively')}\n`;
277
+ help += ` ${chalk.cyan('magentrix create --type class --class-type controller --name UserController')}\n\n`;
278
+ help += ` ${chalk.dim('# Create a trigger')}\n`;
279
+ help += ` ${chalk.cyan('magentrix create --type class --class-type trigger --entity-id abc123 --name AccountTrigger')}\n\n`;
280
+ help += ` ${chalk.dim('# Create a page')}\n`;
281
+ help += ` ${chalk.cyan('magentrix create --type page --name HomePage --description "Main landing page"')}\n\n`;
282
+ help += ` ${chalk.dim('# Create a template')}\n`;
283
+ help += ` ${chalk.cyan('magentrix create --type template --name EmailTemplate')}\n\n`;
284
+ help += ` ${chalk.dim('# Mix interactive and non-interactive (will prompt for missing info)')}\n`;
285
+ help += ` ${chalk.cyan('magentrix create --name MyClass')}\n`;
286
+
287
+ help += `\n${divider}\n`;
288
+
289
+ return help;
290
+ }
291
+ });
292
+ program.command('status').description('Show file conflicts').action(withWorkspaceCheck(status));
293
+ program.command('autopublish').description('Watch & sync changes in real time').action(withWorkspaceCheck(autoPublish));
294
+ // Publish does its own comprehensive file scanning, so skip the pre-cache middleware
295
+ program.command('publish').description('Publish pending changes to the remote server').action(withWorkspaceCheck(publish));
296
+ program.command('update').description('Update MagentrixCLI to the latest version').action(update);
297
+
298
+ // Config command - interactive wizard
299
+ program
300
+ .command('config')
301
+ .description('Configure CLI settings')
302
+ .action(requireMagentrixWorkspace(configWizard));
303
+
304
+ // Iris commands for Vue.js app management
305
+ program
306
+ .command('iris-app-link')
307
+ .description('Link a Vue project to the CLI for deployment')
308
+ .option('--path <path>', 'Path to the Vue project')
309
+ .option('--unlink', 'Unlink a project instead of linking')
310
+ .option('--list', 'List all linked projects')
311
+ .option('--cleanup', 'Remove invalid (non-existent) linked projects')
312
+ .action(irisLink);
313
+
314
+ program
315
+ .command('vue-run-build')
316
+ .description('Build a Vue project and stage it for publishing')
317
+ .option('--path <path>', 'Path to the Vue project')
318
+ .option('--skip-build', 'Skip build step and use existing dist/')
319
+ .option('--workspace <workspace>', 'Path to Magentrix workspace to stage into')
320
+ .action(vueBuildStage);
321
+
322
+ program
323
+ .command('vue-run-dev')
324
+ .description('Start Vue dev server with platform assets injected')
325
+ .option('--path <path>', 'Path to the Vue project')
326
+ .option('--no-inject', 'Skip asset injection')
327
+ .action(irisDev);
328
+
329
+ program
330
+ .command('iris-app-delete')
331
+ .description('Delete a published Iris app with recovery backup')
332
+ .action(irisDelete);
333
+
334
+ program
335
+ .command('iris-app-recover')
336
+ .description('Recover a deleted Iris app from backup')
337
+ .option('--list', 'List available recovery backups')
338
+ .action(irisRecover);
339
+
340
+ // ── Unknown Command Handler ──────────────────
341
+ program.argument('[command]', 'command to run').action((cmd) => {
342
+ const runMain = async () => {
343
+ // Check if in Vue project
344
+ if (isInVueProject()) {
345
+ console.error(`\n${chalk.bgRed.white.bold(' ERROR ')} ${chalk.redBright('This command must be run in a Magentrix workspace')}\n`);
346
+ console.error(chalk.yellow('It looks like you\'re in a Vue project directory.'));
347
+ console.error(chalk.gray('This command requires a Magentrix workspace with global API key and instance URL.\n'));
348
+ console.error(chalk.cyan('Available commands in Vue projects:'));
349
+ console.error(chalk.gray(' • magentrix iris-app-link'));
350
+ console.error(chalk.gray(' • magentrix vue-run-build'));
351
+ console.error(chalk.gray(' • magentrix vue-run-dev'));
352
+ console.error(chalk.gray(' • magentrix update\n'));
353
+ process.exit(1);
354
+ }
355
+
356
+ await preMiddleware();
357
+ await main();
358
+ await postMiddleware();
359
+ };
360
+
361
+ if (cmd) {
362
+ console.log(`\n${chalk.bgRed.white.bold(' ERROR ')} ${chalk.redBright(`Unknown command:`)} ${chalk.bold(cmd)}\n`);
363
+ console.log(`${chalk.yellow('💡 Tip:')} Run ${chalk.cyan('magentrix --help')} to view available commands.\n`);
364
+ process.exit(1);
365
+ } else {
366
+ runMain().catch(handleFatal);
367
+ }
368
+ });
369
+
370
+ // ── Global Error Handler ─────────────────────
371
+ function handleFatal(err) {
372
+ const divider = chalk.gray('──────────────────────────────────────────────');
373
+ const header = `${chalk.bgRed.white.bold(' FATAL ERROR ')}`;
374
+
375
+ console.error(`\n${divider}\n${header}`);
376
+ console.error(`${chalk.redBright(err?.message || 'An unexpected error occurred.')}\n`);
377
+
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
+ }
385
+ } else {
386
+ console.log(`${chalk.yellow('💡 Run with')} ${chalk.cyan('--debug')} ${chalk.yellow('for full details.')}`);
387
+ }
388
+
389
+ console.log(divider + '\n');
390
+ process.exit(1);
391
+ }
392
+
393
+ program.parseAsync(process.argv).catch(handleFatal);