easybuild-nox 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,571 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const { spawn, execSync } = require('child_process');
5
+ const logger = require('./logger');
6
+
7
+ const EASY_HOME = path.join(os.homedir(), '.easy');
8
+ const SHARED_MODULES = path.join(EASY_HOME, 'modules');
9
+ const MODULES_CACHE = path.join(EASY_HOME, 'cache');
10
+ const MODULES_DB = path.join(EASY_HOME, 'modules.json');
11
+ const ELECTRON_DIR = path.join(EASY_HOME, 'electron');
12
+
13
+ // Special packages that need special handling
14
+ const SPECIAL_PACKAGES = {
15
+ electron: {
16
+ handler: 'installElectron',
17
+ description: 'Electron binary (downloaded separately)',
18
+ },
19
+ '@electron/remote': {
20
+ handler: 'installElectron',
21
+ description: 'Electron remote module',
22
+ },
23
+ };
24
+
25
+ class SharedModules {
26
+ constructor() {
27
+ this.initialized = false;
28
+ }
29
+
30
+ async init() {
31
+ if (this.initialized) return;
32
+
33
+ await fs.ensureDir(EASY_HOME);
34
+ await fs.ensureDir(SHARED_MODULES);
35
+ await fs.ensureDir(MODULES_CACHE);
36
+
37
+ if (!(await fs.pathExists(MODULES_DB))) {
38
+ await fs.writeJson(MODULES_DB, { modules: {}, lastUpdate: null }, { spaces: 2 });
39
+ }
40
+
41
+ this.initialized = true;
42
+ }
43
+
44
+ async getModulesDb() {
45
+ await this.init();
46
+ return await fs.readJson(MODULES_DB);
47
+ }
48
+
49
+ async saveModulesDb(db) {
50
+ await this.init();
51
+ await fs.writeJson(MODULES_DB, db, { spaces: 2 });
52
+ }
53
+
54
+ async isModuleInstalled(name, version = '*') {
55
+ await this.init();
56
+ const db = await this.getModulesDb();
57
+ const moduleKey = `${name}@${version}`;
58
+
59
+ if (!db.modules[moduleKey]) return false;
60
+
61
+ const modulePath = path.join(SHARED_MODULES, name);
62
+ return await fs.pathExists(modulePath);
63
+ }
64
+
65
+ async installModule(name, version = '*', options = {}) {
66
+ await this.init();
67
+ const db = await this.getModulesDb();
68
+ const moduleKey = `${name}@${version}`;
69
+
70
+ // Check if already installed
71
+ if (await this.isModuleInstalled(name, version) && !options.force) {
72
+ logger.dim(` ${name}@${version} already installed`);
73
+ return { installed: false, cached: true };
74
+ }
75
+
76
+ // Handle special packages like Electron
77
+ if (SPECIAL_PACKAGES[name]) {
78
+ return await this.installSpecialPackage(name, version, options);
79
+ }
80
+
81
+ // Install to shared folder
82
+ logger.info(` Installing ${name}@${version}...`);
83
+
84
+ try {
85
+ // Create package.json for the module
86
+ const tempDir = path.join(MODULES_CACHE, `install-${Date.now()}`);
87
+ await fs.ensureDir(tempDir);
88
+
89
+ await fs.writeJson(path.join(tempDir, 'package.json'), {
90
+ name: 'temp-install',
91
+ version: '1.0.0',
92
+ dependencies: {
93
+ [name]: version,
94
+ },
95
+ });
96
+
97
+ // Run npm install in temp directory
98
+ await this.runNpmInstall(tempDir);
99
+
100
+ // Move to shared modules
101
+ const installedPath = path.join(tempDir, 'node_modules', name);
102
+ if (await fs.pathExists(installedPath)) {
103
+ const modulePath = path.join(SHARED_MODULES, name);
104
+ await fs.remove(modulePath);
105
+ await fs.move(installedPath, modulePath);
106
+
107
+ // Update database
108
+ db.modules[moduleKey] = {
109
+ name,
110
+ version,
111
+ installedAt: new Date().toISOString(),
112
+ path: modulePath,
113
+ };
114
+ await this.saveModulesDb(db);
115
+ }
116
+
117
+ // Cleanup temp directory
118
+ await fs.remove(tempDir);
119
+
120
+ logger.dim(` ${name}@${version} installed`);
121
+ return { installed: true, cached: false };
122
+ } catch (error) {
123
+ logger.error(` Failed to install ${name}: ${error.message}`);
124
+ return { installed: false, cached: false, error: error.message };
125
+ }
126
+ }
127
+
128
+ async installSpecialPackage(name, version, options) {
129
+ await this.init();
130
+ const db = await this.getModulesDb();
131
+ const moduleKey = `${name}@${version}`;
132
+
133
+ logger.info(` Installing ${name} (special handling)...`);
134
+
135
+ try {
136
+ if (name === 'electron') {
137
+ return await this.installElectron(version, options);
138
+ }
139
+
140
+ // For other special packages, use regular install
141
+ const tempDir = path.join(MODULES_CACHE, `install-${Date.now()}`);
142
+ await fs.ensureDir(tempDir);
143
+
144
+ await fs.writeJson(path.join(tempDir, 'package.json'), {
145
+ name: 'temp-install',
146
+ version: '1.0.0',
147
+ dependencies: { [name]: version },
148
+ });
149
+
150
+ await this.runNpmInstall(tempDir);
151
+
152
+ const installedPath = path.join(tempDir, 'node_modules', name);
153
+ const modulePath = path.join(SHARED_MODULES, name);
154
+
155
+ if (await fs.pathExists(installedPath)) {
156
+ await fs.remove(modulePath);
157
+ await fs.move(installedPath, modulePath);
158
+
159
+ db.modules[moduleKey] = {
160
+ name,
161
+ version,
162
+ installedAt: new Date().toISOString(),
163
+ path: modulePath,
164
+ };
165
+ await this.saveModulesDb(db);
166
+ }
167
+
168
+ await fs.remove(tempDir);
169
+ logger.dim(` ${name}@${version} installed`);
170
+ return { installed: true, cached: false };
171
+
172
+ } catch (error) {
173
+ logger.error(` Failed to install ${name}: ${error.message}`);
174
+ return { installed: false, cached: false, error: error.message };
175
+ }
176
+ }
177
+
178
+ async installElectron(version, options) {
179
+ await fs.ensureDir(ELECTRON_DIR);
180
+
181
+ const db = await this.getModulesDb();
182
+ const electronVersion = version === '*' ? 'latest' : version;
183
+ const moduleKey = `electron@${electronVersion}`;
184
+
185
+ // Check if electron is already installed
186
+ const electronPath = path.join(ELECTRON_DIR, 'dist', 'electron');
187
+ if (await fs.pathExists(electronPath) && !options.force) {
188
+ logger.dim(' Electron already installed');
189
+
190
+ // Update database
191
+ db.modules[moduleKey] = {
192
+ name: 'electron',
193
+ version: electronVersion,
194
+ installedAt: new Date().toISOString(),
195
+ path: ELECTRON_DIR,
196
+ };
197
+ await this.saveModulesDb(db);
198
+
199
+ return { installed: false, cached: true };
200
+ }
201
+
202
+ logger.info(' Downloading Electron binary...');
203
+ logger.dim(' This may take a few minutes...');
204
+
205
+ // Create temp directory for electron installation
206
+ const tempDir = path.join(MODULES_CACHE, `electron-${Date.now()}`);
207
+ await fs.ensureDir(tempDir);
208
+
209
+ try {
210
+ // Create package.json for electron
211
+ await fs.writeJson(path.join(tempDir, 'package.json'), {
212
+ name: 'temp-electron',
213
+ version: '1.0.0',
214
+ dependencies: {
215
+ electron: electronVersion,
216
+ },
217
+ });
218
+
219
+ // Install electron with special flags
220
+ await new Promise((resolve, reject) => {
221
+ const child = spawn('npm', ['install', '--ignore-scripts'], {
222
+ cwd: tempDir,
223
+ stdio: 'pipe',
224
+ shell: false,
225
+ });
226
+
227
+ child.on('close', (code) => {
228
+ if (code === 0) resolve();
229
+ else reject(new Error('npm install failed'));
230
+ });
231
+
232
+ child.on('error', reject);
233
+ });
234
+
235
+ // Run electron's postinstall to download the binary
236
+ await new Promise((resolve, reject) => {
237
+ const child = spawn('node', ['node_modules/electron/install.js'], {
238
+ cwd: tempDir,
239
+ stdio: 'inherit',
240
+ shell: false,
241
+ env: {
242
+ ...process.env,
243
+ ELECTRON_MIRROR: 'https://github.com/nicedoc/electron/releases/download/v',
244
+ },
245
+ });
246
+
247
+ child.on('close', (code) => {
248
+ if (code === 0) resolve();
249
+ else reject(new Error('Electron binary download failed'));
250
+ });
251
+
252
+ child.on('error', reject);
253
+ });
254
+
255
+ // Move to electron directory
256
+ const installedPath = path.join(tempDir, 'node_modules', 'electron');
257
+ if (await fs.pathExists(installedPath)) {
258
+ await fs.remove(ELECTRON_DIR);
259
+ await fs.move(installedPath, ELECTRON_DIR);
260
+
261
+ // Update database
262
+ db.modules[moduleKey] = {
263
+ name: 'electron',
264
+ version: electronVersion,
265
+ installedAt: new Date().toISOString(),
266
+ path: ELECTRON_DIR,
267
+ };
268
+ await this.saveModulesDb(db);
269
+ }
270
+
271
+ // Cleanup
272
+ await fs.remove(tempDir);
273
+
274
+ logger.dim(' Electron installed successfully');
275
+ return { installed: true, cached: false };
276
+
277
+ } catch (error) {
278
+ // Cleanup on error
279
+ await fs.remove(tempDir);
280
+ throw error;
281
+ }
282
+ }
283
+
284
+ async installFromPackageJson(packageJsonPath, options = {}) {
285
+ await this.init();
286
+ const pkg = await fs.readJson(packageJsonPath);
287
+ const dependencies = pkg.dependencies || {};
288
+ const devDependencies = pkg.devDependencies || {};
289
+
290
+ const allDeps = { ...dependencies, ...devDependencies };
291
+ const results = { installed: 0, cached: 0, failed: 0 };
292
+
293
+ logger.info('Resolving dependencies from package.json...');
294
+
295
+ for (const [name, version] of Object.entries(allDeps)) {
296
+ const result = await this.installModule(name, version, options);
297
+ if (result.installed) results.installed++;
298
+ else if (result.cached) results.cached++;
299
+ else results.failed++;
300
+ }
301
+
302
+ return results;
303
+ }
304
+
305
+ async updateAllModules(options = {}) {
306
+ await this.init();
307
+ const db = await this.getModulesDb();
308
+ const results = { updated: 0, failed: 0 };
309
+
310
+ logger.info('Updating all shared modules...');
311
+
312
+ for (const [moduleKey, moduleInfo] of Object.entries(db.modules)) {
313
+ try {
314
+ logger.info(` Updating ${moduleInfo.name}...`);
315
+
316
+ // Remove old version
317
+ const modulePath = path.join(SHARED_MODULES, moduleInfo.name);
318
+ if (await fs.pathExists(modulePath)) {
319
+ await fs.remove(modulePath);
320
+ }
321
+
322
+ // Reinstall
323
+ const result = await this.installModule(moduleInfo.name, moduleInfo.version, { force: true });
324
+ if (result.installed) results.updated++;
325
+ else results.failed++;
326
+ } catch (error) {
327
+ logger.error(` Failed to update ${moduleInfo.name}: ${error.message}`);
328
+ results.failed++;
329
+ }
330
+ }
331
+
332
+ db.lastUpdate = new Date().toISOString();
333
+ await this.saveModulesDb(db);
334
+
335
+ return results;
336
+ }
337
+
338
+ async updateModule(name, options = {}) {
339
+ await this.init();
340
+ const db = await this.getModulesDb();
341
+
342
+ // Find all versions of this module
343
+ const moduleEntries = Object.entries(db.modules).filter(
344
+ ([key, info]) => info.name === name
345
+ );
346
+
347
+ if (moduleEntries.length === 0) {
348
+ logger.warning(`Module ${name} not found in shared modules`);
349
+ return { updated: false };
350
+ }
351
+
352
+ for (const [moduleKey, moduleInfo] of moduleEntries) {
353
+ logger.info(` Updating ${moduleInfo.name}@${moduleInfo.version}...`);
354
+
355
+ // Remove old version
356
+ const modulePath = path.join(SHARED_MODULES, moduleInfo.name);
357
+ if (await fs.pathExists(modulePath)) {
358
+ await fs.remove(modulePath);
359
+ }
360
+
361
+ // Reinstall
362
+ await this.installModule(moduleInfo.name, moduleInfo.version, { force: true });
363
+ }
364
+
365
+ return { updated: true };
366
+ }
367
+
368
+ async listModules() {
369
+ await this.init();
370
+ const db = await this.getModulesDb();
371
+ return db.modules;
372
+ }
373
+
374
+ async getModulePath(name) {
375
+ await this.init();
376
+ const modulePath = path.join(SHARED_MODULES, name);
377
+ if (await fs.pathExists(modulePath)) {
378
+ return modulePath;
379
+ }
380
+ return null;
381
+ }
382
+
383
+ async cleanUnused() {
384
+ await this.init();
385
+ const db = await this.getModulesDb();
386
+ const results = { removed: 0, kept: 0 };
387
+
388
+ logger.info('Cleaning unused modules...');
389
+
390
+ // Get all projects that use easy
391
+ const projects = await this.findEasyProjects();
392
+
393
+ // Get all dependencies used by projects
394
+ const usedModules = new Set();
395
+ for (const project of projects) {
396
+ try {
397
+ const pkg = await fs.readJson(path.join(project, 'package.json'));
398
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
399
+ for (const [name] of Object.entries(deps)) {
400
+ usedModules.add(name);
401
+ }
402
+ } catch (e) {
403
+ // Skip invalid projects
404
+ }
405
+ }
406
+
407
+ // Remove unused modules
408
+ for (const [moduleKey, moduleInfo] of Object.entries(db.modules)) {
409
+ if (!usedModules.has(moduleInfo.name)) {
410
+ const modulePath = path.join(SHARED_MODULES, moduleInfo.name);
411
+ if (await fs.pathExists(modulePath)) {
412
+ await fs.remove(modulePath);
413
+ delete db.modules[moduleKey];
414
+ logger.dim(` Removed ${moduleInfo.name}`);
415
+ results.removed++;
416
+ }
417
+ } else {
418
+ results.kept++;
419
+ }
420
+ }
421
+
422
+ await this.saveModulesDb(db);
423
+ return results;
424
+ }
425
+
426
+ async findEasyProjects() {
427
+ const projects = [];
428
+ const homeDir = os.homedir();
429
+
430
+ // Check common locations
431
+ const locations = [
432
+ path.join(homeDir, 'projects'),
433
+ path.join(homeDir, 'dev'),
434
+ path.join(homeDir, 'code'),
435
+ path.join(homeDir, 'workspace'),
436
+ path.join(homeDir, 'repos'),
437
+ ];
438
+
439
+ for (const location of locations) {
440
+ if (await fs.pathExists(location)) {
441
+ const items = await fs.readdir(location);
442
+ for (const item of items) {
443
+ const itemPath = path.join(location, item);
444
+ const stat = await fs.stat(itemPath);
445
+ if (stat.isDirectory()) {
446
+ if (await fs.pathExists(path.join(itemPath, 'package.json'))) {
447
+ projects.push(itemPath);
448
+ }
449
+ }
450
+ }
451
+ }
452
+ }
453
+
454
+ return projects;
455
+ }
456
+
457
+ async getStats() {
458
+ await this.init();
459
+ const db = await this.getModulesDb();
460
+ const moduleCount = Object.keys(db.modules).length;
461
+
462
+ // Calculate total size
463
+ let totalSize = 0;
464
+ const items = await fs.readdir(SHARED_MODULES);
465
+ for (const item of items) {
466
+ const itemPath = path.join(SHARED_MODULES, item);
467
+ const stat = await fs.stat(itemPath);
468
+ totalSize += stat.size;
469
+ }
470
+
471
+ return {
472
+ moduleCount,
473
+ totalSize,
474
+ lastUpdate: db.lastUpdate,
475
+ sharedPath: SHARED_MODULES,
476
+ };
477
+ }
478
+
479
+ async runNpmInstall(cwd) {
480
+ return new Promise((resolve, reject) => {
481
+ const child = spawn('npm', ['install', '--ignore-scripts', '--no-audit', '--no-fund'], {
482
+ cwd,
483
+ stdio: 'pipe',
484
+ shell: false,
485
+ });
486
+
487
+ child.on('close', (code) => {
488
+ if (code === 0) resolve();
489
+ else reject(new Error('npm install failed'));
490
+ });
491
+
492
+ child.on('error', reject);
493
+ });
494
+ }
495
+
496
+ getSharedModulesPath() {
497
+ return SHARED_MODULES;
498
+ }
499
+
500
+ getNodeModulesPaths() {
501
+ return [
502
+ SHARED_MODULES,
503
+ path.join(process.cwd(), 'node_modules'),
504
+ ];
505
+ }
506
+
507
+ async linkFromLocal() {
508
+ await this.init();
509
+ const localNodeModules = path.join(process.cwd(), 'node_modules');
510
+ const db = await this.getModulesDb();
511
+ const results = { linked: 0, skipped: 0 };
512
+
513
+ if (!(await fs.pathExists(localNodeModules))) {
514
+ logger.warning('No local node_modules found');
515
+ return results;
516
+ }
517
+
518
+ logger.info('Linking local modules to shared folder...');
519
+
520
+ const items = await fs.readdir(localNodeModules);
521
+ for (const item of items) {
522
+ // Skip hidden files and .bin
523
+ if (item.startsWith('.') || item === '.bin') continue;
524
+
525
+ const localPath = path.join(localNodeModules, item);
526
+ const sharedPath = path.join(SHARED_MODULES, item);
527
+
528
+ try {
529
+ const stat = await fs.lstat(localPath);
530
+ if (!stat.isDirectory()) continue;
531
+
532
+ // Check if already in shared
533
+ if (await fs.pathExists(sharedPath)) {
534
+ results.skipped++;
535
+ continue;
536
+ }
537
+
538
+ // Read package.json to get version
539
+ const pkgPath = path.join(localPath, 'package.json');
540
+ let version = '*';
541
+ if (await fs.pathExists(pkgPath)) {
542
+ const pkg = await fs.readJson(pkgPath);
543
+ version = pkg.version || '*';
544
+ }
545
+
546
+ // Copy to shared folder
547
+ await fs.copy(localPath, sharedPath);
548
+
549
+ // Update database
550
+ const moduleKey = `${item}@${version}`;
551
+ db.modules[moduleKey] = {
552
+ name: item,
553
+ version,
554
+ installedAt: new Date().toISOString(),
555
+ path: sharedPath,
556
+ linkedFromLocal: true,
557
+ };
558
+
559
+ results.linked++;
560
+ logger.dim(` Linked ${item}@${version}`);
561
+ } catch (e) {
562
+ // Skip modules that can't be read
563
+ }
564
+ }
565
+
566
+ await this.saveModulesDb(db);
567
+ return results;
568
+ }
569
+ }
570
+
571
+ module.exports = new SharedModules();