stigmergy 1.2.13 → 1.3.1

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.
Files changed (88) hide show
  1. package/README.md +39 -3
  2. package/STIGMERGY.md +3 -0
  3. package/config/builtin-skills.json +43 -0
  4. package/config/enhanced-cli-config.json +438 -0
  5. package/docs/CLI_TOOLS_AGENT_SKILL_ANALYSIS.md +463 -0
  6. package/docs/DESIGN_CLI_HELP_ANALYZER_REFACTOR.md +726 -0
  7. package/docs/ENHANCED_CLI_AGENT_SKILL_CONFIG.md +285 -0
  8. package/docs/IMPLEMENTATION_CHECKLIST_CLI_HELP_ANALYZER_REFACTOR.md +1268 -0
  9. package/docs/INSTALLER_ARCHITECTURE.md +257 -0
  10. package/docs/LESSONS_LEARNED.md +252 -0
  11. package/docs/SPECS_CLI_HELP_ANALYZER_REFACTOR.md +287 -0
  12. package/docs/SUDO_PROBLEM_AND_SOLUTION.md +529 -0
  13. package/docs/correct-skillsio-implementation.md +368 -0
  14. package/docs/development_guidelines.md +276 -0
  15. package/docs/independent-resume-implementation.md +198 -0
  16. package/docs/resumesession-final-implementation.md +195 -0
  17. package/docs/resumesession-usage.md +87 -0
  18. package/package.json +146 -136
  19. package/scripts/analyze-router.js +168 -0
  20. package/scripts/run-comprehensive-tests.js +230 -0
  21. package/scripts/run-quick-tests.js +90 -0
  22. package/scripts/test-runner.js +344 -0
  23. package/skills/resumesession/INDEPENDENT_SKILL.md +403 -0
  24. package/skills/resumesession/README.md +381 -0
  25. package/skills/resumesession/SKILL.md +211 -0
  26. package/skills/resumesession/__init__.py +33 -0
  27. package/skills/resumesession/implementations/simple-resume.js +13 -0
  28. package/skills/resumesession/independent-resume.js +750 -0
  29. package/skills/resumesession/package.json +1 -0
  30. package/skills/resumesession/skill.json +1 -0
  31. package/src/adapters/claude/install_claude_integration.js +9 -1
  32. package/src/adapters/codebuddy/install_codebuddy_integration.js +3 -1
  33. package/src/adapters/codex/install_codex_integration.js +15 -5
  34. package/src/adapters/gemini/install_gemini_integration.js +3 -1
  35. package/src/adapters/qwen/install_qwen_integration.js +3 -1
  36. package/src/cli/commands/autoinstall.js +65 -0
  37. package/src/cli/commands/errors.js +190 -0
  38. package/src/cli/commands/independent-resume.js +395 -0
  39. package/src/cli/commands/install.js +179 -0
  40. package/src/cli/commands/permissions.js +108 -0
  41. package/src/cli/commands/project.js +485 -0
  42. package/src/cli/commands/scan.js +97 -0
  43. package/src/cli/commands/simple-resume.js +377 -0
  44. package/src/cli/commands/skills.js +158 -0
  45. package/src/cli/commands/status.js +113 -0
  46. package/src/cli/commands/stigmergy-resume.js +775 -0
  47. package/src/cli/commands/system.js +301 -0
  48. package/src/cli/commands/universal-resume.js +394 -0
  49. package/src/cli/router-beta.js +471 -0
  50. package/src/cli/utils/environment.js +75 -0
  51. package/src/cli/utils/formatters.js +47 -0
  52. package/src/cli/utils/skills_cache.js +92 -0
  53. package/src/core/cache_cleaner.js +1 -0
  54. package/src/core/cli_adapters.js +345 -0
  55. package/src/core/cli_help_analyzer.js +1236 -680
  56. package/src/core/cli_path_detector.js +702 -709
  57. package/src/core/cli_tools.js +515 -160
  58. package/src/core/coordination/nodejs/CLIIntegrationManager.js +18 -0
  59. package/src/core/coordination/nodejs/HookDeploymentManager.js +242 -412
  60. package/src/core/coordination/nodejs/HookDeploymentManager.refactored.js +323 -0
  61. package/src/core/coordination/nodejs/generators/CLIAdapterGenerator.js +363 -0
  62. package/src/core/coordination/nodejs/generators/ResumeSessionGenerator.js +932 -0
  63. package/src/core/coordination/nodejs/generators/SkillsIntegrationGenerator.js +1395 -0
  64. package/src/core/coordination/nodejs/generators/index.js +12 -0
  65. package/src/core/enhanced_cli_installer.js +1208 -608
  66. package/src/core/enhanced_cli_parameter_handler.js +402 -0
  67. package/src/core/execution_mode_detector.js +222 -0
  68. package/src/core/installer.js +151 -106
  69. package/src/core/local_skill_scanner.js +732 -0
  70. package/src/core/multilingual/language-pattern-manager.js +1 -1
  71. package/src/core/skills/BuiltinSkillsDeployer.js +188 -0
  72. package/src/core/skills/StigmergySkillManager.js +123 -16
  73. package/src/core/skills/embedded-openskills/SkillParser.js +7 -3
  74. package/src/core/smart_router.js +550 -261
  75. package/src/index.js +10 -4
  76. package/src/utils.js +66 -7
  77. package/test/cli-integration.test.js +304 -0
  78. package/test/direct_smart_router_test.js +88 -0
  79. package/test/enhanced-cli-agent-skill-test.js +485 -0
  80. package/test/simple_test.js +82 -0
  81. package/test/smart_router_test_runner.js +123 -0
  82. package/test/smart_routing_edge_cases.test.js +284 -0
  83. package/test/smart_routing_simple_verification.js +139 -0
  84. package/test/smart_routing_verification.test.js +346 -0
  85. package/test/specific-cli-agent-skill-analysis.js +385 -0
  86. package/test/unit/smart_router.test.js +295 -0
  87. package/test/very_simple_test.js +54 -0
  88. package/src/cli/router.js +0 -1783
