@magentrix-corp/magentrix-cli 1.3.1 → 1.3.3

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
@@ -480,7 +480,7 @@ magentrix iris-link
480
480
  - `--cleanup` - Remove invalid (non-existent) linked projects
481
481
 
482
482
  **Menu options:**
483
- - Link a new Vue project
483
+ - Link a Vue project
484
484
  - View all linked projects
485
485
  - Unlink a project
486
486
  - Cleanup invalid projects
@@ -510,13 +510,13 @@ magentrix iris-dev
510
510
  **Options:**
511
511
  - `--path <dir>` - Specify Vue project path
512
512
  - `--no-inject` - Skip asset injection, just run dev server
513
- - `--restore` - Restore config.ts from backup without running
513
+ - `--restore` - Restore `.env.development` from backup without running
514
514
 
515
515
  **Process:**
516
516
  1. Fetch platform assets from Magentrix
517
- 2. Backup `config.ts` and inject assets
517
+ 2. Backup `.env.development` and inject assets
518
518
  3. Run `npm run dev`
519
- 4. Restore `config.ts` on exit (Ctrl+C)
519
+ 4. Restore `.env.development` on exit (Ctrl+C)
520
520
 
521
521
  #### Delete an Iris App
522
522
  ```bash
@@ -558,22 +558,33 @@ magentrix iris-recover
558
558
 
559
559
  ### Vue Project Requirements
560
560
 
561
- Your Vue project must have a `config.ts` file with these required fields:
561
+ Your Vue project needs two configuration files:
562
+
563
+ **1. `config.ts`** - App metadata (required):
562
564
 
563
565
  ```typescript
564
566
  // src/config.ts
565
567
  export const config = {
566
- appPath: "my-app", // App identifier (folder name on server)
567
- appName: "My Application", // Display name in navigation menu
568
- siteUrl: "https://yourinstance.magentrix.com",
569
- assets: [] // Injected automatically by iris-dev
568
+ appPath: "my-app", // Required: App identifier (folder name on server)
569
+ appName: "My Application", // Required: Display name in navigation menu
570
+ appDescription: "", // Optional: App description
571
+ appIconId: "", // Optional: App icon ID
570
572
  }
571
573
  ```
572
574
 
573
- **Accepted field names:**
575
+ **2. `.env.development`** - Environment variables:
576
+
577
+ ```bash
578
+ VITE_SITE_URL = https://yourinstance.magentrix.com
579
+ VITE_REFRESH_TOKEN = your-api-key
580
+ VITE_ASSETS = '[]' # Injected automatically by iris-dev
581
+ ```
582
+
583
+ **Accepted field names in config.ts:**
574
584
  - Slug: `appPath`, `slug`, or `app_path`
575
585
  - Name: `appName`, `app_name`, or `name`
576
- - URL: `siteUrl`, `site_url`, `baseUrl`, or `base_url`
586
+ - Description: `appDescription` or `app_description`
587
+ - Icon: `appIconId` or `app_icon_id`
577
588
 
578
589
  ### Typical Development Workflow
579
590
 
@@ -639,14 +650,22 @@ cd ~/magentrix-workspace
639
650
  magentrix vue-build-stage --path ~/my-vue-app # ✓ Works!
640
651
  ```
641
652
 
642
- #### "Missing required field: slug"
653
+ #### "Missing required field in config.ts: slug (appPath)"
643
654
  Your Vue project's `config.ts` is missing the app identifier. Add an `appPath` or `slug` field.
644
655
 
645
- #### "Missing required field: appName"
656
+ #### "Missing required field in config.ts: appName"
646
657
  Your Vue project's `config.ts` is missing the display name. Add an `appName` field.
647
658
 
648
- #### Build fails with "VITE_SITE_URL is not defined"
649
- This is a Vue project configuration issue, not a CLI issue. Check your Vue project's `.env` file or `vite.config.ts` for missing environment variables.
659
+ #### "VITE_SITE_URL not set in .env.development"
660
+ Create a `.env.development` file in your Vue project with `VITE_SITE_URL = https://yourinstance.magentrix.com`.
661
+
662
+ #### "No .env.development file found"
663
+ The CLI requires a `.env.development` file for environment variables. Create one with:
664
+ ```bash
665
+ VITE_SITE_URL = https://yourinstance.magentrix.com
666
+ VITE_REFRESH_TOKEN = your-api-key
667
+ VITE_ASSETS = '[]'
668
+ ```
650
669
 
651
670
  #### "No config.ts found"
652
671
  The CLI looks for config in these locations:
@@ -7,25 +7,28 @@ import {
7
7
  readVueConfig,
8
8
  formatMissingConfigError,
9
9
  formatConfigErrors,
10
- findConfigFile,
11
- backupConfigFile,
12
- restoreConfigFile,
13
- injectAssetsIntoConfig
10
+ backupFile,
11
+ restoreFile,
12
+ injectAssets,
13
+ getInjectionTarget
14
14
  } from '../../utils/iris/config-reader.js';
15
15
  import { getIrisAssets } from '../../utils/magentrix/api/iris.js';
16
+ import { getAccessToken } from '../../utils/magentrix/api/auth.js';
16
17
  import {
17
18
  getLinkedProjectsWithStatus,
18
19
  buildProjectChoices
19
20
  } from '../../utils/iris/linker.js';
20
- import { ensureValidCredentials } from '../../utils/cli/helpers/ensureCredentials.js';
21
21
 
22
22
  /**
23
23
  * iris-dev command - Start Vue dev server with platform assets injected.
24
24
  *
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.
27
+ *
25
28
  * Options:
26
29
  * --path <dir> Specify Vue project path
27
30
  * --no-inject Skip asset injection, just run dev server
28
- * --restore Restore config.ts from backup without running
31
+ * --restore Restore .env.development or config.ts from backup
29
32
  */
