@magentrix-corp/magentrix-cli 1.3.4 → 1.3.6

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,15 +491,21 @@ 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
@@ -512,13 +518,11 @@ magentrix run-dev
512
518
  **Options:**
513
519
  - `--path <dir>` - Specify Vue project path
514
520
  - `--no-inject` - Skip asset injection, just run dev server
515
- - `--restore` - Restore `.env.development` from backup without running
516
521
 
517
522
  **Process:**
518
523
  1. Fetch platform assets from Magentrix using `.env.development` credentials
519
- 2. Backup `.env.development` and inject assets
524
+ 2. Update `VITE_ASSETS` in `.env.development` (changes are kept)
520
525
  3. Run `npm run dev`
521
- 4. Restore `.env.development` on exit (Ctrl+C)
522
526
 
523
527
  #### Delete an Iris App
524
528
  ```bash
@@ -592,14 +596,15 @@ VITE_ASSETS = '[]' # Injected automatically by run-dev
592
596
 
593
597
  **In Vue project directories** (detected by presence of `config.ts`):
594
598
  - ✓ `magentrix iris-link` - Link project to CLI
599
+ - ✓ `magentrix vue-build-stage` - Build and stage (prompts for target workspace)
595
600
  - ✓ `magentrix run-dev` - Start dev server (uses `.env.development` credentials)
596
- - ✓ `magentrix vue-build-stage` - Build and stage
601
+ - ✓ `magentrix update` - Update CLI to latest version
597
602
 
598
603
  **In Magentrix workspace directories** (has `.magentrix/` folder):
599
604
  - ✓ All standard commands (`setup`, `pull`, `publish`, etc.)
600
- - ✓ All Iris commands (`iris-delete`, `iris-recover`, etc.)
605
+ - ✓ All Iris commands (`vue-build-stage`, `iris-delete`, `iris-recover`, etc.)
601
606
 
602
- **Note**: Commands like `pull`, `publish`, `autopublish` require a Magentrix workspace and will show an error if run inside a Vue project directory.
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.
603
608
 
604
609
  ### Typical Development Workflow
605
610
 
@@ -607,22 +612,28 @@ VITE_ASSETS = '[]' # Injected automatically by run-dev
607
612
  # First time setup
608
613
  magentrix iris-link # Link your Vue project
609
614
 
610
- # Development
615
+ # Development (run from Vue project folder)
616
+ cd ~/my-vue-app # Work from your Vue project
611
617
  magentrix run-dev # Start dev server with platform assets
612
618
  # Make changes, test locally
613
619
  # Press Ctrl+C to stop
614
620
 
615
- # 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
+ # Prompted: "Do you want to publish to Magentrix now?" → Yes/No
625
+
626
+ # Deployment - Option B (from Magentrix workspace)
616
627
  cd ~/magentrix-workspace # Navigate to Magentrix workspace
617
628
  magentrix vue-build-stage --path ~/my-vue-app # Build and stage
618
629
  # Prompted: "Do you want to publish to Magentrix now?" → Yes/No
619
630
  # If autopublish is running, it auto-deploys instead
620
631
 
621
- # Deleting an app
632
+ # Deleting an app (from workspace)
622
633
  magentrix iris-delete # Select app, confirm, auto-backup created
623
634
  magentrix publish # Sync deletion to server
624
635
 
625
- # Recovering a deleted app
636
+ # Recovering a deleted app (from workspace)
626
637
  magentrix iris-recover # Select backup, restore files
627
638
  magentrix publish # Sync recovery to server
628
639
  ```
@@ -642,29 +653,31 @@ magentrix vue-build-stage
642
653
 
643
654
  ### Troubleshooting Iris Apps
644
655
 
645
- #### "Warning: Magentrix Workspace Not Detected"
646
- This warning appears when running `magentrix vue-build-stage` outside your Magentrix CLI workspace.
647
-
648
- **Why it happens:**
649
- - The command stages build files to `src/iris-apps/<slug>/` in your Magentrix workspace
650
- - You ran it from your Vue project directory or another location
656
+ #### Running vue-build-stage from different locations
651
657
 