@@ -1,609 +1,1209 @@
1
- /**
2
- * Optimized Enhanced CLI Installer with Batch Permission Handling
3
- *
4
- * This version optimizes permission handling by:
5
- * 1. Initial permission setup once at the beginning of the process
6
- * 2. Streamlined tool installation without per-tool permission checks
7
- * 3. Automatic permission escalation only when needed
8
- */
9
-
10
- const path = require('path');
11
- const fs = require('fs').promises;
12
- const { spawn, spawnSync } = require('child_process');
13
- const os = require('os');
14
- const chalk = require('chalk');
15
-
16
- class EnhancedCLIInstaller {
17
- constructor(options = {}) {
18
- this.options = {
19
- verbose: options.verbose || false,
20
- skipPermissionCheck: options.skipPermissionCheck || false,
21
- autoRetry: options.autoRetry !== false,
22
- maxRetries: options.maxRetries || 3,
23
- timeout: options.timeout || 300000, // 5 minutes
24
- ...options
25
- };
26
-
27
- this.results = {
28
- permissionSetup: null,
29
- installations: {},
30
- failedInstallations: [],
31
- npmConfigured: false,
32
- workingDirectory: null,
33
- permissionMode: 'standard' // 'standard', 'elevated', 'failed'
34
- };
35
-
36
- this.cliTools = require('./cli_tools').CLI_TOOLS;
37
- this.permissionConfigured = false;
38
- }
39
-
40
- /**
41
- * Log messages with color and formatting
42
- */
43
- log(level, message, data = null) {
44
- const timestamp = new Date().toLocaleTimeString();
45
- let prefix = '';
46
- let color = chalk.white;
47
-
48
- switch (level) {
49
- case 'info':
50
- prefix = `[INFO] ${timestamp}`;
51
- color = chalk.blue;
52
- break;
53
- case 'success':
54
- prefix = `[SUCCESS] ${timestamp}`;
55
- color = chalk.green;
56
- break;
57
- case 'warn':
58
- prefix = `[WARN] ${timestamp}`;
59
- color = chalk.yellow;
60
- break;
61
- case 'error':
62
- prefix = `[ERROR] ${timestamp}`;
63
- color = chalk.red;
64
- break;
65
- case 'debug':
66
- prefix = `[DEBUG] ${timestamp}`;
67
- color = chalk.gray;
68
- break;
69
- }
70
-
71
- const logMessage = color(`${prefix} ${message}`);
72
- if (level === 'error') {
73
- console.error(logMessage);
74
- } else {
75
- console.log(logMessage);
76
- }
77
-
78
- if (data && this.options.verbose) {
79
- console.log(' Data:', JSON.stringify(data, null, 2));
80
- }
81
- }
82
-
83
- /**
84
- * One-time permission setup at the beginning of the process
85
- */
86
- async setupPermissions() {
87
- if (this.permissionConfigured) {
88
- return { success: true, mode: this.permissionMode };
89
- }
90
-
91
- this.log('info', 'Setting up permissions for CLI tool installation...');
92
-
93
- try {
94
- // Check if we're already in an elevated context
95
- const isElevated = await this.checkElevatedContext();
96
-
97
- if (isElevated) {
98
- this.log('info', 'Already running with elevated permissions');
99
- this.permissionMode = 'elevated';
100
- this.permissionConfigured = true;
101
- return { success: true, mode: 'elevated' };
102
- }
103
-
104
- // Attempt standard installation first
105
- const testResult = await this.attemptTestInstallation();
106
-
107
- if (testResult.success) {
108
- this.log('success', 'Standard permissions are sufficient');
109
- this.permissionMode = 'standard';
110
- this.permissionConfigured = true;
111
- return { success: true, mode: 'standard' };
112
- }
113
-
114
- // If standard installation fails, check if it's a permission issue
115
- if (this.isPermissionError(testResult.error)) {
116
- this.log('warn', 'Permission issue detected, setting up elevated context...');
117
-
118
- const elevatedSetup = await this.setupElevatedContext();
119
- if (elevatedSetup.success) {
120
- this.log('success', 'Elevated permissions configured');
121
- this.permissionMode = 'elevated';
122
- this.permissionConfigured = true;
123
- return { success: true, mode: 'elevated' };
124
- } else {
125
- this.log('error', 'Failed to set up elevated permissions');
126
- this.permissionMode = 'failed';
127
- this.permissionConfigured = true;
128
- return { success: false, mode: 'failed', error: elevatedSetup.error };
129
- }
130
- }
131
-
132
- // Other types of errors
133
- this.log('error', `Non-permission error during setup: ${testResult.error}`);
134
- this.permissionMode = 'failed';
135
- this.permissionConfigured = true;
136
- return { success: false, mode: 'failed', error: testResult.error };
137
-
138
- } catch (error) {
139
- this.log('error', `Permission setup failed: ${error.message}`);
140
- this.permissionMode = 'failed';
141
- this.permissionConfigured = true;
142
- return { success: false, mode: 'failed', error: error.message };
143
- }
144
- }
145
-
146
- /**
147
- * Check if we're already running in an elevated context
148
- */
149
- async checkElevatedContext() {
150
- const platform = process.platform;
151
-
152
- if (platform === 'win32') {
153
- // Windows: Check if running as administrator
154
- try {
155
- const { execSync } = require('child_process');
156
- const result = execSync('net session', { encoding: 'utf8' });
157
- return result.includes('Administrator');
158
- } catch {
159
- return false;
160
- }
161
- } else {
162
- // Unix-like systems: Check if we have root privileges
163
- return process.getuid && process.getuid() === 0;
164
- }
165
- }
166
-
167
- /**
168
- * Attempt a test installation to check permissions
169
- */
170
- async attemptTestInstallation() {
171
- try {
172
- // Try a simple npm command to test permissions
173
- const result = spawnSync('npm', ['config', 'get', 'prefix'], {
174
- stdio: 'pipe',
175
- shell: true,
176
- encoding: 'utf8',
177
- timeout: 10000
178
- });
179
-
180
- if (result.status === 0) {
181
- return { success: true, error: null };
182
- } else {
183
- return { success: false, error: result.stderr || result.stdout };
184
- }
185
- } catch (error) {
186
- return { success: false, error: error.message };
187
- }
188
- }
189
-
190
- /**
191
- * Setup elevated context based on platform
192
- */
193
- async setupElevatedContext() {
194
- const platform = process.platform;
195
-
196
- if (platform === 'win32') {
197
- return this.setupWindowsElevatedContext();
198
- } else {
199
- return this.setupUnixElevatedContext();
200
- }
201
- }
202
-
203
- /**
204
- * Setup Windows elevated context
205
- */
206
- async setupWindowsElevatedContext() {
207
- this.log('info', 'Windows: Preparing for elevated installation...');
208
-
209
- // On Windows, we'll handle elevation per-installation
210
- // since we can't maintain elevated state across commands
211
- return {
212
- success: true,
213
- platform: 'windows',
214
- note: 'Will elevate per installation as needed'
215
- };
216
- }
217
-
218
- /**
219
- * Setup Unix elevated context
220
- */
221
- async setupUnixElevatedContext() {
222
- this.log('info', 'Unix: Checking sudo availability...');
223
-
224
- try {
225
- const result = spawnSync('sudo', ['-n', 'true'], {
226
- stdio: 'pipe',
227
- timeout: 5000
228
- });
229
-
230
- // If sudo is available and configured for passwordless use
231
- if (result.status === 0) {
232
- return {
233
- success: true,
234
- platform: 'unix',
235
- sudo: 'passwordless'
236
- };
237
- } else {
238
- return {
239
- success: true,
240
- platform: 'unix',
241
- sudo: 'password_required',
242
- note: 'Will prompt for password per installation'
243
- };
244
- }
245
- } catch (error) {
246
- return {
247
- success: false,
248
- platform: 'unix',
249
- error: error.message
250
- };
251
- }
252
- }
253
-
254
- /**
255
- * Install a tool using the pre-configured permission mode
256
- */
257
- async installTool(toolName, toolInfo, retryCount = 0) {
258
- // Ensure permissions are configured first
259
- if (!this.permissionConfigured) {
260
- await this.setupPermissions();
261
- }
262
-
263
- this.log('info', `Installing ${toolInfo.name} (${toolName})...`);
264
-
265
- try {
266
- const installResult = await this.executeInstallation(toolInfo);
267
-
268
- if (installResult.success) {
269
- this.log('success', `Successfully installed ${toolInfo.name}`);
270
- this.results.installations[toolName] = {
271
- success: true,
272
- tool: toolInfo.name,
273
- command: toolInfo.install,
274
- duration: Date.now() - (this.results.installations[toolName]?.startTime || Date.now()),
275
- permissionMode: this.permissionMode,
276
- permissionHandled: this.permissionMode === 'elevated'
277
- };
278
- return true;
279
- } else {
280
- throw new Error(installResult.error);
281
- }
282
-
283
- } catch (error) {
284
- this.log('error', `Failed to install ${toolInfo.name}: ${error.message}`);
285
-
286
- this.results.installations[toolName] = {
287
- success: false,
288
- tool: toolInfo.name,
289
- command: toolInfo.install,
290
- error: error.message,
291
- retryCount: retryCount,
292
- permissionMode: this.permissionMode
293
- };
294
-
295
- this.results.failedInstallations.push(toolName);
296
-
297
- // Retry logic for non-permission errors
298
- if (this.options.autoRetry && retryCount < this.options.maxRetries) {
299
- this.log('warn', `Retrying installation of ${toolInfo.name} (${retryCount + 1}/${this.options.maxRetries})...`);
300
- await new Promise(resolve => setTimeout(resolve, 2000));
301
- return await this.installTool(toolName, toolInfo, retryCount + 1);
302
- }
303
-
304
- return false;
305
- }
306
- }
307
-
308
- /**
309
- * Execute installation based on pre-configured permission mode
310
- */
311
- async executeInstallation(toolInfo) {
312
- switch (this.permissionMode) {
313
- case 'standard':
314
- return await this.executeStandardInstallation(toolInfo);
315
- case 'elevated':
316
- return await this.executeElevatedInstallation(toolInfo);
317
- case 'failed':
318
- return await this.executeFallbackInstallation(toolInfo);
319
- default:
320
- // Try standard first, then escalate if needed
321
- const standardResult = await this.executeStandardInstallation(toolInfo);
322
- if (standardResult.success) {
323
- return standardResult;
324
- }
325
-
326
- if (this.isPermissionError(standardResult.error)) {
327
- this.log('warn', `Permission error, escalating to elevated installation...`);
328
- this.permissionMode = 'elevated';
329
- return await this.executeElevatedInstallation(toolInfo);
330
- }
331
-
332
- return standardResult;
333
- }
334
- }
335
-
336
- /**
337
- * Execute standard installation without permission elevation
338
- */
339
- async executeStandardInstallation(toolInfo) {
340
- try {
341
- const [command, ...args] = toolInfo.install.split(' ');
342
-
343
- this.log('debug', `Executing: ${toolInfo.install}`);
344
-
345
- const result = spawnSync(command, args, {
346
- stdio: this.options.verbose ? 'inherit' : 'pipe',
347
- shell: true,
348
- encoding: 'utf8',
349
- timeout: this.options.timeout,
350
- env: {
351
- ...process.env,
352
- npm_config_prefix: process.env.npm_config_prefix,
353
- npm_config_global: 'true',
354
- npm_config_update: 'false',
355
- npm_config_progress: 'false'
356
- }
357
- });
358
-
359
- if (result.status === 0) {
360
- return { success: true, error: null };
361
- } else {
362
- const errorMessage = result.stderr || result.stdout || `Exit code ${result.status}`;
363
- return { success: false, error: errorMessage };
364
- }
365
-
366
- } catch (error) {
367
- return { success: false, error: error.message };
368
- }
369
- }
370
-
371
- /**
372
- * Execute installation with permission elevation
373
- */
374
- async executeElevatedInstallation(toolInfo) {
375
- const platform = process.platform;
376
-
377
- if (platform === 'win32') {
378
- return await this.executeWindowsElevatedInstallation(toolInfo);
379
- } else {
380
- return await this.executeUnixElevatedInstallation(toolInfo);
381
- }
382
- }
383
-
384
- /**
385
- * Execute Windows elevated installation
386
- */
387
- async executeWindowsElevatedInstallation(toolInfo) {
388
- const command = toolInfo.install;
389
-
390
- try {
391
- this.log('info', `Creating Windows elevated installation for: ${toolInfo.name}`);
392
-
393
- const scriptPath = path.join(os.tmpdir(), `stigmergy-install-${Date.now()}.ps1`);
394
- const scriptContent = `
395
- Write-Host "以管理员权限安装: ${toolInfo.name}" -ForegroundColor Yellow
396
- try {
397
- ${command}
398
- if ($LASTEXITCODE -eq 0) {
399
- Write-Host "安装成功: ${toolInfo.name}" -ForegroundColor Green
400
- exit 0
401
- } else {
402
- Write-Host "安装失败: ${toolInfo.name}" -ForegroundColor Red
403
- exit $LASTEXITCODE
404
- }
405
- } catch {
406
- Write-Host "安装异常: ${toolInfo.name}" -ForegroundColor Red
407
- Write-Host $\_.Exception.Message -ForegroundColor Red
408
- exit 1
409
- }
410
- `;
411
-
412
- require('fs').writeFileSync(scriptPath, scriptContent, 'utf8');
413
-
414
- const result = spawnSync('powershell', [
415
- '-Command', `Start-Process PowerShell -Verb RunAs -ArgumentList "-File '${scriptPath}'" -Wait`
416
- ], {
417
- stdio: 'pipe',
418
- timeout: this.options.timeout * 2,
419
- encoding: 'utf8'
420
- });
421
-
422
- try {
423
- require('fs').unlinkSync(scriptPath);
424
- } catch (cleanupError) {
425
- this.log('warn', `Could not clean up temp script: ${cleanupError.message}`);
426
- }
427
-
428
- return {
429
- success: result.status === 0,
430
- error: result.status !== 0 ? 'Windows elevated installation failed' : null
431
- };
432
- } catch (error) {
433
- return { success: false, error: error.message };
434
- }
435
- }
436
-
437
- /**
438
- * Execute Unix elevated installation
439
- */
440
- async executeUnixElevatedInstallation(toolInfo) {
441
- const command = `sudo ${toolInfo.install}`;
442
-
443
- try {
444
- this.log('info', `Using sudo for elevated installation of: ${toolInfo.name}`);
445
-
446
- const result = spawnSync('bash', ['-c', command], {
447
- stdio: this.options.verbose ? 'inherit' : 'pipe',
448
- timeout: this.options.timeout * 2,
449
- encoding: 'utf8'
450
- });
451
-
452
- if (result.status === 0) {
453
- return { success: true, error: null };
454
- } else {
455
- const errorMessage = result.stderr || result.stdout || `Exit code ${result.status}`;
456
- return { success: false, error: errorMessage };
457
- }
458
- } catch (error) {
459
- return { success: false, error: error.message };
460
- }
461
- }
462
-
463
- /**
464
- * Fallback installation method
465
- */
466
- async executeFallbackInstallation(toolInfo) {
467
- this.log('warn', 'Attempting fallback installation method...');
468
-
469
- // Try without some npm flags that might cause permission issues
470
- const [command, ...args] = toolInfo.install.split(' ');
471
- const fallbackArgs = args.filter(arg => !arg.startsWith('--'));
472
-
473
- try {
474
- const result = spawnSync(command, fallbackArgs, {
475
- stdio: 'inherit',
476
- shell: true,
477
- encoding: 'utf8',
478
- timeout: this.options.timeout
479
- });
480
-
481
- if (result.status === 0) {
482
- return { success: true, error: null };
483
- } else {
484
- return { success: false, error: `Fallback failed: ${result.stderr}` };
485
- }
486
- } catch (error) {
487
- return { success: false, error: error.message };
488
- }
489
- }
490
-
491
- /**
492
- * Check if an error is related to permissions
493
- */
494
- isPermissionError(errorMessage) {
495
- if (!errorMessage || typeof errorMessage !== 'string') {
496
- return false;
497
- }
498
-
499
- const permissionIndicators = [
500
- 'EACCES', 'EPERM', 'permission denied',
501
- 'access denied', 'unauthorized', 'EISDIR',
502
- 'operation not permitted', 'code EACCES',
503
- 'code EPERM', 'permission error', 'cannot create directory',
504
- 'write EACCES', 'mkdir', 'denied'
505
- ];
506
-
507
- const lowerError = errorMessage.toLowerCase();
508
- return permissionIndicators.some(indicator =>
509
- lowerError.includes(indicator.toLowerCase())
510
- );
511
- }
512
-
513
- /**
514
- * Install multiple CLI tools
515
- */
516
- async installTools(toolNames, toolInfos) {
517
- this.log('info', 'Starting batch installation of CLI tools...');
518
-
519
- // One-time permission setup for the entire batch
520
- const permissionSetup = await this.setupPermissions();
521
- if (!permissionSetup.success && permissionSetup.mode !== 'elevated') {
522
- this.log('warn', 'Permission setup failed, but proceeding with individual installations...');
523
- }
524
-
525
- let successCount = 0;
526
- const totalCount = toolNames.length;
527
-
528
- this.log('info', `Installing ${totalCount} CLI tools in ${this.permissionMode} mode...`);
529
-
530
- for (const toolName of toolNames) {
531
- const toolInfo = toolInfos[toolName];
532
- if (!toolInfo) continue;
533
-
534
- this.results.installations[toolName] = {
535
- startTime: Date.now(),
536
- ...this.results.installations[toolName]
537
- };
538
-
539
- const success = await this.installTool(toolName, toolInfo);
540
- if (success) {
541
- successCount++;
542
- }
543
- }
544
-
545
- this.log('info', `Batch installation completed: ${successCount}/${totalCount} successful`);
546
-
547
- return {
548
- success: successCount === totalCount,
549
- total: totalCount,
550
- successful: successCount,
551
- failed: totalCount - successCount,
552
- permissionMode: this.permissionMode,
553
- results: this.results
554
- };
555
- }
556
-
557
- /**
558
- * Upgrade CLI tools
559
- */
560
- async upgradeTools(toolNames, toolInfos) {
561
- this.log('info', 'Starting batch upgrade of CLI tools...');
562
-
563
- // One-time permission setup for the entire batch
564
- const permissionSetup = await this.setupPermissions();
565
- if (!permissionSetup.success && permissionSetup.mode !== 'elevated') {
566
- this.log('warn', 'Permission setup failed, but proceeding with individual upgrades...');
567
- }
568
-
569
- let successCount = 0;
570
- const totalCount = toolNames.length;
571
-
572
- this.log('info', `Upgrading ${totalCount} CLI tools in ${this.permissionMode} mode...`);
573
-
574
- for (const toolName of toolNames) {
575
- const toolInfo = {
576
- ...toolInfos[toolName],
577
- install: `npm upgrade -g ${toolName}`,
578
- name: `${toolInfo.name} (Upgrade)`
579
- };
580
-
581
- if (!toolInfo) continue;
582
-
583
- const success = await this.installTool(toolName, toolInfo);
584
- if (success) {
585
- successCount++;
586
- }
587
- }
588
-
589
- this.log('info', `Batch upgrade completed: ${successCount}/${totalCount} successful`);
590
-
591
- return {
592
- success: successCount === totalCount,
593
- total: totalCount,
594
- successful: successCount,
595
- failed: totalCount - successCount,
596
- permissionMode: this.permissionMode,
597
- results: this.results
598
- };
599
- }
600
-
601
- /**
602
- * Get installation results
603
- */
604
- getResults() {
605
- return this.results;
606
- }
607
- }
608
-
1
+ /**
2
+ * Optimized Enhanced CLI Installer with Batch Permission Handling
3
+ *
4
+ * This version optimizes permission handling by:
5
+ * 1. Initial permission setup once at the beginning of the process
6
+ * 2. Streamlined tool installation without per-tool permission checks
7
+ * 3. Automatic permission escalation only when needed
8
+ */
9
+
10
+ const path = require('path');
11
+ const fs = require('fs').promises;
12
+ const { spawn, spawnSync } = require('child_process');
13
+ const os = require('os');
14
+ const chalk = require('chalk');
15
+
16
+ class EnhancedCLIInstaller {
17
+ constructor(options = {}) {
18
+ this.options = {
19
+ verbose: options.verbose || false,
20
+ skipPermissionCheck: options.skipPermissionCheck || false,
21
+ autoRetry: options.autoRetry !== false,
22
+ maxRetries: options.maxRetries || 3,
23
+ timeout: options.timeout || 300000, // 5 minutes
24
+ ...options
25
+ };
26
+
27
+ // Expose commonly used options as properties for easier access
28
+ this.verbose = this.options.verbose;
29
+ this.force = options.force || false;
30
+ this.homeDir = options.homeDir || require('os').homedir();
31
+ this.results = {
32
+ permissionSetup: null,
33
+ installations: {},
34
+ failedInstallations: [],
35
+ npmConfigured: false,
36
+ workingDirectory: null,
37
+ permissionMode: 'standard', // 'standard', 'elevated', 'failed'
38
+ errors: []
39
+ };
40
+
41
+ this.cliTools = require('./cli_tools').CLI_TOOLS;
42
+ this.permissionConfigured = false;
43
+ }
44
+
45
+ /**
46
+ * Log messages with color and formatting
47
+ */
48
+ log(level, message, data = null) {
49
+ const timestamp = new Date().toLocaleTimeString();
50
+ let prefix = '';
51
+ let color = chalk.white;
52
+
53
+ switch (level) {
54
+ case 'info':
55
+ prefix = `[INFO] ${timestamp}`;
56
+ color = chalk.blue;
57
+ break;
58
+ case 'success':
59
+ prefix = `[SUCCESS] ${timestamp}`;
60
+ color = chalk.green;
61
+ break;
62
+ case 'warn':
63
+ prefix = `[WARN] ${timestamp}`;
64
+ color = chalk.yellow;
65
+ break;
66
+ case 'error':
67
+ prefix = `[ERROR] ${timestamp}`;
68
+ color = chalk.red;
69
+ break;
70
+ case 'debug':
71
+ prefix = `[DEBUG] ${timestamp}`;
72
+ color = chalk.gray;
73
+ break;
74
+ }
75
+
76
+ const logMessage = color(`${prefix} ${message}`);
77
+ if (level === 'error') {
78
+ console.error(logMessage);
79
+ } else {
80
+ console.log(logMessage);
81
+ }
82
+
83
+ if (data && this.options.verbose) {
84
+ console.log(' Data:', JSON.stringify(data, null, 2));
85
+ }
86
+ }
87
+
88
+ /**
89
+ * One-time permission setup at the beginning of the process
90
+ */
91
+ async setupPermissions() {
92
+ if (this.permissionConfigured) {
93
+ return { success: true, mode: this.permissionMode };
94
+ }
95
+
96
+ this.log('info', 'Setting up permissions for CLI tool installation...');
97
+
98
+ try {
99
+ // Check if we're already in an elevated context
100
+ const isElevated = await this.checkElevatedContext();
101
+
102
+ if (isElevated) {
103
+ this.log('info', 'Already running with elevated permissions');
104
+ this.permissionMode = 'elevated';
105
+ this.permissionConfigured = true;
106
+ return { success: true, mode: 'elevated' };
107
+ }
108
+
109
+ // Check if we're in a container environment (common indicators)
110
+ const inContainer = await this.checkContainerEnvironment();
111
+ if (inContainer) {
112
+ this.log('info', 'Detected container environment, using user-space installation');
113
+ this.permissionMode = 'user-space';
114
+ this.permissionConfigured = true;
115
+ return { success: true, mode: 'user-space' };
116
+ }
117
+
118
+ // Attempt standard installation first
119
+ const testResult = await this.attemptTestInstallation();
120
+
121
+ if (testResult.success) {
122
+ this.log('success', 'Standard permissions are sufficient');
123
+ this.permissionMode = 'standard';
124
+ this.permissionConfigured = true;
125
+ return { success: true, mode: 'standard' };
126
+ }
127
+
128
+ // If standard installation fails, check if it's a permission issue
129
+ if (this.isPermissionError(testResult.error)) {
130
+ this.log('warn', 'Permission issue detected, setting up elevated context...');
131
+
132
+ const elevatedSetup = await this.setupElevatedContext();
133
+ if (elevatedSetup.success) {
134
+ this.log('success', 'Elevated permissions configured');
135
+ this.permissionMode = 'elevated';
136
+ this.permissionConfigured = true;
137
+ return { success: true, mode: 'elevated' };
138
+ } else {
139
+ this.log('error', 'Failed to set up elevated permissions');
140
+ this.log('info', 'Falling back to user-space installation');
141
+ this.permissionMode = 'user-space';
142
+ this.permissionConfigured = true;
143
+ return { success: true, mode: 'user-space' };
144
+ }
145
+ }
146
+
147
+ // Other types of errors
148
+ this.log('error', `Non-permission error during setup: ${testResult.error}`);
149
+ this.permissionMode = 'failed';
150
+ this.permissionConfigured = true;
151
+ return { success: false, mode: 'failed', error: testResult.error };
152
+
153
+ } catch (error) {
154
+ this.log('error', `Permission setup failed: ${error.message}`);
155
+ this.log('info', 'Falling back to user-space installation');
156
+ this.permissionMode = 'user-space';
157
+ this.permissionConfigured = true;
158
+ return { success: true, mode: 'user-space', error: error.message };
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Check if we're already running in an elevated context
164
+ */
165
+ async checkElevatedContext() {
166
+ const platform = process.platform;
167
+
168
+ if (platform === 'win32') {
169
+ // Windows: Check if running as administrator
170
+ try {
171
+ const { execSync } = require('child_process');
172
+ const result = execSync('net session', { encoding: 'utf8' });
173
+ return result.includes('Administrator');
174
+ } catch {
175
+ return false;
176
+ }
177
+ } else {
178
+ // Unix-like systems: Check if we have root privileges
179
+ return process.getuid && process.getuid() === 0;
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Check if we're running in a container environment
185
+ */
186
+ async checkContainerEnvironment() {
187
+ try {
188
+ // Check for container indicators
189
+ const fs = require('fs');
190
+
191
+ // Check for .dockerenv file
192
+ if (fs.existsSync('/.dockerenv')) {
193
+ return true;
194
+ }
195
+
196
+ // Check for container environment variables
197
+ if (process.env.container || process.env.DOCKER_CONTAINER) {
198
+ return true;
199
+ }
200
+
201
+ // Check cgroup for container indicators
202
+ try {
203
+ if (fs.existsSync('/proc/1/cgroup')) {
204
+ const cgroupContent = fs.readFileSync('/proc/1/cgroup', 'utf8');
205
+ if (cgroupContent.includes('docker') || cgroupContent.includes('containerd')) {
206
+ return true;
207
+ }
208
+ }
209
+ } catch (e) {
210
+ // Ignore errors reading cgroup
211
+ }
212
+
213
+ // Check for container-specific files
214
+ const containerIndicators = [
215
+ '/run/.containerenv', // Podman/Docker
216
+ '/sys/fs/cgroup/cpu/cpu.cfs_quota_us', // Common in containers
217
+ ];
218
+
219
+ for (const indicator of containerIndicators) {
220
+ try {
221
+ if (fs.existsSync(indicator)) {
222
+ return true;
223
+ }
224
+ } catch (e) {
225
+ // Ignore errors
226
+ }
227
+ }
228
+
229
+ return false;
230
+ } catch (error) {
231
+ // If we can't determine, assume not in container
232
+ return false;
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Attempt a test installation to check permissions
238
+ */
239
+ async attemptTestInstallation() {
240
+ try {
241
+ // Try a simple npm command to test permissions
242
+ const result = spawnSync('npm', ['config', 'get', 'prefix'], {
243
+ stdio: 'pipe',
244
+ shell: true,
245
+ encoding: 'utf8',
246
+ timeout: 10000
247
+ });
248
+
249
+ if (result.status === 0) {
250
+ return { success: true, error: null };
251
+ } else {
252
+ return { success: false, error: result.stderr || result.stdout };
253
+ }
254
+ } catch (error) {
255
+ return { success: false, error: error.message };
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Setup elevated context based on platform
261
+ */
262
+ async setupElevatedContext() {
263
+ const platform = process.platform;
264
+
265
+ if (platform === 'win32') {
266
+ return this.setupWindowsElevatedContext();
267
+ } else {
268
+ return this.setupUnixElevatedContext();
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Setup Windows elevated context
274
+ */
275
+ async setupWindowsElevatedContext() {
276
+ this.log('info', 'Windows: Preparing for elevated installation...');
277
+
278
+ // On Windows, we'll handle elevation per-installation
279
+ // since we can't maintain elevated state across commands
280
+ return {
281
+ success: true,
282
+ platform: 'windows',
283
+ note: 'Will elevate per installation as needed'
284
+ };
285
+ }
286
+
287
+ /**
288
+ * Setup Unix elevated context
289
+ */
290
+ async setupUnixElevatedContext() {
291
+ this.log('info', 'Unix: Checking privilege escalation methods...');
292
+
293
+ // List of privilege escalation tools to check (in order of preference)
294
+ const privilegeEscalationTools = [
295
+ { name: 'sudo', testCmd: 'sudo', testArgs: ['-n', 'true'] },
296
+ { name: 'doas', testCmd: 'doas', testArgs: ['-n', 'true'] },
297
+ { name: 'run0', testCmd: 'run0', testArgs: ['-n', 'true'] },
298
+ { name: 'pkexec', testCmd: 'pkexec', testArgs: ['--help'] },
299
+ ];
300
+
301
+ let availableTool = null;
302
+ let requiresPassword = false;
303
+
304
+ for (const tool of privilegeEscalationTools) {
305
+ try {
306
+ const result = spawnSync(tool.testCmd, tool.testArgs, {
307
+ stdio: 'pipe',
308
+ timeout: 5000
309
+ });
310
+
311
+ // Tool exists
312
+ if (result.status === 0 || (result.status !== null && result.error?.code !== 'ENOENT')) {
313
+ // Check if it's passwordless
314
+ if (result.status === 0) {
315
+ availableTool = tool.name;
316
+ requiresPassword = false;
317
+ this.log('success', `Found ${tool.name} (passwordless)`);
318
+ break;
319
+ } else if (result.error?.code !== 'ENOENT') {
320
+ // Tool exists but requires password
321
+ availableTool = tool.name;
322
+ requiresPassword = true;
323
+ this.log('info', `Found ${tool.name} (requires password)`);
324
+ break;
325
+ }
326
+ }
327
+ } catch (error) {
328
+ // Tool doesn't exist, continue to next
329
+ continue;
330
+ }
331
+ }
332
+
333
+ if (availableTool) {
334
+ return {
335
+ success: true,
336
+ platform: 'unix',
337
+ privilegeTool: availableTool,
338
+ requiresPassword: requiresPassword,
339
+ note: requiresPassword ? 'Will prompt for password per installation' : 'Passwordless access available'
340
+ };
341
+ } else {
342
+ // No privilege escalation tool found
343
+ this.log('warn', 'No privilege escalation tool found (sudo, doas, run0, pkexec)');
344
+ this.log('info', 'Will attempt user-space installation without privileges');
345
+ return {
346
+ success: true,
347
+ platform: 'unix',
348
+ privilegeTool: null,
349
+ requiresPassword: false,
350
+ userSpaceOnly: true,
351
+ note: 'Installation will be performed in user directory without privileges'
352
+ };
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Install a tool using the pre-configured permission mode
358
+ */
359
+ async installTool(toolName, toolInfo, retryCount = 0) {
360
+ // Check if install command exists
361
+ if (!toolInfo.install) {
362
+ this.log('warn', `Tool ${toolName} has no install command, skipping...`);
363
+ return false;
364
+ }
365
+
366
+ // Ensure permissions are configured first
367
+ if (!this.permissionConfigured) {
368
+ await this.setupPermissions();
369
+ }
370
+
371
+ // Check if we're in a container environment and force user-space mode if needed
372
+ if (this.permissionMode !== 'user-space') {
373
+ const inContainer = await this.checkContainerEnvironment();
374
+ if (inContainer) {
375
+ this.log('info', 'Detected container environment, switching to user-space installation');
376
+ this.permissionMode = 'user-space';
377
+ }
378
+ }
379
+
380
+ this.log('info', `Installing ${toolInfo.name} (${toolName})...`);
381
+
382
+ try {
383
+ const installResult = await this.executeInstallation(toolInfo);
384
+
385
+ if (installResult.success) {
386
+ this.log('success', `Successfully installed ${toolInfo.name}`);
387
+ this.results.installations[toolName] = {
388
+ success: true,
389
+ tool: toolInfo.name,
390
+ command: toolInfo.install,
391
+ duration: Date.now() - (this.results.installations[toolName]?.startTime || Date.now()),
392
+ permissionMode: this.permissionMode,
393
+ permissionHandled: this.permissionMode === 'elevated'
394
+ };
395
+ return true;
396
+ } else {
397
+ throw new Error(installResult.error);
398
+ }
399
+
400
+ } catch (error) {
401
+ this.log('error', `Failed to install ${toolInfo.name}: ${error.message}`);
402
+
403
+ this.results.installations[toolName] = {
404
+ success: false,
405
+ tool: toolInfo.name,
406
+ command: toolInfo.install,
407
+ error: error.message,
408
+ retryCount: retryCount,
409
+ permissionMode: this.permissionMode
410
+ };
411
+
412
+ this.results.failedInstallations.push(toolName);
413
+
414
+ // Retry logic for non-permission errors
415
+ if (this.options.autoRetry && retryCount < this.options.maxRetries) {
416
+ this.log('warn', `Retrying installation of ${toolInfo.name} (${retryCount + 1}/${this.options.maxRetries})...`);
417
+ await new Promise(resolve => setTimeout(resolve, 2000));
418
+ return await this.installTool(toolName, toolInfo, retryCount + 1);
419
+ }
420
+
421
+ return false;
422
+ }
423
+ }
424
+
425
+ /**
426
+ * Execute installation based on pre-configured permission mode
427
+ */
428
+ async executeInstallation(toolInfo) {
429
+ switch (this.permissionMode) {
430
+ case 'standard':
431
+ return await this.executeStandardInstallation(toolInfo);
432
+ case 'elevated':
433
+ return await this.executeElevatedInstallation(toolInfo);
434
+ case 'user-space':
435
+ return await this.executeUserSpaceInstallation(toolInfo);
436
+ case 'failed':
437
+ return await this.executeFallbackInstallation(toolInfo);
438
+ default:
439
+ // Try standard first, then escalate if needed
440
+ const standardResult = await this.executeStandardInstallation(toolInfo);
441
+ if (standardResult.success) {
442
+ return standardResult;
443
+ }
444
+
445
+ if (this.isPermissionError(standardResult.error)) {
446
+ this.log('warn', `Permission error, escalating to elevated installation...`);
447
+ this.permissionMode = 'elevated';
448
+ return await this.executeElevatedInstallation(toolInfo);
449
+ }
450
+
451
+ return standardResult;
452
+ }
453
+ }
454
+
455
+ /**
456
+ * Execute standard installation without permission elevation
457
+ */
458
+ async executeStandardInstallation(toolInfo) {
459
+ try {
460
+ // Check if install command exists
461
+ if (!toolInfo.install) {
462
+ return {
463
+ success: false,
464
+ error: `No install command specified for ${toolInfo.name || 'unknown tool'}`
465
+ };
466
+ }
467
+
468
+ // Check if tool requires bun runtime
469
+ if (toolInfo.requiresBun) {
470
+ const bunAvailable = await this.checkBunAvailable();
471
+ if (!bunAvailable) {
472
+ this.log('warn', `${toolInfo.name} requires bun, but bun is not installed`);
473
+ this.log('info', `Installing bun first...`);
474
+
475
+ const bunResult = await this.executeInstallationCommand('npm install bun -g');
476
+
477
+ if (!bunResult.success) {
478
+ return {
479
+ success: false,
480
+ error: `Failed to install bun (required for ${toolInfo.name}): ${bunResult.error}`
481
+ };
482
+ }
483
+
484
+ this.log('success', 'Bun installed successfully');
485
+ }
486
+ }
487
+
488
+ // Check if install command contains multiple steps (&&)
489
+ if (toolInfo.install.includes('&&')) {
490
+ return await this.executeMultiStepInstallation(toolInfo);
491
+ }
492
+
493
+ const [command, ...args] = toolInfo.install.split(' ');
494
+
495
+ this.log('debug', `Executing: ${toolInfo.install}`);
496
+
497
+ const result = spawnSync(command, args, {
498
+ stdio: this.options.verbose ? 'inherit' : 'pipe',
499
+ shell: true,
500
+ encoding: 'utf8',
501
+ timeout: this.options.timeout,
502
+ env: {
503
+ ...process.env,
504
+ npm_config_prefix: process.env.npm_config_prefix,
505
+ npm_config_global: 'true',
506
+ npm_config_update: 'false',
507
+ npm_config_progress: 'false'
508
+ }
509
+ });
510
+
511
+ if (result.status === 0) {
512
+ return { success: true, error: null };
513
+ } else {
514
+ const errorMessage = result.stderr || result.stdout || `Exit code ${result.status}`;
515
+
516
+ // Check if this is a permission error and switch to user-space if needed
517
+ if (this.isPermissionError(errorMessage)) {
518
+ this.log('warn', `Standard installation failed due to permission error, switching to user-space installation...`);
519
+ this.permissionMode = 'user-space';
520
+ return await this.executeUserSpaceInstallation(toolInfo);
521
+ }
522
+
523
+ return { success: false, error: errorMessage };
524
+ }
525
+
526
+ } catch (error) {
527
+ // Check if this is a permission error and switch to user-space if needed
528
+ if (this.isPermissionError(error.message)) {
529
+ this.log('warn', `Standard installation failed due to permission error, switching to user-space installation...`);
530
+ this.permissionMode = 'user-space';
531
+ return await this.executeUserSpaceInstallation(toolInfo);
532
+ }
533
+
534
+ return { success: false, error: error.message };
535
+ }
536
+ }
537
+
538
+ /**
539
+ * Execute installation with permission elevation
540
+ */
541
+ async executeElevatedInstallation(toolInfo) {
542
+ const platform = process.platform;
543
+
544
+ if (platform === 'win32') {
545
+ return await this.executeWindowsElevatedInstallation(toolInfo);
546
+ } else {
547
+ return await this.executeUnixElevatedInstallation(toolInfo);
548
+ }
549
+ }
550
+
551
+ /**
552
+ * Execute Windows elevated installation
553
+ */
554
+ async executeWindowsElevatedInstallation(toolInfo) {
555
+ const command = toolInfo.install;
556
+
557
+ try {
558
+ this.log('info', `Creating Windows elevated installation for: ${toolInfo.name}`);
559
+
560
+ const scriptPath = path.join(os.tmpdir(), `stigmergy-install-${Date.now()}.ps1`);
561
+ const scriptContent = `
562
+ Write-Host "以管理员权限安装: ${toolInfo.name}" -ForegroundColor Yellow
563
+ try {
564
+ ${command}
565
+ if ($LASTEXITCODE -eq 0) {
566
+ Write-Host "安装成功: ${toolInfo.name}" -ForegroundColor Green
567
+ exit 0
568
+ } else {
569
+ Write-Host "安装失败: ${toolInfo.name}" -ForegroundColor Red
570
+ exit $LASTEXITCODE
571
+ }
572
+ } catch {
573
+ Write-Host "安装异常: ${toolInfo.name}" -ForegroundColor Red
574
+ Write-Host $\_.Exception.Message -ForegroundColor Red
575
+ exit 1
576
+ }
577
+ `;
578
+
579
+ require('fs').writeFileSync(scriptPath, scriptContent, 'utf8');
580
+
581
+ const result = spawnSync('powershell', [
582
+ '-Command', `Start-Process PowerShell -Verb RunAs -ArgumentList "-File '${scriptPath}'" -Wait`
583
+ ], {
584
+ stdio: 'pipe',
585
+ timeout: this.options.timeout * 2,
586
+ encoding: 'utf8'
587
+ });
588
+
589
+ try {
590
+ require('fs').unlinkSync(scriptPath);
591
+ } catch (cleanupError) {
592
+ this.log('warn', `Could not clean up temp script: ${cleanupError.message}`);
593
+ }
594
+
595
+ if (result.status === 0) {
596
+ return {
597
+ success: true,
598
+ error: null
599
+ };
600
+ } else {
601
+ // If elevated installation failed, try user-space installation
602
+ this.log('warn', `Elevated installation failed, trying user-space installation...`);
603
+ this.permissionMode = 'user-space';
604
+ return await this.executeUserSpaceInstallation(toolInfo);
605
+ }
606
+ } catch (error) {
607
+ // If elevated installation failed, try user-space installation
608
+ this.log('warn', `Elevated installation failed (${error.message}), trying user-space installation...`);
609
+ this.permissionMode = 'user-space';
610
+ return await this.executeUserSpaceInstallation(toolInfo);
611
+ }
612
+ }
613
+
614
+ /**
615
+ * Execute Unix elevated installation
616
+ */
617
+ async executeUnixElevatedInstallation(toolInfo) {
618
+ // Use the detected privilege escalation tool
619
+ const privilegeSetup = await this.setupUnixElevatedContext();
620
+
621
+ if (!privilegeSetup.success) {
622
+ this.log('warn', 'No privilege escalation tool available, using user-space installation...');
623
+ return await this.executeUserSpaceInstallation(toolInfo);
624
+ }
625
+
626
+ // If no privilege escalation tool is available, use user-space installation
627
+ if (privilegeSetup.userSpaceOnly) {
628
+ return await this.executeUserSpaceInstallation(toolInfo);
629
+ }
630
+
631
+ // Use the detected privilege escalation tool
632
+ const privilegeTool = privilegeSetup.privilegeTool || 'sudo';
633
+ const command = `${privilegeTool} ${toolInfo.install}`;
634
+
635
+ try {
636
+ this.log('info', `Using ${privilegeTool} for elevated installation of: ${toolInfo.name}`);
637
+
638
+ const result = spawnSync('bash', ['-c', command], {
639
+ stdio: this.options.verbose ? 'inherit' : 'pipe',
640
+ timeout: this.options.timeout * 2,
641
+ encoding: 'utf8'
642
+ });
643
+
644
+ if (result.status === 0) {
645
+ return { success: true, error: null };
646
+ } else {
647
+ const errorMessage = result.stderr || result.stdout || `Exit code ${result.status}`;
648
+ // If privilege escalation failed, try user-space installation as fallback
649
+ this.log('warn', `Privilege escalation failed, trying user-space installation...`);
650
+ return await this.executeUserSpaceInstallation(toolInfo);
651
+ }
652
+ } catch (error) {
653
+ this.log('warn', `Privilege escalation error: ${error.message}, trying user-space installation...`);
654
+ return await this.executeUserSpaceInstallation(toolInfo);
655
+ }
656
+ }
657
+
658
+ /**
659
+ * Execute user-space installation (no privileges required)
660
+ */
661
+ async executeUserSpaceInstallation(toolInfo) {
662
+ try {
663
+ // Install to user directory using --prefix flag
664
+ const os = require('os');
665
+ const path = require('path');
666
+
667
+ // Get user's npm global directory
668
+ let userNpmDir = process.env.NPM_CONFIG_PREFIX ||
669
+ path.join(os.homedir(), '.npm-global');
670
+
671
+ // Ensure directory exists
672
+ const fs = require('fs');
673
+ if (!fs.existsSync(userNpmDir)) {
674
+ fs.mkdirSync(userNpmDir, { recursive: true, mode: 0o755 });
675
+ }
676
+
677
+ // Extract package name from install command
678
+ // Format: "npm install -g @package/tool" or "npm install -g tool"
679
+ const installMatch = toolInfo.install.match(/npm\s+(?:install|upgrade)\s+(?:-g\s+)?(.+)/);
680
+ if (!installMatch) {
681
+ throw new Error('Cannot parse install command');
682
+ }
683
+
684
+ const packageName = installMatch[1].trim();
685
+
686
+ // Create user-space install command
687
+ const userCommand = `npm install -g --prefix "${userNpmDir}" ${packageName}`;
688
+
689
+ this.log('info', `Installing ${toolInfo.name} to user directory: ${userNpmDir}`);
690
+ this.log('info', `Command: ${userCommand}`);
691
+
692
+ // Set PATH to include user npm directory
693
+ const env = {
694
+ ...process.env,
695
+ PATH: `${path.join(userNpmDir, 'bin')}:${process.env.PATH}`
696
+ };
697
+
698
+ const result = spawnSync('bash', ['-c', userCommand], {
699
+ stdio: this.options.verbose ? 'inherit' : 'pipe',
700
+ timeout: this.options.timeout * 3, // Longer timeout for user-space install
701
+ encoding: 'utf8',
702
+ env: env
703
+ });
704
+
705
+ if (result.status === 0) {
706
+ this.log('success', `Successfully installed ${toolInfo.name} to user directory`);
707
+
708
+ // Provide PATH setup instructions
709
+ const binDir = path.join(userNpmDir, 'bin');
710
+ this.log('info', '⚠️ Make sure to add the bin directory to your PATH:');
711
+ this.log('info', ` export PATH="${binDir}:$PATH"`);
712
+
713
+ // Add to shell config files automatically
714
+ await this.addPathToShellConfig(binDir);
715
+
716
+ return { success: true, error: null, userSpace: true, binDir };
717
+ } else {
718
+ const errorMessage = result.stderr || result.stdout || `Exit code ${result.status}`;
719
+ return { success: false, error: `User-space installation failed: ${errorMessage}` };
720
+ }
721
+ } catch (error) {
722
+ return { success: false, error: error.message };
723
+ }
724
+ }
725
+
726
+ /**
727
+ * Add PATH to shell configuration files
728
+ */
729
+ async addPathToShellConfig(binDir) {
730
+ const os = require('os');
731
+ const path = require('path');
732
+ const fs = require('fs/promises');
733
+
734
+ const shellConfigs = [
735
+ { file: path.join(os.homedir(), '.bashrc'), marker: '# Stigmergy CLI PATH' },
736
+ { file: path.join(os.homedir(), '.zshrc'), marker: '# Stigmergy CLI PATH' },
737
+ { file: path.join(os.homedir(), '.profile'), marker: '# Stigmergy CLI PATH' },
738
+ ];
739
+
740
+ const pathLine = `export PATH="${binDir}:$PATH"\n`;
741
+
742
+ for (const config of shellConfigs) {
743
+ try {
744
+ let content = '';
745
+ try {
746
+ content = await fs.readFile(config.file, 'utf8');
747
+ } catch (err) {
748
+ // File doesn't exist, will create it
749
+ }
750
+
751
+ // Check if PATH is already configured
752
+ if (content.includes(config.marker)) {
753
+ continue; // Already configured
754
+ }
755
+
756
+ // Append PATH configuration
757
+ await fs.appendFile(config.file, `\n${config.marker}\n${pathLine}`);
758
+ this.log('info', `Added PATH to ${path.basename(config.file)}`);
759
+ } catch (error) {
760
+ // Ignore errors for config files
761
+ }
762
+ }
763
+ }
764
+
765
+ /**
766
+ * Fallback installation method
767
+ */
768
+ async executeFallbackInstallation(toolInfo) {
769
+ this.log('warn', 'Attempting fallback installation method...');
770
+
771
+ // Check if install command exists
772
+ if (!toolInfo.install) {
773
+ return {
774
+ success: false,
775
+ error: `No install command specified for ${toolInfo.name || 'unknown tool'}`
776
+ };
777
+ }
778
+
779
+ // Try without some npm flags that might cause permission issues
780
+ const [command, ...args] = toolInfo.install.split(' ');
781
+ const fallbackArgs = args.filter(arg => !arg.startsWith('--'));
782
+
783
+ try {
784
+ const result = spawnSync(command, fallbackArgs, {
785
+ stdio: 'inherit',
786
+ shell: true,
787
+ encoding: 'utf8',
788
+ timeout: this.options.timeout
789
+ });
790
+
791
+ if (result.status === 0) {
792
+ return { success: true, error: null };
793
+ } else {
794
+ const errorMessage = result.stderr || `Fallback failed with exit code ${result.status}`;
795
+ // If fallback failed due to permissions, try user-space installation
796
+ if (this.isPermissionError(errorMessage)) {
797
+ this.log('warn', `Fallback installation failed due to permission error, switching to user-space installation...`);
798
+ this.permissionMode = 'user-space';
799
+ return await this.executeUserSpaceInstallation(toolInfo);
800
+ }
801
+ return { success: false, error: errorMessage };
802
+ }
803
+ } catch (error) {
804
+ // If fallback failed due to permissions, try user-space installation
805
+ if (this.isPermissionError(error.message)) {
806
+ this.log('warn', `Fallback installation failed due to permission error, switching to user-space installation...`);
807
+ this.permissionMode = 'user-space';
808
+ return await this.executeUserSpaceInstallation(toolInfo);
809
+ }
810
+ return { success: false, error: error.message };
811
+ }
812
+ }
813
+
814
+ /**
815
+ * Check if an error is related to permissions
816
+ */
817
+ isPermissionError(errorMessage) {
818
+ if (!errorMessage || typeof errorMessage !== 'string') {
819
+ return false;
820
+ }
821
+
822
+ const permissionIndicators = [
823
+ 'EACCES', 'EPERM', 'permission denied',
824
+ 'access denied', 'unauthorized', 'EISDIR',
825
+ 'operation not permitted', 'code EACCES',
826
+ 'code EPERM', 'permission error', 'cannot create directory',
827
+ 'write EACCES', 'mkdir', 'denied'
828
+ ];
829
+
830
+ const lowerError = errorMessage.toLowerCase();
831
+ return permissionIndicators.some(indicator =>
832
+ lowerError.includes(indicator.toLowerCase())
833
+ );
834
+ }
835
+
836
+ /**
837
+ * Install multiple CLI tools
838
+ */
839
+ async installTools(toolNames, toolInfos) {
840
+ this.log('info', 'Starting batch installation of CLI tools...');
841
+
842
+ // One-time permission setup for the entire batch
843
+ const permissionSetup = await this.setupPermissions();
844
+ if (!permissionSetup.success && permissionSetup.mode !== 'elevated') {
845
+ this.log('warn', 'Permission setup failed, but proceeding with individual installations...');
846
+ }
847
+
848
+ let successCount = 0;
849
+ const totalCount = toolNames.length;
850
+
851
+ this.log('info', `Installing ${totalCount} CLI tools in ${this.permissionMode} mode...`);
852
+
853
+ for (const toolName of toolNames) {
854
+ const toolInfo = toolInfos[toolName];
855
+ if (!toolInfo) continue;
856
+
857
+ // Skip tools without install command (internal functions)
858
+ if (!toolInfo.install) {
859
+ this.log('debug', `Tool ${toolName} has no install command, skipping...`);
860
+ continue;
861
+ }
862
+
863
+ this.results.installations[toolName] = {
864
+ startTime: Date.now(),
865
+ ...this.results.installations[toolName]
866
+ };
867
+
868
+ const success = await this.installTool(toolName, toolInfo);
869
+ if (success) {
870
+ successCount++;
871
+ }
872
+ }
873
+
874
+ this.log('info', `Batch installation completed: ${successCount}/${totalCount} successful`);
875
+
876
+ return {
877
+ success: successCount === totalCount,
878
+ total: totalCount,
879
+ successful: successCount,
880
+ failed: totalCount - successCount,
881
+ permissionMode: this.permissionMode,
882
+ results: this.results
883
+ };
884
+ }
885
+
886
+ /**
887
+ * Upgrade CLI tools
888
+ */
889
+ async upgradeTools(toolNames, toolInfos) {
890
+ this.log('info', 'Starting batch upgrade of CLI tools...');
891
+
892
+ // One-time permission setup for the entire batch
893
+ const permissionSetup = await this.setupPermissions();
894
+ if (!permissionSetup.success && permissionSetup.mode !== 'elevated') {
895
+ this.log('warn', 'Permission setup failed, but proceeding with individual upgrades...');
896
+ }
897
+
898
+ let successCount = 0;
899
+ const totalCount = toolNames.length;
900
+
901
+ this.log('info', `Upgrading ${totalCount} CLI tools in ${this.permissionMode} mode...`);
902
+
903
+ for (const toolName of toolNames) {
904
+ const originalInfo = toolInfos[toolName];
905
+ if (!originalInfo) {
906
+ this.log('warn', `Tool ${toolName} not found in toolInfos, skipping...`);
907
+ continue;
908
+ }
909
+
910
+ // Skip tools without install command (internal functions)
911
+ if (!originalInfo.install) {
912
+ this.log('debug', `Tool ${toolName} has no install command, skipping upgrade...`);
913
+ continue;
914
+ }
915
+
916
+ // Determine the appropriate upgrade command based on permission mode
917
+ let upgradeCommand;
918
+ if (this.permissionMode === 'user-space') {
919
+ // For user-space installations, upgrade to user directory
920
+ const os = require('os');
921
+ const path = require('path');
922
+ let userNpmDir = process.env.NPM_CONFIG_PREFIX || path.join(os.homedir(), '.npm-global');
923
+ upgradeCommand = `npm install -g --prefix "${userNpmDir}" ${toolName}`;
924
+ } else {
925
+ upgradeCommand = `npm upgrade -g ${toolName}`;
926
+ }
927
+
928
+ const toolInfo = {
929
+ ...originalInfo,
930
+ install: upgradeCommand,
931
+ name: `${originalInfo.name} (Upgrade)`
932
+ };
933
+
934
+ const success = await this.installTool(toolName, toolInfo);
935
+ if (success) {
936
+ successCount++;
937
+ }
938
+ }
939
+
940
+ this.log('info', `Batch upgrade completed: ${successCount}/${totalCount} successful`);
941
+
942
+ return {
943
+ success: successCount === totalCount,
944
+ total: totalCount,
945
+ successful: successCount,
946
+ failed: totalCount - successCount,
947
+ permissionMode: this.permissionMode,
948
+ results: this.results
949
+ };
950
+ }
951
+
952
+ /**
953
+ * Get installation results
954
+ */
955
+ getResults() {
956
+ return this.results;
957
+ }
958
+
959
+ /**
960
+ * Detect permission availability for the current platform
961
+ */
962
+ detectPermissionAvailability() {
963
+ const platform = process.platform;
964
+
965
+ if (platform === 'win32') {
966
+ // Windows: Check if running as administrator
967
+ try {
968
+ const { execSync } = require('child_process');
969
+ const result = execSync('net session', { encoding: 'utf8' });
970
+ return result.includes('Administrator');
971
+ } catch {
972
+ return false;
973
+ }
974
+ } else {
975
+ // Unix-like systems: Check available privilege escalation tools
976
+ const privilegeEscalationTools = ['sudo', 'doas', 'run0', 'pkexec'];
977
+ for (const tool of privilegeEscalationTools) {
978
+ try {
979
+ const result = spawnSync(tool, ['--version'], {
980
+ stdio: 'pipe',
981
+ timeout: 5000
982
+ });
983
+ if (result.status !== null || result.error?.code !== 'ENOENT') {
984
+ return true;
985
+ }
986
+ } catch {
987
+ continue;
988
+ }
989
+ }
990
+ return false;
991
+ }
992
+ }
993
+
994
+ /**
995
+ * Detect available privilege escalation tools
996
+ */
997
+ detectPrivilegeTools() {
998
+ const platform = process.platform;
999
+ const availableTools = [];
1000
+
1001
+ if (platform === 'win32') {
1002
+ // Windows: Check for admin privileges
1003
+ try {
1004
+ const { execSync } = require('child_process');
1005
+ const result = execSync('net session', { encoding: 'utf8' });
1006
+ if (result.includes('Administrator')) {
1007
+ availableTools.push('admin');
1008
+ }
1009
+ } catch {
1010
+ // Not running as admin
1011
+ }
1012
+ } else {
1013
+ // Unix-like systems: Check privilege escalation tools
1014
+ const tools = [
1015
+ { name: 'sudo', checkCmd: ['-n', 'true'] },
1016
+ { name: 'doas', checkCmd: ['-n', 'true'] },
1017
+ { name: 'run0', checkCmd: ['-n', 'true'] },
1018
+ { name: 'pkexec', checkCmd: ['--help'] }
1019
+ ];
1020
+
1021
+ for (const tool of tools) {
1022
+ try {
1023
+ const result = spawnSync(tool.name, tool.checkCmd, {
1024
+ stdio: 'pipe',
1025
+ timeout: 5000
1026
+ });
1027
+ if (result.status !== null || result.error?.code !== 'ENOENT') {
1028
+ availableTools.push(tool.name);
1029
+ }
1030
+ } catch {
1031
+ continue;
1032
+ }
1033
+ }
1034
+ }
1035
+
1036
+ return availableTools;
1037
+ }
1038
+
1039
+ /**
1040
+ * Upgrade a single tool
1041
+ */
1042
+ async upgradeTool(toolName, toolInfo) {
1043
+ this.log('info', `Upgrading ${toolName}...`);
1044
+
1045
+ if (!toolInfo || !toolInfo.install) {
1046
+ this.log('warn', `Tool ${toolName} has no install command, skipping upgrade...`);
1047
+ this.results.errors.push({
1048
+ tool: toolName,
1049
+ error: 'No install command specified',
1050
+ timestamp: new Date().toISOString()
1051
+ });
1052
+ return false;
1053
+ }
1054
+
1055
+ // Determine the appropriate upgrade command based on permission mode
1056
+ let upgradeCommand;
1057
+ if (this.permissionMode === 'user-space') {
1058
+ // For user-space installations, upgrade to user directory
1059
+ const os = require('os');
1060
+ const path = require('path');
1061
+ let userNpmDir = process.env.NPM_CONFIG_PREFIX || path.join(os.homedir(), '.npm-global');
1062
+ upgradeCommand = `npm install -g --prefix "${userNpmDir}" ${toolName}`;
1063
+ } else {
1064
+ upgradeCommand = `npm upgrade -g ${toolName}`;
1065
+ }
1066
+
1067
+ this.results.installations[toolName] = {
1068
+ startTime: Date.now(),
1069
+ ...this.results.installations[toolName]
1070
+ };
1071
+
1072
+ try {
1073
+ const success = await this.installTool(toolName, {
1074
+ ...toolInfo,
1075
+ install: upgradeCommand,
1076
+ name: `${toolInfo.name} (Upgrade)`
1077
+ });
1078
+
1079
+ return success;
1080
+ } catch (error) {
1081
+ this.log('error', `Failed to upgrade ${toolName}: ${error.message}`);
1082
+ this.results.errors.push({
1083
+ tool: toolName,
1084
+ error: error.message,
1085
+ timestamp: new Date().toISOString()
1086
+ });
1087
+ return false;
1088
+ }
1089
+ }
1090
+
1091
+ /**
1092
+ * Get installation summary
1093
+ */
1094
+ getInstallationSummary() {
1095
+ const installations = this.results.installations || {};
1096
+ const errors = this.results.errors || [];
1097
+
1098
+ const total = Object.keys(installations).length;
1099
+ const successful = Object.values(installations).filter(i => i.success !== false).length;
1100
+ const failed = total - successful;
1101
+
1102
+ return {
1103
+ total,
1104
+ successful,
1105
+ failed,
1106
+ permissionMode: this.permissionMode,
1107
+ npmConfigured: this.results.npmConfigured,
1108
+ workingDirectory: this.results.workingDirectory,
1109
+ errors: errors.map(e => ({
1110
+ tool: e.tool,
1111
+ error: e.error,
1112
+ timestamp: e.timestamp
1113
+ })),
1114
+ details: installations
1115
+ };
1116
+ }
1117
+
1118
+ /**
1119
+ * Check if bun runtime is available
1120
+ * @returns {Promise<boolean>} True if bun is available
1121
+ */
1122
+ async checkBunAvailable() {
1123
+ const { spawnSync } = require('child_process');
1124
+
1125
+ try {
1126
+ const result = spawnSync('bun', ['--version'], {
1127
+ stdio: 'pipe',
1128
+ shell: true
1129
+ });
1130
+
1131
+ return result.status === 0 || result.error === undefined;
1132
+ } catch (error) {
1133
+ return false;
1134
+ }
1135
+ }
1136
+
1137
+ /**
1138
+ * Execute multi-step installation (commands with &&)
1139
+ * @param {Object} toolInfo - Tool information
1140
+ * @returns {Promise<Object>} Installation result
1141
+ */
1142
+ async executeMultiStepInstallation(toolInfo) {
1143
+ const steps = toolInfo.install.split('&&').map(s => s.trim());
1144
+
1145
+ this.log('info', `Executing multi-step installation (${steps.length} steps) for ${toolInfo.name}...`);
1146
+
1147
+ for (let i = 0; i < steps.length; i++) {
1148
+ const step = steps[i];
1149
+ const stepNumber = i + 1;
1150
+
1151
+ this.log('info', `Step ${stepNumber}/${steps.length}: ${step}`);
1152
+
1153
+ const result = await this.executeInstallationCommand(step);
1154
+
1155
+ if (!result.success) {
1156
+ this.log('error', `Step ${stepNumber} failed: ${result.error}`);
1157
+ return {
1158
+ success: false,
1159
+ error: `Installation failed at step ${stepNumber}: ${result.error}`,
1160
+ failedAtStep: stepNumber,
1161
+ failedCommand: step
1162
+ };
1163
+ }
1164
+
1165
+ this.log('success', `Step ${stepNumber}/${steps.length} completed`);
1166
+ }
1167
+
1168
+ this.log('success', `All ${steps.length} steps completed successfully for ${toolInfo.name}`);
1169
+
1170
+ return {
1171
+ success: true,
1172
+ stepsCompleted: steps.length
1173
+ };
1174
+ }
1175
+
1176
+ /**
1177
+ * Execute a single installation command
1178
+ * @param {string} command - Installation command
1179
+ * @returns {Promise<Object>} Execution result
1180
+ */
1181
+ async executeInstallationCommand(command) {
1182
+ const { executeCommand } = require('../utils');
1183
+
1184
+ try {
1185
+ const result = await executeCommand(command, [], {
1186
+ stdio: this.verbose ? 'inherit' : 'pipe',
1187
+ shell: true,
1188
+ timeout: 300000 // 5 minutes
1189
+ });
1190
+
1191
+ if (result.success) {
1192
+ return { success: true };
1193
+ } else {
1194
+ return {
1195
+ success: false,
1196
+ error: result.error || `Command exited with code ${result.code}`,
1197
+ code: result.code
1198
+ };
1199
+ }
1200
+ } catch (error) {
1201
+ return {
1202
+ success: false,
1203
+ error: error.message
1204
+ };
1205
+ }
1206
+ }
1207
+ }
1208
+
609
1209
  module.exports = EnhancedCLIInstaller;