30
33
  export const irisDev = async (options = {}) => {
31
34
  process.stdout.write('\x1Bc'); // Clear console
@@ -80,7 +83,6 @@ export const irisDev = async (options = {}) => {
80
83
  }
81
84
 
82
85
  const { slug, appName, siteUrl } = vueConfig;
83
- const configPath = findConfigFile(projectPath);
84
86
 
85
87
  console.log(chalk.blue('\nIris Development Server'));
86
88
  console.log(chalk.gray('─'.repeat(48)));
@@ -93,52 +95,68 @@ export const irisDev = async (options = {}) => {
93
95
 
94
96
  let backupPath = null;
95
97
  let assetsInjected = false;
98
+ let modifiedFilePath = null;
99
+ let modifiedFileName = null;
96
100
 
97
101
  // Inject assets if enabled
98
102
  if (inject && siteUrl) {
99
- console.log(chalk.blue('Fetching platform assets...'));
100
-
101
- try {
102
- // Get credentials for API call
103
- const { instanceUrl, token } = await ensureValidCredentials();
104
-
105
- // Use siteUrl from config if different from instanceUrl
106
- const targetUrl = siteUrl || instanceUrl;
107
-
108
- const assetsResult = await getIrisAssets(targetUrl, token.value);
109
-
110
- if (assetsResult.success && assetsResult.assets?.length > 0) {
111
- console.log(chalk.green(`\u2713 Found ${assetsResult.assets.length} platform assets`));
112
-
113
- // Backup config file
114
- console.log(chalk.blue('Backing up config.ts...'));
115
- backupPath = backupConfigFile(configPath);
116
- console.log(chalk.green(`\u2713 Backup created: ${backupPath}`));
117
-
118
- // Inject assets
119
- console.log(chalk.blue('Injecting assets into config.ts...'));
120
- const injected = injectAssetsIntoConfig(configPath, assetsResult.assets);
121
-
122
- if (injected) {
123
- assetsInjected = true;
124
- console.log(chalk.green('\u2713 Assets injected'));
103
+ // Check if we have the refresh token for authentication
104
+ if (!vueConfig.refreshToken) {
105
+ console.log(chalk.yellow('Warning: VITE_REFRESH_TOKEN not set in .env.development'));
106
+ console.log(chalk.gray('Asset injection requires authentication. Continuing without assets.'));
107
+ } else {
108
+ console.log(chalk.blue('Fetching platform assets...'));
109
+
110
+ try {
111
+ // Get access token using the refresh token from .env.development
112
+ const tokenData = await getAccessToken(vueConfig.refreshToken, siteUrl);
113
+ const assetsResult = await getIrisAssets(siteUrl, tokenData.token);
114
+
115
+ if (assetsResult.success && assetsResult.assets?.length > 0) {
116
+ console.log(chalk.green(`\u2713 Found ${assetsResult.assets.length} platform assets`));
117
+
118
+ // Determine which file will be modified
119
+ const { targetFile, targetName } = getInjectionTarget(projectPath);
120
+
121
+ if (!targetFile) {
122
+ console.log(chalk.yellow('Warning: No .env.development file found. Cannot inject assets.'));
123
+ console.log(chalk.gray('Create a .env.development file to enable asset injection.'));
124
+ } 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...'));
135
+ const injectResult = injectAssets(projectPath, assetsResult.assets);
136
+
137
+ if (injectResult.success) {
138
+ assetsInjected = true;
139
+ console.log(chalk.green(`\u2713 Assets injected into ${injectResult.targetName}`));
140
+ } else {
141
+ console.log(chalk.yellow('Warning: Could not inject assets. Continuing without injection.'));
142
+ }
143
+ }
144
+ } else if (assetsResult.error) {
145
+ console.log(chalk.yellow(`Warning: Could not fetch assets: ${assetsResult.error}`));
146
+ console.log(chalk.gray('Continuing without asset injection.'));
125
147
  } else {
126
- console.log(chalk.yellow('Warning: Could not inject assets. Continuing without injection.'));
148
+ console.log(chalk.yellow('No platform assets found.'));
127
149
  }
128
- } else if (assetsResult.error) {
129
- console.log(chalk.yellow(`Warning: Could not fetch assets: ${assetsResult.error}`));
150
+ } catch (err) {
151
+ console.log(chalk.yellow(`Warning: Error fetching assets: ${err.message}`));
130
152
  console.log(chalk.gray('Continuing without asset injection.'));
131
- } else {
132
- console.log(chalk.yellow('No platform assets found.'));
133
153
  }
134
- } catch (err) {
135
- console.log(chalk.yellow(`Warning: Error fetching assets: ${err.message}`));
136
- console.log(chalk.gray('Continuing without asset injection.'));
137
154
  }
138
155
  } else if (!inject) {
139
156
  console.log(chalk.gray('Skipping asset injection (--no-inject)'));
140
157
  } else if (!siteUrl) {
141
- console.log(chalk.yellow('Warning: No siteUrl in config.ts. Cannot fetch platform assets.'));
158
+ console.log(chalk.yellow('Warning: No siteUrl found. Cannot fetch platform assets.'));
159
+ console.log(chalk.gray('Set VITE_SITE_URL in .env.development'));
142
160
  }
143
161
 
144
162
  // Start dev server
@@ -147,13 +165,13 @@ export const irisDev = async (options = {}) => {
147
165
  console.log(chalk.gray('Press Ctrl+C to stop'));
148
166
  console.log();
149
167
 
150
- await runDevServer(projectPath, configPath, backupPath, assetsInjected);
168
+ await runDevServer(projectPath, modifiedFilePath, modifiedFileName, backupPath, assetsInjected);
151
169
  };
152
170
 
153
171
  /**
154
172
  * Run the Vue development server.
155
173
  */
156
- async function runDevServer(projectPath, configPath, backupPath, assetsInjected) {
174
+ async function runDevServer(projectPath, modifiedFilePath, modifiedFileName, backupPath, assetsInjected) {
157
175
  return new Promise((resolvePromise) => {
158
176
  const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
159
177
 
@@ -165,14 +183,14 @@ async function runDevServer(projectPath, configPath, backupPath, assetsInjected)
165
183
 
166
184
  // Handle cleanup on exit
167
185
  const cleanup = () => {
168
- if (assetsInjected && backupPath) {
186
+ if (assetsInjected && backupPath && modifiedFilePath) {
169
187
  console.log();
170
- console.log(chalk.blue('Restoring config.ts from backup...'));
171
- const restored = restoreConfigFile(configPath);
188
+ console.log(chalk.blue(`Restoring ${modifiedFileName} from backup...`));
189
+ const restored = restoreFile(modifiedFilePath);
172
190
  if (restored) {
173
- console.log(chalk.green('\u2713 config.ts restored'));
191
+ console.log(chalk.green(`\u2713 ${modifiedFileName} restored`));
174
192
  } else {
175
- console.log(chalk.yellow('Warning: Could not restore config.ts'));
193
+ console.log(chalk.yellow(`Warning: Could not restore ${modifiedFileName}`));
176
194
  console.log(chalk.gray(`Backup is at: ${backupPath}`));
177
195
  }
178
196
  }
@@ -242,22 +260,23 @@ async function handleRestore(pathOption) {
242
260
  }
243
261
 
244
262
  projectPath = resolve(projectPath);
245
- const configPath = findConfigFile(projectPath);
246
263
 
247
- if (!configPath) {
248
- console.log(chalk.red('No config.ts found in project.'));
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.'));
249
269
  return;
250
270
  }
251
271
 
252
- console.log(chalk.blue('Restoring config.ts from backup...'));
253
-
254
- const restored = restoreConfigFile(configPath);
272
+ console.log(chalk.blue(`Restoring ${targetName} from backup...`));
273
+ const restored = restoreFile(targetFile);
255
274
 
256
275
  if (restored) {
257
- console.log(chalk.green('\u2713 config.ts restored from backup'));
276
+ console.log(chalk.green(`\u2713 ${targetName} restored from backup`));
258
277
  } else {
259
278
  console.log(chalk.yellow('No backup file found.'));
260
- console.log(chalk.gray(`Expected backup at: ${configPath}.bak`));
279
+ console.log(chalk.gray(`Expected backup at: ${targetFile}.bak`));
261
280
  }
262
281
  }
263
282
 
@@ -77,9 +77,9 @@ async function showMainMenu() {
77
77
 
78
78
  const choices = [
79
79
  {
80
- name: 'Link a new Vue project',
80
+ name: 'Link a Vue project',
81
81
  value: 'link',
82
- description: 'Add a Vue project to the CLI'
82
+ description: chalk.dim('Add a Vue project to the CLI')
83
83
  }
84
84
  ];
85
85
 
@@ -87,12 +87,12 @@ async function showMainMenu() {
87
87
  choices.push({
88
88
  name: 'View linked projects',
89
89
  value: 'list',
90
- description: 'Show all linked Vue projects with status'
90
+ description: chalk.dim('Show all linked Vue projects with status')
91
91
  });
92
92
  choices.push({
93
93
  name: 'Unlink a project',
94
94
  value: 'unlink',
95
- description: 'Remove a Vue project from the CLI'
95
+ description: chalk.dim('Remove a Vue project from the CLI')
96
96
  });
97
97
  }
98
98
 
@@ -100,7 +100,7 @@ async function showMainMenu() {
100
100
  choices.push({
101
101
  name: `Clean up invalid projects (${invalidCount})`,
102
102
  value: 'cleanup',
103
- description: 'Remove projects with missing paths'
103
+ description: chalk.dim('Remove projects with missing paths')
104
104
  });
105
105
  }
106
106
 
@@ -152,33 +152,31 @@ async function handleLink(pathOption) {
152
152
  if (cwdConfig.found && cwdConfig.errors.length === 0) {
153
153
  // Valid Vue project
154
154
  choices.push({
155
- name: `Current directory - ${cwdConfig.appName} (${cwdConfig.slug})`,
155
+ name: `Current directory (${cwdPath})`,
156
156
  value: cwdPath,
157
- description: cwdPath
157
+ description: chalk.dim(`→ App: "${cwdConfig.appName}" (${cwdConfig.slug})`)
158
158
  });
159
159
  } else if (cwdConfig.found && cwdConfig.errors.length > 0) {
160
160
  // Has config.ts but with errors
161
161
  const errorMsg = cwdConfig.errors[0] || 'Invalid config';
162
162
  choices.push({
163
- name: `Current directory`,
163
+ name: `Current directory (${cwdPath})`,
164
164
  value: '__disabled__',
165
- disabled: `Config error: ${errorMsg}`,
166
- description: cwdPath
165
+ disabled: `Config error: ${errorMsg}`
167
166
  });
168
167
  } else {
169
168
  // No config.ts found
170
169
  choices.push({
171
- name: `Current directory`,
170
+ name: `Current directory (${cwdPath})`,
172
171
  value: '__disabled__',
173
- disabled: 'No config.ts found',
174
- description: cwdPath
172
+ disabled: 'No config.ts found'
175
173
  });
176
174
  }
177
175
 
178
176
  choices.push({
179
177
  name: 'Enter path manually',
180
178
  value: '__manual__',
181
- description: 'Specify the full path to a Vue project'
179
+ description: chalk.dim('Specify the full path to a Vue project')
182
180
  });
183
181
 
184
182
  choices.push({
@@ -303,7 +301,7 @@ async function handleUnlink(pathOption) {
303
301
  return {
304
302
  name: `${prefix}${displayName} (${displaySlug})`,
305
303
  value: p.slug,
306
- description: p.path
304
+ description: chalk.dim(`→ Path: ${p.path}`)
307
305
  };
308
306
  });
309
307
 
@@ -288,13 +288,17 @@ const handlePublishIrisAppAction = async (instanceUrl, apiKey, action) => {
288
288
  // Create zip from the app folder
289
289
  const zipBuffer = await createIrisZip(action.appPath, action.slug);
290
290
 
291
- // Publish via API with app-name parameter
291
+ // Publish via API with app metadata
292
292
  const response = await publishApp(
293
293
  instanceUrl,
294
294
  apiKey,
295
295
  zipBuffer,
296
296
  `${action.slug}.zip`,
297
- action.appName
297
+ action.appName,
298
+ {
299
+ appDescription: action.appDescription,
300
+ appIconId: action.appIconId
301
+ }
298
302
  );
299
303
 
300
304
  return response;
@@ -1190,10 +1194,12 @@ export const runPublish = async (options = {}) => {
1190
1194
  continue;
1191
1195
  }
1192
1196
 
1193
- // Get app name from linked project config (stored globally) or use slug
1197
+ // Get app metadata from linked project config (stored globally) or use slug
1194
1198
  const linkedProjects = getLinkedProjects();
1195
1199
  const linkedProject = linkedProjects.find(p => p.slug === slug);
1196
1200
  const appName = linkedProject?.appName || slug;
1201
+ const appDescription = linkedProject?.appDescription || null;
1202
+ const appIconId = linkedProject?.appIconId || null;
1197
1203
 
1198
1204
  // Calculate content hash for change detection
1199
1205
  const currentHash = hashIrisAppFolder(appPath);
@@ -1206,6 +1212,8 @@ export const runPublish = async (options = {}) => {
1206
1212
  action: 'create_iris_app',
1207
1213
  slug,
1208
1214
  appName,
1215
+ appDescription,
1216
+ appIconId,
1209
1217
  appPath,
1210
1218
  contentHash: currentHash
1211
1219
  });
@@ -1219,6 +1227,8 @@ export const runPublish = async (options = {}) => {
1219
1227
  action: 'update_iris_app',
1220
1228
  slug,
1221
1229
  appName: linkedProject?.appName || cachedApp?.appName || slug,
1230
+ appDescription,
1231
+ appIconId,
1222
1232
  appPath,
1223
1233
  contentHash: currentHash
1224
1234
  });
package/actions/pull.js CHANGED
@@ -2,22 +2,19 @@ import { ensureValidCredentials } from "../utils/cli/helpers/ensureCredentials.j
2
2
  import Config from "../utils/config.js";
3
3
  import { meqlQuery } from "../utils/magentrix/api/meqlQuery.js";
4
4
  import fs from "fs";
5
- import { withSpinner } from "../utils/spinner.js";
6
5
  import { ProgressTracker } from "../utils/progress.js";
7
6
  import { createLogger, Logger } from "../utils/logger.js";
8
- import { EXPORT_ROOT, TYPE_DIR_MAP, IRIS_APPS_DIR } from "../vars/global.js";
7
+ import { EXPORT_ROOT, IRIS_APPS_DIR } from "../vars/global.js";
9
8
  import { mapRecordToFile, writeRecords } from "../utils/cli/writeRecords.js";
10
- import { updateBase, removeFromBase, removeFromBaseBulk } from "../utils/updateFileBase.js";
11
- import { compareAllFilesAndLogStatus, promptConflictResolution, showCurrentConflicts } from "../utils/cli/helpers/compare.js";
9
+ import { updateBase, removeFromBaseBulk } from "../utils/updateFileBase.js";
10
+ import { promptConflictResolution } from "../utils/cli/helpers/compare.js";
12
11
  import path from "path";
13
12
  import { compareLocalAndRemote } from "../utils/compare.js";
14
13
  import chalk from 'chalk';
15
- import { getFileTag, setFileTag } from "../utils/filetag.js";
16
- import { downloadAssetsZip, listAssets } from "../utils/magentrix/api/assets.js";
17
- import { downloadAssets, walkAssets } from "../utils/downloadAssets.js";
14
+ import { setFileTag } from "../utils/filetag.js";
15
+ import { downloadAssets } from "../utils/downloadAssets.js";
18
16
  import { listApps, downloadApp } from "../utils/magentrix/api/iris.js";
19
17
  import { extractIrisZip, hashIrisAppFolder } from "../utils/iris/zipper.js";
20
- import { v4 as uuidv4 } from 'uuid';
21
18
  import readlineSync from 'readline-sync';
22
19
 
23
20
  const config = new Config();
package/bin/magentrix.js CHANGED
@@ -3,6 +3,8 @@
3
3
  // Imports
4
4
  import { Command } from 'commander';
5
5
  import chalk from 'chalk';
6
+ import { existsSync } from 'node:fs';
7
+ import { join } from 'node:path';
6
8
  import { VERSION } from '../vars/config.js';
7
9
  import { setup } from '../actions/setup.js';
8
10
  import { main } from '../actions/main.js';
@@ -17,6 +19,40 @@ import { update } from '../actions/update.js';
17
19
  import { configWizard } from '../actions/config.js';
18
20
  import { irisLink, irisDev, irisDelete, irisRecover, vueBuildStage } from '../actions/iris/index.js';
19
21
 
22
+ // ── Vue Project Detection ────────────────────────────────
23
+ /**
24
+ * Check if current directory is a Vue project (has config.ts)
25
+ */
26
+ function isInVueProject() {
27
+ const configLocations = [
28
+ 'src/config.ts',
29
+ 'config.ts',
30
+ 'src/iris-config.ts',
31
+ 'iris-config.ts'
32
+ ];
33
+ return configLocations.some(loc => existsSync(join(process.cwd(), loc)));
34
+ }
35
+
36
+ /**
37
+ * Block non-iris commands when in a Vue project
38
+ */
39
+ function requireMagentrixWorkspace(fn) {
40
+ return async (...args) => {
41
+ if (isInVueProject()) {
42
+ console.error(`\n${chalk.bgRed.white.bold(' ERROR ')} ${chalk.redBright('This command must be run in a Magentrix workspace')}\n`);
43
+ console.error(chalk.yellow('It looks like you\'re in a Vue project directory.'));
44
+ console.error(chalk.gray('This command requires a Magentrix workspace with global API key and instance URL.\n'));
45
+ console.error(chalk.cyan('Available commands in Vue projects:'));
46
+ 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'));
49
+ process.exit(1);
50
+ }
51
+ // Execute the command
52
+ await fn(...args);
53
+ };
54
+ }
55
+
20
56
  // ── Middleware ────────────────────────────────
21
57
  async function preMiddleware() {
22
58
  await recacheFileIdIndex(EXPORT_ROOT);
@@ -32,6 +68,11 @@ const withMiddleware = ({ pre, post }) => (fn) => async (...args) => {
32
68
  if (post) await post(...args);
33
69
  };
34
70
 
71
+ const withDefault = withMiddleware({ pre: preMiddleware, post: postMiddleware });
72
+
73
+ // Combined wrapper: check for Vue project + run middleware
74
+ const withWorkspaceCheck = (fn) => requireMagentrixWorkspace(withDefault(fn));
75
+
35
76
  // ── CLI Setup ────────────────────────────────
36
77
  const program = new Command();
37
78
  program
@@ -93,8 +134,6 @@ program
93
134
  }
94
135
  });
95
136
 
96
- const withDefault = withMiddleware({ pre: preMiddleware, post: postMiddleware });
97
-
98
137
  // ── Error Handlers ───────────────────────────
99
138
  program.showHelpAfterError(false);
100
139
  program.configureOutput({
@@ -128,8 +167,8 @@ program
128
167
  .description('Configure your Magentrix API key')
129
168
  .option('--api-key <apiKey>', 'Magentrix API key')
130
169
  .option('--instance-url <instanceUrl>', 'Magentrix instance URL (e.g., https://example.magentrixcloud.com)')
131
- .action(withDefault(setup));
132
- program.command('pull').description('Pull files from the remote server').action(withDefault(pull));
170
+ .action(withWorkspaceCheck(setup));
171
+ program.command('pull').description('Pull files from the remote server').action(withWorkspaceCheck(pull));
133
172
  const createCommand = program
134
173
  .command('create')
135
174
  .description('Create files locally')
@@ -138,7 +177,7 @@ const createCommand = program
138
177
  .option('--name <name>', 'Name of the file to create')
139
178
  .option('--description <description>', 'Optional description')
140
179
  .option('--entity-id <entityId>', 'Entity ID (required for triggers)')
141
- .action(withDefault(create));
180
+ .action(withWorkspaceCheck(create));
142
181
 
143
182
  // Override help for create command to show options
144
183
  createCommand.configureHelp({
@@ -185,17 +224,17 @@ createCommand.configureHelp({
185
224
  return help;
186
225
  }
187
226
  });
188
- program.command('status').description('Show file conflicts').action(withDefault(status));
189
- program.command('autopublish').description('Watch & sync changes in real time').action(withDefault(autoPublish));
227
+ program.command('status').description('Show file conflicts').action(withWorkspaceCheck(status));
228
+ program.command('autopublish').description('Watch & sync changes in real time').action(withWorkspaceCheck(autoPublish));
190
229
  // Publish does its own comprehensive file scanning, so skip the pre-cache middleware
191
- program.command('publish').description('Publish pending changes to the remote server').action(withDefault(publish));
192
- program.command('update').description('Update MagentrixCLI to the latest version').action(update);
230
+ 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));
193
232
 
194
233
  // Config command - interactive wizard
195
234
  program
196
235
  .command('config')
197
236
  .description('Configure CLI settings')
198
- .action(configWizard);
237
+ .action(requireMagentrixWorkspace(configWizard));
199
238
 
200
239
  // Iris commands for Vue.js app management
201
240
  program
@@ -236,6 +275,18 @@ program
236
275
  // ── Unknown Command Handler ──────────────────
237
276
  program.argument('[command]', 'command to run').action((cmd) => {
238
277
  const runMain = async () => {
278
+ // Check if in Vue project
279
+ if (isInVueProject()) {
280
+ console.error(`\n${chalk.bgRed.white.bold(' ERROR ')} ${chalk.redBright('This command must be run in a Magentrix workspace')}\n`);
281
+ console.error(chalk.yellow('It looks like you\'re in a Vue project directory.'));
282
+ console.error(chalk.gray('This command requires a Magentrix workspace with global API key and instance URL.\n'));
283
+ console.error(chalk.cyan('Available commands in Vue projects:'));
284
+ 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'));
287
+ process.exit(1);
288
+ }
289
+
239
290
  await preMiddleware();
240
291
  await main();
241
292
  await postMiddleware();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magentrix-corp/magentrix-cli",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
4
4
  "description": "CLI tool for synchronizing local files with Magentrix cloud platform",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -11,6 +11,108 @@ const CONFIG_LOCATIONS = [
11
11
  'iris-config.ts'
12
12
  ];
13
13
 
14
+ /**
15
+ * The .env file to read configuration from.
16
+ * Currently only .env.development is supported.
17
+ */
18
+ const ENV_FILE = '.env.development';
19
+
20
+ /**
21
+ * Parse a .env file and return key-value pairs.
22
+ *
23
+ * @param {string} envPath - Path to the .env file
24
+ * @returns {Record<string, string>} - Parsed environment variables
25
+ */
26
+ function parseEnvFile(envPath) {
27
+ const result = {};
28
+
29
+ if (!existsSync(envPath)) {
30
+ return result;
31
+ }
32
+
33
+ const content = readFileSync(envPath, 'utf-8');
34
+ const lines = content.split('\n');
35
+
36
+ for (const line of lines) {
37
+ // Skip comments and empty lines
38
+ const trimmed = line.trim();
39
+ if (!trimmed || trimmed.startsWith('#')) {
40
+ continue;
41
+ }
42
+
43
+ // Parse KEY=VALUE or KEY = VALUE
44
+ const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
45
+ if (match) {
46
+ const key = match[1];
47
+ let value = match[2].trim();
48
+
49
+ // Remove surrounding quotes if present
50
+ if ((value.startsWith('"') && value.endsWith('"')) ||
51
+ (value.startsWith("'") && value.endsWith("'"))) {
52
+ value = value.slice(1, -1);
53
+ }
54
+
55
+ result[key] = value;
56
+ }
57
+ }
58
+
59
+ return result;
60
+ }
61
+
62
+ /**
63
+ * Read configuration from .env.development file.
64
+ * Reads siteUrl, assets, and refreshToken.
65
+ *
66
+ * @param {string} projectPath - Path to the Vue project
67
+ * @returns {{
68
+ * siteUrl: string | null,
69
+ * assets: string[],
70
+ * refreshToken: string | null,
71
+ * envFileUsed: string | null
72
+ * }}
73
+ */
74
+ export function readEnvConfig(projectPath) {
75
+ const result = {
76
+ siteUrl: null,
77
+ assets: [],
78
+ refreshToken: null,
79
+ envFileUsed: null
80
+ };
81
+
82
+ const envPath = join(projectPath, ENV_FILE);
83
+
84
+ if (!existsSync(envPath)) {
85
+ return result;
86
+ }
87
+
88
+ const envVars = parseEnvFile(envPath);
89
+ result.envFileUsed = ENV_FILE;
90
+
91
+ // Read VITE_SITE_URL
92
+ if (envVars.VITE_SITE_URL) {
93
+ result.siteUrl = envVars.VITE_SITE_URL;
94
+ }
95
+
96
+ // Read VITE_ASSETS
97
+ if (envVars.VITE_ASSETS) {
98
+ try {
99
+ const parsed = JSON.parse(envVars.VITE_ASSETS);
100
+ if (Array.isArray(parsed)) {
101
+ result.assets = parsed;
102
+ }
103
+ } catch {
104
+ // If not valid JSON, skip
105
+ }
106
+ }
107
+
108
+ // Read VITE_REFRESH_TOKEN
109
+ if (envVars.VITE_REFRESH_TOKEN) {
110
+ result.refreshToken = envVars.VITE_REFRESH_TOKEN;
111
+ }
112
+
113
+ return result;
114
+ }
115
+
14
116
  /**
15
117
  * Find the config.ts file in a Vue project.
16
118
  *
@@ -35,6 +137,8 @@ export function findConfigFile(projectPath) {
35
137
  * @returns {{
36
138
  * slug: string | null,
37
139
  * appName: string | null,
140
+ * appDescription: string | null,
141
+ * appIconId: string | null,
38
142
  * siteUrl: string | null,
39
143
  * assets: string[],
40
144
  * raw: string
@@ -44,6 +148,8 @@ export function parseConfigFile(configPath) {
44
148
  const result = {
45
149
  slug: null,
46
150
  appName: null,
151
+ appDescription: null,
152
+ appIconId: null,
47
153
  siteUrl: null,
48
154
  assets: [],
49
155
  raw: ''
@@ -82,6 +188,20 @@ export function parseConfigFile(configPath) {
82
188
  }
83
189
  }
84
190
 
191
+ // Extract appDescription (optional)
192
+ // appDescription: "Description text"
193
+ const appDescriptionMatch = content.match(/(?:appDescription|app_description)\s*:\s*["'`]([^"'`]*)["'`]/);
194
+ if (appDescriptionMatch) {
195
+ result.appDescription = appDescriptionMatch[1];
196
+ }
197
+
198
+ // Extract appIconId (optional)
199
+ // appIconId: "icon-id-here"
200
+ const appIconIdMatch = content.match(/(?:appIconId|app_icon_id)\s*:\s*["'`]([^"'`]*)["'`]/);
201
+ if (appIconIdMatch) {
202
+ result.appIconId = appIconIdMatch[1];
203
+ }
204
+
85
205
  // Extract siteUrl (various patterns)
86
206
  // siteUrl: "https://...", site_url: "https://...", baseUrl: "https://..."
87
207
  // Also handles: siteUrl: env.siteUrl || "https://..."
@@ -114,15 +234,24 @@ export function parseConfigFile(configPath) {
114
234
  /**
115
235
  * Read Vue project configuration.
116
236
  *
237
+ * Reads from multiple sources:
238
+ * - slug, appName, appDescription, appIconId: from config.ts only
239
+ * - siteUrl, assets, refreshToken: from .env.development only (no fallback)
240
+ *
117
241
  * @param {string} projectPath - Path to the Vue project
118
242
  * @returns {{
119
243
  * found: boolean,
120
244
  * configPath: string | null,
121
245
  * slug: string | null,
122
246
  * appName: string | null,
247
+ * appDescription: string | null,
248
+ * appIconId: string | null,
123
249
  * siteUrl: string | null,
124
250
  * assets: string[],
125
- * errors: string[]
251
+ * refreshToken: string | null,
252
+ * envFileUsed: string | null,
253
+ * errors: string[],
254
+ * warnings: string[]
126
255
  * }}
127
256
  */
128
257
  export function readVueConfig(projectPath) {
@@ -131,9 +260,14 @@ export function readVueConfig(projectPath) {
131
260
  configPath: null,
132
261
  slug: null,
133
262
  appName: null,
263
+ appDescription: null,
264
+ appIconId: null,
134
265
  siteUrl: null,
135
266
  assets: [],
136
- errors: []
267
+ refreshToken: null,
268
+ envFileUsed: null,
269
+ errors: [],
270
+ warnings: []
137
271
  };
138
272
 
139
273
  const configPath = findConfigFile(projectPath);
@@ -146,93 +280,130 @@ export function readVueConfig(projectPath) {
146
280
  result.found = true;
147
281
  result.configPath = configPath;
148
282
 
283
+ // Parse config.ts for slug, appName, appDescription, appIconId (always from config.ts)
149
284
  const parsed = parseConfigFile(configPath);
150
285
  result.slug = parsed.slug;
151
286
  result.appName = parsed.appName;
152
- result.siteUrl = parsed.siteUrl;
153
- result.assets = parsed.assets;
287
+ result.appDescription = parsed.appDescription;
288
+ result.appIconId = parsed.appIconId;
289
+
290
+ // Read .env.development for siteUrl, assets, and refreshToken (no fallback to config.ts)
291
+ const envConfig = readEnvConfig(projectPath);
292
+ result.envFileUsed = envConfig.envFileUsed;
293
+ result.siteUrl = envConfig.siteUrl;
294
+ result.assets = envConfig.assets;
295
+ result.refreshToken = envConfig.refreshToken;
154
296
 
155
- // Validate required fields
297
+ // Validate required fields in config.ts
156
298
  if (!result.slug) {
157
- result.errors.push('Missing required field: slug');
299
+ result.errors.push('Missing required field in config.ts: slug (appPath)');
158
300
  }
159
301
  if (!result.appName) {
160
- result.errors.push('Missing required field: appName');
302
+ result.errors.push('Missing required field in config.ts: appName');
303
+ }
304
+
305
+ // Warnings for missing .env.development values
306
+ if (!envConfig.envFileUsed) {
307
+ result.warnings.push('No .env.development file found');
308
+ } else {
309
+ if (!result.siteUrl) {
310
+ result.warnings.push('VITE_SITE_URL not set in .env.development');
311
+ }
161
312
  }
162
313
 
163
314
  return result;
164
315
  }
165
316
 
166
317
  /**
167
- * Create a backup of the config file.
318
+ * Create a backup of a file.
168
319
  *
169
- * @param {string} configPath - Path to the config.ts file
320
+ * @param {string} filePath - Path to the file to backup
170
321
  * @returns {string} - Path to the backup file
171
322
  */
172
- export function backupConfigFile(configPath) {
173
- const backupPath = `${configPath}.bak`;
174
- copyFileSync(configPath, backupPath);
323
+ export function backupFile(filePath) {
324
+ const backupPath = `${filePath}.bak`;
325
+ copyFileSync(filePath, backupPath);
175
326
  return backupPath;
176
327
  }
177
328
 
178
329
  /**
179
- * Restore config file from backup.
330
+ * Restore a file from backup.
180
331
  *
181
- * @param {string} configPath - Path to the config.ts file
332
+ * @param {string} filePath - Path to the file to restore
182
333
  * @returns {boolean} - True if restored, false if backup not found
183
334
  */
184
- export function restoreConfigFile(configPath) {
185
- const backupPath = `${configPath}.bak`;
335
+ export function restoreFile(filePath) {
336
+ const backupPath = `${filePath}.bak`;
186
337
  if (!existsSync(backupPath)) {
187
338
  return false;
188
339
  }
189
- copyFileSync(backupPath, configPath);
340
+ copyFileSync(backupPath, filePath);
190
341
  return true;
191
342
  }
192
343
 
193
344
  /**
194
- * Inject assets into config.ts file.
345
+ * Determine which file will be used for asset injection.
346
+ * Only .env.development is supported for asset injection.
195
347
  *
196
- * @param {string} configPath - Path to the config.ts file
348
+ * @param {string} projectPath - Path to the Vue project
349
+ * @returns {{targetFile: string | null, targetName: string | null}}
350
+ */
351
+ export function getInjectionTarget(projectPath) {
352
+ const envPath = join(projectPath, ENV_FILE);
353
+
354
+ if (existsSync(envPath)) {
355
+ return { targetFile: envPath, targetName: '.env.development' };
356
+ }
357
+
358
+ return { targetFile: null, targetName: null };
359
+ }
360
+
361
+ /**
362
+ * Inject assets into .env.development file.
363
+ * Returns failure if .env.development does not exist.
364
+ *
365
+ * @param {string} projectPath - Path to the Vue project
366
+ * @param {string[]} assets - Array of asset URLs to inject
367
+ * @returns {{success: boolean, targetFile: string | null, targetName: string | null}}
368
+ */
369
+ export function injectAssets(projectPath, assets) {
370
+ const { targetFile, targetName } = getInjectionTarget(projectPath);
371
+
372
+ if (!targetFile) {
373
+ return { success: false, targetFile: null, targetName: null };
374
+ }
375
+
376
+ const success = injectAssetsIntoEnv(targetFile, assets);
377
+ return { success, targetFile, targetName };
378
+ }
379
+
380
+ /**
381
+ * Inject assets into a .env file.
382
+ *
383
+ * @param {string} envPath - Path to the .env file
197
384
  * @param {string[]} assets - Array of asset URLs to inject
198
385
  * @returns {boolean} - True if successful
199
386
  */
200
- export function injectAssetsIntoConfig(configPath, assets) {
201
- if (!existsSync(configPath)) {
387
+ export function injectAssetsIntoEnv(envPath, assets) {
388
+ if (!existsSync(envPath)) {
202
389
  return false;
203
390
  }
204
391
 
205
- let content = readFileSync(configPath, 'utf-8');
206
-
207
- // Format assets array
208
- const assetsStr = assets.map(url => ` "${url}"`).join(',\n');
209
- const newAssetsBlock = `assets: [\n // Injected by magentrix iris-dev\n${assetsStr}\n ]`;
392
+ let content = readFileSync(envPath, 'utf-8');
393
+ const assetsJson = JSON.stringify(assets);
394
+ const newLine = `VITE_ASSETS = '${assetsJson}'`;
210
395
 
211
- // Check if assets array exists
212
- const assetsPattern = /assets\s*:\s*\[[\s\S]*?\]/;
396
+ // Check if VITE_ASSETS already exists
397
+ const assetsPattern = /^VITE_ASSETS\s*=.*$/m;
213
398
  if (assetsPattern.test(content)) {
214
- // Replace existing assets array
215
- content = content.replace(assetsPattern, newAssetsBlock);
399
+ // Replace existing VITE_ASSETS line
400
+ content = content.replace(assetsPattern, newLine);
216
401
  } else {
217
- // Need to add assets array - find a good place
218
- // Look for the end of the config object
219
- const configObjectPattern = /(export\s+(?:const|let|var)\s+\w+\s*=\s*\{[\s\S]*?)(}\s*;?\s*$)/;
220
- const match = content.match(configObjectPattern);
221
- if (match) {
222
- // Insert before closing brace
223
- const beforeClose = match[1].trimEnd();
224
- const needsComma = !beforeClose.endsWith(',') && !beforeClose.endsWith('{');
225
- content = content.replace(
226
- configObjectPattern,
227
- `${match[1]}${needsComma ? ',' : ''}\n ${newAssetsBlock}\n${match[2]}`
228
- );
229
- } else {
230
- // Can't find a good place to inject
231
- return false;
232
- }
402
+ // Add new line at the end
403
+ content = content.trimEnd() + '\n' + newLine + '\n';
233
404
  }
234
405
 
235
- writeFileSync(configPath, content, 'utf-8');
406
+ writeFileSync(envPath, content, 'utf-8');
236
407
  return true;
237
408
  }
238
409
 
@@ -250,22 +421,29 @@ Could not find config.ts in the Vue project.
250
421
 
251
422
  Expected location: ${join(projectPath, 'src/config.ts')}
252
423
 
253
- Required fields:
254
- - slug: App identifier (used as folder name)
424
+ Required fields in config.ts:
425
+ - appPath (slug): App identifier (used as folder name)
255
426
  - appName: Display name for navigation menu
256
- - siteUrl: Magentrix instance URL
427
+
428
+ Required fields in .env.development:
429
+ - VITE_SITE_URL: Magentrix instance URL
430
+ - VITE_REFRESH_TOKEN: API refresh token
431
+ - VITE_ASSETS: Platform assets (managed by CLI)
257
432
 
258
433
  Example config.ts:
259
434
  export const config = {
260
- slug: "my-app",
261
- appName: "My Application",
262
- siteUrl: "https://yourinstance.magentrix.com",
263
- assets: []
264
- }`;
435
+ appPath: "my-app",
436
+ appName: "My Application"
437
+ }
438
+
439
+ Example .env.development:
440
+ VITE_SITE_URL = https://yourinstance.magentrix.com
441
+ VITE_REFRESH_TOKEN = your-api-key
442
+ VITE_ASSETS = '[]'`;
265
443
  }
266
444
 
267
445
  /**
268
- * Format config validation errors.
446
+ * Format config validation errors and warnings.
269
447
  *
270
448
  * @param {ReturnType<typeof readVueConfig>} config - Config read result
271
449
  * @returns {string} - Formatted error message
@@ -276,21 +454,36 @@ export function formatConfigErrors(config) {
276
454
  '────────────────────────────────────────────────────',
277
455
  '',
278
456
  `Config file: ${config.configPath || 'not found'}`,
279
- ''
280
457
  ];
281
458
 
459
+ if (config.envFileUsed) {
460
+ lines.push(`Env file: ${config.envFileUsed}`);
461
+ } else {
462
+ lines.push('Env file: .env.development (not found)');
463
+ }
464
+
465
+ lines.push('');
466
+
282
467
  if (config.errors.length > 0) {
283
- lines.push('Issues:');
468
+ lines.push('Errors:');
284
469
  for (const error of config.errors) {
285
470
  lines.push(` ✗ ${error}`);
286
471
  }
472
+ lines.push('');
473
+ }
474
+
475
+ if (config.warnings?.length > 0) {
476
+ lines.push('Warnings:');
477
+ for (const warning of config.warnings) {
478
+ lines.push(` ⚠ ${warning}`);
479
+ }
480
+ lines.push('');
287
481
  }
288
482
 
289
- lines.push('');
290
483
  lines.push('Current values:');
291
484
  lines.push(` slug: ${config.slug || '(missing)'}`);
292
485
  lines.push(` appName: ${config.appName || '(missing)'}`);
293
- lines.push(` siteUrl: ${config.siteUrl || '(missing)'}`);
486
+ lines.push(` siteUrl: ${config.siteUrl || '(not set)'}`);
294
487
 
295
488
  return lines.join('\n');
296
489
  }
@@ -163,6 +163,8 @@ export function findLinkedProjectByPath(projectPath) {
163
163
  * path: string,
164
164
  * slug: string,
165
165
  * appName: string,
166
+ * appDescription: string | null,
167
+ * appIconId: string | null,
166
168
  * siteUrl: string | null,
167
169
  * lastBuild: string | null
168
170
  * } | null,
@@ -224,6 +226,8 @@ export function linkVueProject(projectPath) {
224
226
  ...projects[index],
225
227
  slug: vueConfig.slug,
226
228
  appName: vueConfig.appName,
229
+ appDescription: vueConfig.appDescription,
230
+ appIconId: vueConfig.appIconId,
227
231
  siteUrl: vueConfig.siteUrl
228
232
  };
229
233
  saveLinkedProjects(projects);
@@ -252,6 +256,8 @@ export function linkVueProject(projectPath) {
252
256
  path: normalizedPath,
253
257
  slug: vueConfig.slug,
254
258
  appName: vueConfig.appName,
259
+ appDescription: vueConfig.appDescription,
260
+ appIconId: vueConfig.appIconId,
255
261
  siteUrl: vueConfig.siteUrl,
256
262
  lastBuild: null
257
263
  };
@@ -30,9 +30,12 @@ export const listApps = async (instanceUrl, token) => {
30
30
  * @param {Buffer} zipBuffer - The zip file as a Buffer
31
31
  * @param {string} filename - The filename for the zip (e.g., "my-app.zip")
32
32
  * @param {string} appName - The user-friendly display name (required for navigation)
33
+ * @param {Object} options - Optional parameters
34
+ * @param {string} [options.appDescription] - App description
35
+ * @param {string} [options.appIconId] - App icon ID
33
36
  * @returns {Promise<{success: boolean, message: string, folderName: string}>}
34
37
  */
35
- export const publishApp = async (instanceUrl, token, zipBuffer, filename, appName) => {
38
+ export const publishApp = async (instanceUrl, token, zipBuffer, filename, appName, options = {}) => {
36
39
  if (!instanceUrl || !token) {
37
40
  throw new Error('Missing required Magentrix instanceUrl or token');
38
41
  }
@@ -55,11 +58,22 @@ export const publishApp = async (instanceUrl, token, zipBuffer, filename, appNam
55
58
  const formData = new FormData();
56
59
  formData.append('file', file);
57
60
 
58
- // app-name is passed as URL parameter (required for navigation)
61
+ // Build query parameters
62
+ const params = new URLSearchParams();
63
+ params.append('app-name', appName);
64
+
65
+ if (options.appDescription) {
66
+ params.append('app-description', options.appDescription);
67
+ }
68
+
69
+ if (options.appIconId) {
70
+ params.append('app-icon-id', options.appIconId);
71
+ }
72
+
59
73
  const response = await fetchMagentrix({
60
74
  instanceUrl,
61
75
  token,
62
- path: `/iris/publishapp?app-name=${encodeURIComponent(appName)}`,
76
+ path: `/iris/publishapp?${params.toString()}`,
63
77
  method: 'POST',
64
78
  body: formData,
65
79
  ignoreContentType: true,