@magentrix-corp/magentrix-cli 1.3.3 → 1.3.5

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/README.md CHANGED
@@ -491,32 +491,38 @@ magentrix vue-build-stage
491
491
  ```
492
492
  **What it does**: Builds a linked Vue project and copies the output to `src/iris-apps/<slug>/` for publishing.
493
493
 
494
+ **Two modes of operation:**
495
+
496
+ 1. **From Magentrix workspace**: Prompts for which Vue project to build, stages to current workspace
497
+ 2. **From Vue project directory**: Prompts for which Magentrix workspace to stage into
498
+
494
499
  **Options:**
495
500
  - `--path <dir>` - Specify Vue project path directly
496
501
  - `--skip-build` - Use existing `dist/` folder without rebuilding
502
+ - `--workspace <dir>` - Specify Magentrix workspace path directly (when running from Vue project)
497
503
 
498
504
  **Process:**
499
505
  1. Select from linked projects or enter path manually
500
506
  2. Run `npm run build` in the Vue project
501
507
  3. Validate the build output
502
- 4. Copy to CLI workspace `src/iris-apps/<app-slug>/`
508
+ 4. Copy to workspace `src/iris-apps/<app-slug>/`
503
509
 
504
510
  #### Vue Development Server
505
511
  ```bash
506
- magentrix iris-dev
512
+ magentrix run-dev
507
513
  ```
508
514
  **What it does**: Starts the Vue development server with platform assets (CSS, fonts) automatically injected from your Magentrix instance.
509
515
 
516
+ **Authentication**: Uses `VITE_REFRESH_TOKEN` from `.env.development` (no CLI authentication required). This command can be run inside the Vue project directory.
517
+
510
518
  **Options:**
511
519
  - `--path <dir>` - Specify Vue project path
512
520
  - `--no-inject` - Skip asset injection, just run dev server
513
- - `--restore` - Restore `.env.development` from backup without running
514
521
 
515
522
  **Process:**
516
- 1. Fetch platform assets from Magentrix
517
- 2. Backup `.env.development` and inject assets
523
+ 1. Fetch platform assets from Magentrix using `.env.development` credentials
524
+ 2. Update `VITE_ASSETS` in `.env.development` (changes are kept)
518
525
  3. Run `npm run dev`
519
- 4. Restore `.env.development` on exit (Ctrl+C)
520
526
 
521
527
  #### Delete an Iris App
522
528
  ```bash
