@magentrix-corp/magentrix-cli 1.2.1 → 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.
- package/README.md +282 -2
- package/actions/autopublish.js +9 -48
- package/actions/iris/buildStage.js +330 -0
- package/actions/iris/delete.js +211 -0
- package/actions/iris/dev.js +338 -0
- package/actions/iris/index.js +6 -0
- package/actions/iris/link.js +377 -0
- package/actions/iris/recover.js +228 -0
- package/actions/publish.js +183 -9
- package/actions/pull.js +107 -4
- package/bin/magentrix.js +43 -1
- package/package.json +2 -1
- package/utils/autopublishLock.js +77 -0
- package/utils/cli/helpers/compare.js +4 -5
- package/utils/iris/backup.js +201 -0
- package/utils/iris/builder.js +304 -0
- package/utils/iris/config-reader.js +296 -0
- package/utils/iris/deleteHelper.js +102 -0
- package/utils/iris/linker.js +490 -0
- package/utils/iris/validator.js +281 -0
- package/utils/iris/zipper.js +239 -0
- package/utils/logger.js +13 -5
- package/utils/magentrix/api/iris.js +235 -0
- package/utils/permissionError.js +70 -0
- package/utils/progress.js +87 -1
- package/utils/updateFileBase.js +10 -2
- package/vars/global.js +1 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Required file patterns for a valid Iris build.
|
|
6
|
+
*/
|
|
7
|
+
const REQUIRED_FILES = {
|
|
8
|
+
remoteEntry: {
|
|
9
|
+
pattern: /^remoteEntry\.js$/,
|
|
10
|
+
location: 'root',
|
|
11
|
+
description: 'remoteEntry.js (required at dist root)'
|
|
12
|
+
},
|
|
13
|
+
hostInit: {
|
|
14
|
+
pattern: /^hostInit.*\.js$/,
|
|
15
|
+
location: 'assets',
|
|
16
|
+
description: 'assets/hostInit*.js (required in assets folder)'
|
|
17
|
+
},
|
|
18
|
+
main: {
|
|
19
|
+
pattern: /^main.*\.js$/,
|
|
20
|
+
location: 'assets',
|
|
21
|
+
description: 'assets/main*.js (required in assets folder)'
|
|
22
|
+
},
|
|
23
|
+
index: {
|
|
24
|
+
pattern: /^index.*\.js$/,
|
|
25
|
+
location: 'assets',
|
|
26
|
+
description: 'assets/index*.js (required in assets folder)'
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Validate that a build output directory contains all required Iris files.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} distPath - Path to the build output directory (e.g., ./dist)
|
|
34
|
+
* @returns {{
|
|
35
|
+
* valid: boolean,
|
|
36
|
+
* errors: string[],
|
|
37
|
+
* warnings: string[],
|
|
38
|
+
* files: {
|
|
39
|
+
* remoteEntry: string | null,
|
|
40
|
+
* hostInit: string | null,
|
|
41
|
+
* main: string | null,
|
|
42
|
+
* index: string | null
|
|
43
|
+
* },
|
|
44
|
+
* assetsPath: string | null
|
|
45
|
+
* }}
|
|
46
|
+
*/
|
|
47
|
+
export function validateIrisBuild(distPath) {
|
|
48
|
+
const result = {
|
|
49
|
+
valid: true,
|
|
50
|
+
errors: [],
|
|
51
|
+
warnings: [],
|
|
52
|
+
files: {
|
|
53
|
+
remoteEntry: null,
|
|
54
|
+
hostInit: null,
|
|
55
|
+
main: null,
|
|
56
|
+
index: null
|
|
57
|
+
},
|
|
58
|
+
assetsPath: null
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Check if dist path exists
|
|
62
|
+
if (!existsSync(distPath)) {
|
|
63
|
+
result.valid = false;
|
|
64
|
+
result.errors.push(`Build output directory not found: ${distPath}`);
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Check if it's a directory
|
|
69
|
+
const distStat = statSync(distPath);
|
|
70
|
+
if (!distStat.isDirectory()) {
|
|
71
|
+
result.valid = false;
|
|
72
|
+
result.errors.push(`Build output path is not a directory: ${distPath}`);
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Get root files
|
|
77
|
+
let rootFiles;
|
|
78
|
+
try {
|
|
79
|
+
rootFiles = readdirSync(distPath);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
result.valid = false;
|
|
82
|
+
result.errors.push(`Failed to read build directory: ${err.message}`);
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check for remoteEntry.js in root
|
|
87
|
+
const remoteEntryFile = rootFiles.find(f => REQUIRED_FILES.remoteEntry.pattern.test(f));
|
|
88
|
+
if (remoteEntryFile) {
|
|
89
|
+
result.files.remoteEntry = path.join(distPath, remoteEntryFile);
|
|
90
|
+
} else {
|
|
91
|
+
result.valid = false;
|
|
92
|
+
result.errors.push(REQUIRED_FILES.remoteEntry.description);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Check for assets directory
|
|
96
|
+
const assetsDir = rootFiles.find(f => f.toLowerCase() === 'assets');
|
|
97
|
+
if (!assetsDir) {
|
|
98
|
+
result.valid = false;
|
|
99
|
+
result.errors.push('assets/ directory not found in build output');
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const assetsPath = path.join(distPath, assetsDir);
|
|
104
|
+
result.assetsPath = assetsPath;
|
|
105
|
+
|
|
106
|
+
// Check if assets is a directory
|
|
107
|
+
const assetsStat = statSync(assetsPath);
|
|
108
|
+
if (!assetsStat.isDirectory()) {
|
|
109
|
+
result.valid = false;
|
|
110
|
+
result.errors.push('assets is not a directory');
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Get assets files
|
|
115
|
+
let assetsFiles;
|
|
116
|
+
try {
|
|
117
|
+
assetsFiles = readdirSync(assetsPath);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
result.valid = false;
|
|
120
|
+
result.errors.push(`Failed to read assets directory: ${err.message}`);
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Check for required files in assets
|
|
125
|
+
const hostInitFile = assetsFiles.find(f => REQUIRED_FILES.hostInit.pattern.test(f));
|
|
126
|
+
if (hostInitFile) {
|
|
127
|
+
result.files.hostInit = path.join(assetsPath, hostInitFile);
|
|
128
|
+
} else {
|
|
129
|
+
result.valid = false;
|
|
130
|
+
result.errors.push(REQUIRED_FILES.hostInit.description);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const mainFile = assetsFiles.find(f => REQUIRED_FILES.main.pattern.test(f));
|
|
134
|
+
if (mainFile) {
|
|
135
|
+
result.files.main = path.join(assetsPath, mainFile);
|
|
136
|
+
} else {
|
|
137
|
+
result.valid = false;
|
|
138
|
+
result.errors.push(REQUIRED_FILES.main.description);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const indexFile = assetsFiles.find(f => REQUIRED_FILES.index.pattern.test(f));
|
|
142
|
+
if (indexFile) {
|
|
143
|
+
result.files.index = path.join(assetsPath, indexFile);
|
|
144
|
+
} else {
|
|
145
|
+
result.valid = false;
|
|
146
|
+
result.errors.push(REQUIRED_FILES.index.description);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Warnings for potential issues
|
|
150
|
+
if (rootFiles.includes('index.html')) {
|
|
151
|
+
result.warnings.push('Found index.html in root - this file is typically not needed for Iris apps');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Format validation errors for display to the user.
|
|
159
|
+
*
|
|
160
|
+
* @param {ReturnType<typeof validateIrisBuild>} validation - Validation result
|
|
161
|
+
* @returns {string} - Formatted error message
|
|
162
|
+
*/
|
|
163
|
+
export function formatValidationErrors(validation) {
|
|
164
|
+
const lines = [
|
|
165
|
+
'Invalid Build Output',
|
|
166
|
+
'────────────────────────────────────────────────────',
|
|
167
|
+
'',
|
|
168
|
+
'The build output is missing required Iris files.',
|
|
169
|
+
'',
|
|
170
|
+
'Status:'
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
// Show status of each required file
|
|
174
|
+
const fileStatus = [
|
|
175
|
+
{
|
|
176
|
+
name: 'remoteEntry.js',
|
|
177
|
+
found: validation.files.remoteEntry !== null,
|
|
178
|
+
foundName: validation.files.remoteEntry ? path.basename(validation.files.remoteEntry) : null
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: 'assets/hostInit*.js',
|
|
182
|
+
found: validation.files.hostInit !== null,
|
|
183
|
+
foundName: validation.files.hostInit ? path.basename(validation.files.hostInit) : null
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: 'assets/main*.js',
|
|
187
|
+
found: validation.files.main !== null,
|
|
188
|
+
foundName: validation.files.main ? path.basename(validation.files.main) : null
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: 'assets/index*.js',
|
|
192
|
+
found: validation.files.index !== null,
|
|
193
|
+
foundName: validation.files.index ? path.basename(validation.files.index) : null
|
|
194
|
+
}
|
|
195
|
+
];
|
|
196
|
+
|
|
197
|
+
for (const file of fileStatus) {
|
|
198
|
+
if (file.found) {
|
|
199
|
+
lines.push(` ✓ ${file.name} (found: ${file.foundName})`);
|
|
200
|
+
} else {
|
|
201
|
+
lines.push(` ✗ ${file.name}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
lines.push('');
|
|
206
|
+
lines.push('Expected structure:');
|
|
207
|
+
lines.push(' dist/');
|
|
208
|
+
lines.push(' ├── remoteEntry.js');
|
|
209
|
+
lines.push(' └── assets/');
|
|
210
|
+
lines.push(' ├── hostInit-[hash].js');
|
|
211
|
+
lines.push(' ├── main-[hash].js');
|
|
212
|
+
lines.push(' └── index-[hash].js');
|
|
213
|
+
lines.push('');
|
|
214
|
+
lines.push('Make sure your Vue.js project is configured for Module Federation.');
|
|
215
|
+
|
|
216
|
+
if (validation.warnings.length > 0) {
|
|
217
|
+
lines.push('');
|
|
218
|
+
lines.push('Warnings:');
|
|
219
|
+
for (const warning of validation.warnings) {
|
|
220
|
+
lines.push(` ⚠ ${warning}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return lines.join('\n');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Get a summary of found files for successful validation.
|
|
229
|
+
*
|
|
230
|
+
* @param {ReturnType<typeof validateIrisBuild>} validation - Validation result
|
|
231
|
+
* @returns {string} - Summary of found files
|
|
232
|
+
*/
|
|
233
|
+
export function getValidationSummary(validation) {
|
|
234
|
+
const files = [];
|
|
235
|
+
|
|
236
|
+
if (validation.files.remoteEntry) {
|
|
237
|
+
files.push(` • ${path.basename(validation.files.remoteEntry)}`);
|
|
238
|
+
}
|
|
239
|
+
if (validation.files.hostInit) {
|
|
240
|
+
files.push(` • assets/${path.basename(validation.files.hostInit)}`);
|
|
241
|
+
}
|
|
242
|
+
if (validation.files.main) {
|
|
243
|
+
files.push(` • assets/${path.basename(validation.files.main)}`);
|
|
244
|
+
}
|
|
245
|
+
if (validation.files.index) {
|
|
246
|
+
files.push(` • assets/${path.basename(validation.files.index)}`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return `Found ${files.length} required files:\n${files.join('\n')}`;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Validate an Iris app folder (already staged, not a dist folder).
|
|
254
|
+
* This is used to validate apps in src/iris-apps/<slug>/.
|
|
255
|
+
*
|
|
256
|
+
* @param {string} appPath - Path to the app folder
|
|
257
|
+
* @returns {{valid: boolean, errors: string[], slug: string | null}}
|
|
258
|
+
*/
|
|
259
|
+
export function validateIrisAppFolder(appPath) {
|
|
260
|
+
const result = {
|
|
261
|
+
valid: true,
|
|
262
|
+
errors: [],
|
|
263
|
+
slug: null
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
if (!existsSync(appPath)) {
|
|
267
|
+
result.valid = false;
|
|
268
|
+
result.errors.push(`App folder not found: ${appPath}`);
|
|
269
|
+
return result;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
result.slug = path.basename(appPath);
|
|
273
|
+
|
|
274
|
+
// Use the same validation as build output
|
|
275
|
+
const validation = validateIrisBuild(appPath);
|
|
276
|
+
|
|
277
|
+
result.valid = validation.valid;
|
|
278
|
+
result.errors = validation.errors;
|
|
279
|
+
|
|
280
|
+
return result;
|
|
281
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { mkdirSync, rmSync, writeFileSync, readFileSync, existsSync, readdirSync, statSync, chmodSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join, basename } from 'node:path';
|
|
4
|
+
import { randomUUID, createHash } from 'node:crypto';
|
|
5
|
+
import archiver from 'archiver';
|
|
6
|
+
import extractZip from 'extract-zip';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Recursively fix permissions on extracted files.
|
|
10
|
+
* Sets directories to 0o755 and files to 0o644.
|
|
11
|
+
* This ensures the current user can read/write/delete the files.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} dir - Directory to fix permissions for
|
|
14
|
+
*/
|
|
15
|
+
function fixPermissions(dir) {
|
|
16
|
+
if (!existsSync(dir)) return;
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const stat = statSync(dir);
|
|
20
|
+
if (stat.isDirectory()) {
|
|
21
|
+
chmodSync(dir, 0o755);
|
|
22
|
+
const entries = readdirSync(dir);
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
fixPermissions(join(dir, entry));
|
|
25
|
+
}
|
|
26
|
+
} else {
|
|
27
|
+
chmodSync(dir, 0o644);
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
// Ignore permission errors - best effort
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Create a zip file from a build directory with proper Iris structure.
|
|
36
|
+
* The zip will contain a single root folder with the app slug.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} distPath - Path to the build output directory (e.g., ./dist)
|
|
39
|
+
* @param {string} appSlug - Slug for the app (becomes the root folder in zip)
|
|
40
|
+
* @returns {Promise<Buffer>} - The zip file as a Buffer
|
|
41
|
+
*/
|
|
42
|
+
export async function createIrisZip(distPath, appSlug) {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const chunks = [];
|
|
45
|
+
|
|
46
|
+
const archive = archiver('zip', {
|
|
47
|
+
zlib: { level: 9 } // Maximum compression
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
archive.on('data', (chunk) => {
|
|
51
|
+
chunks.push(chunk);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
archive.on('error', (err) => {
|
|
55
|
+
reject(err);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
archive.on('end', () => {
|
|
59
|
+
resolve(Buffer.concat(chunks));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Add the dist directory contents under the app slug folder
|
|
63
|
+
// This creates the structure: appSlug/remoteEntry.js, appSlug/assets/...
|
|
64
|
+
archive.directory(distPath, appSlug);
|
|
65
|
+
|
|
66
|
+
archive.finalize();
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Extract a zip buffer to a directory.
|
|
72
|
+
*
|
|
73
|
+
* @param {Buffer} zipBuffer - The zip file as a Buffer
|
|
74
|
+
* @param {string} outputDir - Directory to extract to
|
|
75
|
+
* @returns {Promise<string>} - Path to the extracted directory
|
|
76
|
+
*/
|
|
77
|
+
export async function extractIrisZip(zipBuffer, outputDir) {
|
|
78
|
+
// Create a temporary file to hold the zip
|
|
79
|
+
const tempDir = join(tmpdir(), `iris-extract-${randomUUID()}`);
|
|
80
|
+
const tempZipPath = join(tempDir, 'temp.zip');
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
// Create temp directory
|
|
84
|
+
mkdirSync(tempDir, { recursive: true });
|
|
85
|
+
|
|
86
|
+
// Write buffer to temp file
|
|
87
|
+
writeFileSync(tempZipPath, zipBuffer);
|
|
88
|
+
|
|
89
|
+
// Ensure output directory exists
|
|
90
|
+
mkdirSync(outputDir, { recursive: true });
|
|
91
|
+
|
|
92
|
+
// Extract
|
|
93
|
+
await extractZip(tempZipPath, { dir: outputDir });
|
|
94
|
+
|
|
95
|
+
// Fix permissions so files can be deleted/modified later
|
|
96
|
+
fixPermissions(outputDir);
|
|
97
|
+
|
|
98
|
+
return outputDir;
|
|
99
|
+
} finally {
|
|
100
|
+
// Cleanup temp directory
|
|
101
|
+
try {
|
|
102
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
103
|
+
} catch {
|
|
104
|
+
// Ignore cleanup errors
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get the size of a buffer in human-readable format.
|
|
111
|
+
*
|
|
112
|
+
* @param {number} bytes - Size in bytes
|
|
113
|
+
* @returns {string} - Human-readable size (e.g., "1.2 MB")
|
|
114
|
+
*/
|
|
115
|
+
export function formatFileSize(bytes) {
|
|
116
|
+
if (bytes === 0) return '0 B';
|
|
117
|
+
|
|
118
|
+
const units = ['B', 'KB', 'MB', 'GB'];
|
|
119
|
+
const k = 1024;
|
|
120
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
121
|
+
|
|
122
|
+
if (i === 0) return `${bytes} ${units[0]}`;
|
|
123
|
+
|
|
124
|
+
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${units[i]}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Count files in a directory recursively.
|
|
129
|
+
*
|
|
130
|
+
* @param {string} dir - Directory path
|
|
131
|
+
* @returns {number} - Number of files
|
|
132
|
+
*/
|
|
133
|
+
export function countFiles(dir) {
|
|
134
|
+
if (!existsSync(dir)) return 0;
|
|
135
|
+
|
|
136
|
+
let count = 0;
|
|
137
|
+
|
|
138
|
+
function walk(currentDir) {
|
|
139
|
+
const entries = readdirSync(currentDir);
|
|
140
|
+
for (const entry of entries) {
|
|
141
|
+
const fullPath = join(currentDir, entry);
|
|
142
|
+
const stat = statSync(fullPath);
|
|
143
|
+
if (stat.isDirectory()) {
|
|
144
|
+
walk(fullPath);
|
|
145
|
+
} else {
|
|
146
|
+
count++;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
walk(dir);
|
|
152
|
+
return count;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Calculate total size of files in a directory recursively.
|
|
157
|
+
*
|
|
158
|
+
* @param {string} dir - Directory path
|
|
159
|
+
* @returns {number} - Total size in bytes
|
|
160
|
+
*/
|
|
161
|
+
export function calculateDirSize(dir) {
|
|
162
|
+
if (!existsSync(dir)) return 0;
|
|
163
|
+
|
|
164
|
+
let totalSize = 0;
|
|
165
|
+
|
|
166
|
+
function walk(currentDir) {
|
|
167
|
+
const entries = readdirSync(currentDir);
|
|
168
|
+
for (const entry of entries) {
|
|
169
|
+
const fullPath = join(currentDir, entry);
|
|
170
|
+
const stat = statSync(fullPath);
|
|
171
|
+
if (stat.isDirectory()) {
|
|
172
|
+
walk(fullPath);
|
|
173
|
+
} else {
|
|
174
|
+
totalSize += stat.size;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
walk(dir);
|
|
180
|
+
return totalSize;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Get list of all files in a directory recursively with relative paths.
|
|
185
|
+
*
|
|
186
|
+
* @param {string} dir - Directory path
|
|
187
|
+
* @param {string} basePath - Base path for relative path calculation
|
|
188
|
+
* @returns {string[]} - Array of relative file paths
|
|
189
|
+
*/
|
|
190
|
+
export function getFilesRecursive(dir, basePath = dir) {
|
|
191
|
+
if (!existsSync(dir)) return [];
|
|
192
|
+
|
|
193
|
+
const files = [];
|
|
194
|
+
|
|
195
|
+
function walk(currentDir) {
|
|
196
|
+
const entries = readdirSync(currentDir);
|
|
197
|
+
for (const entry of entries) {
|
|
198
|
+
const fullPath = join(currentDir, entry);
|
|
199
|
+
const stat = statSync(fullPath);
|
|
200
|
+
if (stat.isDirectory()) {
|
|
201
|
+
walk(fullPath);
|
|
202
|
+
} else {
|
|
203
|
+
const relativePath = fullPath.slice(basePath.length + 1);
|
|
204
|
+
files.push(relativePath);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
walk(dir);
|
|
210
|
+
return files;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Calculate a content hash for an entire Iris app folder.
|
|
215
|
+
* Hashes all file contents and their relative paths to detect any changes.
|
|
216
|
+
*
|
|
217
|
+
* @param {string} dir - Directory path to the Iris app
|
|
218
|
+
* @returns {string} - SHA256 hash of all files
|
|
219
|
+
*/
|
|
220
|
+
export function hashIrisAppFolder(dir) {
|
|
221
|
+
if (!existsSync(dir)) return '';
|
|
222
|
+
|
|
223
|
+
const hash = createHash('sha256');
|
|
224
|
+
const files = getFilesRecursive(dir).sort(); // Sort for consistent ordering
|
|
225
|
+
|
|
226
|
+
for (const relativePath of files) {
|
|
227
|
+
const fullPath = join(dir, relativePath);
|
|
228
|
+
try {
|
|
229
|
+
const content = readFileSync(fullPath);
|
|
230
|
+
// Include the relative path in the hash so renames are detected
|
|
231
|
+
hash.update(relativePath);
|
|
232
|
+
hash.update(content);
|
|
233
|
+
} catch {
|
|
234
|
+
// Skip files we can't read
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return hash.digest('hex');
|
|
239
|
+
}
|
package/utils/logger.js
CHANGED
|
@@ -129,8 +129,12 @@ export class Logger {
|
|
|
129
129
|
|
|
130
130
|
/**
|
|
131
131
|
* Log an error message
|
|
132
|
+
* @param {string} message - Error message
|
|
133
|
+
* @param {Error|null} error - Error object
|
|
134
|
+
* @param {object|null} details - Additional details
|
|
135
|
+
* @param {string|null} hint - Helpful hint for resolving the error
|
|
132
136
|
*/
|
|
133
|
-
error(message, error = null, details = null) {
|
|
137
|
+
error(message, error = null, details = null, hint = null) {
|
|
134
138
|
const timestamp = new Date().toISOString();
|
|
135
139
|
const entry = {
|
|
136
140
|
timestamp,
|
|
@@ -138,7 +142,8 @@ export class Logger {
|
|
|
138
142
|
message,
|
|
139
143
|
error: error ? error.toString() : null,
|
|
140
144
|
stack: error?.stack || null,
|
|
141
|
-
details
|
|
145
|
+
details,
|
|
146
|
+
hint
|
|
142
147
|
};
|
|
143
148
|
this.errors.push(entry);
|
|
144
149
|
|
|
@@ -194,13 +199,16 @@ export class Logger {
|
|
|
194
199
|
console.log(chalk.red(` ✗ ${summary.errors} error(s)`));
|
|
195
200
|
|
|
196
201
|
// Show preview of recent errors
|
|
197
|
-
const recentErrors = this.getRecentErrors(
|
|
202
|
+
const recentErrors = this.getRecentErrors(3);
|
|
198
203
|
recentErrors.forEach((err, idx) => {
|
|
199
204
|
console.log(chalk.red(` ${idx + 1}. ${err.message}`));
|
|
205
|
+
if (err.hint) {
|
|
206
|
+
console.log(chalk.yellow(` ${err.hint}`));
|
|
207
|
+
}
|
|
200
208
|
});
|
|
201
209
|
|
|
202
|
-
if (summary.errors >
|
|
203
|
-
console.log(chalk.gray(` ... and ${summary.errors -
|
|
210
|
+
if (summary.errors > 3) {
|
|
211
|
+
console.log(chalk.gray(` ... and ${summary.errors - 3} more`));
|
|
204
212
|
}
|
|
205
213
|
}
|
|
206
214
|
|