@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 +34 -14
- package/actions/iris/dev.js +51 -34
- package/actions/publish.js +20 -5
- package/actions/pull.js +5 -8
- package/package.json +1 -1
- package/utils/iris/config-reader.js +253 -60
- package/utils/iris/linker.js +6 -0
- package/utils/magentrix/api/iris.js +17 -3
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
|
|
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
|
|
517
|
+
2. Backup `.env.development` and inject assets
|
|
517
518
|
3. Run `npm run dev`
|
|
518
|
-
4. Restore
|
|
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
|
|
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
|
-
|
|
568
|
-
|
|
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
|
-
**
|
|
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
|
-
-
|
|
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
|
-
####
|
|
648
|
-
|
|
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:
|
package/actions/iris/dev.js
CHANGED
|
@@ -7,10 +7,10 @@ import {
|
|
|
7
7
|
readVueConfig,
|
|
8
8
|
formatMissingConfigError,
|
|
9
9
|
formatConfigErrors,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
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
|
-
//
|
|
114
|
-
|
|
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 (
|
|
123
|
-
|
|
124
|
-
console.log(chalk.
|
|
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
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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(
|
|
171
|
-
const restored =
|
|
186
|
+
console.log(chalk.blue(`Restoring ${modifiedFileName} from backup...`));
|
|
187
|
+
const restored = restoreFile(modifiedFilePath);
|
|
172
188
|
if (restored) {
|
|
173
|
-
console.log(chalk.green(
|
|
189
|
+
console.log(chalk.green(`\u2713 ${modifiedFileName} restored`));
|
|
174
190
|
} else {
|
|
175
|
-
console.log(chalk.yellow(
|
|
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
|
-
|
|
248
|
-
|
|
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(
|
|
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(
|
|
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: ${
|
|
277
|
+
console.log(chalk.gray(`Expected backup at: ${targetFile}.bak`));
|
|
261
278
|
}
|
|
262
279
|
}
|
|
263
280
|
|
package/actions/publish.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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,
|
|
7
|
+
import { EXPORT_ROOT, IRIS_APPS_DIR } from "../vars/global.js";
|
|
9
8
|
import { mapRecordToFile, writeRecords } from "../utils/cli/writeRecords.js";
|
|
10
|
-
import { updateBase,
|
|
11
|
-
import {
|
|
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 {
|
|
16
|
-
import {
|
|
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
|
@@ -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
|
-
*
|
|
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
|
-
|
|
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.
|
|
153
|
-
result.
|
|
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
|
|
318
|
+
* Create a backup of a file.
|
|
168
319
|
*
|
|
169
|
-
* @param {string}
|
|
320
|
+
* @param {string} filePath - Path to the file to backup
|
|
170
321
|
* @returns {string} - Path to the backup file
|
|
171
322
|
*/
|
|
172
|
-
export function
|
|
173
|
-
const backupPath = `${
|
|
174
|
-
copyFileSync(
|
|
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
|
|
330
|
+
* Restore a file from backup.
|
|
180
331
|
*
|
|
181
|
-
* @param {string}
|
|
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
|
|
185
|
-
const backupPath = `${
|
|
335
|
+
export function restoreFile(filePath) {
|
|
336
|
+
const backupPath = `${filePath}.bak`;
|
|
186
337
|
if (!existsSync(backupPath)) {
|
|
187
338
|
return false;
|
|
188
339
|
}
|
|
189
|
-
copyFileSync(backupPath,
|
|
340
|
+
copyFileSync(backupPath, filePath);
|
|
190
341
|
return true;
|
|
191
342
|
}
|
|
192
343
|
|
|
193
344
|
/**
|
|
194
|
-
*
|
|
345
|
+
* Determine which file will be used for asset injection.
|
|
346
|
+
* Only .env.development is supported for asset injection.
|
|
195
347
|
*
|
|
196
|
-
* @param {string}
|
|
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
|
|
201
|
-
if (!existsSync(
|
|
387
|
+
export function injectAssetsIntoEnv(envPath, assets) {
|
|
388
|
+
if (!existsSync(envPath)) {
|
|
202
389
|
return false;
|
|
203
390
|
}
|
|
204
391
|
|
|
205
|
-
let content = readFileSync(
|
|
206
|
-
|
|
207
|
-
|
|
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
|
|
212
|
-
const assetsPattern =
|
|
396
|
+
// Check if VITE_ASSETS already exists
|
|
397
|
+
const assetsPattern = /^VITE_ASSETS\s*=.*$/m;
|
|
213
398
|
if (assetsPattern.test(content)) {
|
|
214
|
-
// Replace existing
|
|
215
|
-
content = content.replace(assetsPattern,
|
|
399
|
+
// Replace existing VITE_ASSETS line
|
|
400
|
+
content = content.replace(assetsPattern, newLine);
|
|
216
401
|
} else {
|
|
217
|
-
//
|
|
218
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
261
|
-
appName: "My Application"
|
|
262
|
-
|
|
263
|
-
|
|
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('
|
|
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 || '(
|
|
486
|
+
lines.push(` siteUrl: ${config.siteUrl || '(not set)'}`);
|
|
294
487
|
|
|
295
488
|
return lines.join('\n');
|
|
296
489
|
}
|
package/utils/iris/linker.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
76
|
+
path: `/iris/publishapp?${params.toString()}`,
|
|
63
77
|
method: 'POST',
|
|
64
78
|
body: formData,
|
|
65
79
|
ignoreContentType: true,
|