652
- **How to fix:**
653
- 1. Navigate to your Magentrix CLI workspace (the folder with `.magentrix/` and `src/`)
654
- 2. Run the command from there
655
- 3. Use `--path` to specify your Vue project: `magentrix vue-build-stage --path /path/to/vue-project`
658
+ The `vue-build-stage` command works from both locations:
656
659
 
657
- **Example:**
660
+ **From Vue project directory:**
658
661
  ```bash
659
- # Wrong - running from Vue project
660
662
  cd ~/my-vue-app
661
- magentrix vue-build-stage # Warning!
663
+ magentrix vue-build-stage # Prompts for which workspace to stage into
664
+ ```
662
665
 
663
- # Right - running from Magentrix workspace
666
+ **From Magentrix workspace:**
667
+ ```bash
664
668
  cd ~/magentrix-workspace
665
- 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
666
671
  ```
667
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
+
668
681
  #### "Missing required field in config.ts: slug (appPath)"
669
682
  Your Vue project's `config.ts` is missing the app identifier. Add an `appPath` or `slug` field.
670
683
 
@@ -766,16 +779,24 @@ magentrix status # Verify everything is in sync
766
779
  ```
767
780
 
768
781
  ### Deploying a Vue.js App
769
- ```bash
770
- # Important: Run from your Magentrix CLI workspace, NOT the Vue project folder
771
- cd ~/magentrix-workspace # Navigate to Magentrix workspace
772
782
 
783
+ **Option A: From Vue project folder**
784
+ ```bash
785
+ cd ~/my-vue-app # Navigate to your Vue project
773
786
  magentrix iris-link # Link project (first time only)
787
+ magentrix vue-build-stage # Build and select workspace to stage into
788
+ # Prompted: "Do you want to publish to Magentrix now?" → Yes/No
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)
774
795
  magentrix vue-build-stage --path ~/my-vue-app # Build and stage
775
796
  # Prompted: "Do you want to publish to Magentrix now?" (unless autopublish is running)
776
797
  ```
777
798
 
778
- **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.
779
800
 
780
801
  ### Deleting and Recovering Apps
