@magentrix-corp/magentrix-cli 1.3.15 → 1.3.17
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/LICENSE +25 -25
- package/README.md +1166 -1166
- package/actions/autopublish.old.js +293 -293
- package/actions/config.js +182 -182
- package/actions/create.js +466 -466
- package/actions/help.js +164 -164
- package/actions/iris/buildStage.js +874 -874
- package/actions/iris/delete.js +256 -256
- package/actions/iris/dev.js +391 -391
- package/actions/iris/index.js +6 -6
- package/actions/iris/link.js +375 -375
- package/actions/iris/recover.js +268 -268
- package/actions/main.js +80 -80
- package/actions/publish.js +1420 -1420
- package/actions/pull.js +684 -684
- package/actions/setup.js +148 -148
- package/actions/status.js +17 -17
- package/actions/update.js +248 -248
- package/bin/magentrix.js +393 -393
- package/package.json +55 -55
- package/utils/assetPaths.js +158 -158
- package/utils/autopublishLock.js +77 -77
- package/utils/cacher.js +206 -206
- package/utils/cli/checkInstanceUrl.js +76 -45
- package/utils/cli/helpers/compare.js +282 -282
- package/utils/cli/helpers/ensureApiKey.js +63 -63
- package/utils/cli/helpers/ensureCredentials.js +68 -68
- package/utils/cli/helpers/ensureInstanceUrl.js +75 -75
- package/utils/cli/writeRecords.js +262 -262
- package/utils/compare.js +135 -135
- package/utils/compress.js +17 -17
- package/utils/config.js +527 -527
- package/utils/debug.js +144 -144
- package/utils/diagnostics/testPublishLogic.js +96 -96
- package/utils/diff.js +49 -49
- package/utils/downloadAssets.js +291 -291
- package/utils/filetag.js +115 -115
- package/utils/hash.js +14 -14
- package/utils/iris/backup.js +411 -411
- package/utils/iris/builder.js +541 -541
- package/utils/iris/config-reader.js +664 -664
- package/utils/iris/deleteHelper.js +150 -150
- package/utils/iris/errors.js +537 -537
- package/utils/iris/linker.js +601 -601
- package/utils/iris/lock.js +360 -360
- package/utils/iris/validation.js +360 -360
- package/utils/iris/validator.js +281 -281
- package/utils/iris/zipper.js +248 -248
- package/utils/logger.js +291 -291
- package/utils/magentrix/api/assets.js +220 -220
- package/utils/magentrix/api/auth.js +107 -107
- package/utils/magentrix/api/createEntity.js +61 -61
- package/utils/magentrix/api/deleteEntity.js +55 -55
- package/utils/magentrix/api/iris.js +251 -251
- package/utils/magentrix/api/meqlQuery.js +36 -36
- package/utils/magentrix/api/retrieveEntity.js +86 -86
- package/utils/magentrix/api/updateEntity.js +66 -66
- package/utils/magentrix/fetch.js +168 -168
- package/utils/merge.js +22 -22
- package/utils/permissionError.js +70 -70
- package/utils/preferences.js +40 -40
- package/utils/progress.js +469 -469
- package/utils/spinner.js +43 -43
- package/utils/template.js +52 -52
- package/utils/updateFileBase.js +121 -121
- package/utils/workspaces.js +108 -108
- package/vars/config.js +11 -11
- package/vars/global.js +50 -50
|
@@ -1,874 +1,874 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import { select, input, confirm } from '@inquirer/prompts';
|
|
3
|
-
import { existsSync } from 'node:fs';
|
|
4
|
-
import { resolve, join } from 'node:path';
|
|
5
|
-
import { spawn } from 'node:child_process';
|
|
6
|
-
import Config from '../../utils/config.js';
|
|
7
|
-
import { runPublish } from '../publish.js';
|
|
8
|
-
import { isAutopublishRunning } from '../../utils/autopublishLock.js';
|
|
9
|
-
import {
|
|
10
|
-
buildVueProject,
|
|
11
|
-
stageToWorkspace,
|
|
12
|
-
findDistDirectory,
|
|
13
|
-
formatBuildError,
|
|
14
|
-
formatValidationError
|
|
15
|
-
} from '../../utils/iris/builder.js';
|
|
16
|
-
import { validateIrisBuild } from '../../utils/iris/validator.js';
|
|
17
|
-
import {
|
|
18
|
-
readVueConfig,
|
|
19
|
-
formatMissingConfigError,
|
|
20
|
-
formatConfigErrors
|
|
21
|
-
} from '../../utils/iris/config-reader.js';
|
|
22
|
-
import {
|
|
23
|
-
getLinkedProjectsWithStatus,
|
|
24
|
-
linkVueProject,
|
|
25
|
-
findLinkedProjectByPath,
|
|
26
|
-
buildProjectChoices
|
|
27
|
-
} from '../../utils/iris/linker.js';
|
|
28
|
-
import { EXPORT_ROOT, IRIS_APPS_DIR, HASHED_CWD } from '../../vars/global.js';
|
|
29
|
-
import { getValidWorkspaces } from '../../utils/workspaces.js';
|
|
30
|
-
import { formatTimeoutError } from '../../utils/iris/errors.js';
|
|
31
|
-
|
|
32
|
-
const config = new Config();
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Timeout for status check operations (30 seconds).
|
|
36
|
-
*/
|
|
37
|
-
const STATUS_CHECK_TIMEOUT = 30000;
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Timeout for command execution (5 minutes).
|
|
41
|
-
*/
|
|
42
|
-
const COMMAND_TIMEOUT = 5 * 60 * 1000;
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Display warnings from build/stage operations.
|
|
46
|
-
*
|
|
47
|
-
* @param {string[]} warnings - Array of warning messages
|
|
48
|
-
*/
|
|
49
|
-
function displayWarnings(warnings) {
|
|
50
|
-
if (!warnings || warnings.length === 0) return;
|
|
51
|
-
|
|
52
|
-
for (const warning of warnings) {
|
|
53
|
-
console.log(chalk.yellow(`⚠ ${warning}`));
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Check if the current directory is a Vue project (has config.ts).
|
|
59
|
-
* @returns {boolean}
|
|
60
|
-
*/
|
|
61
|
-
function isInVueProject() {
|
|
62
|
-
const configLocations = [
|
|
63
|
-
'src/config.ts',
|
|
64
|
-
'config.ts',
|
|
65
|
-
'src/iris-config.ts',
|
|
66
|
-
'iris-config.ts'
|
|
67
|
-
];
|
|
68
|
-
return configLocations.some(loc => existsSync(join(process.cwd(), loc)));
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Check if the current directory appears to be a Magentrix workspace.
|
|
73
|
-
* @returns {boolean} - True if in a Magentrix workspace
|
|
74
|
-
*/
|
|
75
|
-
function isMagentrixWorkspace() {
|
|
76
|
-
const magentrixFolder = join(process.cwd(), '.magentrix');
|
|
77
|
-
const srcFolder = join(process.cwd(), 'src');
|
|
78
|
-
|
|
79
|
-
// Check for .magentrix folder
|
|
80
|
-
if (!existsSync(magentrixFolder)) return false;
|
|
81
|
-
|
|
82
|
-
// Check for src folder (typical workspace structure)
|
|
83
|
-
if (!existsSync(srcFolder)) return false;
|
|
84
|
-
|
|
85
|
-
// Check if credentials are configured (instanceUrl or apiKey in global config)
|
|
86
|
-
const instanceUrl = config.read('instanceUrl', { global: true, pathHash: HASHED_CWD });
|
|
87
|
-
const apiKey = config.read('apiKey', { global: true, pathHash: HASHED_CWD });
|
|
88
|
-
|
|
89
|
-
return !!(instanceUrl || apiKey);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* vue-run-build command - Build a Vue project and stage to CLI workspace.
|
|
94
|
-
*
|
|
95
|
-
* Two modes of operation:
|
|
96
|
-
* 1. Run from Magentrix workspace: prompts for which Vue project to build
|
|
97
|
-
* 2. Run from Vue project: prompts for which workspace to stage into
|
|
98
|
-
*
|
|
99
|
-
* Options:
|
|
100
|
-
* --path <dir> Specify Vue project path directly
|
|
101
|
-
* --skip-build Use existing dist/ without rebuilding
|
|
102
|
-
* --workspace <dir> Specify Magentrix workspace path directly
|
|
103
|
-
*/
|
|
104
|
-
export const vueBuildStage = async (options = {}) => {
|
|
105
|
-
process.stdout.write('\x1Bc'); // Clear console
|
|
106
|
-
|
|
107
|
-
const { path: pathOption, skipBuild, workspace: workspaceOption } = options;
|
|
108
|
-
|
|
109
|
-
// Detect which mode we're in
|
|
110
|
-
const inVueProject = isInVueProject();
|
|
111
|
-
const inWorkspace = isMagentrixWorkspace();
|
|
112
|
-
|
|
113
|
-
// If run from a Vue project, use reversed logic
|
|
114
|
-
if (inVueProject && !inWorkspace) {
|
|
115
|
-
await buildFromVueProject(options);
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Standard mode: run from workspace, select Vue project
|
|
120
|
-
// Warn if not in a Magentrix workspace
|
|
121
|
-
if (!inWorkspace) {
|
|
122
|
-
console.log(chalk.yellow('⚠ Warning: Magentrix Workspace Not Detected'));
|
|
123
|
-
console.log(chalk.gray('─'.repeat(48)));
|
|
124
|
-
console.log(chalk.white('\nThis command should be run from your Magentrix CLI workspace directory.'));
|
|
125
|
-
console.log(chalk.white('It stages Vue.js build files to ') + chalk.cyan('src/iris-apps/<slug>/'));
|
|
126
|
-
console.log();
|
|
127
|
-
console.log(chalk.white('Expected workspace indicators:'));
|
|
128
|
-
console.log(chalk.gray(' • .magentrix/ folder (config and cache)'));
|
|
129
|
-
console.log(chalk.gray(' • src/ folder (code files and assets)'));
|
|
130
|
-
console.log(chalk.gray(' • Magentrix credentials configured'));
|
|
131
|
-
console.log();
|
|
132
|
-
console.log(chalk.white('Current directory: ') + chalk.gray(process.cwd()));
|
|
133
|
-
console.log();
|
|
134
|
-
|
|
135
|
-
const shouldContinue = await confirm({
|
|
136
|
-
message: 'Do you want to continue anyway?',
|
|
137
|
-
default: false
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
if (!shouldContinue) {
|
|
141
|
-
console.log(chalk.gray('\nCancelled. Run this command from your Magentrix workspace.'));
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
console.log();
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Determine which project to build
|
|
149
|
-
let projectPath = pathOption;
|
|
150
|
-
let vueConfig = null;
|
|
151
|
-
|
|
152
|
-
if (projectPath) {
|
|
153
|
-
// Path provided via option
|
|
154
|
-
projectPath = resolve(projectPath);
|
|
155
|
-
|
|
156
|
-
if (!existsSync(projectPath)) {
|
|
157
|
-
console.log(chalk.red(`Error: Path does not exist: ${projectPath}`));
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
vueConfig = readVueConfig(projectPath);
|
|
162
|
-
} else {
|
|
163
|
-
// Prompt user to select a project
|
|
164
|
-
const result = await selectProject();
|
|
165
|
-
if (!result) return; // User cancelled
|
|
166
|
-
|
|
167
|
-
projectPath = result.path;
|
|
168
|
-
vueConfig = result.config;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Validate Vue config
|
|
172
|
-
if (!vueConfig.found) {
|
|
173
|
-
console.log(chalk.red(formatMissingConfigError(projectPath)));
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (vueConfig.errors.length > 0) {
|
|
178
|
-
console.log(chalk.red(formatConfigErrors(vueConfig)));
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const { slug, appName } = vueConfig;
|
|
183
|
-
|
|
184
|
-
console.log(chalk.blue('\nVue Build & Stage'));
|
|
185
|
-
console.log(chalk.gray('─'.repeat(48)));
|
|
186
|
-
console.log(chalk.white(` Project: ${chalk.cyan(appName)} (${slug})`));
|
|
187
|
-
console.log(chalk.white(` Path: ${chalk.gray(projectPath)}`));
|
|
188
|
-
console.log();
|
|
189
|
-
|
|
190
|
-
// Ensure project is linked
|
|
191
|
-
const linked = findLinkedProjectByPath(projectPath);
|
|
192
|
-
if (!linked) {
|
|
193
|
-
const shouldLink = await confirm({
|
|
194
|
-
message: 'This project is not linked. Link it now?',
|
|
195
|
-
default: true
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
if (shouldLink) {
|
|
199
|
-
const linkResult = linkVueProject(projectPath);
|
|
200
|
-
if (!linkResult.success) {
|
|
201
|
-
console.log(chalk.red(`Failed to link project: ${linkResult.error}`));
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
console.log(chalk.green(`\u2713 Project linked`));
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
let distPath;
|
|
209
|
-
|
|
210
|
-
if (skipBuild) {
|
|
211
|
-
// Use existing dist
|
|
212
|
-
distPath = findDistDirectory(projectPath);
|
|
213
|
-
|
|
214
|
-
if (!distPath) {
|
|
215
|
-
console.log(chalk.red('No existing dist/ directory found.'));
|
|
216
|
-
console.log(chalk.gray('Run without --skip-build to build the project.'));
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
console.log(chalk.yellow(`Using existing dist: ${distPath}`));
|
|
221
|
-
|
|
222
|
-
// Validate the existing build
|
|
223
|
-
const validation = validateIrisBuild(distPath);
|
|
224
|
-
if (!validation.valid) {
|
|
225
|
-
console.log(chalk.red(formatValidationError(distPath, validation.errors)));
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
console.log(chalk.green('\u2713 Existing build is valid'));
|
|
230
|
-
} else {
|
|
231
|
-
// Build the project
|
|
232
|
-
console.log(chalk.blue('Building project...'));
|
|
233
|
-
console.log();
|
|
234
|
-
|
|
235
|
-
const buildResult = await buildVueProject(projectPath, { silent: false });
|
|
236
|
-
|
|
237
|
-
if (!buildResult.success) {
|
|
238
|
-
console.log();
|
|
239
|
-
console.log(chalk.red(formatBuildError(projectPath, buildResult.error)));
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
distPath = buildResult.distPath;
|
|
244
|
-
console.log();
|
|
245
|
-
console.log(chalk.green(`\u2713 Build completed successfully`));
|
|
246
|
-
console.log(chalk.gray(` Output: ${distPath}`));
|
|
247
|
-
|
|
248
|
-
// Display any build warnings
|
|
249
|
-
displayWarnings(buildResult.warnings);
|
|
250
|
-
|
|
251
|
-
// Validate build output
|
|
252
|
-
const validation = validateIrisBuild(distPath);
|
|
253
|
-
if (!validation.valid) {
|
|
254
|
-
console.log();
|
|
255
|
-
console.log(chalk.red(formatValidationError(distPath, validation.errors)));
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
console.log(chalk.green('\u2713 Build output validated'));
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Stage to CLI project (current workspace)
|
|
263
|
-
console.log();
|
|
264
|
-
console.log(chalk.blue('Staging to CLI workspace...'));
|
|
265
|
-
|
|
266
|
-
const stageResult = stageToWorkspace(distPath, slug, process.cwd());
|
|
267
|
-
|
|
268
|
-
if (!stageResult.success) {
|
|
269
|
-
console.log(chalk.red(`Failed to stage: ${stageResult.error}`));
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
console.log(chalk.green(`\u2713 Staged ${stageResult.fileCount} files to ${stageResult.stagedPath}`));
|
|
274
|
-
|
|
275
|
-
// Display any staging warnings
|
|
276
|
-
displayWarnings(stageResult.warnings);
|
|
277
|
-
|
|
278
|
-
// Summary
|
|
279
|
-
console.log();
|
|
280
|
-
console.log(chalk.green('─'.repeat(48)));
|
|
281
|
-
console.log(chalk.green.bold('\u2713 Build & Stage Complete!'));
|
|
282
|
-
console.log();
|
|
283
|
-
console.log(chalk.gray(`Staged to: ${EXPORT_ROOT}/${IRIS_APPS_DIR}/${slug}/`));
|
|
284
|
-
console.log();
|
|
285
|
-
|
|
286
|
-
// Check if autopublish is running
|
|
287
|
-
if (isAutopublishRunning()) {
|
|
288
|
-
console.log(chalk.cyan('✓ Autopublish is running - changes will be deployed automatically'));
|
|
289
|
-
} else {
|
|
290
|
-
// Prompt to run publish now
|
|
291
|
-
const shouldPublish = await confirm({
|
|
292
|
-
message: 'Do you want to publish to Magentrix now?',
|
|
293
|
-
default: true
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
if (shouldPublish) {
|
|
297
|
-
console.log();
|
|
298
|
-
console.log(chalk.blue('Running publish...'));
|
|
299
|
-
console.log();
|
|
300
|
-
|
|
301
|
-
try {
|
|
302
|
-
await runPublish();
|
|
303
|
-
} catch (error) {
|
|
304
|
-
console.log(chalk.red(`\nPublish failed: ${error.message}`));
|
|
305
|
-
console.log(chalk.gray('You can run it manually later with:'), chalk.yellow('magentrix publish'));
|
|
306
|
-
}
|
|
307
|
-
} else {
|
|
308
|
-
console.log();
|
|
309
|
-
console.log(chalk.cyan('Next steps:'));
|
|
310
|
-
console.log(chalk.white(` • Run ${chalk.yellow('magentrix publish')} to deploy to Magentrix`));
|
|
311
|
-
console.log(chalk.white(` • Or use ${chalk.yellow('magentrix autopublish')} for automatic deployment`));
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Build and stage when running from inside a Vue project.
|
|
318
|
-
* Prompts user to select which workspace to stage into.
|
|
319
|
-
*/
|
|
320
|
-
async function buildFromVueProject(options) {
|
|
321
|
-
const { skipBuild, workspace: workspaceOption } = options;
|
|
322
|
-
|
|
323
|
-
// Use current directory as Vue project
|
|
324
|
-
const projectPath = process.cwd();
|
|
325
|
-
const vueConfig = readVueConfig(projectPath);
|
|
326
|
-
|
|
327
|
-
// Validate Vue config
|
|
328
|
-
if (!vueConfig.found) {
|
|
329
|
-
console.log(chalk.red(formatMissingConfigError(projectPath)));
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
if (vueConfig.errors.length > 0) {
|
|
334
|
-
console.log(chalk.red(formatConfigErrors(vueConfig)));
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
const { slug, appName } = vueConfig;
|
|
339
|
-
|
|
340
|
-
console.log(chalk.blue('\nVue Build & Stage'));
|
|
341
|
-
console.log(chalk.gray('─'.repeat(48)));
|
|
342
|
-
console.log(chalk.white(` Project: ${chalk.cyan(appName)} (${slug})`));
|
|
343
|
-
console.log(chalk.white(` Path: ${chalk.gray(projectPath)}`));
|
|
344
|
-
console.log();
|
|
345
|
-
|
|
346
|
-
// Determine which workspace to stage into
|
|
347
|
-
let workspacePath = workspaceOption;
|
|
348
|
-
|
|
349
|
-
if (workspacePath) {
|
|
350
|
-
workspacePath = resolve(workspacePath);
|
|
351
|
-
if (!existsSync(workspacePath)) {
|
|
352
|
-
console.log(chalk.red(`Error: Workspace path does not exist: ${workspacePath}`));
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
} else {
|
|
356
|
-
// Prompt user to select a workspace
|
|
357
|
-
const result = await selectWorkspace();
|
|
358
|
-
if (!result) return; // User cancelled
|
|
359
|
-
workspacePath = result;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// Ensure project is linked
|
|
363
|
-
const linked = findLinkedProjectByPath(projectPath);
|
|
364
|
-
if (!linked) {
|
|
365
|
-
const shouldLink = await confirm({
|
|
366
|
-
message: 'This project is not linked. Link it now?',
|
|
367
|
-
default: true
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
if (shouldLink) {
|
|
371
|
-
const linkResult = linkVueProject(projectPath);
|
|
372
|
-
if (!linkResult.success) {
|
|
373
|
-
console.log(chalk.red(`Failed to link project: ${linkResult.error}`));
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
console.log(chalk.green(`\u2713 Project linked`));
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
let distPath;
|
|
381
|
-
|
|
382
|
-
if (skipBuild) {
|
|
383
|
-
// Use existing dist
|
|
384
|
-
distPath = findDistDirectory(projectPath);
|
|
385
|
-
|
|
386
|
-
if (!distPath) {
|
|
387
|
-
console.log(chalk.red('No existing dist/ directory found.'));
|
|
388
|
-
console.log(chalk.gray('Run without --skip-build to build the project.'));
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
console.log(chalk.yellow(`Using existing dist: ${distPath}`));
|
|
393
|
-
|
|
394
|
-
// Validate the existing build
|
|
395
|
-
const validation = validateIrisBuild(distPath);
|
|
396
|
-
if (!validation.valid) {
|
|
397
|
-
console.log(chalk.red(formatValidationError(distPath, validation.errors)));
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
console.log(chalk.green('\u2713 Existing build is valid'));
|
|
402
|
-
} else {
|
|
403
|
-
// Build the project
|
|
404
|
-
console.log(chalk.blue('Building project...'));
|
|
405
|
-
console.log();
|
|
406
|
-
|
|
407
|
-
const buildResult = await buildVueProject(projectPath, { silent: false });
|
|
408
|
-
|
|
409
|
-
if (!buildResult.success) {
|
|
410
|
-
console.log();
|
|
411
|
-
console.log(chalk.red(formatBuildError(projectPath, buildResult.error)));
|
|
412
|
-
return;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
distPath = buildResult.distPath;
|
|
416
|
-
console.log();
|
|
417
|
-
console.log(chalk.green(`\u2713 Build completed successfully`));
|
|
418
|
-
console.log(chalk.gray(` Output: ${distPath}`));
|
|
419
|
-
|
|
420
|
-
// Display any build warnings
|
|
421
|
-
displayWarnings(buildResult.warnings);
|
|
422
|
-
|
|
423
|
-
// Validate build output
|
|
424
|
-
const validation = validateIrisBuild(distPath);
|
|
425
|
-
if (!validation.valid) {
|
|
426
|
-
console.log();
|
|
427
|
-
console.log(chalk.red(formatValidationError(distPath, validation.errors)));
|
|
428
|
-
return;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
console.log(chalk.green('\u2713 Build output validated'));
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// Check if workspace might be out of sync and offer to pull first (BEFORE staging)
|
|
435
|
-
// This ensures pull doesn't overwrite our staged files
|
|
436
|
-
console.log();
|
|
437
|
-
console.log(chalk.gray('Checking workspace sync status...'));
|
|
438
|
-
|
|
439
|
-
// Check for a previous incomplete pull first — `magentrix status` only checks
|
|
440
|
-
// code entities, so a partial pull that synced code but not assets would falsely
|
|
441
|
-
// report "in sync". The marker file catches this case.
|
|
442
|
-
const workspaceConfig = new Config({ projectDir: workspacePath });
|
|
443
|
-
const hadIncompletePull = workspaceConfig.read('pullIncomplete', { global: false, filename: 'config.json' });
|
|
444
|
-
|
|
445
|
-
const syncStatus = await checkWorkspaceSyncStatus(workspacePath);
|
|
446
|
-
const needsPull = syncStatus.needsPull || !!hadIncompletePull;
|
|
447
|
-
|
|
448
|
-
if (needsPull) {
|
|
449
|
-
console.log();
|
|
450
|
-
if (hadIncompletePull && !syncStatus.needsPull) {
|
|
451
|
-
console.log(chalk.yellow('⚠ A previous pull did not complete. Your workspace may be out of sync.'));
|
|
452
|
-
} else {
|
|
453
|
-
console.log(chalk.yellow('⚠ Your workspace may be out of sync with the server.'));
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
const shouldPull = await confirm({
|
|
457
|
-
message: 'Would you like to pull latest changes first?',
|
|
458
|
-
default: true
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
if (shouldPull) {
|
|
462
|
-
console.log();
|
|
463
|
-
console.log(chalk.blue('Running pull from workspace...'));
|
|
464
|
-
console.log();
|
|
465
|
-
|
|
466
|
-
// Mark pull as in-progress before starting
|
|
467
|
-
workspaceConfig.save('pullIncomplete', true, { global: false, filename: 'config.json' });
|
|
468
|
-
|
|
469
|
-
const pullSuccess = await runCommandFromWorkspace(workspacePath, 'pull');
|
|
470
|
-
|
|
471
|
-
if (pullSuccess) {
|
|
472
|
-
// Pull completed successfully — clear the marker
|
|
473
|
-
workspaceConfig.removeKey('pullIncomplete', { filename: 'config.json' });
|
|
474
|
-
} else {
|
|
475
|
-
// Pull failed or was cancelled — marker stays for next run
|
|
476
|
-
console.log();
|
|
477
|
-
console.log(chalk.yellow('Pull encountered issues. You may want to resolve them manually.'));
|
|
478
|
-
|
|
479
|
-
const continueAnyway = await confirm({
|
|
480
|
-
message: 'Do you still want to continue with staging and publishing?',
|
|
481
|
-
default: false
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
if (!continueAnyway) {
|
|
485
|
-
console.log();
|
|
486
|
-
console.log(chalk.cyan('To continue manually:'));
|
|
487
|
-
console.log(chalk.white(` 1. Navigate to workspace: ${chalk.yellow(`cd "${workspacePath}"`)}`));
|
|
488
|
-
console.log(chalk.white(` 2. Run ${chalk.yellow('magentrix pull')} to resolve conflicts`));
|
|
489
|
-
console.log(chalk.white(` 3. Run ${chalk.yellow('magentrix vue-run-build --skip-build')} to stage`));
|
|
490
|
-
console.log(chalk.white(` 4. Run ${chalk.yellow('magentrix publish')} to deploy`));
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
console.log();
|
|
495
|
-
} else {
|
|
496
|
-
// User declined to pull - block publishing from out-of-sync workspace
|
|
497
|
-
console.log();
|
|
498
|
-
console.log(chalk.red('Publishing Iris apps from an out-of-sync workspace is not allowed.'));
|
|
499
|
-
console.log(chalk.gray('This prevents conflicts and ensures your deployment is based on the latest server state.'));
|
|
500
|
-
console.log();
|
|
501
|
-
console.log(chalk.cyan('To continue:'));
|
|
502
|
-
console.log(chalk.white(` 1. Navigate to workspace: ${chalk.yellow(`cd "${workspacePath}"`)}`));
|
|
503
|
-
console.log(chalk.white(` 2. Run ${chalk.yellow('magentrix pull')} to sync with server`));
|
|
504
|
-
console.log(chalk.white(` 3. Run ${chalk.yellow('magentrix publish')} to deploy`));
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
|
-
} else if (syncStatus.checked) {
|
|
508
|
-
console.log(chalk.green('✓ Workspace is in sync'));
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// Stage to selected workspace (AFTER pull to avoid overwriting)
|
|
512
|
-
console.log();
|
|
513
|
-
console.log(chalk.blue(`Staging to workspace: ${workspacePath}`));
|
|
514
|
-
|
|
515
|
-
const stageResult = stageToWorkspace(distPath, slug, workspacePath);
|
|
516
|
-
|
|
517
|
-
if (!stageResult.success) {
|
|
518
|
-
console.log(chalk.red(`Failed to stage: ${stageResult.error}`));
|
|
519
|
-
return;
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
console.log(chalk.green(`\u2713 Staged ${stageResult.fileCount} files to ${stageResult.stagedPath}`));
|
|
523
|
-
|
|
524
|
-
// Display any staging warnings
|
|
525
|
-
displayWarnings(stageResult.warnings);
|
|
526
|
-
|
|
527
|
-
// Summary
|
|
528
|
-
console.log();
|
|
529
|
-
console.log(chalk.green('─'.repeat(48)));
|
|
530
|
-
console.log(chalk.green.bold('\u2713 Build & Stage Complete!'));
|
|
531
|
-
console.log();
|
|
532
|
-
console.log(chalk.gray(`Staged to: ${stageResult.stagedPath}`));
|
|
533
|
-
console.log();
|
|
534
|
-
|
|
535
|
-
// Ask if they want to publish now
|
|
536
|
-
const shouldPublish = await confirm({
|
|
537
|
-
message: 'Do you want to publish to Magentrix now?',
|
|
538
|
-
default: true
|
|
539
|
-
});
|
|
540
|
-
|
|
541
|
-
if (shouldPublish) {
|
|
542
|
-
console.log();
|
|
543
|
-
console.log(chalk.blue('Running publish from workspace...'));
|
|
544
|
-
console.log();
|
|
545
|
-
|
|
546
|
-
const publishSuccess = await runCommandFromWorkspace(workspacePath, 'publish');
|
|
547
|
-
|
|
548
|
-
if (!publishSuccess) {
|
|
549
|
-
console.log();
|
|
550
|
-
console.log(chalk.cyan('To publish manually:'));
|
|
551
|
-
console.log(chalk.white(` 1. Navigate to workspace: ${chalk.yellow(`cd "${workspacePath}"`)}`));
|
|
552
|
-
console.log(chalk.white(` 2. Run ${chalk.yellow('magentrix publish')}`));
|
|
553
|
-
}
|
|
554
|
-
} else {
|
|
555
|
-
console.log();
|
|
556
|
-
console.log(chalk.cyan('Next steps:'));
|
|
557
|
-
console.log(chalk.white(` 1. Navigate to workspace: ${chalk.yellow(`cd "${workspacePath}"`)}`));
|
|
558
|
-
console.log(chalk.white(` 2. Run ${chalk.yellow('magentrix publish')} to deploy to Magentrix`));
|
|
559
|
-
console.log(chalk.white(` Or use ${chalk.yellow('magentrix autopublish')} for automatic deployment`));
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
/**
|
|
564
|
-
* Check if a workspace needs to pull (has remote changes or conflicts).
|
|
565
|
-
*
|
|
566
|
-
* @param {string} workspacePath - Path to the Magentrix workspace
|
|
567
|
-
* @returns {Promise<{checked: boolean, needsPull: boolean, timedOut: boolean}>}
|
|
568
|
-
*/
|
|
569
|
-
async function checkWorkspaceSyncStatus(workspacePath) {
|
|
570
|
-
return new Promise((resolvePromise) => {
|
|
571
|
-
const isWindows = process.platform === 'win32';
|
|
572
|
-
const npmCmd = isWindows ? 'npx.cmd' : 'npx';
|
|
573
|
-
|
|
574
|
-
let output = '';
|
|
575
|
-
let resolved = false;
|
|
576
|
-
|
|
577
|
-
const child = spawn(npmCmd, ['magentrix', 'status'], {
|
|
578
|
-
cwd: workspacePath,
|
|
579
|
-
stdio: ['inherit', 'pipe', 'pipe'],
|
|
580
|
-
shell: isWindows
|
|
581
|
-
});
|
|
582
|
-
|
|
583
|
-
// Set timeout for status check
|
|
584
|
-
const timeout = setTimeout(() => {
|
|
585
|
-
if (!resolved) {
|
|
586
|
-
resolved = true;
|
|
587
|
-
try {
|
|
588
|
-
child.kill('SIGTERM');
|
|
589
|
-
} catch {
|
|
590
|
-
// Ignore kill errors
|
|
591
|
-
}
|
|
592
|
-
console.log(chalk.yellow(`\n⚠ Status check timed out after ${STATUS_CHECK_TIMEOUT / 1000} seconds`));
|
|
593
|
-
resolvePromise({ checked: false, needsPull: false, timedOut: true });
|
|
594
|
-
}
|
|
595
|
-
}, STATUS_CHECK_TIMEOUT);
|
|
596
|
-
|
|
597
|
-
child.stdout.on('data', (data) => {
|
|
598
|
-
output += data.toString();
|
|
599
|
-
});
|
|
600
|
-
|
|
601
|
-
child.stderr.on('data', (data) => {
|
|
602
|
-
output += data.toString();
|
|
603
|
-
});
|
|
604
|
-
|
|
605
|
-
child.on('close', (code) => {
|
|
606
|
-
if (resolved) return;
|
|
607
|
-
resolved = true;
|
|
608
|
-
clearTimeout(timeout);
|
|
609
|
-
|
|
610
|
-
// Check output for specific issue indicators from status command
|
|
611
|
-
// Note: We avoid checking for "remote" as it appears in normal output
|
|
612
|
-
// ("Checking local files vs remote Magentrix...")
|
|
613
|
-
const lowerOutput = output.toLowerCase();
|
|
614
|
-
|
|
615
|
-
// If everything is in sync, this message appears
|
|
616
|
-
const isInSync = lowerOutput.includes('all files are up to date') ||
|
|
617
|
-
lowerOutput.includes('in sync');
|
|
618
|
-
|
|
619
|
-
// Check for specific issue keywords from logFileStatus output
|
|
620
|
-
const hasConflict = lowerOutput.includes('conflict');
|
|
621
|
-
const isOutdated = lowerOutput.includes('outdated');
|
|
622
|
-
const isAhead = lowerOutput.includes('is ahead');
|
|
623
|
-
const isMissing = lowerOutput.includes('is missing');
|
|
624
|
-
const hasContentMismatch = lowerOutput.includes('content mismatch');
|
|
625
|
-
const hasWarnings = lowerOutput.includes('⚠️') || lowerOutput.includes('🛑');
|
|
626
|
-
|
|
627
|
-
const needsPull = !isInSync && (hasConflict || isOutdated || isAhead || isMissing || hasContentMismatch || hasWarnings);
|
|
628
|
-
|
|
629
|
-
resolvePromise({ checked: code === 0, needsPull, timedOut: false });
|
|
630
|
-
});
|
|
631
|
-
|
|
632
|
-
child.on('error', (err) => {
|
|
633
|
-
if (resolved) return;
|
|
634
|
-
resolved = true;
|
|
635
|
-
clearTimeout(timeout);
|
|
636
|
-
|
|
637
|
-
// If we can't check, assume it's fine and let them proceed
|
|
638
|
-
console.log(chalk.yellow(`\n⚠ Could not check sync status: ${err.message}`));
|
|
639
|
-
resolvePromise({ checked: false, needsPull: false, timedOut: false });
|
|
640
|
-
});
|
|
641
|
-
});
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
/**
|
|
645
|
-
* Run a magentrix command from a specific workspace directory.
|
|
646
|
-
*
|
|
647
|
-
* @param {string} workspacePath - Path to the Magentrix workspace
|
|
648
|
-
* @param {string} command - The magentrix command to run (e.g., 'pull', 'publish')
|
|
649
|
-
* @param {number} timeout - Optional timeout in milliseconds (defaults to COMMAND_TIMEOUT)
|
|
650
|
-
* @returns {Promise<boolean>} - True if command succeeded
|
|
651
|
-
*/
|
|
652
|
-
async function runCommandFromWorkspace(workspacePath, command, timeout = COMMAND_TIMEOUT) {
|
|
653
|
-
return new Promise((resolvePromise) => {
|
|
654
|
-
const isWindows = process.platform === 'win32';
|
|
655
|
-
const npmCmd = isWindows ? 'npx.cmd' : 'npx';
|
|
656
|
-
|
|
657
|
-
let resolved = false;
|
|
658
|
-
|
|
659
|
-
const child = spawn(npmCmd, ['magentrix', command], {
|
|
660
|
-
cwd: workspacePath,
|
|
661
|
-
stdio: 'inherit',
|
|
662
|
-
shell: isWindows
|
|
663
|
-
});
|
|
664
|
-
|
|
665
|
-
// Set timeout for command execution
|
|
666
|
-
const timeoutId = setTimeout(() => {
|
|
667
|
-
if (!resolved) {
|
|
668
|
-
resolved = true;
|
|
669
|
-
try {
|
|
670
|
-
child.kill('SIGTERM');
|
|
671
|
-
} catch {
|
|
672
|
-
// Ignore kill errors
|
|
673
|
-
}
|
|
674
|
-
console.log();
|
|
675
|
-
console.log(chalk.red(formatTimeoutError({
|
|
676
|
-
operation: `magentrix ${command}`,
|
|
677
|
-
timeout: timeout,
|
|
678
|
-
suggestion: `The ${command} operation is taking too long. Check your network connection or try again later.`
|
|
679
|
-
})));
|
|
680
|
-
resolvePromise(false);
|
|
681
|
-
}
|
|
682
|
-
}, timeout);
|
|
683
|
-
|
|
684
|
-
child.on('close', (code) => {
|
|
685
|
-
if (resolved) return;
|
|
686
|
-
resolved = true;
|
|
687
|
-
clearTimeout(timeoutId);
|
|
688
|
-
resolvePromise(code === 0);
|
|
689
|
-
});
|
|
690
|
-
|
|
691
|
-
child.on('error', (err) => {
|
|
692
|
-
if (resolved) return;
|
|
693
|
-
resolved = true;
|
|
694
|
-
clearTimeout(timeoutId);
|
|
695
|
-
console.log(chalk.yellow(`Warning: Could not run ${command}: ${err.message}`));
|
|
696
|
-
resolvePromise(false);
|
|
697
|
-
});
|
|
698
|
-
});
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
/**
|
|
702
|
-
* Prompt user to select a Magentrix workspace.
|
|
703
|
-
*
|
|
704
|
-
* @returns {Promise<string | null>} - Selected workspace path or null if cancelled
|
|
705
|
-
*/
|
|
706
|
-
async function selectWorkspace() {
|
|
707
|
-
const workspaces = getValidWorkspaces();
|
|
708
|
-
|
|
709
|
-
if (workspaces.length === 0) {
|
|
710
|
-
console.log(chalk.yellow('No Magentrix workspaces found.'));
|
|
711
|
-
console.log();
|
|
712
|
-
console.log(chalk.gray('To register a workspace:'));
|
|
713
|
-
console.log(chalk.white(` • Run ${chalk.cyan('magentrix')} from an existing workspace (auto-registers it)`));
|
|
714
|
-
console.log(chalk.white(` • Or run ${chalk.cyan('magentrix setup')} in a new directory to create one`));
|
|
715
|
-
console.log();
|
|
716
|
-
console.log(chalk.gray('Or specify a workspace path directly:'));
|
|
717
|
-
console.log(chalk.white(` ${chalk.cyan('magentrix vue-run-build --workspace /path/to/workspace')}`));
|
|
718
|
-
console.log();
|
|
719
|
-
|
|
720
|
-
// Allow manual entry
|
|
721
|
-
const manualPath = await input({
|
|
722
|
-
message: 'Enter the path to your Magentrix workspace (or leave empty to cancel):',
|
|
723
|
-
validate: (value) => {
|
|
724
|
-
if (!value.trim()) return true; // Allow empty for cancel
|
|
725
|
-
const resolved = resolve(value);
|
|
726
|
-
if (!existsSync(resolved)) {
|
|
727
|
-
return `Path does not exist: ${resolved}`;
|
|
728
|
-
}
|
|
729
|
-
const magentrixFolder = join(resolved, '.magentrix');
|
|
730
|
-
if (!existsSync(magentrixFolder)) {
|
|
731
|
-
return `Not a Magentrix workspace (missing .magentrix folder): ${resolved}`;
|
|
732
|
-
}
|
|
733
|
-
return true;
|
|
734
|
-
}
|
|
735
|
-
});
|
|
736
|
-
|
|
737
|
-
if (!manualPath.trim()) {
|
|
738
|
-
console.log(chalk.gray('Cancelled.'));
|
|
739
|
-
return null;
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
return resolve(manualPath);
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
// Build choices from registered workspaces
|
|
746
|
-
const choices = workspaces.map(w => ({
|
|
747
|
-
name: `${w.path}`,
|
|
748
|
-
value: w.path,
|
|
749
|
-
description: chalk.dim(`→ ${w.instanceUrl}`)
|
|
750
|
-
}));
|
|
751
|
-
|
|
752
|
-
choices.push({
|
|
753
|
-
name: 'Enter path manually',
|
|
754
|
-
value: '__manual__',
|
|
755
|
-
description: chalk.dim('→ Specify the full path to a workspace')
|
|
756
|
-
});
|
|
757
|
-
|
|
758
|
-
choices.push({
|
|
759
|
-
name: 'Cancel',
|
|
760
|
-
value: '__cancel__'
|
|
761
|
-
});
|
|
762
|
-
|
|
763
|
-
const choice = await select({
|
|
764
|
-
message: 'Which workspace do you want to stage into?',
|
|
765
|
-
choices
|
|
766
|
-
});
|
|
767
|
-
|
|
768
|
-
if (choice === '__cancel__') {
|
|
769
|
-
console.log(chalk.gray('Cancelled.'));
|
|
770
|
-
return null;
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
if (choice === '__manual__') {
|
|
774
|
-
const manualPath = await input({
|
|
775
|
-
message: 'Enter the path to your Magentrix workspace:',
|
|
776
|
-
validate: (value) => {
|
|
777
|
-
if (!value.trim()) {
|
|
778
|
-
return 'Path is required';
|
|
779
|
-
}
|
|
780
|
-
const resolved = resolve(value);
|
|
781
|
-
if (!existsSync(resolved)) {
|
|
782
|
-
return `Path does not exist: ${resolved}`;
|
|
783
|
-
}
|
|
784
|
-
const magentrixFolder = join(resolved, '.magentrix');
|
|
785
|
-
if (!existsSync(magentrixFolder)) {
|
|
786
|
-
return `Not a Magentrix workspace (missing .magentrix folder): ${resolved}`;
|
|
787
|
-
}
|
|
788
|
-
return true;
|
|
789
|
-
}
|
|
790
|
-
});
|
|
791
|
-
|
|
792
|
-
return resolve(manualPath);
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
return choice;
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
/**
|
|
799
|
-
* Prompt user to select a project.
|
|
800
|
-
*
|
|
801
|
-
* @returns {Promise<{path: string, config: object} | null>}
|
|
802
|
-
*/
|
|
803
|
-
async function selectProject() {
|
|
804
|
-
const projectsWithStatus = getLinkedProjectsWithStatus();
|
|
805
|
-
|
|
806
|
-
// Check if CWD is a Vue project (and not already linked)
|
|
807
|
-
const cwdConfig = readVueConfig(process.cwd());
|
|
808
|
-
const inVueProject = cwdConfig.found && cwdConfig.errors.length === 0;
|
|
809
|
-
const cwdAlreadyLinked = projectsWithStatus.some(p =>
|
|
810
|
-
resolve(p.path) === resolve(process.cwd())
|
|
811
|
-
);
|
|
812
|
-
|
|
813
|
-
// Build choices using the helper
|
|
814
|
-
const choices = buildProjectChoices({
|
|
815
|
-
includeManual: true,
|
|
816
|
-
includeCancel: true,
|
|
817
|
-
showInvalid: true
|
|
818
|
-
});
|
|
819
|
-
|
|
820
|
-
// If CWD is a valid Vue project and not linked, add it at the top
|
|
821
|
-
if (inVueProject && !cwdAlreadyLinked) {
|
|
822
|
-
choices.unshift({
|
|
823
|
-
name: `${cwdConfig.appName} (${cwdConfig.slug}) - Current directory (not linked)`,
|
|
824
|
-
value: { type: 'cwd', path: process.cwd() }
|
|
825
|
-
});
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
// Check if we have any projects to show
|
|
829
|
-
const hasProjects = choices.some(c => c.value?.type === 'linked' || c.value?.type === 'cwd');
|
|
830
|
-
|
|
831
|
-
if (!hasProjects) {
|
|
832
|
-
console.log(chalk.yellow('No linked Vue projects found.'));
|
|
833
|
-
console.log();
|
|
834
|
-
console.log(chalk.gray('To get started:'));
|
|
835
|
-
console.log(chalk.white(` 1. Link a Vue project: ${chalk.cyan('magentrix iris-app-link')}`));
|
|
836
|
-
console.log(chalk.white(` 2. Or specify path: ${chalk.cyan('magentrix vue-run-build --path /path/to/vue-project')}`));
|
|
837
|
-
console.log();
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
const choice = await select({
|
|
841
|
-
message: 'Which project do you want to build?',
|
|
842
|
-
choices
|
|
843
|
-
});
|
|
844
|
-
|
|
845
|
-
if (choice.type === 'cancel') {
|
|
846
|
-
console.log(chalk.gray('Cancelled.'));
|
|
847
|
-
return null;
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
if (choice.type === 'manual') {
|
|
851
|
-
const manualPath = await input({
|
|
852
|
-
message: 'Enter the path to your Vue project:',
|
|
853
|
-
validate: (value) => {
|
|
854
|
-
if (!value.trim()) {
|
|
855
|
-
return 'Path is required';
|
|
856
|
-
}
|
|
857
|
-
const resolved = resolve(value);
|
|
858
|
-
if (!existsSync(resolved)) {
|
|
859
|
-
return `Path does not exist: ${resolved}`;
|
|
860
|
-
}
|
|
861
|
-
return true;
|
|
862
|
-
}
|
|
863
|
-
});
|
|
864
|
-
|
|
865
|
-
const config = readVueConfig(resolve(manualPath));
|
|
866
|
-
return { path: resolve(manualPath), config };
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
// Linked project or CWD
|
|
870
|
-
const config = readVueConfig(choice.path);
|
|
871
|
-
return { path: choice.path, config };
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
export default vueBuildStage;
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { select, input, confirm } from '@inquirer/prompts';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { resolve, join } from 'node:path';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import Config from '../../utils/config.js';
|
|
7
|
+
import { runPublish } from '../publish.js';
|
|
8
|
+
import { isAutopublishRunning } from '../../utils/autopublishLock.js';
|
|
9
|
+
import {
|
|
10
|
+
buildVueProject,
|
|
11
|
+
stageToWorkspace,
|
|
12
|
+
findDistDirectory,
|
|
13
|
+
formatBuildError,
|
|
14
|
+
formatValidationError
|
|
15
|
+
} from '../../utils/iris/builder.js';
|
|
16
|
+
import { validateIrisBuild } from '../../utils/iris/validator.js';
|
|
17
|
+
import {
|
|
18
|
+
readVueConfig,
|
|
19
|
+
formatMissingConfigError,
|
|
20
|
+
formatConfigErrors
|
|
21
|
+
} from '../../utils/iris/config-reader.js';
|
|
22
|
+
import {
|
|
23
|
+
getLinkedProjectsWithStatus,
|
|
24
|
+
linkVueProject,
|
|
25
|
+
findLinkedProjectByPath,
|
|
26
|
+
buildProjectChoices
|
|
27
|
+
} from '../../utils/iris/linker.js';
|
|
28
|
+
import { EXPORT_ROOT, IRIS_APPS_DIR, HASHED_CWD } from '../../vars/global.js';
|
|
29
|
+
import { getValidWorkspaces } from '../../utils/workspaces.js';
|
|
30
|
+
import { formatTimeoutError } from '../../utils/iris/errors.js';
|
|
31
|
+
|
|
32
|
+
const config = new Config();
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Timeout for status check operations (30 seconds).
|
|
36
|
+
*/
|
|
37
|
+
const STATUS_CHECK_TIMEOUT = 30000;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Timeout for command execution (5 minutes).
|
|
41
|
+
*/
|
|
42
|
+
const COMMAND_TIMEOUT = 5 * 60 * 1000;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Display warnings from build/stage operations.
|
|
46
|
+
*
|
|
47
|
+
* @param {string[]} warnings - Array of warning messages
|
|
48
|
+
*/
|
|
49
|
+
function displayWarnings(warnings) {
|
|
50
|
+
if (!warnings || warnings.length === 0) return;
|
|
51
|
+
|
|
52
|
+
for (const warning of warnings) {
|
|
53
|
+
console.log(chalk.yellow(`⚠ ${warning}`));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check if the current directory is a Vue project (has config.ts).
|
|
59
|
+
* @returns {boolean}
|
|
60
|
+
*/
|
|
61
|
+
function isInVueProject() {
|
|
62
|
+
const configLocations = [
|
|
63
|
+
'src/config.ts',
|
|
64
|
+
'config.ts',
|
|
65
|
+
'src/iris-config.ts',
|
|
66
|
+
'iris-config.ts'
|
|
67
|
+
];
|
|
68
|
+
return configLocations.some(loc => existsSync(join(process.cwd(), loc)));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check if the current directory appears to be a Magentrix workspace.
|
|
73
|
+
* @returns {boolean} - True if in a Magentrix workspace
|
|
74
|
+
*/
|
|
75
|
+
function isMagentrixWorkspace() {
|
|
76
|
+
const magentrixFolder = join(process.cwd(), '.magentrix');
|
|
77
|
+
const srcFolder = join(process.cwd(), 'src');
|
|
78
|
+
|
|
79
|
+
// Check for .magentrix folder
|
|
80
|
+
if (!existsSync(magentrixFolder)) return false;
|
|
81
|
+
|
|
82
|
+
// Check for src folder (typical workspace structure)
|
|
83
|
+
if (!existsSync(srcFolder)) return false;
|
|
84
|
+
|
|
85
|
+
// Check if credentials are configured (instanceUrl or apiKey in global config)
|
|
86
|
+
const instanceUrl = config.read('instanceUrl', { global: true, pathHash: HASHED_CWD });
|
|
87
|
+
const apiKey = config.read('apiKey', { global: true, pathHash: HASHED_CWD });
|
|
88
|
+
|
|
89
|
+
return !!(instanceUrl || apiKey);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* vue-run-build command - Build a Vue project and stage to CLI workspace.
|
|
94
|
+
*
|
|
95
|
+
* Two modes of operation:
|
|
96
|
+
* 1. Run from Magentrix workspace: prompts for which Vue project to build
|
|
97
|
+
* 2. Run from Vue project: prompts for which workspace to stage into
|
|
98
|
+
*
|
|
99
|
+
* Options:
|
|
100
|
+
* --path <dir> Specify Vue project path directly
|
|
101
|
+
* --skip-build Use existing dist/ without rebuilding
|
|
102
|
+
* --workspace <dir> Specify Magentrix workspace path directly
|
|
103
|
+
*/
|
|
104
|
+
export const vueBuildStage = async (options = {}) => {
|
|
105
|
+
process.stdout.write('\x1Bc'); // Clear console
|
|
106
|
+
|
|
107
|
+
const { path: pathOption, skipBuild, workspace: workspaceOption } = options;
|
|
108
|
+
|
|
109
|
+
// Detect which mode we're in
|
|
110
|
+
const inVueProject = isInVueProject();
|
|
111
|
+
const inWorkspace = isMagentrixWorkspace();
|
|
112
|
+
|
|
113
|
+
// If run from a Vue project, use reversed logic
|
|
114
|
+
if (inVueProject && !inWorkspace) {
|
|
115
|
+
await buildFromVueProject(options);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Standard mode: run from workspace, select Vue project
|
|
120
|
+
// Warn if not in a Magentrix workspace
|
|
121
|
+
if (!inWorkspace) {
|
|
122
|
+
console.log(chalk.yellow('⚠ Warning: Magentrix Workspace Not Detected'));
|
|
123
|
+
console.log(chalk.gray('─'.repeat(48)));
|
|
124
|
+
console.log(chalk.white('\nThis command should be run from your Magentrix CLI workspace directory.'));
|
|
125
|
+
console.log(chalk.white('It stages Vue.js build files to ') + chalk.cyan('src/iris-apps/<slug>/'));
|
|
126
|
+
console.log();
|
|
127
|
+
console.log(chalk.white('Expected workspace indicators:'));
|
|
128
|
+
console.log(chalk.gray(' • .magentrix/ folder (config and cache)'));
|
|
129
|
+
console.log(chalk.gray(' • src/ folder (code files and assets)'));
|
|
130
|
+
console.log(chalk.gray(' • Magentrix credentials configured'));
|
|
131
|
+
console.log();
|
|
132
|
+
console.log(chalk.white('Current directory: ') + chalk.gray(process.cwd()));
|
|
133
|
+
console.log();
|
|
134
|
+
|
|
135
|
+
const shouldContinue = await confirm({
|
|
136
|
+
message: 'Do you want to continue anyway?',
|
|
137
|
+
default: false
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (!shouldContinue) {
|
|
141
|
+
console.log(chalk.gray('\nCancelled. Run this command from your Magentrix workspace.'));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.log();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Determine which project to build
|
|
149
|
+
let projectPath = pathOption;
|
|
150
|
+
let vueConfig = null;
|
|
151
|
+
|
|
152
|
+
if (projectPath) {
|
|
153
|
+
// Path provided via option
|
|
154
|
+
projectPath = resolve(projectPath);
|
|
155
|
+
|
|
156
|
+
if (!existsSync(projectPath)) {
|
|
157
|
+
console.log(chalk.red(`Error: Path does not exist: ${projectPath}`));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
vueConfig = readVueConfig(projectPath);
|
|
162
|
+
} else {
|
|
163
|
+
// Prompt user to select a project
|
|
164
|
+
const result = await selectProject();
|
|
165
|
+
if (!result) return; // User cancelled
|
|
166
|
+
|
|
167
|
+
projectPath = result.path;
|
|
168
|
+
vueConfig = result.config;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Validate Vue config
|
|
172
|
+
if (!vueConfig.found) {
|
|
173
|
+
console.log(chalk.red(formatMissingConfigError(projectPath)));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (vueConfig.errors.length > 0) {
|
|
178
|
+
console.log(chalk.red(formatConfigErrors(vueConfig)));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const { slug, appName } = vueConfig;
|
|
183
|
+
|
|
184
|
+
console.log(chalk.blue('\nVue Build & Stage'));
|
|
185
|
+
console.log(chalk.gray('─'.repeat(48)));
|
|
186
|
+
console.log(chalk.white(` Project: ${chalk.cyan(appName)} (${slug})`));
|
|
187
|
+
console.log(chalk.white(` Path: ${chalk.gray(projectPath)}`));
|
|
188
|
+
console.log();
|
|
189
|
+
|
|
190
|
+
// Ensure project is linked
|
|
191
|
+
const linked = findLinkedProjectByPath(projectPath);
|
|
192
|
+
if (!linked) {
|
|
193
|
+
const shouldLink = await confirm({
|
|
194
|
+
message: 'This project is not linked. Link it now?',
|
|
195
|
+
default: true
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
if (shouldLink) {
|
|
199
|
+
const linkResult = linkVueProject(projectPath);
|
|
200
|
+
if (!linkResult.success) {
|
|
201
|
+
console.log(chalk.red(`Failed to link project: ${linkResult.error}`));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
console.log(chalk.green(`\u2713 Project linked`));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
let distPath;
|
|
209
|
+
|
|
210
|
+
if (skipBuild) {
|
|
211
|
+
// Use existing dist
|
|
212
|
+
distPath = findDistDirectory(projectPath);
|
|
213
|
+
|
|
214
|
+
if (!distPath) {
|
|
215
|
+
console.log(chalk.red('No existing dist/ directory found.'));
|
|
216
|
+
console.log(chalk.gray('Run without --skip-build to build the project.'));
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
console.log(chalk.yellow(`Using existing dist: ${distPath}`));
|
|
221
|
+
|
|
222
|
+
// Validate the existing build
|
|
223
|
+
const validation = validateIrisBuild(distPath);
|
|
224
|
+
if (!validation.valid) {
|
|
225
|
+
console.log(chalk.red(formatValidationError(distPath, validation.errors)));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
console.log(chalk.green('\u2713 Existing build is valid'));
|
|
230
|
+
} else {
|
|
231
|
+
// Build the project
|
|
232
|
+
console.log(chalk.blue('Building project...'));
|
|
233
|
+
console.log();
|
|
234
|
+
|
|
235
|
+
const buildResult = await buildVueProject(projectPath, { silent: false });
|
|
236
|
+
|
|
237
|
+
if (!buildResult.success) {
|
|
238
|
+
console.log();
|
|
239
|
+
console.log(chalk.red(formatBuildError(projectPath, buildResult.error)));
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
distPath = buildResult.distPath;
|
|
244
|
+
console.log();
|
|
245
|
+
console.log(chalk.green(`\u2713 Build completed successfully`));
|
|
246
|
+
console.log(chalk.gray(` Output: ${distPath}`));
|
|
247
|
+
|
|
248
|
+
// Display any build warnings
|
|
249
|
+
displayWarnings(buildResult.warnings);
|
|
250
|
+
|
|
251
|
+
// Validate build output
|
|
252
|
+
const validation = validateIrisBuild(distPath);
|
|
253
|
+
if (!validation.valid) {
|
|
254
|
+
console.log();
|
|
255
|
+
console.log(chalk.red(formatValidationError(distPath, validation.errors)));
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
console.log(chalk.green('\u2713 Build output validated'));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Stage to CLI project (current workspace)
|
|
263
|
+
console.log();
|
|
264
|
+
console.log(chalk.blue('Staging to CLI workspace...'));
|
|
265
|
+
|
|
266
|
+
const stageResult = stageToWorkspace(distPath, slug, process.cwd());
|
|
267
|
+
|
|
268
|
+
if (!stageResult.success) {
|
|
269
|
+
console.log(chalk.red(`Failed to stage: ${stageResult.error}`));
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
console.log(chalk.green(`\u2713 Staged ${stageResult.fileCount} files to ${stageResult.stagedPath}`));
|
|
274
|
+
|
|
275
|
+
// Display any staging warnings
|
|
276
|
+
displayWarnings(stageResult.warnings);
|
|
277
|
+
|
|
278
|
+
// Summary
|
|
279
|
+
console.log();
|
|
280
|
+
console.log(chalk.green('─'.repeat(48)));
|
|
281
|
+
console.log(chalk.green.bold('\u2713 Build & Stage Complete!'));
|
|
282
|
+
console.log();
|
|
283
|
+
console.log(chalk.gray(`Staged to: ${EXPORT_ROOT}/${IRIS_APPS_DIR}/${slug}/`));
|
|
284
|
+
console.log();
|
|
285
|
+
|
|
286
|
+
// Check if autopublish is running
|
|
287
|
+
if (isAutopublishRunning()) {
|
|
288
|
+
console.log(chalk.cyan('✓ Autopublish is running - changes will be deployed automatically'));
|
|
289
|
+
} else {
|
|
290
|
+
// Prompt to run publish now
|
|
291
|
+
const shouldPublish = await confirm({
|
|
292
|
+
message: 'Do you want to publish to Magentrix now?',
|
|
293
|
+
default: true
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
if (shouldPublish) {
|
|
297
|
+
console.log();
|
|
298
|
+
console.log(chalk.blue('Running publish...'));
|
|
299
|
+
console.log();
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
await runPublish();
|
|
303
|
+
} catch (error) {
|
|
304
|
+
console.log(chalk.red(`\nPublish failed: ${error.message}`));
|
|
305
|
+
console.log(chalk.gray('You can run it manually later with:'), chalk.yellow('magentrix publish'));
|
|
306
|
+
}
|
|
307
|
+
} else {
|
|
308
|
+
console.log();
|
|
309
|
+
console.log(chalk.cyan('Next steps:'));
|
|
310
|
+
console.log(chalk.white(` • Run ${chalk.yellow('magentrix publish')} to deploy to Magentrix`));
|
|
311
|
+
console.log(chalk.white(` • Or use ${chalk.yellow('magentrix autopublish')} for automatic deployment`));
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Build and stage when running from inside a Vue project.
|
|
318
|
+
* Prompts user to select which workspace to stage into.
|
|
319
|
+
*/
|
|
320
|
+
async function buildFromVueProject(options) {
|
|
321
|
+
const { skipBuild, workspace: workspaceOption } = options;
|
|
322
|
+
|
|
323
|
+
// Use current directory as Vue project
|
|
324
|
+
const projectPath = process.cwd();
|
|
325
|
+
const vueConfig = readVueConfig(projectPath);
|
|
326
|
+
|
|
327
|
+
// Validate Vue config
|
|
328
|
+
if (!vueConfig.found) {
|
|
329
|
+
console.log(chalk.red(formatMissingConfigError(projectPath)));
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (vueConfig.errors.length > 0) {
|
|
334
|
+
console.log(chalk.red(formatConfigErrors(vueConfig)));
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const { slug, appName } = vueConfig;
|
|
339
|
+
|
|
340
|
+
console.log(chalk.blue('\nVue Build & Stage'));
|
|
341
|
+
console.log(chalk.gray('─'.repeat(48)));
|
|
342
|
+
console.log(chalk.white(` Project: ${chalk.cyan(appName)} (${slug})`));
|
|
343
|
+
console.log(chalk.white(` Path: ${chalk.gray(projectPath)}`));
|
|
344
|
+
console.log();
|
|
345
|
+
|
|
346
|
+
// Determine which workspace to stage into
|
|
347
|
+
let workspacePath = workspaceOption;
|
|
348
|
+
|
|
349
|
+
if (workspacePath) {
|
|
350
|
+
workspacePath = resolve(workspacePath);
|
|
351
|
+
if (!existsSync(workspacePath)) {
|
|
352
|
+
console.log(chalk.red(`Error: Workspace path does not exist: ${workspacePath}`));
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
} else {
|
|
356
|
+
// Prompt user to select a workspace
|
|
357
|
+
const result = await selectWorkspace();
|
|
358
|
+
if (!result) return; // User cancelled
|
|
359
|
+
workspacePath = result;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Ensure project is linked
|
|
363
|
+
const linked = findLinkedProjectByPath(projectPath);
|
|
364
|
+
if (!linked) {
|
|
365
|
+
const shouldLink = await confirm({
|
|
366
|
+
message: 'This project is not linked. Link it now?',
|
|
367
|
+
default: true
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
if (shouldLink) {
|
|
371
|
+
const linkResult = linkVueProject(projectPath);
|
|
372
|
+
if (!linkResult.success) {
|
|
373
|
+
console.log(chalk.red(`Failed to link project: ${linkResult.error}`));
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
console.log(chalk.green(`\u2713 Project linked`));
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
let distPath;
|
|
381
|
+
|
|
382
|
+
if (skipBuild) {
|
|
383
|
+
// Use existing dist
|
|
384
|
+
distPath = findDistDirectory(projectPath);
|
|
385
|
+
|
|
386
|
+
if (!distPath) {
|
|
387
|
+
console.log(chalk.red('No existing dist/ directory found.'));
|
|
388
|
+
console.log(chalk.gray('Run without --skip-build to build the project.'));
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
console.log(chalk.yellow(`Using existing dist: ${distPath}`));
|
|
393
|
+
|
|
394
|
+
// Validate the existing build
|
|
395
|
+
const validation = validateIrisBuild(distPath);
|
|
396
|
+
if (!validation.valid) {
|
|
397
|
+
console.log(chalk.red(formatValidationError(distPath, validation.errors)));
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
console.log(chalk.green('\u2713 Existing build is valid'));
|
|
402
|
+
} else {
|
|
403
|
+
// Build the project
|
|
404
|
+
console.log(chalk.blue('Building project...'));
|
|
405
|
+
console.log();
|
|
406
|
+
|
|
407
|
+
const buildResult = await buildVueProject(projectPath, { silent: false });
|
|
408
|
+
|
|
409
|
+
if (!buildResult.success) {
|
|
410
|
+
console.log();
|
|
411
|
+
console.log(chalk.red(formatBuildError(projectPath, buildResult.error)));
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
distPath = buildResult.distPath;
|
|
416
|
+
console.log();
|
|
417
|
+
console.log(chalk.green(`\u2713 Build completed successfully`));
|
|
418
|
+
console.log(chalk.gray(` Output: ${distPath}`));
|
|
419
|
+
|
|
420
|
+
// Display any build warnings
|
|
421
|
+
displayWarnings(buildResult.warnings);
|
|
422
|
+
|
|
423
|
+
// Validate build output
|
|
424
|
+
const validation = validateIrisBuild(distPath);
|
|
425
|
+
if (!validation.valid) {
|
|
426
|
+
console.log();
|
|
427
|
+
console.log(chalk.red(formatValidationError(distPath, validation.errors)));
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
console.log(chalk.green('\u2713 Build output validated'));
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Check if workspace might be out of sync and offer to pull first (BEFORE staging)
|
|
435
|
+
// This ensures pull doesn't overwrite our staged files
|
|
436
|
+
console.log();
|
|
437
|
+
console.log(chalk.gray('Checking workspace sync status...'));
|
|
438
|
+
|
|
439
|
+
// Check for a previous incomplete pull first — `magentrix status` only checks
|
|
440
|
+
// code entities, so a partial pull that synced code but not assets would falsely
|
|
441
|
+
// report "in sync". The marker file catches this case.
|
|
442
|
+
const workspaceConfig = new Config({ projectDir: workspacePath });
|
|
443
|
+
const hadIncompletePull = workspaceConfig.read('pullIncomplete', { global: false, filename: 'config.json' });
|
|
444
|
+
|
|
445
|
+
const syncStatus = await checkWorkspaceSyncStatus(workspacePath);
|
|
446
|
+
const needsPull = syncStatus.needsPull || !!hadIncompletePull;
|
|
447
|
+
|
|
448
|
+
if (needsPull) {
|
|
449
|
+
console.log();
|
|
450
|
+
if (hadIncompletePull && !syncStatus.needsPull) {
|
|
451
|
+
console.log(chalk.yellow('⚠ A previous pull did not complete. Your workspace may be out of sync.'));
|
|
452
|
+
} else {
|
|
453
|
+
console.log(chalk.yellow('⚠ Your workspace may be out of sync with the server.'));
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const shouldPull = await confirm({
|
|
457
|
+
message: 'Would you like to pull latest changes first?',
|
|
458
|
+
default: true
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
if (shouldPull) {
|
|
462
|
+
console.log();
|
|
463
|
+
console.log(chalk.blue('Running pull from workspace...'));
|
|
464
|
+
console.log();
|
|
465
|
+
|
|
466
|
+
// Mark pull as in-progress before starting
|
|
467
|
+
workspaceConfig.save('pullIncomplete', true, { global: false, filename: 'config.json' });
|
|
468
|
+
|
|
469
|
+
const pullSuccess = await runCommandFromWorkspace(workspacePath, 'pull');
|
|
470
|
+
|
|
471
|
+
if (pullSuccess) {
|
|
472
|
+
// Pull completed successfully — clear the marker
|
|
473
|
+
workspaceConfig.removeKey('pullIncomplete', { filename: 'config.json' });
|
|
474
|
+
} else {
|
|
475
|
+
// Pull failed or was cancelled — marker stays for next run
|
|
476
|
+
console.log();
|
|
477
|
+
console.log(chalk.yellow('Pull encountered issues. You may want to resolve them manually.'));
|
|
478
|
+
|
|
479
|
+
const continueAnyway = await confirm({
|
|
480
|
+
message: 'Do you still want to continue with staging and publishing?',
|
|
481
|
+
default: false
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
if (!continueAnyway) {
|
|
485
|
+
console.log();
|
|
486
|
+
console.log(chalk.cyan('To continue manually:'));
|
|
487
|
+
console.log(chalk.white(` 1. Navigate to workspace: ${chalk.yellow(`cd "${workspacePath}"`)}`));
|
|
488
|
+
console.log(chalk.white(` 2. Run ${chalk.yellow('magentrix pull')} to resolve conflicts`));
|
|
489
|
+
console.log(chalk.white(` 3. Run ${chalk.yellow('magentrix vue-run-build --skip-build')} to stage`));
|
|
490
|
+
console.log(chalk.white(` 4. Run ${chalk.yellow('magentrix publish')} to deploy`));
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
console.log();
|
|
495
|
+
} else {
|
|
496
|
+
// User declined to pull - block publishing from out-of-sync workspace
|
|
497
|
+
console.log();
|
|
498
|
+
console.log(chalk.red('Publishing Iris apps from an out-of-sync workspace is not allowed.'));
|
|
499
|
+
console.log(chalk.gray('This prevents conflicts and ensures your deployment is based on the latest server state.'));
|
|
500
|
+
console.log();
|
|
501
|
+
console.log(chalk.cyan('To continue:'));
|
|
502
|
+
console.log(chalk.white(` 1. Navigate to workspace: ${chalk.yellow(`cd "${workspacePath}"`)}`));
|
|
503
|
+
console.log(chalk.white(` 2. Run ${chalk.yellow('magentrix pull')} to sync with server`));
|
|
504
|
+
console.log(chalk.white(` 3. Run ${chalk.yellow('magentrix publish')} to deploy`));
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
} else if (syncStatus.checked) {
|
|
508
|
+
console.log(chalk.green('✓ Workspace is in sync'));
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Stage to selected workspace (AFTER pull to avoid overwriting)
|
|
512
|
+
console.log();
|
|
513
|
+
console.log(chalk.blue(`Staging to workspace: ${workspacePath}`));
|
|
514
|
+
|
|
515
|
+
const stageResult = stageToWorkspace(distPath, slug, workspacePath);
|
|
516
|
+
|
|
517
|
+
if (!stageResult.success) {
|
|
518
|
+
console.log(chalk.red(`Failed to stage: ${stageResult.error}`));
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
console.log(chalk.green(`\u2713 Staged ${stageResult.fileCount} files to ${stageResult.stagedPath}`));
|
|
523
|
+
|
|
524
|
+
// Display any staging warnings
|
|
525
|
+
displayWarnings(stageResult.warnings);
|
|
526
|
+
|
|
527
|
+
// Summary
|
|
528
|
+
console.log();
|
|
529
|
+
console.log(chalk.green('─'.repeat(48)));
|
|
530
|
+
console.log(chalk.green.bold('\u2713 Build & Stage Complete!'));
|
|
531
|
+
console.log();
|
|
532
|
+
console.log(chalk.gray(`Staged to: ${stageResult.stagedPath}`));
|
|
533
|
+
console.log();
|
|
534
|
+
|
|
535
|
+
// Ask if they want to publish now
|
|
536
|
+
const shouldPublish = await confirm({
|
|
537
|
+
message: 'Do you want to publish to Magentrix now?',
|
|
538
|
+
default: true
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
if (shouldPublish) {
|
|
542
|
+
console.log();
|
|
543
|
+
console.log(chalk.blue('Running publish from workspace...'));
|
|
544
|
+
console.log();
|
|
545
|
+
|
|
546
|
+
const publishSuccess = await runCommandFromWorkspace(workspacePath, 'publish');
|
|
547
|
+
|
|
548
|
+
if (!publishSuccess) {
|
|
549
|
+
console.log();
|
|
550
|
+
console.log(chalk.cyan('To publish manually:'));
|
|
551
|
+
console.log(chalk.white(` 1. Navigate to workspace: ${chalk.yellow(`cd "${workspacePath}"`)}`));
|
|
552
|
+
console.log(chalk.white(` 2. Run ${chalk.yellow('magentrix publish')}`));
|
|
553
|
+
}
|
|
554
|
+
} else {
|
|
555
|
+
console.log();
|
|
556
|
+
console.log(chalk.cyan('Next steps:'));
|
|
557
|
+
console.log(chalk.white(` 1. Navigate to workspace: ${chalk.yellow(`cd "${workspacePath}"`)}`));
|
|
558
|
+
console.log(chalk.white(` 2. Run ${chalk.yellow('magentrix publish')} to deploy to Magentrix`));
|
|
559
|
+
console.log(chalk.white(` Or use ${chalk.yellow('magentrix autopublish')} for automatic deployment`));
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Check if a workspace needs to pull (has remote changes or conflicts).
|
|
565
|
+
*
|
|
566
|
+
* @param {string} workspacePath - Path to the Magentrix workspace
|
|
567
|
+
* @returns {Promise<{checked: boolean, needsPull: boolean, timedOut: boolean}>}
|
|
568
|
+
*/
|
|
569
|
+
async function checkWorkspaceSyncStatus(workspacePath) {
|
|
570
|
+
return new Promise((resolvePromise) => {
|
|
571
|
+
const isWindows = process.platform === 'win32';
|
|
572
|
+
const npmCmd = isWindows ? 'npx.cmd' : 'npx';
|
|
573
|
+
|
|
574
|
+
let output = '';
|
|
575
|
+
let resolved = false;
|
|
576
|
+
|
|
577
|
+
const child = spawn(npmCmd, ['magentrix', 'status'], {
|
|
578
|
+
cwd: workspacePath,
|
|
579
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
580
|
+
shell: isWindows
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
// Set timeout for status check
|
|
584
|
+
const timeout = setTimeout(() => {
|
|
585
|
+
if (!resolved) {
|
|
586
|
+
resolved = true;
|
|
587
|
+
try {
|
|
588
|
+
child.kill('SIGTERM');
|
|
589
|
+
} catch {
|
|
590
|
+
// Ignore kill errors
|
|
591
|
+
}
|
|
592
|
+
console.log(chalk.yellow(`\n⚠ Status check timed out after ${STATUS_CHECK_TIMEOUT / 1000} seconds`));
|
|
593
|
+
resolvePromise({ checked: false, needsPull: false, timedOut: true });
|
|
594
|
+
}
|
|
595
|
+
}, STATUS_CHECK_TIMEOUT);
|
|
596
|
+
|
|
597
|
+
child.stdout.on('data', (data) => {
|
|
598
|
+
output += data.toString();
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
child.stderr.on('data', (data) => {
|
|
602
|
+
output += data.toString();
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
child.on('close', (code) => {
|
|
606
|
+
if (resolved) return;
|
|
607
|
+
resolved = true;
|
|
608
|
+
clearTimeout(timeout);
|
|
609
|
+
|
|
610
|
+
// Check output for specific issue indicators from status command
|
|
611
|
+
// Note: We avoid checking for "remote" as it appears in normal output
|
|
612
|
+
// ("Checking local files vs remote Magentrix...")
|
|
613
|
+
const lowerOutput = output.toLowerCase();
|
|
614
|
+
|
|
615
|
+
// If everything is in sync, this message appears
|
|
616
|
+
const isInSync = lowerOutput.includes('all files are up to date') ||
|
|
617
|
+
lowerOutput.includes('in sync');
|
|
618
|
+
|
|
619
|
+
// Check for specific issue keywords from logFileStatus output
|
|
620
|
+
const hasConflict = lowerOutput.includes('conflict');
|
|
621
|
+
const isOutdated = lowerOutput.includes('outdated');
|
|
622
|
+
const isAhead = lowerOutput.includes('is ahead');
|
|
623
|
+
const isMissing = lowerOutput.includes('is missing');
|
|
624
|
+
const hasContentMismatch = lowerOutput.includes('content mismatch');
|
|
625
|
+
const hasWarnings = lowerOutput.includes('⚠️') || lowerOutput.includes('🛑');
|
|
626
|
+
|
|
627
|
+
const needsPull = !isInSync && (hasConflict || isOutdated || isAhead || isMissing || hasContentMismatch || hasWarnings);
|
|
628
|
+
|
|
629
|
+
resolvePromise({ checked: code === 0, needsPull, timedOut: false });
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
child.on('error', (err) => {
|
|
633
|
+
if (resolved) return;
|
|
634
|
+
resolved = true;
|
|
635
|
+
clearTimeout(timeout);
|
|
636
|
+
|
|
637
|
+
// If we can't check, assume it's fine and let them proceed
|
|
638
|
+
console.log(chalk.yellow(`\n⚠ Could not check sync status: ${err.message}`));
|
|
639
|
+
resolvePromise({ checked: false, needsPull: false, timedOut: false });
|
|
640
|
+
});
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Run a magentrix command from a specific workspace directory.
|
|
646
|
+
*
|
|
647
|
+
* @param {string} workspacePath - Path to the Magentrix workspace
|
|
648
|
+
* @param {string} command - The magentrix command to run (e.g., 'pull', 'publish')
|
|
649
|
+
* @param {number} timeout - Optional timeout in milliseconds (defaults to COMMAND_TIMEOUT)
|
|
650
|
+
* @returns {Promise<boolean>} - True if command succeeded
|
|
651
|
+
*/
|
|
652
|
+
async function runCommandFromWorkspace(workspacePath, command, timeout = COMMAND_TIMEOUT) {
|
|
653
|
+
return new Promise((resolvePromise) => {
|
|
654
|
+
const isWindows = process.platform === 'win32';
|
|
655
|
+
const npmCmd = isWindows ? 'npx.cmd' : 'npx';
|
|
656
|
+
|
|
657
|
+
let resolved = false;
|
|
658
|
+
|
|
659
|
+
const child = spawn(npmCmd, ['magentrix', command], {
|
|
660
|
+
cwd: workspacePath,
|
|
661
|
+
stdio: 'inherit',
|
|
662
|
+
shell: isWindows
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
// Set timeout for command execution
|
|
666
|
+
const timeoutId = setTimeout(() => {
|
|
667
|
+
if (!resolved) {
|
|
668
|
+
resolved = true;
|
|
669
|
+
try {
|
|
670
|
+
child.kill('SIGTERM');
|
|
671
|
+
} catch {
|
|
672
|
+
// Ignore kill errors
|
|
673
|
+
}
|
|
674
|
+
console.log();
|
|
675
|
+
console.log(chalk.red(formatTimeoutError({
|
|
676
|
+
operation: `magentrix ${command}`,
|
|
677
|
+
timeout: timeout,
|
|
678
|
+
suggestion: `The ${command} operation is taking too long. Check your network connection or try again later.`
|
|
679
|
+
})));
|
|
680
|
+
resolvePromise(false);
|
|
681
|
+
}
|
|
682
|
+
}, timeout);
|
|
683
|
+
|
|
684
|
+
child.on('close', (code) => {
|
|
685
|
+
if (resolved) return;
|
|
686
|
+
resolved = true;
|
|
687
|
+
clearTimeout(timeoutId);
|
|
688
|
+
resolvePromise(code === 0);
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
child.on('error', (err) => {
|
|
692
|
+
if (resolved) return;
|
|
693
|
+
resolved = true;
|
|
694
|
+
clearTimeout(timeoutId);
|
|
695
|
+
console.log(chalk.yellow(`Warning: Could not run ${command}: ${err.message}`));
|
|
696
|
+
resolvePromise(false);
|
|
697
|
+
});
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Prompt user to select a Magentrix workspace.
|
|
703
|
+
*
|
|
704
|
+
* @returns {Promise<string | null>} - Selected workspace path or null if cancelled
|
|
705
|
+
*/
|
|
706
|
+
async function selectWorkspace() {
|
|
707
|
+
const workspaces = getValidWorkspaces();
|
|
708
|
+
|
|
709
|
+
if (workspaces.length === 0) {
|
|
710
|
+
console.log(chalk.yellow('No Magentrix workspaces found.'));
|
|
711
|
+
console.log();
|
|
712
|
+
console.log(chalk.gray('To register a workspace:'));
|
|
713
|
+
console.log(chalk.white(` • Run ${chalk.cyan('magentrix')} from an existing workspace (auto-registers it)`));
|
|
714
|
+
console.log(chalk.white(` • Or run ${chalk.cyan('magentrix setup')} in a new directory to create one`));
|
|
715
|
+
console.log();
|
|
716
|
+
console.log(chalk.gray('Or specify a workspace path directly:'));
|
|
717
|
+
console.log(chalk.white(` ${chalk.cyan('magentrix vue-run-build --workspace /path/to/workspace')}`));
|
|
718
|
+
console.log();
|
|
719
|
+
|
|
720
|
+
// Allow manual entry
|
|
721
|
+
const manualPath = await input({
|
|
722
|
+
message: 'Enter the path to your Magentrix workspace (or leave empty to cancel):',
|
|
723
|
+
validate: (value) => {
|
|
724
|
+
if (!value.trim()) return true; // Allow empty for cancel
|
|
725
|
+
const resolved = resolve(value);
|
|
726
|
+
if (!existsSync(resolved)) {
|
|
727
|
+
return `Path does not exist: ${resolved}`;
|
|
728
|
+
}
|
|
729
|
+
const magentrixFolder = join(resolved, '.magentrix');
|
|
730
|
+
if (!existsSync(magentrixFolder)) {
|
|
731
|
+
return `Not a Magentrix workspace (missing .magentrix folder): ${resolved}`;
|
|
732
|
+
}
|
|
733
|
+
return true;
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
if (!manualPath.trim()) {
|
|
738
|
+
console.log(chalk.gray('Cancelled.'));
|
|
739
|
+
return null;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
return resolve(manualPath);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Build choices from registered workspaces
|
|
746
|
+
const choices = workspaces.map(w => ({
|
|
747
|
+
name: `${w.path}`,
|
|
748
|
+
value: w.path,
|
|
749
|
+
description: chalk.dim(`→ ${w.instanceUrl}`)
|
|
750
|
+
}));
|
|
751
|
+
|
|
752
|
+
choices.push({
|
|
753
|
+
name: 'Enter path manually',
|
|
754
|
+
value: '__manual__',
|
|
755
|
+
description: chalk.dim('→ Specify the full path to a workspace')
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
choices.push({
|
|
759
|
+
name: 'Cancel',
|
|
760
|
+
value: '__cancel__'
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
const choice = await select({
|
|
764
|
+
message: 'Which workspace do you want to stage into?',
|
|
765
|
+
choices
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
if (choice === '__cancel__') {
|
|
769
|
+
console.log(chalk.gray('Cancelled.'));
|
|
770
|
+
return null;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
if (choice === '__manual__') {
|
|
774
|
+
const manualPath = await input({
|
|
775
|
+
message: 'Enter the path to your Magentrix workspace:',
|
|
776
|
+
validate: (value) => {
|
|
777
|
+
if (!value.trim()) {
|
|
778
|
+
return 'Path is required';
|
|
779
|
+
}
|
|
780
|
+
const resolved = resolve(value);
|
|
781
|
+
if (!existsSync(resolved)) {
|
|
782
|
+
return `Path does not exist: ${resolved}`;
|
|
783
|
+
}
|
|
784
|
+
const magentrixFolder = join(resolved, '.magentrix');
|
|
785
|
+
if (!existsSync(magentrixFolder)) {
|
|
786
|
+
return `Not a Magentrix workspace (missing .magentrix folder): ${resolved}`;
|
|
787
|
+
}
|
|
788
|
+
return true;
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
return resolve(manualPath);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
return choice;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Prompt user to select a project.
|
|
800
|
+
*
|
|
801
|
+
* @returns {Promise<{path: string, config: object} | null>}
|
|
802
|
+
*/
|
|
803
|
+
async function selectProject() {
|
|
804
|
+
const projectsWithStatus = getLinkedProjectsWithStatus();
|
|
805
|
+
|
|
806
|
+
// Check if CWD is a Vue project (and not already linked)
|
|
807
|
+
const cwdConfig = readVueConfig(process.cwd());
|
|
808
|
+
const inVueProject = cwdConfig.found && cwdConfig.errors.length === 0;
|
|
809
|
+
const cwdAlreadyLinked = projectsWithStatus.some(p =>
|
|
810
|
+
resolve(p.path) === resolve(process.cwd())
|
|
811
|
+
);
|
|
812
|
+
|
|
813
|
+
// Build choices using the helper
|
|
814
|
+
const choices = buildProjectChoices({
|
|
815
|
+
includeManual: true,
|
|
816
|
+
includeCancel: true,
|
|
817
|
+
showInvalid: true
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
// If CWD is a valid Vue project and not linked, add it at the top
|
|
821
|
+
if (inVueProject && !cwdAlreadyLinked) {
|
|
822
|
+
choices.unshift({
|
|
823
|
+
name: `${cwdConfig.appName} (${cwdConfig.slug}) - Current directory (not linked)`,
|
|
824
|
+
value: { type: 'cwd', path: process.cwd() }
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// Check if we have any projects to show
|
|
829
|
+
const hasProjects = choices.some(c => c.value?.type === 'linked' || c.value?.type === 'cwd');
|
|
830
|
+
|
|
831
|
+
if (!hasProjects) {
|
|
832
|
+
console.log(chalk.yellow('No linked Vue projects found.'));
|
|
833
|
+
console.log();
|
|
834
|
+
console.log(chalk.gray('To get started:'));
|
|
835
|
+
console.log(chalk.white(` 1. Link a Vue project: ${chalk.cyan('magentrix iris-app-link')}`));
|
|
836
|
+
console.log(chalk.white(` 2. Or specify path: ${chalk.cyan('magentrix vue-run-build --path /path/to/vue-project')}`));
|
|
837
|
+
console.log();
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const choice = await select({
|
|
841
|
+
message: 'Which project do you want to build?',
|
|
842
|
+
choices
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
if (choice.type === 'cancel') {
|
|
846
|
+
console.log(chalk.gray('Cancelled.'));
|
|
847
|
+
return null;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
if (choice.type === 'manual') {
|
|
851
|
+
const manualPath = await input({
|
|
852
|
+
message: 'Enter the path to your Vue project:',
|
|
853
|
+
validate: (value) => {
|
|
854
|
+
if (!value.trim()) {
|
|
855
|
+
return 'Path is required';
|
|
856
|
+
}
|
|
857
|
+
const resolved = resolve(value);
|
|
858
|
+
if (!existsSync(resolved)) {
|
|
859
|
+
return `Path does not exist: ${resolved}`;
|
|
860
|
+
}
|
|
861
|
+
return true;
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
const config = readVueConfig(resolve(manualPath));
|
|
866
|
+
return { path: resolve(manualPath), config };
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// Linked project or CWD
|
|
870
|
+
const config = readVueConfig(choice.path);
|
|
871
|
+
return { path: choice.path, config };
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
export default vueBuildStage;
|