@magentrix-corp/magentrix-cli 1.3.0 → 1.3.2

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
@@ -210,6 +210,7 @@ This creates a `src` folder with all your files organized into:
210
210
  - `Pages/` - Your ASPX pages (`.aspx` files)
211
211
  - `Templates/` - Your templates (`.aspx` files)
212
212
  - `Assets/` - Your static assets (images, CSS, JavaScript, etc.)
213
+ - `iris-apps/` - Your Iris Vue.js applications
213
214
 
214
215
  ---
215
216
 
@@ -509,13 +510,13 @@ magentrix iris-dev
509
510
  **Options:**
510
511
  - `--path <dir>` - Specify Vue project path
511
512
  - `--no-inject` - Skip asset injection, just run dev server
512
- - `--restore` - Restore config.ts from backup without running
513
+ - `--restore` - Restore `.env.development` from backup without running
513
514
 
514
515
  **Process:**
515
516
  1. Fetch platform assets from Magentrix
516
- 2. Backup `config.ts` and inject assets
517
+ 2. Backup `.env.development` and inject assets
517
518
  3. Run `npm run dev`
518
- 4. Restore `config.ts` on exit (Ctrl+C)
519
+ 4. Restore `.env.development` on exit (Ctrl+C)
519
520
 
520
521
  #### Delete an Iris App
521
522
  ```bash
@@ -557,22 +558,33 @@ magentrix iris-recover
557
558
 
558
559
  ### Vue Project Requirements
559
560
 
560
- 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):
561
564
 
562
565
  ```typescript
563
566
  // src/config.ts
564
567
  export const config = {
565
- appPath: "my-app", // App identifier (folder name on server)
566
- appName: "My Application", // Display name in navigation menu
567
- siteUrl: "https://yourinstance.magentrix.com",
568
- 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
569
572
  }
570
573
  ```
571
574
 
572
- **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:**
573
584
  - Slug: `appPath`, `slug`, or `app_path`
574
585
  - Name: `appName`, `app_name`, or `name`
575
- - URL: `siteUrl`, `site_url`, `baseUrl`, or `base_url`
586
+ - Description: `appDescription` or `app_description`
587
+ - Icon: `appIconId` or `app_icon_id`
576
588
 
577
589
  ### Typical Development Workflow
578
590
 
@@ -638,14 +650,22 @@ cd ~/magentrix-workspace
638
650
  magentrix vue-build-stage --path ~/my-vue-app # ✓ Works!
639
651
  ```
640
652
 
641
- #### "Missing required field: slug"
653
+ #### "Missing required field in config.ts: slug (appPath)"
642
654
  Your Vue project's `config.ts` is missing the app identifier. Add an `appPath` or `slug` field.
643
655
 
644
- #### "Missing required field: appName"
656
+ #### "Missing required field in config.ts: appName"
645
657
  Your Vue project's `config.ts` is missing the display name. Add an `appName` field.
646
658
 