781
802
  ```bash
@@ -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,386 @@ 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
+ // Check if workspace might be out of sync and offer to pull first
423
+ console.log(chalk.gray('Checking workspace sync status...'));
424
+ const syncStatus = await checkWorkspaceSyncStatus(workspacePath);
425
+
426
+ if (syncStatus.needsPull) {
427
+ console.log();
428
+ console.log(chalk.yellow('⚠ Your workspace may be out of sync with the server.'));
429
+
430
+ const shouldPull = await confirm({
431
+ message: 'Would you like to pull latest changes first?',
432
+ default: true
433
+ });
434
+
435
+ if (shouldPull) {
436
+ console.log();
437
+ console.log(chalk.blue('Running pull from workspace...'));
438
+ console.log();
439
+
440
+ const pullSuccess = await runCommandFromWorkspace(workspacePath, 'pull');
441
+
442
+ if (!pullSuccess) {
443
+ console.log();
444
+ console.log(chalk.yellow('Pull encountered issues. You may want to resolve them manually.'));
445
+
446
+ const continueAnyway = await confirm({
447
+ message: 'Do you still want to continue with publishing?',
448
+ default: false
449
+ });
450
+
451
+ if (!continueAnyway) {
452
+ console.log();
453
+ console.log(chalk.cyan('To continue manually:'));
454
+ console.log(chalk.white(` 1. Navigate to workspace: ${chalk.yellow(`cd "${workspacePath}"`)}`));
455
+ console.log(chalk.white(` 2. Run ${chalk.yellow('magentrix pull')} to resolve conflicts`));
456
+ console.log(chalk.white(` 3. Run ${chalk.yellow('magentrix publish')} to deploy`));
457
+ return;
458
+ }
459
+ }
460
+ console.log();
461
+ }
462
+ } else if (syncStatus.checked) {
463
+ console.log(chalk.green('✓ Workspace is in sync'));
464
+ }
465
+
466
+ // Ask if they want to publish now
467
+ const shouldPublish = await confirm({
468
+ message: 'Do you want to publish to Magentrix now?',
469
+ default: true
470
+ });
471
+
472
+ if (shouldPublish) {
473
+ console.log();
474
+ console.log(chalk.blue('Running publish from workspace...'));
475
+ console.log();
476
+
477
+ const publishSuccess = await runCommandFromWorkspace(workspacePath, 'publish');
478
+
479
+ if (!publishSuccess) {
480
+ console.log();
481
+ console.log(chalk.cyan('To publish manually:'));
482
+ console.log(chalk.white(` 1. Navigate to workspace: ${chalk.yellow(`cd "${workspacePath}"`)}`));
483
+ console.log(chalk.white(` 2. Run ${chalk.yellow('magentrix publish')}`));
484
+ }
485
+ } else {
486
+ console.log();
487
+ console.log(chalk.cyan('Next steps:'));
488
+ console.log(chalk.white(` 1. Navigate to workspace: ${chalk.yellow(`cd "${workspacePath}"`)}`));
489
+ console.log(chalk.white(` 2. Run ${chalk.yellow('magentrix publish')} to deploy to Magentrix`));
490
+ console.log(chalk.white(` Or use ${chalk.yellow('magentrix autopublish')} for automatic deployment`));
491
+ }
492
+ }
493
+
494
+ /**
495
+ * Check if a workspace needs to pull (has remote changes or conflicts).
496
+ *
497
+ * @param {string} workspacePath - Path to the Magentrix workspace
498
+ * @returns {Promise<{checked: boolean, needsPull: boolean}>}
499
+ */
500
+ async function checkWorkspaceSyncStatus(workspacePath) {
501
+ return new Promise((resolvePromise) => {
502
+ const isWindows = process.platform === 'win32';
503
+ const npmCmd = isWindows ? 'npx.cmd' : 'npx';
504
+
505
+ let output = '';
506
+
507
+ const child = spawn(npmCmd, ['magentrix', 'status'], {
508
+ cwd: workspacePath,
509
+ stdio: ['inherit', 'pipe', 'pipe'],
510
+ shell: isWindows
511
+ });
512
+
513
+ child.stdout.on('data', (data) => {
514
+ output += data.toString();
515
+ });
516
+
517
+ child.stderr.on('data', (data) => {
518
+ output += data.toString();
519
+ });
520
+
521
+ child.on('close', (code) => {
522
+ // Check output for signs of remote changes or conflicts
523
+ const lowerOutput = output.toLowerCase();
524
+ const needsPull = lowerOutput.includes('conflict') ||
525
+ lowerOutput.includes('remote') ||
526
+ lowerOutput.includes('server has changes') ||
527
+ lowerOutput.includes('out of sync') ||
528
+ lowerOutput.includes('modified on server');
529
+
530
+ resolvePromise({ checked: code === 0, needsPull });
531
+ });
532
+
533
+ child.on('error', () => {
534
+ // If we can't check, assume it's fine and let them proceed
535
+ resolvePromise({ checked: false, needsPull: false });
536
+ });
537
+ });
538
+ }
539
+
540
+ /**
541
+ * Run a magentrix command from a specific workspace directory.
542
+ *
543
+ * @param {string} workspacePath - Path to the Magentrix workspace
544
+ * @param {string} command - The magentrix command to run (e.g., 'pull', 'publish')
545
+ * @returns {Promise<boolean>} - True if command succeeded
546
+ */
547
+ async function runCommandFromWorkspace(workspacePath, command) {
548
+ return new Promise((resolvePromise) => {
549
+ const isWindows = process.platform === 'win32';
550
+ const npmCmd = isWindows ? 'npx.cmd' : 'npx';
551
+
552
+ const child = spawn(npmCmd, ['magentrix', command], {
553
+ cwd: workspacePath,
554
+ stdio: 'inherit',
555
+ shell: isWindows
556
+ });
557
+
558
+ child.on('close', (code) => {
559
+ resolvePromise(code === 0);
560
+ });
561
+
562
+ child.on('error', (err) => {
563
+ console.log(chalk.yellow(`Warning: Could not run ${command}: ${err.message}`));
564
+ resolvePromise(false);
565
+ });
566
+ });
567
+ }
568
+
569
+ /**
570
+ * Prompt user to select a Magentrix workspace.
571
+ *
572
+ * @returns {Promise<string | null>} - Selected workspace path or null if cancelled
573
+ */
574
+ async function selectWorkspace() {
575
+ const workspaces = getValidWorkspaces();
576
+
577
+ if (workspaces.length === 0) {
578
+ console.log(chalk.yellow('No Magentrix workspaces found.'));
579
+ console.log();
580
+ console.log(chalk.gray('To register a workspace:'));
581
+ console.log(chalk.white(` • Run ${chalk.cyan('magentrix')} from an existing workspace (auto-registers it)`));
582
+ console.log(chalk.white(` • Or run ${chalk.cyan('magentrix setup')} in a new directory to create one`));
583
+ console.log();
584
+ console.log(chalk.gray('Or specify a workspace path directly:'));
585
+ console.log(chalk.white(` ${chalk.cyan('magentrix vue-build-stage --workspace /path/to/workspace')}`));
586
+ console.log();
587
+
588
+ // Allow manual entry
589
+ const manualPath = await input({
590
+ message: 'Enter the path to your Magentrix workspace (or leave empty to cancel):',
591
+ validate: (value) => {
592
+ if (!value.trim()) return true; // Allow empty for cancel
593
+ const resolved = resolve(value);
594
+ if (!existsSync(resolved)) {
595
+ return `Path does not exist: ${resolved}`;
596
+ }
597
+ const magentrixFolder = join(resolved, '.magentrix');
598
+ if (!existsSync(magentrixFolder)) {
599
+ return `Not a Magentrix workspace (missing .magentrix folder): ${resolved}`;
600
+ }
601
+ return true;
602
+ }
603
+ });
604
+
605
+ if (!manualPath.trim()) {
606
+ console.log(chalk.gray('Cancelled.'));
607
+ return null;
608
+ }
609
+
610
+ return resolve(manualPath);
611
+ }
612
+
613
+ // Build choices from registered workspaces
614
+ const choices = workspaces.map(w => ({
615
+ name: `${w.path}`,
616
+ value: w.path,
617
+ description: chalk.dim(`→ ${w.instanceUrl}`)
618
+ }));
619
+
620
+ choices.push({
621
+ name: 'Enter path manually',
622
+ value: '__manual__',
623
+ description: chalk.dim('→ Specify the full path to a workspace')
624
+ });
625
+
626
+ choices.push({
627
+ name: 'Cancel',
628
+ value: '__cancel__'
629
+ });
630
+
631
+ const choice = await select({
632
+ message: 'Which workspace do you want to stage into?',
633
+ choices
634
+ });
635
+
636
+ if (choice === '__cancel__') {
637
+ console.log(chalk.gray('Cancelled.'));
638
+ return null;
639
+ }
640
+
641
+ if (choice === '__manual__') {
642
+ const manualPath = await input({
643
+ message: 'Enter the path to your Magentrix workspace:',
644
+ validate: (value) => {
645
+ if (!value.trim()) {
646
+ return 'Path is required';
647
+ }
648
+ const resolved = resolve(value);
649
+ if (!existsSync(resolved)) {
650
+ return `Path does not exist: ${resolved}`;
651
+ }
652
+ const magentrixFolder = join(resolved, '.magentrix');
653
+ if (!existsSync(magentrixFolder)) {
654
+ return `Not a Magentrix workspace (missing .magentrix folder): ${resolved}`;
655
+ }
656
+ return true;
657
+ }
658
+ });
659
+
660
+ return resolve(manualPath);
661
+ }
662
+
663
+ return choice;
664
+ }
665
+
254
666
  /**
255
667
  * Prompt user to select a project.
256
668
  *
@@ -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';
@@ -23,24 +21,16 @@ import {
23
21
  * run-dev command - Start Vue dev server with platform assets injected.
24
22
  *
25
23
  * Uses credentials from .env.development (VITE_REFRESH_TOKEN, VITE_SITE_URL).
26
- * Assets are injected into .env.development only.
27
- * The .env.development file is backed up and restored when the dev server exits.
24
+ * Assets are updated in .env.development (VITE_ASSETS) and kept between runs.
28
25
  *
29
26
  * Options:
30
27
  * --path <dir> Specify Vue project path
31
28
  * --no-inject Skip asset injection, just run dev server
32
- * --restore Restore .env.development from backup
33
29
  */
34
30
  export const irisDev = async (options = {}) => {
35
31
  process.stdout.write('\x1Bc'); // Clear console
36
32
 
37
- const { path: pathOption, inject = true, restore } = options;
38
-
39
- // Handle --restore option
40
- if (restore) {
41
- await handleRestore(pathOption);
42
- return;
43
- }
33
+ const { path: pathOption, inject = true } = options;
44
34
 
45
35
  // Determine which project to use
46
36
  let projectPath = pathOption;
@@ -94,11 +84,6 @@ export const irisDev = async (options = {}) => {
94
84
  }
95
85
  console.log();
96
86
 
97
- let backupPath = null;
98
- let assetsInjected = false;
99
- let modifiedFilePath = null;
100
- let modifiedFileName = null;
101
-
102
87
  // Inject assets if enabled
103
88
  if (inject && siteUrl) {
104
89
  // Check if we have the refresh token for authentication
@@ -117,27 +102,18 @@ export const irisDev = async (options = {}) => {
117
102
  console.log(chalk.green(`\u2713 Found ${assetsResult.assets.length} platform assets`));
118
103
 
119
104
  // Determine which file will be modified
120
- const { targetFile, targetName } = getInjectionTarget(projectPath);
105
+ const { targetFile } = getInjectionTarget(projectPath);
121
106
 
122
107
  if (!targetFile) {
123
108
  console.log(chalk.yellow('Warning: No .env.development file found. Cannot inject assets.'));
124
109
  console.log(chalk.gray('Create a .env.development file to enable asset injection.'));
125
110
  } else {
126
- modifiedFilePath = targetFile;
127
- modifiedFileName = targetName;
128
-
129
- // Backup before modifying
130
- console.log(chalk.blue(`Backing up ${modifiedFileName}...`));
131
- backupPath = backupFile(modifiedFilePath);
132
- console.log(chalk.green(`\u2713 Backup created`));
133
-
134
- // Inject assets
135
- 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...'));
136
113
  const injectResult = injectAssets(projectPath, assetsResult.assets);
137
114
 
138
115
  if (injectResult.success) {
139
- assetsInjected = true;
140
- console.log(chalk.green(`\u2713 Assets injected into ${injectResult.targetName}`));
116
+ console.log(chalk.green(`\u2713 Assets updated in ${injectResult.targetName}`));
141
117
  } else {
142
118
  console.log(chalk.yellow('Warning: Could not inject assets. Continuing without injection.'));
143
119
  }
@@ -166,13 +142,13 @@ export const irisDev = async (options = {}) => {
166
142
  console.log(chalk.gray('Press Ctrl+C to stop'));
167
143
  console.log();
168
144
 
169
- await runDevServer(projectPath, modifiedFilePath, modifiedFileName, backupPath, assetsInjected);
145
+ await runDevServer(projectPath);
170
146
  };
171
147
 
172
148
  /**
173
149
  * Run the Vue development server.
174
150
  */
175
- async function runDevServer(projectPath, modifiedFilePath, modifiedFileName, backupPath, assetsInjected) {
151
+ async function runDevServer(projectPath) {
176
152
  return new Promise((resolvePromise) => {
177
153
  const isWindows = process.platform === 'win32';
178
154
  const npmCmd = isWindows ? 'npm.cmd' : 'npm';
@@ -183,34 +159,17 @@ async function runDevServer(projectPath, modifiedFilePath, modifiedFileName, bac
183
159
  shell: isWindows // Windows requires shell: true for .cmd files
184
160
  });
185
161
 
186
- // Handle cleanup on exit
187
- const cleanup = () => {
188
- if (assetsInjected && backupPath && modifiedFilePath) {
189
- console.log();
190
- console.log(chalk.blue(`Restoring ${modifiedFileName} from backup...`));
191
- const restored = restoreFile(modifiedFilePath);
192
- if (restored) {
193
- console.log(chalk.green(`\u2713 ${modifiedFileName} restored`));
194
- } else {
195
- console.log(chalk.yellow(`Warning: Could not restore ${modifiedFileName}`));
196
- console.log(chalk.gray(`Backup is at: ${backupPath}`));
197
- }
198
- }
199
- };
200
-
201
162
  // Handle process signals
202
163
  process.on('SIGINT', () => {
203
- cleanup();
204
164
  child.kill('SIGINT');
205
165
  });
206
166
 
207
167
  process.on('SIGTERM', () => {
208
- cleanup();
209
168
  child.kill('SIGTERM');
210
169
  });
211
170
 
212
171
  child.on('close', (code) => {
213
- // If dev server exited with error, show helpful context first
172
+ // If dev server exited with error, show helpful context
214
173
  if (code !== 0 && code !== null) {
215
174
  console.log();
216
175
  console.log(chalk.bgYellow.black(' External Process Error '));
@@ -229,59 +188,16 @@ async function runDevServer(projectPath, modifiedFilePath, modifiedFileName, bac
229
188
  console.log(chalk.yellow('─'.repeat(48)));
230
189
  }
231
190
 
232
- // Cleanup after showing error context
233
- cleanup();
234
-
235
191
  resolvePromise(code);
236
192
  });
237
193
 
238
194
  child.on('error', (err) => {
239
195
  console.log(chalk.red(`Failed to start dev server: ${err.message}`));
240
- cleanup();
241
196
  resolvePromise(1);
242
197
  });
243
198
  });
244
199
  }
245
200
 
246
- /**
247
- * Handle --restore option.
248
- */
249
- async function handleRestore(pathOption) {
250
- let projectPath = pathOption;
251
-
252
- if (!projectPath) {
253
- // Try current directory
254
- const cwdConfig = readVueConfig(process.cwd());
255
- if (cwdConfig.found) {
256
- projectPath = process.cwd();
257
- } else {
258
- console.log(chalk.red('No Vue project found in current directory.'));
259
- console.log(chalk.gray('Use --path to specify the project path.'));
260
- return;
261
- }
262
- }
263
-
264
- projectPath = resolve(projectPath);
265
-
266
- // Determine which file would be the injection target
267
- const { targetFile, targetName } = getInjectionTarget(projectPath);
268
-
269
- if (!targetFile) {
270
- console.log(chalk.yellow('No config files found to restore.'));
271
- return;
272
- }
273
-
274
- console.log(chalk.blue(`Restoring ${targetName} from backup...`));
275
- const restored = restoreFile(targetFile);
276
-
277
- if (restored) {
278
- console.log(chalk.green(`\u2713 ${targetName} restored from backup`));
279
- } else {
280
- console.log(chalk.yellow('No backup file found.'));
281
- console.log(chalk.gray(`Expected backup at: ${targetFile}.bak`));
282
- }
283
- }
284
-
285
201
  /**
286
202
  * Prompt user to select a project.
287
203
  */
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'));
51
+ console.error(chalk.gray(' • magentrix vue-build-stage'));
47
52
  console.error(chalk.gray(' • magentrix run-dev'));
48
- console.error(chalk.gray(' • magentrix vue-build-stage\n'));
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}`);
@@ -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,6 +298,7 @@ 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
@@ -258,7 +306,6 @@ program
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'));
332
+ console.error(chalk.gray(' • magentrix vue-build-stage'));
285
333
  console.error(chalk.gray(' • magentrix run-dev'));
286
- console.error(chalk.gray(' • magentrix vue-build-stage\n'));
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.4",
3
+ "version": "1.3.6",
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
@@ -175,9 +176,18 @@ export function stageToCliProject(distPath, slug, cliProjectPath = process.cwd()
175
176
  rmSync(targetDir, { recursive: true, force: true });
176
177
  }
177
178
 
178
- // Copy dist contents to target
179
+ // Files to exclude from staging (not needed for Iris apps)
180
+ const excludeFiles = new Set(['index.html', 'favicon.ico']);
181
+
182
+ // Copy dist contents to target, excluding unnecessary files
179
183
  try {
180
- cpSync(resolvedDistPath, targetDir, { recursive: true });
184
+ cpSync(resolvedDistPath, targetDir, {
185
+ recursive: true,
186
+ filter: (src) => {
187
+ const filename = src.split(/[/\\]/).pop();
188
+ return !excludeFiles.has(filename);
189
+ }
190
+ });
181
191
  } catch (err) {
182
192
  return {
183
193
  success: false,
@@ -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
+ }