@@ -577,7 +583,7 @@ export const config = {
577
583
  ```bash
578
584
  VITE_SITE_URL = https://yourinstance.magentrix.com
579
585
  VITE_REFRESH_TOKEN = your-api-key
580
- VITE_ASSETS = '[]' # Injected automatically by iris-dev
586
+ VITE_ASSETS = '[]' # Injected automatically by run-dev
581
587
  ```
582
588
 
583
589
  **Accepted field names in config.ts:**
@@ -586,28 +592,48 @@ VITE_ASSETS = '[]' # Injected automatically by iris-dev
586
592
  - Description: `appDescription` or `app_description`
587
593
  - Icon: `appIconId` or `app_icon_id`
588
594
 
595
+ ### Command Availability
596
+
597
+ **In Vue project directories** (detected by presence of `config.ts`):
598
+ - ✓ `magentrix iris-link` - Link project to CLI
599
+ - ✓ `magentrix vue-build-stage` - Build and stage (prompts for target workspace)
600
+ - ✓ `magentrix run-dev` - Start dev server (uses `.env.development` credentials)
601
+ - ✓ `magentrix update` - Update CLI to latest version
602
+
603
+ **In Magentrix workspace directories** (has `.magentrix/` folder):
604
+ - ✓ All standard commands (`setup`, `pull`, `publish`, etc.)
605
+ - ✓ All Iris commands (`vue-build-stage`, `iris-delete`, `iris-recover`, etc.)
606
+
607
+ **Note**: The `setup` command cannot be run inside a Vue project directory - run it from your Magentrix workspace. Commands like `pull`, `publish`, `autopublish` require a Magentrix workspace.
608
+
589
609
  ### Typical Development Workflow
590
610
 
591
611
  ```bash
592
612
  # First time setup
593
613
  magentrix iris-link # Link your Vue project
594
614
 
595
- # Development
596
- magentrix iris-dev # Start dev server with platform assets
615
+ # Development (run from Vue project folder)
616
+ cd ~/my-vue-app # Work from your Vue project
617
+ magentrix run-dev # Start dev server with platform assets
597
618
  # Make changes, test locally
598
619
  # Press Ctrl+C to stop
599
620
 
600
- # Deployment (run from Magentrix workspace, not Vue project folder)
621
+ # Deployment - Option A (from Vue project folder)
622
+ cd ~/my-vue-app # Work from your Vue project
623
+ magentrix vue-build-stage # Build and select workspace to stage into
624
+ # Follow prompts, then navigate to workspace to publish
625
+
626
+ # Deployment - Option B (from Magentrix workspace)
601
627
  cd ~/magentrix-workspace # Navigate to Magentrix workspace
602
628
  magentrix vue-build-stage --path ~/my-vue-app # Build and stage
603
629
  # Prompted: "Do you want to publish to Magentrix now?" → Yes/No
604
630
  # If autopublish is running, it auto-deploys instead
605
631
 
606
- # Deleting an app
632
+ # Deleting an app (from workspace)
607
633
  magentrix iris-delete # Select app, confirm, auto-backup created
608
634
  magentrix publish # Sync deletion to server
609
635
 
610
- # Recovering a deleted app
636
+ # Recovering a deleted app (from workspace)
611
637
  magentrix iris-recover # Select backup, restore files
612
638
  magentrix publish # Sync recovery to server
613
639
  ```
@@ -627,29 +653,31 @@ magentrix vue-build-stage
627
653
 
628
654
  ### Troubleshooting Iris Apps
629
655
 
630
- #### "Warning: Magentrix Workspace Not Detected"
631
- This warning appears when running `magentrix vue-build-stage` outside your Magentrix CLI workspace.
656
+ #### Running vue-build-stage from different locations
632
657
 
633
- **Why it happens:**
634
- - The command stages build files to `src/iris-apps/<slug>/` in your Magentrix workspace
635
- - You ran it from your Vue project directory or another location
658
+ The `vue-build-stage` command works from both locations:
636
659
 
637
- **How to fix:**
638
- 1. Navigate to your Magentrix CLI workspace (the folder with `.magentrix/` and `src/`)
639
- 2. Run the command from there
640
- 3. Use `--path` to specify your Vue project: `magentrix vue-build-stage --path /path/to/vue-project`
641
-
642
- **Example:**
660
+ **From Vue project directory:**
643
661
  ```bash
644
- # Wrong - running from Vue project
645
662
  cd ~/my-vue-app
646
- magentrix vue-build-stage # Warning!
663
+ magentrix vue-build-stage # Prompts for which workspace to stage into
664
+ ```
647
665
 
648
- # Right - running from Magentrix workspace
666
+ **From Magentrix workspace:**
667
+ ```bash
649
668
  cd ~/magentrix-workspace
650
- magentrix vue-build-stage --path ~/my-vue-app # Works!
669
+ magentrix vue-build-stage # Prompts for which Vue project to build
670
+ # Or with path: magentrix vue-build-stage --path ~/my-vue-app
651
671
  ```
652
672
 
673
+ #### "No Magentrix workspaces found"
674
+ When running from a Vue project, the command looks for registered workspaces in the global config. To register a workspace:
675
+ 1. Navigate to your Magentrix workspace
676
+ 2. Run `magentrix setup` (this automatically registers it)
677
+ 3. Or specify workspace manually: `magentrix vue-build-stage --workspace /path/to/workspace`
678
+
679
+ **Note**: Existing workspaces are auto-registered when you run any command from them.
680
+
653
681
  #### "Missing required field in config.ts: slug (appPath)"
654
682
  Your Vue project's `config.ts` is missing the app identifier. Add an `appPath` or `slug` field.
655
683
 
@@ -751,16 +779,24 @@ magentrix status # Verify everything is in sync
751
779
  ```
752
780
 
753
781
  ### Deploying a Vue.js App
754
- ```bash
755
- # Important: Run from your Magentrix CLI workspace, NOT the Vue project folder
756
- cd ~/magentrix-workspace # Navigate to Magentrix workspace
757
782
 
783
+ **Option A: From Vue project folder**
784
+ ```bash
785
+ cd ~/my-vue-app # Navigate to your Vue project
758
786
  magentrix iris-link # Link project (first time only)
787
+ magentrix vue-build-stage # Build and select workspace to stage into
788
+ # Navigate to workspace and run: magentrix publish
789
+ ```
790
+
791
+ **Option B: From Magentrix workspace**
792
+ ```bash
793
+ cd ~/magentrix-workspace # Navigate to Magentrix workspace
794
+ magentrix iris-link --path ~/my-vue-app # Link project (first time only)
759
795
  magentrix vue-build-stage --path ~/my-vue-app # Build and stage
760
796
  # Prompted: "Do you want to publish to Magentrix now?" (unless autopublish is running)
761
797
  ```
762
798
 
763
- **Note:** The `vue-build-stage` command must be run from your Magentrix CLI workspace directory (where `.magentrix/` and `src/` folders are). Use `--path` to specify your Vue project location.
799
+ **Note:** When running from a Vue project, the command prompts you to select a registered workspace. Workspaces are auto-registered when you run any command from them.
764
800
 
765
801
  ### Deleting and Recovering Apps
766
802
  ```bash
@@ -1058,7 +1094,7 @@ magentrix status # Shows sync status
1058
1094
  - `magentrix update` - Update to latest version
1059
1095
  - `magentrix iris-link` - Link Vue.js projects for deployment
1060
1096
  - `magentrix vue-build-stage` - Build and stage Vue.js apps
1061
- - `magentrix iris-dev` - Start Vue dev server with platform assets
1097
+ - `magentrix run-dev` - Start Vue dev server with platform assets
1062
1098
  - `magentrix iris-delete` - Delete an Iris app with recovery backup
1063
1099
  - `magentrix iris-recover` - Recover a deleted Iris app
1064
1100
 
@@ -1072,6 +1108,6 @@ Once you're comfortable with basic usage:
1072
1108
  2. **Learn the autopublish workflow** for faster development
1073
1109
  3. **Explore conflict resolution** if you work in a team
1074
1110
  4. **Check out templates** created by the `create` command to understand best practices
1075
- 5. **Deploy Vue.js apps** using `iris-link`, `vue-build-stage`, and `iris-dev` commands
1111
+ 5. **Deploy Vue.js apps** using `iris-link`, `vue-build-stage`, and `run-dev` commands
1076
1112
 
1077
1113
  Happy coding with MagentrixCLI! 🚀
@@ -2,12 +2,13 @@ import chalk from 'chalk';
2
2
  import { select, input, confirm } from '@inquirer/prompts';
3
3
  import { existsSync } from 'node:fs';
4
4
  import { resolve, join } from 'node:path';
5
+ import { spawn } from 'node:child_process';
5
6
  import Config from '../../utils/config.js';
6
7
  import { runPublish } from '../publish.js';
7
8
  import { isAutopublishRunning } from '../../utils/autopublishLock.js';
8
9
  import {
9
10
  buildVueProject,
10
- stageToCliProject,
11
+ stageToWorkspace,
11
12
  findDistDirectory,
12
13
  formatBuildError,
13
14
  formatValidationError
@@ -25,9 +26,24 @@ import {
25
26
  buildProjectChoices
26
27
  } from '../../utils/iris/linker.js';
27
28
  import { EXPORT_ROOT, IRIS_APPS_DIR, HASHED_CWD } from '../../vars/global.js';
29
+ import { getValidWorkspaces } from '../../utils/workspaces.js';
28
30
 
29
31
  const config = new Config();
30
32
 
33
+ /**
34
+ * Check if the current directory is a Vue project (has config.ts).
35
+ * @returns {boolean}
36
+ */
37
+ function isInVueProject() {
38
+ const configLocations = [
39
+ 'src/config.ts',
40
+ 'config.ts',
41
+ 'src/iris-config.ts',
42
+ 'iris-config.ts'
43
+ ];
44
+ return configLocations.some(loc => existsSync(join(process.cwd(), loc)));
45
+ }
46
+
31
47
  /**
32
48
  * Check if the current directory appears to be a Magentrix workspace.
33
49
  * @returns {boolean} - True if in a Magentrix workspace
@@ -52,17 +68,33 @@ function isMagentrixWorkspace() {
52
68
  /**
53
69
  * vue-build-stage command - Build a Vue project and stage to CLI workspace.
54
70
  *
71
+ * Two modes of operation:
72
+ * 1. Run from Magentrix workspace: prompts for which Vue project to build
73
+ * 2. Run from Vue project: prompts for which workspace to stage into
74
+ *
55
75
  * Options:
56
- * --path <dir> Specify Vue project path directly
57
- * --skip-build Use existing dist/ without rebuilding
76
+ * --path <dir> Specify Vue project path directly
77
+ * --skip-build Use existing dist/ without rebuilding
78
+ * --workspace <dir> Specify Magentrix workspace path directly
58
79
  */
59
80
  export const vueBuildStage = async (options = {}) => {
60
81
  process.stdout.write('\x1Bc'); // Clear console
61
82
 
62
- const { path: pathOption, skipBuild } = options;
83
+ const { path: pathOption, skipBuild, workspace: workspaceOption } = options;
84
+
85
+ // Detect which mode we're in
86
+ const inVueProject = isInVueProject();
87
+ const inWorkspace = isMagentrixWorkspace();
63
88
 
89
+ // If run from a Vue project, use reversed logic
90
+ if (inVueProject && !inWorkspace) {
91
+ await buildFromVueProject(options);
92
+ return;
93
+ }
94
+
95
+ // Standard mode: run from workspace, select Vue project
64
96
  // Warn if not in a Magentrix workspace
65
- if (!isMagentrixWorkspace()) {
97
+ if (!inWorkspace) {
66
98
  console.log(chalk.yellow('⚠ Warning: Magentrix Workspace Not Detected'));
67
99
  console.log(chalk.gray('─'.repeat(48)));
68
100
  console.log(chalk.white('\nThis command should be run from your Magentrix CLI workspace directory.'));
@@ -200,11 +232,11 @@ export const vueBuildStage = async (options = {}) => {
200
232
  console.log(chalk.green('\u2713 Build output validated'));
201
233
  }
202
234
 
203
- // Stage to CLI project
235
+ // Stage to CLI project (current workspace)
204
236
  console.log();
205
237
  console.log(chalk.blue('Staging to CLI workspace...'));
206
238
 
207
- const stageResult = stageToCliProject(distPath, slug);
239
+ const stageResult = stageToWorkspace(distPath, slug, process.cwd());
208
240
 
209
241
  if (!stageResult.success) {
210
242
  console.log(chalk.red(`Failed to stage: ${stageResult.error}`));
@@ -251,6 +283,295 @@ export const vueBuildStage = async (options = {}) => {
251
283
  }
252
284
  };
253
285
 
286
+ /**
287
+ * Build and stage when running from inside a Vue project.
288
+ * Prompts user to select which workspace to stage into.
289
+ */
290
+ async function buildFromVueProject(options) {
291
+ const { skipBuild, workspace: workspaceOption } = options;
292
+
293
+ // Use current directory as Vue project
294
+ const projectPath = process.cwd();
295
+ const vueConfig = readVueConfig(projectPath);
296
+
297
+ // Validate Vue config
298
+ if (!vueConfig.found) {
299
+ console.log(chalk.red(formatMissingConfigError(projectPath)));
300
+ return;
301
+ }
302
+
303
+ if (vueConfig.errors.length > 0) {
304
+ console.log(chalk.red(formatConfigErrors(vueConfig)));
305
+ return;
306
+ }
307
+
308
+ const { slug, appName } = vueConfig;
309
+
310
+ console.log(chalk.blue('\nVue Build & Stage'));
311
+ console.log(chalk.gray('─'.repeat(48)));
312
+ console.log(chalk.white(` Project: ${chalk.cyan(appName)} (${slug})`));
313
+ console.log(chalk.white(` Path: ${chalk.gray(projectPath)}`));
314
+ console.log();
315
+
316
+ // Determine which workspace to stage into
317
+ let workspacePath = workspaceOption;
318
+
319
+ if (workspacePath) {
320
+ workspacePath = resolve(workspacePath);
321
+ if (!existsSync(workspacePath)) {
322
+ console.log(chalk.red(`Error: Workspace path does not exist: ${workspacePath}`));
323
+ return;
324
+ }
325
+ } else {
326
+ // Prompt user to select a workspace
327
+ const result = await selectWorkspace();
328
+ if (!result) return; // User cancelled
329
+ workspacePath = result;
330
+ }
331
+
332
+ // Ensure project is linked
333
+ const linked = findLinkedProjectByPath(projectPath);
334
+ if (!linked) {
335
+ const shouldLink = await confirm({
336
+ message: 'This project is not linked. Link it now?',
337
+ default: true
338
+ });
339
+
340
+ if (shouldLink) {
341
+ const linkResult = linkVueProject(projectPath);
342
+ if (!linkResult.success) {
343
+ console.log(chalk.red(`Failed to link project: ${linkResult.error}`));
344
+ return;
345
+ }
346
+ console.log(chalk.green(`\u2713 Project linked`));
347
+ }
348
+ }
349
+
350
+ let distPath;
351
+
352
+ if (skipBuild) {
353
+ // Use existing dist
354
+ distPath = findDistDirectory(projectPath);
355
+
356
+ if (!distPath) {
357
+ console.log(chalk.red('No existing dist/ directory found.'));
358
+ console.log(chalk.gray('Run without --skip-build to build the project.'));
359
+ return;
360
+ }
361
+
362
+ console.log(chalk.yellow(`Using existing dist: ${distPath}`));
363
+
364
+ // Validate the existing build
365
+ const validation = validateIrisBuild(distPath);
366
+ if (!validation.valid) {
367
+ console.log(chalk.red(formatValidationError(distPath, validation.errors)));
368
+ return;
369
+ }
370
+
371
+ console.log(chalk.green('\u2713 Existing build is valid'));
372
+ } else {
373
+ // Build the project
374
+ console.log(chalk.blue('Building project...'));
375
+ console.log();
376
+
377
+ const buildResult = await buildVueProject(projectPath, { silent: false });
378
+
379
+ if (!buildResult.success) {
380
+ console.log();
381
+ console.log(chalk.red(formatBuildError(projectPath, buildResult.error)));
382
+ return;
383
+ }
384
+
385
+ distPath = buildResult.distPath;
386
+ console.log();
387
+ console.log(chalk.green(`\u2713 Build completed successfully`));
388
+ console.log(chalk.gray(` Output: ${distPath}`));
389
+
390
+ // Validate build output
391
+ const validation = validateIrisBuild(distPath);
392
+ if (!validation.valid) {
393
+ console.log();
394
+ console.log(chalk.red(formatValidationError(distPath, validation.errors)));
395
+ return;
396
+ }
397
+
398
+ console.log(chalk.green('\u2713 Build output validated'));
399
+ }
400
+
401
+ // Stage to selected workspace
402
+ console.log();
403
+ console.log(chalk.blue(`Staging to workspace: ${workspacePath}`));
404
+
405
+ const stageResult = stageToWorkspace(distPath, slug, workspacePath);
406
+
407
+ if (!stageResult.success) {
408
+ console.log(chalk.red(`Failed to stage: ${stageResult.error}`));
409
+ return;
410
+ }
411
+
412
+ console.log(chalk.green(`\u2713 Staged ${stageResult.fileCount} files to ${stageResult.stagedPath}`));
413
+
414
+ // Summary
415
+ console.log();
416
+ console.log(chalk.green('─'.repeat(48)));
417
+ console.log(chalk.green.bold('\u2713 Build & Stage Complete!'));
418
+ console.log();
419
+ console.log(chalk.gray(`Staged to: ${stageResult.stagedPath}`));
420
+ console.log();
421
+
422
+ // Ask if they want to publish now
423
+ const shouldPublish = await confirm({
424
+ message: 'Do you want to publish to Magentrix now?',
425
+ default: true
426
+ });
427
+
428
+ if (shouldPublish) {
429
+ console.log();
430
+ console.log(chalk.blue('Running publish from workspace...'));
431
+ console.log();
432
+
433
+ const publishSuccess = await runPublishFromWorkspace(workspacePath);
434
+
435
+ if (!publishSuccess) {
436
+ console.log();
437
+ console.log(chalk.cyan('To publish manually:'));
438
+ console.log(chalk.white(` 1. Navigate to workspace: ${chalk.yellow(`cd "${workspacePath}"`)}`));
439
+ console.log(chalk.white(` 2. Run ${chalk.yellow('magentrix publish')}`));
440
+ }
441
+ } else {
442
+ console.log();
443
+ console.log(chalk.cyan('Next steps:'));
444
+ console.log(chalk.white(` 1. Navigate to workspace: ${chalk.yellow(`cd "${workspacePath}"`)}`));
445
+ console.log(chalk.white(` 2. Run ${chalk.yellow('magentrix publish')} to deploy to Magentrix`));
446
+ console.log(chalk.white(` Or use ${chalk.yellow('magentrix autopublish')} for automatic deployment`));
447
+ }
448
+ }
449
+
450
+ /**
451
+ * Run magentrix publish from a specific workspace directory.
452
+ *
453
+ * @param {string} workspacePath - Path to the Magentrix workspace
454
+ * @returns {Promise<boolean>} - True if publish succeeded
455
+ */
456
+ async function runPublishFromWorkspace(workspacePath) {
457
+ return new Promise((resolvePromise) => {
458
+ const isWindows = process.platform === 'win32';
459
+ const npmCmd = isWindows ? 'npx.cmd' : 'npx';
460
+
461
+ const child = spawn(npmCmd, ['magentrix', 'publish'], {
462
+ cwd: workspacePath,
463
+ stdio: 'inherit',
464
+ shell: isWindows
465
+ });
466
+
467
+ child.on('close', (code) => {
468
+ resolvePromise(code === 0);
469
+ });
470
+
471
+ child.on('error', (err) => {
472
+ console.log(chalk.yellow(`Warning: Could not run publish: ${err.message}`));
473
+ resolvePromise(false);
474
+ });
475
+ });
476
+ }
477
+
478
+ /**
479
+ * Prompt user to select a Magentrix workspace.
480
+ *
481
+ * @returns {Promise<string | null>} - Selected workspace path or null if cancelled
482
+ */
483
+ async function selectWorkspace() {
484
+ const workspaces = getValidWorkspaces();
485
+
486
+ if (workspaces.length === 0) {
487
+ console.log(chalk.yellow('No Magentrix workspaces found.'));
488
+ console.log();
489
+ console.log(chalk.gray('To register a workspace:'));
490
+ console.log(chalk.white(` • Run ${chalk.cyan('magentrix')} from an existing workspace (auto-registers it)`));
491
+ console.log(chalk.white(` • Or run ${chalk.cyan('magentrix setup')} in a new directory to create one`));
492
+ console.log();
493
+ console.log(chalk.gray('Or specify a workspace path directly:'));
494
+ console.log(chalk.white(` ${chalk.cyan('magentrix vue-build-stage --workspace /path/to/workspace')}`));
495
+ console.log();
496
+
497
+ // Allow manual entry
498
+ const manualPath = await input({
499
+ message: 'Enter the path to your Magentrix workspace (or leave empty to cancel):',
500
+ validate: (value) => {
501
+ if (!value.trim()) return true; // Allow empty for cancel
502
+ const resolved = resolve(value);
503
+ if (!existsSync(resolved)) {
504
+ return `Path does not exist: ${resolved}`;
505
+ }
506
+ const magentrixFolder = join(resolved, '.magentrix');
507
+ if (!existsSync(magentrixFolder)) {
508
+ return `Not a Magentrix workspace (missing .magentrix folder): ${resolved}`;
509
+ }
510
+ return true;
511
+ }
512
+ });
513
+
514
+ if (!manualPath.trim()) {
515
+ console.log(chalk.gray('Cancelled.'));
516
+ return null;
517
+ }
518
+
519
+ return resolve(manualPath);
520
+ }
521
+
522
+ // Build choices from registered workspaces
523
+ const choices = workspaces.map(w => ({
524
+ name: `${w.path}`,
525
+ value: w.path,
526
+ description: chalk.dim(`→ ${w.instanceUrl}`)
527
+ }));
528
+
529
+ choices.push({
530
+ name: 'Enter path manually',
531
+ value: '__manual__',
532
+ description: chalk.dim('→ Specify the full path to a workspace')
533
+ });
534
+
535
+ choices.push({
536
+ name: 'Cancel',
537
+ value: '__cancel__'
538
+ });
539
+
540
+ const choice = await select({
541
+ message: 'Which workspace do you want to stage into?',
542
+ choices
543
+ });
544
+
545
+ if (choice === '__cancel__') {
546
+ console.log(chalk.gray('Cancelled.'));
547
+ return null;
548
+ }
549
+
550
+ if (choice === '__manual__') {
551
+ const manualPath = await input({
552
+ message: 'Enter the path to your Magentrix workspace:',
553
+ validate: (value) => {
554
+ if (!value.trim()) {
555
+ return 'Path is required';
556
+ }
557
+ const resolved = resolve(value);
558
+ if (!existsSync(resolved)) {
559
+ return `Path does not exist: ${resolved}`;
560
+ }
561
+ const magentrixFolder = join(resolved, '.magentrix');
562
+ if (!existsSync(magentrixFolder)) {
563
+ return `Not a Magentrix workspace (missing .magentrix folder): ${resolved}`;
564
+ }
565
+ return true;
566
+ }
567
+ });
568
+
569
+ return resolve(manualPath);
570
+ }
571
+
572
+ return choice;
573
+ }
574
+
254
575
  /**
255
576
  * Prompt user to select a project.
256
577
  *
@@ -7,8 +7,6 @@ import {
7
7
  readVueConfig,
8
8
  formatMissingConfigError,
9
9
  formatConfigErrors,
10
- backupFile,
11
- restoreFile,
12
10
  injectAssets,
13
11
  getInjectionTarget
14
12
  } from '../../utils/iris/config-reader.js';
@@ -20,26 +18,19 @@ import {
20
18
  } from '../../utils/iris/linker.js';
21
19
 
22
20
  /**
23
- * iris-dev command - Start Vue dev server with platform assets injected.
21
+ * run-dev command - Start Vue dev server with platform assets injected.
24
22
  *
25
- * Assets are injected into .env.development (if exists) or config.ts.
26
- * The modified file is backed up and restored when the dev server exits.
23
+ * Uses credentials from .env.development (VITE_REFRESH_TOKEN, VITE_SITE_URL).
24
+ * Assets are updated in .env.development (VITE_ASSETS) and kept between runs.
27
25
  *
28
26
  * Options:
29
27
  * --path <dir> Specify Vue project path
30
28
  * --no-inject Skip asset injection, just run dev server
31
- * --restore Restore .env.development or config.ts from backup
32
29
  */
33
30
  export const irisDev = async (options = {}) => {
34
31
  process.stdout.write('\x1Bc'); // Clear console
35
32
 
36
- const { path: pathOption, inject = true, restore } = options;
37
-
38
- // Handle --restore option
39
- if (restore) {
40
- await handleRestore(pathOption);
41
- return;
42
- }
33
+ const { path: pathOption, inject = true } = options;
43
34
 
44
35
  // Determine which project to use
45
36
  let projectPath = pathOption;
@@ -93,11 +84,6 @@ export const irisDev = async (options = {}) => {
93
84
  }
94
85
  console.log();
95
86
 
96
- let backupPath = null;
97
- let assetsInjected = false;
98
- let modifiedFilePath = null;
99
- let modifiedFileName = null;
100
-
101
87
  // Inject assets if enabled
102
88
  if (inject && siteUrl) {
103
89
  // Check if we have the refresh token for authentication
@@ -116,27 +102,18 @@ export const irisDev = async (options = {}) => {
116
102
  console.log(chalk.green(`\u2713 Found ${assetsResult.assets.length} platform assets`));
117
103
 
118
104
  // Determine which file will be modified
119
- const { targetFile, targetName } = getInjectionTarget(projectPath);
105
+ const { targetFile } = getInjectionTarget(projectPath);
120
106
 
121
107
  if (!targetFile) {
122
108
  console.log(chalk.yellow('Warning: No .env.development file found. Cannot inject assets.'));
123
109
  console.log(chalk.gray('Create a .env.development file to enable asset injection.'));
124
110
  } else {
125
- modifiedFilePath = targetFile;
126
- modifiedFileName = targetName;
127
-
128
- // Backup before modifying
129
- console.log(chalk.blue(`Backing up ${modifiedFileName}...`));
130
- backupPath = backupFile(modifiedFilePath);
131
- console.log(chalk.green(`\u2713 Backup created`));
132
-
133
- // Inject assets
134
- console.log(chalk.blue('Injecting assets...'));
111
+ // Inject assets (no backup needed - we keep the changes)
112
+ console.log(chalk.blue('Updating assets in .env.development...'));
135
113
  const injectResult = injectAssets(projectPath, assetsResult.assets);
136
114
 
137
115
  if (injectResult.success) {
138
- assetsInjected = true;
139
- console.log(chalk.green(`\u2713 Assets injected into ${injectResult.targetName}`));
116
+ console.log(chalk.green(`\u2713 Assets updated in ${injectResult.targetName}`));
140
117
  } else {
141
118
  console.log(chalk.yellow('Warning: Could not inject assets. Continuing without injection.'));
142
119
  }
@@ -165,50 +142,34 @@ export const irisDev = async (options = {}) => {
165
142
  console.log(chalk.gray('Press Ctrl+C to stop'));
166
143
  console.log();
167
144
 
168
- await runDevServer(projectPath, modifiedFilePath, modifiedFileName, backupPath, assetsInjected);
145
+ await runDevServer(projectPath);
169
146
  };
170
147
 
171
148
  /**
172
149
  * Run the Vue development server.
173
150
  */
174
- async function runDevServer(projectPath, modifiedFilePath, modifiedFileName, backupPath, assetsInjected) {
151
+ async function runDevServer(projectPath) {
175
152
  return new Promise((resolvePromise) => {
176
- const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
153
+ const isWindows = process.platform === 'win32';
154
+ const npmCmd = isWindows ? 'npm.cmd' : 'npm';
177
155
 
178
156
  const child = spawn(npmCmd, ['run', 'dev'], {
179
157
  cwd: projectPath,
180
158
  stdio: 'inherit',
181
- shell: false
159
+ shell: isWindows // Windows requires shell: true for .cmd files
182
160
  });
183
161
 
184
- // Handle cleanup on exit
185
- const cleanup = () => {
186
- if (assetsInjected && backupPath && modifiedFilePath) {
187
- console.log();
188
- console.log(chalk.blue(`Restoring ${modifiedFileName} from backup...`));
189
- const restored = restoreFile(modifiedFilePath);
190
- if (restored) {
191
- console.log(chalk.green(`\u2713 ${modifiedFileName} restored`));
192
- } else {
193
- console.log(chalk.yellow(`Warning: Could not restore ${modifiedFileName}`));
194
- console.log(chalk.gray(`Backup is at: ${backupPath}`));
195
- }
196
- }
197
- };
198
-
199
162
  // Handle process signals
200
163
  process.on('SIGINT', () => {
201
- cleanup();
202
164
  child.kill('SIGINT');
203
165
  });
204
166
 
205
167
  process.on('SIGTERM', () => {
206
- cleanup();
207
168
  child.kill('SIGTERM');
208
169
  });
209
170
 
210
171
  child.on('close', (code) => {
211
- // If dev server exited with error, show helpful context first
172
+ // If dev server exited with error, show helpful context
212
173
  if (code !== 0 && code !== null) {
213
174
  console.log();
214
175
  console.log(chalk.bgYellow.black(' External Process Error '));
@@ -227,59 +188,16 @@ async function runDevServer(projectPath, modifiedFilePath, modifiedFileName, bac
227
188
  console.log(chalk.yellow('─'.repeat(48)));
228
189
  }
229
190
 
230
- // Cleanup after showing error context
231
- cleanup();
232
-
233
191
  resolvePromise(code);
234
192
  });
235
193
 
236
194
  child.on('error', (err) => {
237
195
  console.log(chalk.red(`Failed to start dev server: ${err.message}`));
238
- cleanup();
239
196
  resolvePromise(1);
240
197
  });
241
198
  });
242
199
  }
243
200
 
244
- /**
245
- * Handle --restore option.
246
- */
247
- async function handleRestore(pathOption) {
248
- let projectPath = pathOption;
249
-
250
- if (!projectPath) {
251
- // Try current directory
252
- const cwdConfig = readVueConfig(process.cwd());
253
- if (cwdConfig.found) {
254
- projectPath = process.cwd();
255
- } else {
256
- console.log(chalk.red('No Vue project found in current directory.'));
257
- console.log(chalk.gray('Use --path to specify the project path.'));
258
- return;
259
- }
260
- }
261
-
262
- projectPath = resolve(projectPath);
263
-
264
- // Determine which file would be the injection target
265
- const { targetFile, targetName } = getInjectionTarget(projectPath);
266
-
267
- if (!targetFile) {
268
- console.log(chalk.yellow('No config files found to restore.'));
269
- return;
270
- }
271
-
272
- console.log(chalk.blue(`Restoring ${targetName} from backup...`));
273
- const restored = restoreFile(targetFile);
274
-
275
- if (restored) {
276
- console.log(chalk.green(`\u2713 ${targetName} restored from backup`));
277
- } else {
278
- console.log(chalk.yellow('No backup file found.'));
279
- console.log(chalk.gray(`Expected backup at: ${targetFile}.bak`));
280
- }
281
- }
282
-
283
201
  /**
284
202
  * Prompt user to select a project.
285
203
  */
@@ -316,7 +234,7 @@ async function selectProject() {
316
234
  console.log();
317
235
  console.log(chalk.gray('To get started:'));
318
236
  console.log(chalk.white(` 1. Link a Vue project: ${chalk.cyan('magentrix iris-link')}`));
319
- console.log(chalk.white(` 2. Or specify path: ${chalk.cyan('magentrix iris-dev --path /path/to/vue-project')}`));
237
+ console.log(chalk.white(` 2. Or specify path: ${chalk.cyan('magentrix run-dev --path /path/to/vue-project')}`));
320
238
  console.log();
321
239
  }
322
240
 
package/actions/setup.js CHANGED
@@ -3,8 +3,9 @@ import { ensureInstanceUrl } from "../utils/cli/helpers/ensureInstanceUrl.js";
3
3
  import Config from "../utils/config.js";
4
4
  import { getAccessToken, tryAuthenticate } from "../utils/magentrix/api/auth.js";
5
5
  import { ensureVSCodeFileAssociation } from "../utils/preferences.js";
6
- import { EXPORT_ROOT, HASHED_CWD } from "../vars/global.js";
6
+ import { CWD, EXPORT_ROOT, HASHED_CWD } from "../vars/global.js";
7
7
  import { select } from "@inquirer/prompts";
8
+ import { registerWorkspace } from "../utils/workspaces.js";
8
9
 
9
10
  const config = new Config();
10
11
 
@@ -116,6 +117,9 @@ export const setup = async (cliOptions = {}) => {
116
117
  { global: true, pathHash: HASHED_CWD }
117
118
  );
118
119
 
120
+ // Register this workspace in the global registry
121
+ registerWorkspace(CWD, instanceUrl);
122
+
119
123
  // Set up the editor
120
124
  await ensureVSCodeFileAssociation('./');
121
125
 
package/bin/magentrix.js CHANGED
@@ -13,11 +13,15 @@ import { create } from '../actions/create.js';
13
13
  import { autoPublish } from '../actions/autopublish.js';
14
14
  import { status } from '../actions/status.js';
15
15
  import { cacheDir, recacheFileIdIndex } from '../utils/cacher.js';
16
- import { EXPORT_ROOT } from '../vars/global.js';
16
+ import { CWD, EXPORT_ROOT, HASHED_CWD } from '../vars/global.js';
17
17
  import { publish } from '../actions/publish.js';
18
18
  import { update } from '../actions/update.js';
19
19
  import { configWizard } from '../actions/config.js';
20
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
+
24
+ const config = new Config();
21
25
 
22
26
  // ── Vue Project Detection ────────────────────────────────
23
27
  /**
@@ -44,8 +48,25 @@ function requireMagentrixWorkspace(fn) {
44
48
  console.error(chalk.gray('This command requires a Magentrix workspace with global API key and instance URL.\n'));
45
49
  console.error(chalk.cyan('Available commands in Vue projects:'));
46
50
  console.error(chalk.gray(' • magentrix iris-link'));
47
- console.error(chalk.gray(' • magentrix iris-dev'));
48
- console.error(chalk.gray(' • magentrix vue-build-stage\n'));
51
+ console.error(chalk.gray(' • magentrix vue-build-stage'));
52
+ console.error(chalk.gray(' • magentrix run-dev'));
53
+ console.error(chalk.gray(' • magentrix update\n'));
54
+ process.exit(1);
55
+ }
56
+ // Execute the command
57
+ await fn(...args);
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Block commands that should not run in Vue projects (but don't require a workspace)
63
+ */
64
+ function blockInVueProject(fn) {
65
+ return async (...args) => {
66
+ if (isInVueProject()) {
67
+ console.error(`\n${chalk.bgRed.white.bold(' ERROR ')} ${chalk.redBright('This command cannot be run in a Vue project')}\n`);
68
+ console.error(chalk.yellow('It looks like you\'re in a Vue project directory.'));
69
+ console.error(chalk.gray('Please run this command from a Magentrix workspace or an empty directory.\n'));
49
70
  process.exit(1);
50
71
  }
51
72
  // Execute the command
@@ -54,7 +75,33 @@ function requireMagentrixWorkspace(fn) {
54
75
  }
55
76
 
56
77
  // ── Middleware ────────────────────────────────
78
+
79
+ /**
80
+ * Auto-register existing workspaces for backwards compatibility.
81
+ * If the current directory has credentials configured but isn't in the
82
+ * global workspace registry, register it automatically.
83
+ */
84
+ function ensureWorkspaceRegistered() {
85
+ // Check if current directory has credentials configured
86
+ const instanceUrl = config.read('instanceUrl', { global: true, pathHash: HASHED_CWD });
87
+ const apiKey = config.read('apiKey', { global: true, pathHash: HASHED_CWD });
88
+
89
+ if (!instanceUrl || !apiKey) {
90
+ return; // Not a configured workspace
91
+ }
92
+
93
+ // Check if already registered
94
+ const workspaces = getRegisteredWorkspaces();
95
+ const alreadyRegistered = workspaces.some(w => w.path === CWD);
96
+
97
+ if (!alreadyRegistered) {
98
+ // Auto-register for backwards compatibility
99
+ registerWorkspace(CWD, instanceUrl);
100
+ }
101
+ }
102
+
57
103
  async function preMiddleware() {
104
+ ensureWorkspaceRegistered();
58
105
  await recacheFileIdIndex(EXPORT_ROOT);
59
106
  await cacheDir(EXPORT_ROOT);
60
107
  }
@@ -80,7 +127,7 @@ program
80
127
  .description('Manage Magentrix assets and automation')
81
128
  .version(VERSION)
82
129
  .configureHelp({
83
- formatHelp: (cmd, helper) => {
130
+ formatHelp: (_cmd, _helper) => {
84
131
  const divider = chalk.gray('━'.repeat(60));
85
132
  const titleBar = chalk.bold.bgBlue.white(' Magentrix CLI ');
86
133
  const version = chalk.dim(`v${VERSION}`);
@@ -105,7 +152,7 @@ program
105
152
  { name: 'update', desc: 'Update MagentrixCLI to the latest version', icon: '⬆️ ' },
106
153
  { name: 'iris-link', desc: 'Link a Vue project to the CLI', icon: '🔗 ' },
107
154
  { name: 'vue-build-stage', desc: 'Build Vue project and stage for publish', icon: '🏗️ ' },
108
- { name: 'iris-dev', desc: 'Start Vue dev server with platform assets', icon: '🌐 ' },
155
+ { name: 'run-dev', desc: 'Start Vue dev server with platform assets', icon: '🌐 ' },
109
156
  { name: 'iris-delete', desc: 'Delete an Iris app with backup', icon: '🗑️ ' },
110
157
  { name: 'iris-recover', desc: 'Recover a deleted Iris app from backup', icon: '♻️ ' }
111
158
  ];
@@ -137,7 +184,7 @@ program
137
184
  // ── Error Handlers ───────────────────────────
138
185
  program.showHelpAfterError(false);
139
186
  program.configureOutput({
140
- outputError: (str, write) => {
187
+ outputError: (str, _write) => {
141
188
  // Custom error message for unknown options
142
189
  if (str.includes('unknown option')) {
143
190
  const match = str.match(/'([^']+)'/);
@@ -167,7 +214,7 @@ program
167
214
  .description('Configure your Magentrix API key')
168
215
  .option('--api-key <apiKey>', 'Magentrix API key')
169
216
  .option('--instance-url <instanceUrl>', 'Magentrix instance URL (e.g., https://example.magentrixcloud.com)')
170
- .action(withWorkspaceCheck(setup));
217
+ .action(blockInVueProject(setup));
171
218
  program.command('pull').description('Pull files from the remote server').action(withWorkspaceCheck(pull));
172
219
  const createCommand = program
173
220
  .command('create')
@@ -228,7 +275,7 @@ program.command('status').description('Show file conflicts').action(withWorkspac
228
275
  program.command('autopublish').description('Watch & sync changes in real time').action(withWorkspaceCheck(autoPublish));
229
276
  // Publish does its own comprehensive file scanning, so skip the pre-cache middleware
230
277
  program.command('publish').description('Publish pending changes to the remote server').action(withWorkspaceCheck(publish));
231
- program.command('update').description('Update MagentrixCLI to the latest version').action(requireMagentrixWorkspace(update));
278
+ program.command('update').description('Update MagentrixCLI to the latest version').action(update);
232
279
 
233
280
  // Config command - interactive wizard
234
281
  program
@@ -251,14 +298,14 @@ program
251
298
  .description('Build a Vue project and stage it for publishing')
252
299
  .option('--path <path>', 'Path to the Vue project')
253
300
  .option('--skip-build', 'Skip build step and use existing dist/')
301
+ .option('--workspace <workspace>', 'Path to Magentrix workspace to stage into')
254
302
  .action(vueBuildStage);
255
303
 
256
304
  program
257
- .command('iris-dev')
305
+ .command('run-dev')
258
306
  .description('Start Vue dev server with platform assets injected')
259
307
  .option('--path <path>', 'Path to the Vue project')
260
308
  .option('--no-inject', 'Skip asset injection')
261
- .option('--restore', 'Restore config.ts from backup')
262
309
  .action(irisDev);
263
310
 
264
311
  program
@@ -282,8 +329,9 @@ program.argument('[command]', 'command to run').action((cmd) => {
282
329
  console.error(chalk.gray('This command requires a Magentrix workspace with global API key and instance URL.\n'));
283
330
  console.error(chalk.cyan('Available commands in Vue projects:'));
284
331
  console.error(chalk.gray(' • magentrix iris-link'));
285
- console.error(chalk.gray(' • magentrix iris-dev'));
286
- console.error(chalk.gray(' • magentrix vue-build-stage\n'));
332
+ console.error(chalk.gray(' • magentrix vue-build-stage'));
333
+ console.error(chalk.gray(' • magentrix run-dev'));
334
+ console.error(chalk.gray(' • magentrix update\n'));
287
335
  process.exit(1);
288
336
  }
289
337
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magentrix-corp/magentrix-cli",
3
- "version": "1.3.3",
3
+ "version": "1.3.5",
4
4
  "description": "CLI tool for synchronizing local files with Magentrix cloud platform",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -47,11 +47,12 @@ export async function buildVueProject(projectPath, options = {}) {
47
47
  const errorOutput = [];
48
48
 
49
49
  // Determine npm command based on platform
50
- const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
50
+ const isWindows = process.platform === 'win32';
51
+ const npmCmd = isWindows ? 'npm.cmd' : 'npm';
51
52
 
52
53
  const child = spawn(npmCmd, ['run', 'build'], {
53
54
  cwd: resolvedPath,
54
- shell: false,
55
+ shell: isWindows, // Windows requires shell: true for .cmd files
55
56
  env: { ...process.env, FORCE_COLOR: '1' }
56
57
  });
57
58
 
@@ -127,11 +128,11 @@ export async function buildVueProject(projectPath, options = {}) {
127
128
  }
128
129
 
129
130
  /**
130
- * Stage build output to the CLI project's iris-apps directory.
131
+ * Stage build output to a Magentrix workspace's iris-apps directory.
131
132
  *
132
133
  * @param {string} distPath - Path to the build output (dist directory)
133
134
  * @param {string} slug - The app slug (folder name)
134
- * @param {string} cliProjectPath - Path to the CLI project root (defaults to CWD)
135
+ * @param {string} workspacePath - Path to the Magentrix workspace (defaults to CWD)
135
136
  * @returns {{
136
137
  * success: boolean,
137
138
  * stagedPath: string | null,
@@ -139,9 +140,9 @@ export async function buildVueProject(projectPath, options = {}) {
139
140
  * fileCount: number
140
141
  * }}
141
142
  */
142
- export function stageToCliProject(distPath, slug, cliProjectPath = process.cwd()) {
143
+ export function stageToWorkspace(distPath, slug, workspacePath = process.cwd()) {
143
144
  const resolvedDistPath = resolve(distPath);
144
- const irisAppsDir = join(cliProjectPath, EXPORT_ROOT, IRIS_APPS_DIR);
145
+ const irisAppsDir = join(workspacePath, EXPORT_ROOT, IRIS_APPS_DIR);
145
146
  const targetDir = join(irisAppsDir, slug);
146
147
 
147
148
  // Validate dist path exists
@@ -0,0 +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
+ }