647
- #### Build fails with "VITE_SITE_URL is not defined"
648
- 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
+ ```
649
669
 
650
670
  #### "No config.ts found"
651
671
  The CLI looks for config in these locations:
@@ -7,10 +7,10 @@ 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
16
  import {
@@ -22,10 +22,13 @@ import { ensureValidCredentials } from '../../utils/cli/helpers/ensureCredential
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,6 +95,8 @@ 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) {
@@ -110,20 +114,31 @@ export const irisDev = async (options = {}) => {
110
114
  if (assetsResult.success && assetsResult.assets?.length > 0) {
111
115
  console.log(chalk.green(`\u2713 Found ${assetsResult.assets.length} platform assets`));
112
116
 
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);
117
+ // Determine which file will be modified
118
+ const { targetFile, targetName } = getInjectionTarget(projectPath);
121
119
 
122
- if (injected) {
123
- assetsInjected = true;
124
- console.log(chalk.green('\u2713 Assets injected'));
120
+ if (!targetFile) {
121
+ console.log(chalk.yellow('Warning: No .env.development file found. Cannot inject assets.'));
122
+ console.log(chalk.gray('Create a .env.development file to enable asset injection.'));
125
123
  } else {
126
- console.log(chalk.yellow('Warning: Could not inject assets. Continuing without injection.'));
124
+ modifiedFilePath = targetFile;
125
+ modifiedFileName = targetName;
126
+
127
+ // Backup before modifying
128
+ console.log(chalk.blue(`Backing up ${modifiedFileName}...`));
129
+ backupPath = backupFile(modifiedFilePath);
130
+ console.log(chalk.green(`\u2713 Backup created`));
131
+
132
+ // Inject assets
133
+ console.log(chalk.blue('Injecting assets...'));
134
+ const injectResult = injectAssets(projectPath, assetsResult.assets);
135
+
136
+ if (injectResult.success) {
137
+ assetsInjected = true;
138
+ console.log(chalk.green(`\u2713 Assets injected into ${injectResult.targetName}`));
139
+ } else {
140
+ console.log(chalk.yellow('Warning: Could not inject assets. Continuing without injection.'));
141
+ }
127
142
  }
128
143
  } else if (assetsResult.error) {
129
144
  console.log(chalk.yellow(`Warning: Could not fetch assets: ${assetsResult.error}`));
@@ -138,7 +153,8 @@ export const irisDev = async (options = {}) => {
138
153
  } else if (!inject) {
139
154
  console.log(chalk.gray('Skipping asset injection (--no-inject)'));
140
155
  } else if (!siteUrl) {
141
- console.log(chalk.yellow('Warning: No siteUrl in config.ts. Cannot fetch platform assets.'));
156
+ console.log(chalk.yellow('Warning: No siteUrl found. Cannot fetch platform assets.'));
157
+ console.log(chalk.gray('Set VITE_SITE_URL in .env.development'));
142
158
  }
143
159
 
144
160
  // Start dev server
@@ -147,13 +163,13 @@ export const irisDev = async (options = {}) => {
147
163
  console.log(chalk.gray('Press Ctrl+C to stop'));
148
164
  console.log();
149
165
 
150
- await runDevServer(projectPath, configPath, backupPath, assetsInjected);
166
+ await runDevServer(projectPath, modifiedFilePath, modifiedFileName, backupPath, assetsInjected);
151
167
  };
152
168
 
153
169
  /**
154
170
  * Run the Vue development server.
155
171
  */
156
- async function runDevServer(projectPath, configPath, backupPath, assetsInjected) {
172
+ async function runDevServer(projectPath, modifiedFilePath, modifiedFileName, backupPath, assetsInjected) {
157
173
  return new Promise((resolvePromise) => {
158
174
  const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
159
175
 
@@ -165,14 +181,14 @@ async function runDevServer(projectPath, configPath, backupPath, assetsInjected)
165
181
 
166
182
  // Handle cleanup on exit
167
183
  const cleanup = () => {
168
- if (assetsInjected && backupPath) {
184
+ if (assetsInjected && backupPath && modifiedFilePath) {
169
185
  console.log();
170
- console.log(chalk.blue('Restoring config.ts from backup...'));
171
- const restored = restoreConfigFile(configPath);
186
+ console.log(chalk.blue(`Restoring ${modifiedFileName} from backup...`));
187
+ const restored = restoreFile(modifiedFilePath);
172
188
  if (restored) {
173
- console.log(chalk.green('\u2713 config.ts restored'));
189
+ console.log(chalk.green(`\u2713 ${modifiedFileName} restored`));
174
190
  } else {
175
- console.log(chalk.yellow('Warning: Could not restore config.ts'));
191
+ console.log(chalk.yellow(`Warning: Could not restore ${modifiedFileName}`));
176
192
  console.log(chalk.gray(`Backup is at: ${backupPath}`));
177
193
  }
178
194
  }
@@ -242,22 +258,23 @@ async function handleRestore(pathOption) {
242
258
  }
243
259
 
244
260
  projectPath = resolve(projectPath);
245
- const configPath = findConfigFile(projectPath);
246
261
 
247
- if (!configPath) {
248
- console.log(chalk.red('No config.ts found in project.'));
262
+ // Determine which file would be the injection target
263
+ const { targetFile, targetName } = getInjectionTarget(projectPath);
264
+
265
+ if (!targetFile) {
266
+ console.log(chalk.yellow('No config files found to restore.'));
249
267
  return;
250
268
  }
251
269
 
252
- console.log(chalk.blue('Restoring config.ts from backup...'));
253
-
254
- const restored = restoreConfigFile(configPath);
270
+ console.log(chalk.blue(`Restoring ${targetName} from backup...`));
271
+ const restored = restoreFile(targetFile);
255
272
 
256
273
  if (restored) {
257
- console.log(chalk.green('\u2713 config.ts restored from backup'));
274
+ console.log(chalk.green(`\u2713 ${targetName} restored from backup`));
258
275
  } else {
259
276
  console.log(chalk.yellow('No backup file found.'));
260
- console.log(chalk.gray(`Expected backup at: ${configPath}.bak`));
277
+ console.log(chalk.gray(`Expected backup at: ${targetFile}.bak`));
261
278
  }
262
279
  }
263
280
 
@@ -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;
@@ -1114,7 +1118,12 @@ export const runPublish = async (options = {}) => {
1114
1118
  if (!safe) continue;
1115
1119
 
1116
1120
  const { content, hash } = safe;
1117
- const renamed = cacheFile.lastKnownPath !== path.resolve(curFile.path);
1121
+ // Check both paths - only consider renamed if NEITHER matches current path
1122
+ // This prevents false positives from stale/corrupted tracking data
1123
+ const resolvedCurPath = path.resolve(curFile.path);
1124
+ const matchesActualPath = cacheFile.lastKnownActualPath === resolvedCurPath;
1125
+ const matchesExpectedPath = cacheFile.lastKnownPath === resolvedCurPath;
1126
+ const renamed = !matchesActualPath && !matchesExpectedPath;
1118
1127
  const contentChanged = hash !== cacheFile.contentHash;
1119
1128
 
1120
1129
  if (renamed || contentChanged) {
@@ -1127,7 +1136,7 @@ export const runPublish = async (options = {}) => {
1127
1136
  entity,
1128
1137
  fields: { [contentField]: content },
1129
1138
  renamed,
1130
- oldPath: cacheFile.lastKnownPath,
1139
+ oldPath: cacheFile.lastKnownActualPath || cacheFile.lastKnownPath,
1131
1140
  filePath: curFile.path,
1132
1141
  });
1133
1142
  }
@@ -1185,10 +1194,12 @@ export const runPublish = async (options = {}) => {
1185
1194
  continue;
1186
1195
  }
1187
1196
 
1188
- // 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
1189
1198
  const linkedProjects = getLinkedProjects();
1190
1199
  const linkedProject = linkedProjects.find(p => p.slug === slug);
1191
1200
  const appName = linkedProject?.appName || slug;
1201
+ const appDescription = linkedProject?.appDescription || null;
1202
+ const appIconId = linkedProject?.appIconId || null;
1192
1203
 
1193
1204
  // Calculate content hash for change detection
1194
1205
  const currentHash = hashIrisAppFolder(appPath);
@@ -1201,6 +1212,8 @@ export const runPublish = async (options = {}) => {
1201
1212
  action: 'create_iris_app',
1202
1213
  slug,
1203
1214
  appName,
1215
+ appDescription,
1216
+ appIconId,
1204
1217
  appPath,
1205
1218
  contentHash: currentHash
1206
1219
  });
@@ -1214,6 +1227,8 @@ export const runPublish = async (options = {}) => {
1214
1227
  action: 'update_iris_app',
1215
1228
  slug,
1216
1229
  appName: linkedProject?.appName || cachedApp?.appName || slug,
1230
+ appDescription,
1231
+ appIconId,
1217
1232
  appPath,
1218
1233
  contentHash: currentHash
1219
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magentrix-corp/magentrix-cli",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
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,