@magentrix-corp/magentrix-cli 1.2.0 → 1.3.0

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.
@@ -0,0 +1,338 @@
1
+ import chalk from 'chalk';
2
+ import { select, input } from '@inquirer/prompts';
3
+ import { spawn } from 'node:child_process';
4
+ import { existsSync } from 'node:fs';
5
+ import { resolve } from 'node:path';
6
+ import {
7
+ readVueConfig,
8
+ formatMissingConfigError,
9
+ formatConfigErrors,
10
+ findConfigFile,
11
+ backupConfigFile,
12
+ restoreConfigFile,
13
+ injectAssetsIntoConfig
14
+ } from '../../utils/iris/config-reader.js';
15
+ import { getIrisAssets } from '../../utils/magentrix/api/iris.js';
16
+ import {
17
+ getLinkedProjectsWithStatus,
18
+ buildProjectChoices
19
+ } from '../../utils/iris/linker.js';
20
+ import { ensureValidCredentials } from '../../utils/cli/helpers/ensureCredentials.js';
21
+
22
+ /**
23
+ * iris-dev command - Start Vue dev server with platform assets injected.
24
+ *
25
+ * Options:
26
+ * --path <dir> Specify Vue project path
27
+ * --no-inject Skip asset injection, just run dev server
28
+ * --restore Restore config.ts from backup without running
29
+ */
30
+ export const irisDev = async (options = {}) => {
31
+ process.stdout.write('\x1Bc'); // Clear console
32
+
33
+ const { path: pathOption, inject = true, restore } = options;
34
+
35
+ // Handle --restore option
36
+ if (restore) {
37
+ await handleRestore(pathOption);
38
+ return;
39
+ }
40
+
41
+ // Determine which project to use
42
+ let projectPath = pathOption;
43
+ let vueConfig = null;
44
+
45
+ if (projectPath) {
46
+ projectPath = resolve(projectPath);
47
+
48
+ if (!existsSync(projectPath)) {
49
+ console.log(chalk.red(`Error: Path does not exist: ${projectPath}`));
50
+ return;
51
+ }
52
+
53
+ vueConfig = readVueConfig(projectPath);
54
+ } else {
55
+ // Try current directory first
56
+ const cwdConfig = readVueConfig(process.cwd());
57
+ if (cwdConfig.found && cwdConfig.errors.length === 0) {
58
+ projectPath = process.cwd();
59
+ vueConfig = cwdConfig;
60
+ console.log(chalk.gray(`Using current directory: ${projectPath}`));
61
+ } else {
62
+ // Prompt user to select a project
63
+ const result = await selectProject();
64
+ if (!result) return;
65
+
66
+ projectPath = result.path;
67
+ vueConfig = result.config;
68
+ }
69
+ }
70
+
71
+ // Validate Vue config
72
+ if (!vueConfig.found) {
73
+ console.log(chalk.red(formatMissingConfigError(projectPath)));
74
+ return;
75
+ }
76
+
77
+ if (vueConfig.errors.length > 0) {
78
+ console.log(chalk.red(formatConfigErrors(vueConfig)));
79
+ return;
80
+ }
81
+
82
+ const { slug, appName, siteUrl } = vueConfig;
83
+ const configPath = findConfigFile(projectPath);
84
+
85
+ console.log(chalk.blue('\nIris Development Server'));
86
+ console.log(chalk.gray('─'.repeat(48)));
87
+ console.log(chalk.white(` Project: ${chalk.cyan(appName)} (${slug})`));
88
+ console.log(chalk.white(` Path: ${chalk.gray(projectPath)}`));
89
+ if (siteUrl) {
90
+ console.log(chalk.white(` Site: ${chalk.gray(siteUrl)}`));
91
+ }
92
+ console.log();
93
+
94
+ let backupPath = null;
95
+ let assetsInjected = false;
96
+
97
+ // Inject assets if enabled
98
+ 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'));
125
+ } else {
126
+ console.log(chalk.yellow('Warning: Could not inject assets. Continuing without injection.'));
127
+ }
128
+ } else if (assetsResult.error) {
129
+ console.log(chalk.yellow(`Warning: Could not fetch assets: ${assetsResult.error}`));
130
+ console.log(chalk.gray('Continuing without asset injection.'));
131
+ } else {
132
+ console.log(chalk.yellow('No platform assets found.'));
133
+ }
134
+ } catch (err) {
135
+ console.log(chalk.yellow(`Warning: Error fetching assets: ${err.message}`));
136
+ console.log(chalk.gray('Continuing without asset injection.'));
137
+ }
138
+ } else if (!inject) {
139
+ console.log(chalk.gray('Skipping asset injection (--no-inject)'));
140
+ } else if (!siteUrl) {
141
+ console.log(chalk.yellow('Warning: No siteUrl in config.ts. Cannot fetch platform assets.'));
142
+ }
143
+
144
+ // Start dev server
145
+ console.log();
146
+ console.log(chalk.blue('Starting development server...'));
147
+ console.log(chalk.gray('Press Ctrl+C to stop'));
148
+ console.log();
149
+
150
+ await runDevServer(projectPath, configPath, backupPath, assetsInjected);
151
+ };
152
+
153
+ /**
154
+ * Run the Vue development server.
155
+ */
156
+ async function runDevServer(projectPath, configPath, backupPath, assetsInjected) {
157
+ return new Promise((resolvePromise) => {
158
+ const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
159
+
160
+ const child = spawn(npmCmd, ['run', 'dev'], {
161
+ cwd: projectPath,
162
+ stdio: 'inherit',
163
+ shell: false
164
+ });
165
+
166
+ // Handle cleanup on exit
167
+ const cleanup = () => {
168
+ if (assetsInjected && backupPath) {
169
+ console.log();
170
+ console.log(chalk.blue('Restoring config.ts from backup...'));
171
+ const restored = restoreConfigFile(configPath);
172
+ if (restored) {
173
+ console.log(chalk.green('\u2713 config.ts restored'));
174
+ } else {
175
+ console.log(chalk.yellow('Warning: Could not restore config.ts'));
176
+ console.log(chalk.gray(`Backup is at: ${backupPath}`));
177
+ }
178
+ }
179
+ };
180
+
181
+ // Handle process signals
182
+ process.on('SIGINT', () => {
183
+ cleanup();
184
+ child.kill('SIGINT');
185
+ });
186
+
187
+ process.on('SIGTERM', () => {
188
+ cleanup();
189
+ child.kill('SIGTERM');
190
+ });
191
+
192
+ child.on('close', (code) => {
193
+ // If dev server exited with error, show helpful context first
194
+ if (code !== 0 && code !== null) {
195
+ console.log();
196
+ console.log(chalk.bgYellow.black(' External Process Error '));
197
+ console.log(chalk.yellow('─'.repeat(48)));
198
+ console.log(chalk.white('The Vue dev server exited with an error.'));
199
+ console.log(chalk.gray('This is NOT a MagentrixCLI issue.'));
200
+ console.log();
201
+ console.log(chalk.white('Common causes:'));
202
+ console.log(chalk.gray(' • Permission issues in node_modules/'));
203
+ console.log(chalk.gray(' • Missing dependencies (run npm install)'));
204
+ console.log(chalk.gray(' • Port already in use'));
205
+ console.log(chalk.gray(' • Syntax errors in project files'));
206
+ console.log();
207
+ console.log(chalk.white('If you see EACCES/permission errors, try:'));
208
+ console.log(chalk.cyan(` sudo chown -R $(whoami) "${projectPath}/node_modules"`));
209
+ console.log(chalk.yellow('─'.repeat(48)));
210
+ }
211
+
212
+ // Cleanup after showing error context
213
+ cleanup();
214
+
215
+ resolvePromise(code);
216
+ });
217
+
218
+ child.on('error', (err) => {
219
+ console.log(chalk.red(`Failed to start dev server: ${err.message}`));
220
+ cleanup();
221
+ resolvePromise(1);
222
+ });
223
+ });
224
+ }
225
+
226
+ /**
227
+ * Handle --restore option.
228
+ */
229
+ async function handleRestore(pathOption) {
230
+ let projectPath = pathOption;
231
+
232
+ if (!projectPath) {
233
+ // Try current directory
234
+ const cwdConfig = readVueConfig(process.cwd());
235
+ if (cwdConfig.found) {
236
+ projectPath = process.cwd();
237
+ } else {
238
+ console.log(chalk.red('No Vue project found in current directory.'));
239
+ console.log(chalk.gray('Use --path to specify the project path.'));
240
+ return;
241
+ }
242
+ }
243
+
244
+ projectPath = resolve(projectPath);
245
+ const configPath = findConfigFile(projectPath);
246
+
247
+ if (!configPath) {
248
+ console.log(chalk.red('No config.ts found in project.'));
249
+ return;
250
+ }
251
+
252
+ console.log(chalk.blue('Restoring config.ts from backup...'));
253
+
254
+ const restored = restoreConfigFile(configPath);
255
+
256
+ if (restored) {
257
+ console.log(chalk.green('\u2713 config.ts restored from backup'));
258
+ } else {
259
+ console.log(chalk.yellow('No backup file found.'));
260
+ console.log(chalk.gray(`Expected backup at: ${configPath}.bak`));
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Prompt user to select a project.
266
+ */
267
+ async function selectProject() {
268
+ const projectsWithStatus = getLinkedProjectsWithStatus();
269
+
270
+ // Check if CWD is a Vue project (and not already linked)
271
+ const cwdConfig = readVueConfig(process.cwd());
272
+ const inVueProject = cwdConfig.found && cwdConfig.errors.length === 0;
273
+ const cwdAlreadyLinked = projectsWithStatus.some(p =>
274
+ resolve(p.path) === resolve(process.cwd())
275
+ );
276
+
277
+ // Build choices using the helper
278
+ const choices = buildProjectChoices({
279
+ includeManual: true,
280
+ includeCancel: true,
281
+ showInvalid: true
282
+ });
283
+
284
+ // If CWD is a valid Vue project and not linked, add it at the top
285
+ if (inVueProject && !cwdAlreadyLinked) {
286
+ choices.unshift({
287
+ name: `${cwdConfig.appName} (${cwdConfig.slug}) - Current directory (not linked)`,
288
+ value: { type: 'cwd', path: process.cwd() }
289
+ });
290
+ }
291
+
292
+ // Check if we have any projects to show
293
+ const hasProjects = choices.some(c => c.value?.type === 'linked' || c.value?.type === 'cwd');
294
+
295
+ if (!hasProjects) {
296
+ console.log(chalk.yellow('No linked Vue projects found.'));
297
+ console.log();
298
+ console.log(chalk.gray('To get started:'));
299
+ console.log(chalk.white(` 1. Link a Vue project: ${chalk.cyan('magentrix iris-link')}`));
300
+ console.log(chalk.white(` 2. Or specify path: ${chalk.cyan('magentrix iris-dev --path /path/to/vue-project')}`));
301
+ console.log();
302
+ }
303
+
304
+ const choice = await select({
305
+ message: 'Which project do you want to run?',
306
+ choices
307
+ });
308
+
309
+ if (choice.type === 'cancel') {
310
+ console.log(chalk.gray('Cancelled.'));
311
+ return null;
312
+ }
313
+
314
+ if (choice.type === 'manual') {
315
+ const manualPath = await input({
316
+ message: 'Enter the path to your Vue project:',
317
+ validate: (value) => {
318
+ if (!value.trim()) {
319
+ return 'Path is required';
320
+ }
321
+ const resolved = resolve(value);
322
+ if (!existsSync(resolved)) {
323
+ return `Path does not exist: ${resolved}`;
324
+ }
325
+ return true;
326
+ }
327
+ });
328
+
329
+ const config = readVueConfig(resolve(manualPath));
330
+ return { path: resolve(manualPath), config };
331
+ }
332
+
333
+ // Linked project or CWD
334
+ const config = readVueConfig(choice.path);
335
+ return { path: choice.path, config };
336
+ }
337
+
338
+ export default irisDev;
@@ -0,0 +1,6 @@
1
+ // Iris Vue.js App Management Commands
2
+ export { irisLink } from './link.js';
3
+ export { irisDev } from './dev.js';
4
+ export { irisDelete } from './delete.js';
5
+ export { irisRecover } from './recover.js';
6
+ export { vueBuildStage } from './buildStage.js';