stigmergy 1.2.6 → 1.2.8

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 (59) hide show
  1. package/README.md +32 -17
  2. package/STIGMERGY.md +16 -7
  3. package/docs/MULTI_USER_WIKI_COLLABORATION_SYSTEM.md +523 -0
  4. package/docs/PROMPT_BASED_SKILLS_SYSTEM_DESIGN.md +458 -0
  5. package/docs/SKILL_IMPLEMENTATION_CONSTRAINTS_AND_ALIGNMENT.md +423 -0
  6. package/docs/TECHNICAL_FEASIBILITY_ANALYSIS.md +308 -0
  7. package/examples/multilingual-hook-demo.js +125 -0
  8. package/package.json +14 -17
  9. package/scripts/dependency-analyzer.js +101 -0
  10. package/scripts/generate-cli-docs.js +64 -0
  11. package/scripts/postuninstall.js +46 -0
  12. package/scripts/preuninstall.js +75 -0
  13. package/scripts/run-layered-tests.js +3 -3
  14. package/src/adapters/claude/install_claude_integration.js +17 -17
  15. package/src/adapters/codebuddy/install_codebuddy_integration.js +13 -13
  16. package/src/adapters/codex/install_codex_integration.js +27 -27
  17. package/src/adapters/copilot/install_copilot_integration.js +46 -46
  18. package/src/adapters/gemini/install_gemini_integration.js +10 -10
  19. package/src/adapters/iflow/install_iflow_integration.js +7 -7
  20. package/src/adapters/qoder/install_qoder_integration.js +12 -12
  21. package/src/adapters/qwen/install_qwen_integration.js +17 -17
  22. package/src/auth.js +173 -173
  23. package/src/auth_command.js +208 -208
  24. package/src/calculator.js +313 -313
  25. package/src/cli/router.js +151 -7
  26. package/src/core/cache_cleaner.js +767 -767
  27. package/src/core/cli_help_analyzer.js +680 -680
  28. package/src/core/cli_parameter_handler.js +132 -132
  29. package/src/core/cli_tools.js +89 -89
  30. package/src/core/coordination/index.js +16 -16
  31. package/src/core/coordination/nodejs/AdapterManager.js +102 -102
  32. package/src/core/coordination/nodejs/CLCommunication.js +132 -132
  33. package/src/core/coordination/nodejs/CLIIntegrationManager.js +272 -272
  34. package/src/core/coordination/nodejs/HealthChecker.js +76 -76
  35. package/src/core/coordination/nodejs/HookDeploymentManager.js +463 -274
  36. package/src/core/coordination/nodejs/StatisticsCollector.js +71 -71
  37. package/src/core/coordination/nodejs/index.js +90 -90
  38. package/src/core/coordination/nodejs/utils/Logger.js +29 -29
  39. package/src/core/enhanced_installer.js +479 -479
  40. package/src/core/enhanced_uninstaller.js +638 -638
  41. package/src/core/error_handler.js +406 -406
  42. package/src/core/installer.js +32 -32
  43. package/src/core/memory_manager.js +83 -83
  44. package/src/core/multilingual/language-pattern-manager.js +172 -0
  45. package/src/core/rest_client.js +160 -160
  46. package/src/core/smart_router.js +261 -249
  47. package/src/core/upgrade_manager.js +48 -20
  48. package/src/data_encryption.js +143 -143
  49. package/src/data_structures.js +440 -440
  50. package/src/deploy.js +55 -55
  51. package/src/index.js +30 -30
  52. package/src/test/cli-availability-checker.js +194 -194
  53. package/src/test/test-environment.js +289 -289
  54. package/src/utils/helpers.js +35 -35
  55. package/src/utils.js +921 -921
  56. package/src/weatherProcessor.js +228 -228
  57. package/test/multilingual/hook-deployment.test.js +91 -0
  58. package/test/multilingual/language-pattern-manager.test.js +140 -0
  59. package/test/multilingual/system-test.js +85 -0
package/src/utils.js CHANGED
@@ -1,921 +1,921 @@
1
- /**
2
- * Utility functions for the Stigmergy CLI
3
- */
4
-
5
- const { processWeatherData } = require('./weatherProcessor');
6
-
7
- /**
8
- * Simple REST API client
9
- */
10
- class RESTClient {
11
- /**
12
- * Create a new REST client
13
- * @param {string} baseURL - The base URL for the API
14
- * @param {Object} defaultHeaders - Default headers to include in all requests
15
- */
16
- constructor(baseURL = '', defaultHeaders = {}) {
17
- this.baseURL = baseURL;
18
- this.defaultHeaders = {
19
- 'Content-Type': 'application/json',
20
- ...defaultHeaders,
21
- };
22
- }
23
-
24
- /**
25
- * Make an HTTP request
26
- * @param {string} method - HTTP method (GET, POST, PUT, DELETE, etc.)
27
- * @param {string} url - Request URL
28
- * @param {Object} options - Request options
29
- * @returns {Promise} Response promise
30
- */
31
- async request(method, url, options = {}) {
32
- const fullURL = this.baseURL + url;
33
-
34
- const config = {
35
- method,
36
- headers: {
37
- ...this.defaultHeaders,
38
- ...options.headers,
39
- },
40
- ...options,
41
- };
42
-
43
- // Handle JSON body
44
- if (
45
- options.body &&
46
- typeof options.body === 'object' &&
47
- !(options.body instanceof String)
48
- ) {
49
- config.body = JSON.stringify(options.body);
50
- }
51
-
52
- try {
53
- const response = await fetch(fullURL, config);
54
-
55
- // Try to parse JSON response
56
- let data;
57
- const contentType = response.headers.get('content-type');
58
- if (contentType && contentType.includes('application/json')) {
59
- data = await response.json();
60
- } else {
61
- data = await response.text();
62
- }
63
-
64
- // Throw error for non-success status codes
65
- if (!response.ok) {
66
- throw new Error(
67
- `HTTP ${response.status}: ${response.statusText} - ${JSON.stringify(data)}`,
68
- );
69
- }
70
-
71
- return {
72
- status: response.status,
73
- statusText: response.statusText,
74
- headers: response.headers,
75
- data,
76
- };
77
- } catch (error) {
78
- throw new Error(`Request failed: ${error.message}`);
79
- }
80
- }
81
-
82
- /**
83
- * Make a GET request
84
- * @param {string} url - Request URL
85
- * @param {Object} options - Request options
86
- * @returns {Promise} Response promise
87
- */
88
- async get(url, options = {}) {
89
- return this.request('GET', url, options);
90
- }
91
-
92
- /**
93
- * Make a POST request
94
- * @param {string} url - Request URL
95
- * @param {Object} data - Request body data
96
- * @param {Object} options - Request options
97
- * @returns {Promise} Response promise
98
- */
99
- async post(url, data, options = {}) {
100
- return this.request('POST', url, { ...options, body: data });
101
- }
102
-
103
- /**
104
- * Make a PUT request
105
- * @param {string} url - Request URL
106
- * @param {Object} data - Request body data
107
- * @param {Object} options - Request options
108
- * @returns {Promise} Response promise
109
- */
110
- async put(url, data, options = {}) {
111
- return this.request('PUT', url, { ...options, body: data });
112
- }
113
-
114
- /**
115
- * Make a DELETE request
116
- * @param {string} url - Request URL
117
- * @param {Object} options - Request options
118
- * @returns {Promise} Response promise
119
- */
120
- async delete(url, options = {}) {
121
- return this.request('DELETE', url, options);
122
- }
123
- }
124
-
125
- /**
126
- * HashTable implementation with collision handling using chaining
127
- */
128
- class HashTable {
129
- /**
130
- * Create a new HashTable
131
- * @param {number} size - Initial size of the hash table
132
- */
133
- constructor(size = 53) {
134
- this.keyMap = new Array(size);
135
- }
136
-
137
- /**
138
- * Hash function to convert a key to an index
139
- * @param {string} key - Key to hash
140
- * @returns {number} Index in the hash table
141
- */
142
- _hash(key) {
143
- let total = 0;
144
- const WEIRD_PRIME = 31;
145
- for (let i = 0; i < Math.min(key.length, 100); i++) {
146
- const char = key[i];
147
- const value = char.charCodeAt(0) - 96;
148
- total = (total * WEIRD_PRIME + value) % this.keyMap.length;
149
- }
150
- return total;
151
- }
152
-
153
- /**
154
- * Set a key-value pair in the hash table
155
- * @param {string} key - Key to store
156
- * @param {*} value - Value to store
157
- * @returns {HashTable} The hash table instance
158
- */
159
- set(key, value) {
160
- const index = this._hash(key);
161
- if (!this.keyMap[index]) {
162
- this.keyMap[index] = [];
163
- }
164
- this.keyMap[index].push([key, value]);
165
- return this;
166
- }
167
-
168
- /**
169
- * Get a value by its key
170
- * @param {string} key - Key to look up
171
- * @returns {*} The value associated with the key, or undefined if not found
172
- */
173
- get(key) {
174
- const index = this._hash(key);
175
- if (this.keyMap[index]) {
176
- for (let i = 0; i < this.keyMap[index].length; i++) {
177
- if (this.keyMap[index][i][0] === key) {
178
- return this.keyMap[index][i][1];
179
- }
180
- }
181
- }
182
- return undefined;
183
- }
184
-
185
- /**
186
- * Get all keys in the hash table
187
- * @returns {Array} Array of all keys
188
- */
189
- keys() {
190
- const keysArr = [];
191
- for (let i = 0; i < this.keyMap.length; i++) {
192
- if (this.keyMap[i]) {
193
- for (let j = 0; j < this.keyMap[i].length; j++) {
194
- if (!keysArr.includes(this.keyMap[i][j][0])) {
195
- keysArr.push(this.keyMap[i][j][0]);
196
- }
197
- }
198
- }
199
- }
200
- return keysArr;
201
- }
202
-
203
- /**
204
- * Get all values in the hash table
205
- * @returns {Array} Array of all values
206
- */
207
- values() {
208
- const valuesArr = [];
209
- for (let i = 0; i < this.keyMap.length; i++) {
210
- if (this.keyMap[i]) {
211
- for (let j = 0; j < this.keyMap[i].length; j++) {
212
- if (!valuesArr.includes(this.keyMap[i][j][1])) {
213
- valuesArr.push(this.keyMap[i][j][1]);
214
- }
215
- }
216
- }
217
- }
218
- return valuesArr;
219
- }
220
- }
221
-
222
- /**
223
- * Calculate the factorial of a number
224
- * @param {number} n - The number to calculate factorial for
225
- * @returns {number} The factorial of n
226
- */
227
- function factorial(n) {
228
- if (n < 0) {
229
- throw new Error('Factorial is not defined for negative numbers');
230
- }
231
-
232
- if (n === 0 || n === 1) {
233
- return 1;
234
- }
235
-
236
- let result = 1;
237
- for (let i = 2; i <= n; i++) {
238
- result *= i;
239
- }
240
-
241
- return result;
242
- }
243
-
244
- /**
245
- * Calculate the nth Fibonacci number using iteration (efficient)
246
- * @param {number} n - The position in the Fibonacci sequence
247
- * @returns {number} The nth Fibonacci number
248
- */
249
- function fibonacci(n) {
250
- if (n < 0) {
251
- throw new Error('Fibonacci is not defined for negative numbers');
252
- }
253
-
254
- if (n === 0) return 0;
255
- if (n === 1) return 1;
256
-
257
- let a = 0;
258
- let b = 1;
259
-
260
- for (let i = 2; i <= n; i++) {
261
- const temp = a + b;
262
- a = b;
263
- b = temp;
264
- }
265
-
266
- return b;
267
- }
268
-
269
- /**
270
- * Calculate the nth Fibonacci number using recursion (less efficient)
271
- * @param {number} n - The position in the Fibonacci sequence
272
- * @returns {number} The nth Fibonacci number
273
- */
274
- function fibonacciRecursive(n) {
275
- if (n < 0) {
276
- throw new Error('Fibonacci is not defined for negative numbers');
277
- }
278
-
279
- if (n === 0) return 0;
280
- if (n === 1) return 1;
281
-
282
- return fibonacciRecursive(n - 1) + fibonacciRecursive(n - 2);
283
- }
284
-
285
- /**
286
- * Check if a number is prime
287
- * @param {number} n - The number to check for primality
288
- * @returns {boolean} True if the number is prime, false otherwise
289
- */
290
- function isPrime(n) {
291
- // Handle edge cases
292
- if (n <= 1) return false;
293
- if (n <= 3) return true;
294
- if (n % 2 === 0 || n % 3 === 0) return false;
295
-
296
- // Check for divisors from 5 up to sqrt(n)
297
- // Using the fact that all primes > 3 are of the form 6k ± 1
298
- for (let i = 5; i * i <= n; i += 6) {
299
- if (n % i === 0 || n % (i + 2) === 0) {
300
- return false;
301
- }
302
- }
303
-
304
- return true;
305
- }
306
-
307
- /**
308
- * Find the maximum of two numbers
309
- * @param {number} a - First number
310
- * @param {number} b - Second number
311
- * @returns {number} The maximum of a and b
312
- */
313
- function max(a, b) {
314
- return a > b ? a : b;
315
- }
316
-
317
- /**
318
- * Parse JSON data and validate its structure
319
- * @param {string} jsonString - The JSON string to parse
320
- * @param {Object} schema - Optional schema to validate against
321
- * @returns {Object} Parsed and validated JSON data
322
- * @throws {Error} If JSON is invalid or doesn't match schema
323
- */
324
- function parseAndValidateJSON(jsonString, schema = null) {
325
- // Parse the JSON string
326
- let parsedData;
327
- try {
328
- parsedData = JSON.parse(jsonString);
329
- } catch (error) {
330
- throw new Error(`Invalid JSON format: ${error.message}`);
331
- }
332
-
333
- // If no schema provided, return parsed data
334
- if (!schema) {
335
- return parsedData;
336
- }
337
-
338
- // Validate against schema
339
- validateSchema(parsedData, schema);
340
-
341
- return parsedData;
342
- }
343
-
344
- /**
345
- * Validate data against a schema
346
- * @param {*} data - Data to validate
347
- * @param {Object} schema - Schema to validate against
348
- * @throws {Error} If data doesn't match schema
349
- */
350
- function validateSchema(data, schema) {
351
- // Check if schema is an object
352
- if (typeof schema !== 'object' || schema === null) {
353
- throw new Error('Schema must be a valid object');
354
- }
355
-
356
- // Check required fields
357
- if (schema.required && Array.isArray(schema.required)) {
358
- for (const field of schema.required) {
359
- if (!(field in data)) {
360
- throw new Error(`Required field '${field}' is missing`);
361
- }
362
- }
363
- }
364
-
365
- // Validate each field
366
- for (const key in schema.properties) {
367
- if (!(key in data) && schema.required && schema.required.includes(key)) {
368
- throw new Error(`Required field '${key}' is missing`);
369
- }
370
-
371
- if (key in data) {
372
- const propertySchema = schema.properties[key];
373
- const value = data[key];
374
-
375
- // Check type
376
- if (propertySchema.type) {
377
- // Special handling for null values
378
- if (value === null && propertySchema.nullable) {
379
- continue; // Accept null values for nullable fields
380
- }
381
-
382
- if (propertySchema.type === 'array' && !Array.isArray(value)) {
383
- throw new Error(`Field '${key}' should be an array`);
384
- } else if (
385
- propertySchema.type === 'object' &&
386
- (typeof value !== 'object' || value === null || Array.isArray(value))
387
- ) {
388
- throw new Error(`Field '${key}' should be an object`);
389
- } else if (
390
- propertySchema.type !== 'array' &&
391
- propertySchema.type !== 'object' &&
392
- typeof value !== propertySchema.type
393
- ) {
394
- throw new Error(
395
- `Field '${key}' should be of type ${propertySchema.type}, got ${typeof value}`,
396
- );
397
- }
398
- }
399
-
400
- // Check enum values
401
- if (propertySchema.enum && !propertySchema.enum.includes(value)) {
402
- throw new Error(
403
- `Field '${key}' should be one of: ${propertySchema.enum.join(', ')}`,
404
- );
405
- }
406
-
407
- // Check minimum and maximum for numbers
408
- if (typeof value === 'number') {
409
- if (
410
- propertySchema.minimum !== undefined &&
411
- value < propertySchema.minimum
412
- ) {
413
- throw new Error(
414
- `Field '${key}' should be greater than or equal to ${propertySchema.minimum}`,
415
- );
416
- }
417
- if (
418
- propertySchema.maximum !== undefined &&
419
- value > propertySchema.maximum
420
- ) {
421
- throw new Error(
422
- `Field '${key}' should be less than or equal to ${propertySchema.maximum}`,
423
- );
424
- }
425
- }
426
-
427
- // Check minLength and maxLength for strings
428
- if (typeof value === 'string') {
429
- if (
430
- propertySchema.minLength !== undefined &&
431
- value.length < propertySchema.minLength
432
- ) {
433
- throw new Error(
434
- `Field '${key}' should have a minimum length of ${propertySchema.minLength}`,
435
- );
436
- }
437
- if (
438
- propertySchema.maxLength !== undefined &&
439
- value.length > propertySchema.maxLength
440
- ) {
441
- throw new Error(
442
- `Field '${key}' should have a maximum length of ${propertySchema.maxLength}`,
443
- );
444
- }
445
- }
446
-
447
- // Check nested objects recursively
448
- if (propertySchema.type === 'object' && propertySchema.properties) {
449
- validateSchema(value, propertySchema);
450
- }
451
-
452
- // Check array items
453
- if (
454
- propertySchema.type === 'array' &&
455
- propertySchema.items &&
456
- Array.isArray(value)
457
- ) {
458
- // Check minItems and maxItems for arrays
459
- if (
460
- propertySchema.minItems !== undefined &&
461
- value.length < propertySchema.minItems
462
- ) {
463
- throw new Error(
464
- `Array '${key}' should have at least ${propertySchema.minItems} items`,
465
- );
466
- }
467
- if (
468
- propertySchema.maxItems !== undefined &&
469
- value.length > propertySchema.maxItems
470
- ) {
471
- throw new Error(
472
- `Array '${key}' should have at most ${propertySchema.maxItems} items`,
473
- );
474
- }
475
-
476
- for (const [index, item] of value.entries()) {
477
- if (
478
- propertySchema.items.type &&
479
- typeof item !== propertySchema.items.type
480
- ) {
481
- throw new Error(
482
- `Item at index ${index} in array '${key}' should be of type ${propertySchema.items.type}, got ${typeof item}`,
483
- );
484
- }
485
-
486
- // Recursively validate object items
487
- if (
488
- propertySchema.items.type === 'object' &&
489
- propertySchema.items.properties
490
- ) {
491
- validateSchema(item, propertySchema.items);
492
- }
493
- }
494
- }
495
- }
496
- }
497
- }
498
-
499
- /**
500
- * Process CSV data and generate statistics
501
- * @param {string} csvData - The CSV data as a string
502
- * @param {Object} options - Options for processing
503
- * @returns {Object} Statistics about the CSV data
504
- */
505
- function processCSV(csvData, options = {}) {
506
- // Default options
507
- const opts = {
508
- delimiter: ',',
509
- hasHeader: true,
510
- ...options,
511
- };
512
-
513
- // Split CSV data into lines
514
- const lines = csvData.trim().split('\n');
515
-
516
- if (lines.length === 0) {
517
- return { error: 'Empty CSV data' };
518
- }
519
-
520
- // Parse header
521
- let headers = [];
522
- let startIndex = 0;
523
-
524
- if (opts.hasHeader) {
525
- headers = lines[0].split(opts.delimiter).map((h) => h.trim());
526
- startIndex = 1;
527
- }
528
-
529
- // Parse rows
530
- const rows = [];
531
- for (let i = startIndex; i < lines.length; i++) {
532
- if (lines[i].trim()) {
533
- // Skip empty lines
534
- const values = lines[i].split(opts.delimiter).map((v) => v.trim());
535
- const row = {};
536
-
537
- if (opts.hasHeader) {
538
- // Map values to headers
539
- headers.forEach((header, index) => {
540
- row[header] = values[index] || '';
541
- });
542
- } else {
543
- // Use indices as keys
544
- values.forEach((value, index) => {
545
- row[index] = value;
546
- });
547
- }
548
-
549
- rows.push(row);
550
- }
551
- }
552
-
553
- // Generate statistics
554
- const stats = {
555
- rowCount: rows.length,
556
- columnCount: opts.hasHeader
557
- ? headers.length
558
- : rows[0]
559
- ? Object.keys(rows[0]).length
560
- : 0,
561
- headers: opts.hasHeader ? headers : [],
562
- columns: {},
563
- };
564
-
565
- // Initialize column statistics
566
- const columnNames = opts.hasHeader ? headers : Object.keys(rows[0] || {});
567
- columnNames.forEach((column) => {
568
- stats.columns[column] = {
569
- count: 0,
570
- uniqueValues: new Set(),
571
- numericValues: [],
572
- emptyCount: 0,
573
- };
574
- });
575
-
576
- // Process each row
577
- rows.forEach((row) => {
578
- columnNames.forEach((column) => {
579
- const value = row[column];
580
- const columnStats = stats.columns[column];
581
-
582
- columnStats.count++;
583
-
584
- if (value === '' || value === null || value === undefined) {
585
- columnStats.emptyCount++;
586
- } else {
587
- columnStats.uniqueValues.add(value);
588
-
589
- // Try to parse as number
590
- const numValue = parseFloat(value);
591
- if (!isNaN(numValue)) {
592
- columnStats.numericValues.push(numValue);
593
- }
594
- }
595
- });
596
- });
597
-
598
- // Calculate additional statistics
599
- Object.keys(stats.columns).forEach((column) => {
600
- const columnStats = stats.columns[column];
601
- columnStats.uniqueCount = columnStats.uniqueValues.size;
602
- delete columnStats.uniqueValues; // Remove Set object for cleaner output
603
-
604
- // Calculate numeric statistics if applicable
605
- if (columnStats.numericValues.length > 0) {
606
- const nums = columnStats.numericValues;
607
- columnStats.numericStats = {
608
- min: Math.min(...nums),
609
- max: Math.max(...nums),
610
- sum: nums.reduce((a, b) => a + b, 0),
611
- average: nums.reduce((a, b) => a + b, 0) / nums.length,
612
- };
613
- }
614
- delete columnStats.numericValues; // Remove array for cleaner output
615
- });
616
-
617
- return stats;
618
- }
619
-
620
- /**
621
- * Execute a command with reliable cross-platform support
622
- * @param {string} command - The command to execute
623
- * @param {Array} args - Arguments for the command
624
- * @param {Object} options - Options for execution
625
- * @returns {Promise<Object>} Result of the execution
626
- */
627
- async function executeCommand(command, args = [], options = {}) {
628
- const { spawn } = require('child_process');
629
-
630
- // Default options
631
- const opts = {
632
- stdio: 'inherit',
633
- shell: true,
634
- timeout: 300000, // 5 minute timeout
635
- ...options,
636
- };
637
-
638
- return new Promise((resolve, reject) => {
639
- // Don't log the command if it contains sensitive information
640
- if (process.env.DEBUG === 'true') {
641
- console.log(`[EXEC] Running: ${command} ${args.join(' ')}`);
642
- }
643
-
644
- try {
645
- // Validate that command is a string and not empty
646
- if (!command || typeof command !== 'string') {
647
- reject({
648
- error: new Error(
649
- 'Invalid command: command must be a non-empty string',
650
- ),
651
- message: 'Invalid command: command must be a non-empty string',
652
- stdout: '',
653
- stderr: '',
654
- });
655
- return;
656
- }
657
-
658
- // Validate that args is an array
659
- if (!Array.isArray(args)) {
660
- reject({
661
- error: new Error('Invalid arguments: args must be an array'),
662
- message: 'Invalid arguments: args must be an array',
663
- stdout: '',
664
- stderr: '',
665
- });
666
- return;
667
- }
668
-
669
- // Special handling for JS files - ensure they are executed with node
670
- if (command.endsWith('.js') || command.endsWith('.cjs')) {
671
- // Prepend 'node' to the command if it's a JS file
672
- const nodeArgs = [command, ...args];
673
- command = process.execPath; // Use the same node executable
674
- args = nodeArgs;
675
- // Disable shell mode for direct process execution to avoid quoting issues
676
- opts.shell = false;
677
- }
678
-
679
- const child = spawn(command, args, opts);
680
-
681
- // Add debug logging for Windows command execution
682
- if (process.platform === 'win32' && process.env.DEBUG === 'true') {
683
- console.log(`[DEBUG] Spawned process with command: ${command}`);
684
- console.log(`[DEBUG] Spawned process with args: ${JSON.stringify(args)}`);
685
- }
686
-
687
- let stdout = '';
688
- let stderr = '';
689
-
690
- if (child.stdout) {
691
- child.stdout.on('data', (data) => {
692
- stdout += data.toString();
693
- });
694
- }
695
-
696
- if (child.stderr) {
697
- child.stderr.on('data', (data) => {
698
- stderr += data.toString();
699
- });
700
- }
701
-
702
- child.on('close', (code) => {
703
- resolve({
704
- code,
705
- stdout,
706
- stderr,
707
- success: code === 0,
708
- });
709
- });
710
-
711
- child.on('error', (error) => {
712
- // Provide more detailed error information
713
- let errorMessage = error.message;
714
- if (error.code === 'ENOENT') {
715
- errorMessage = `Command not found: ${command}. Please check if the command is installed and in your PATH.`;
716
- } else if (error.code === 'EACCES') {
717
- errorMessage = `Permission denied: Cannot execute ${command}. Please check file permissions.`;
718
- } else if (error.code === 'EISDIR') {
719
- errorMessage = `Cannot execute directory: ${command}. This might be a file path issue.`;
720
- }
721
-
722
- reject({
723
- error,
724
- message: `Failed to execute command: ${errorMessage}`,
725
- stdout,
726
- stderr,
727
- });
728
- });
729
-
730
- // Handle timeout
731
- if (opts.timeout) {
732
- setTimeout(() => {
733
- child.kill();
734
- reject({
735
- error: new Error('Command timeout'),
736
- message: `Command timed out after ${opts.timeout}ms`,
737
- stdout,
738
- stderr,
739
- });
740
- }, opts.timeout);
741
- }
742
- } catch (error) {
743
- reject({
744
- error,
745
- message: `Failed to spawn command: ${error.message}`,
746
- });
747
- }
748
- });
749
- }
750
-
751
- /**
752
- * Safely execute a JavaScript file
753
- * @param {string} jsFilePath - Path to the JS file to execute
754
- * @param {Array} args - Arguments to pass to the JS file
755
- * @param {Object} options - Execution options
756
- * @returns {Promise<Object>} Result of the execution
757
- */
758
- async function executeJSFile(jsFilePath, args = [], options = {}) {
759
- const fs = require('fs').promises;
760
- const path = require('path');
761
-
762
- try {
763
- // Validate that the file exists
764
- await fs.access(jsFilePath);
765
-
766
- // Validate that it's actually a file (not a directory)
767
- const stats = await fs.stat(jsFilePath);
768
- if (!stats.isFile()) {
769
- throw new Error(`Path is not a file: ${jsFilePath}`);
770
- }
771
-
772
- // Validate file extension
773
- const ext = path.extname(jsFilePath).toLowerCase();
774
- if (ext !== '.js' && ext !== '.cjs') {
775
- throw new Error(`File is not a JavaScript file: ${jsFilePath}`);
776
- }
777
-
778
- // Execute the JS file with node
779
- // On Windows, paths with spaces need to be quoted when using shell: true
780
- const nodePath = process.execPath;
781
- return await executeCommand(nodePath, [jsFilePath, ...args], options);
782
- } catch (error) {
783
- throw new Error(
784
- `Failed to execute JS file '${jsFilePath}': ${error.message}`,
785
- );
786
- }
787
- }
788
-
789
- /**
790
- * Encrypts data using AES-256-GCM authenticated encryption
791
- *
792
- * This function provides secure symmetric encryption with authentication.
793
- * It generates a random initialization vector for each encryption operation
794
- * and returns the encrypted data along with the IV and authentication tag.
795
- *
796
- * @param {string|Buffer} data - The plaintext data to encrypt
797
- * @param {string|Buffer} secretKey - The secret key for encryption (must be 32 bytes for AES-256)
798
- * @returns {Object} Object containing encrypted data, IV, and authentication tag
799
- * @throws {Error} If encryption fails due to invalid inputs or cryptographic errors
800
- */
801
- function encryptData(data, secretKey) {
802
- const crypto = require('crypto');
803
-
804
- // Validate inputs
805
- if (!data) {
806
- throw new Error('Data to encrypt cannot be empty');
807
- }
808
-
809
- if (!secretKey) {
810
- throw new Error('Secret key is required');
811
- }
812
-
813
- // Generate a random initialization vector
814
- const iv = crypto.randomBytes(16);
815
-
816
- // Create cipher using AES-256-GCM
817
- const cipher = crypto.createCipheriv('aes-256-gcm', secretKey, iv);
818
-
819
- // Encrypt the data
820
- let encrypted;
821
- if (typeof data === 'string') {
822
- encrypted = cipher.update(data, 'utf8', 'hex');
823
- } else {
824
- encrypted = cipher.update(data);
825
- encrypted = encrypted.toString('hex');
826
- }
827
- cipher.final();
828
-
829
- // Get the authentication tag
830
- const authTag = cipher.getAuthTag();
831
-
832
- // Return encrypted data with IV and auth tag
833
- return {
834
- encryptedData: encrypted,
835
- iv: iv.toString('base64'),
836
- authTag: authTag.toString('base64'),
837
- };
838
- }
839
-
840
- /**
841
- * Decrypts data using AES-256-GCM authenticated decryption
842
- *
843
- * This function decrypts data that was encrypted with encryptData().
844
- * It requires the encrypted data object containing the encrypted data,
845
- * initialization vector, and authentication tag.
846
- *
847
- * @param {Object} encryptedObj - Object containing encrypted data, IV, and auth tag
848
- * @param {string|Buffer} secretKey - The secret key used for encryption
849
- * @returns {string} The decrypted plaintext data
850
- * @throws {Error} If decryption fails due to invalid inputs, tampered data, or cryptographic errors
851
- */
852
- function decryptData(encryptedObj, secretKey) {
853
- const crypto = require('crypto');
854
-
855
- // Validate inputs
856
- if (
857
- !encryptedObj ||
858
- !encryptedObj.encryptedData ||
859
- !encryptedObj.iv ||
860
- !encryptedObj.authTag
861
- ) {
862
- throw new Error('Invalid encrypted object');
863
- }
864
-
865
- if (!secretKey) {
866
- throw new Error('Secret key is required');
867
- }
868
-
869
- // Decode base64 encoded values
870
- const iv = Buffer.from(encryptedObj.iv, 'base64');
871
- const authTag = Buffer.from(encryptedObj.authTag, 'base64');
872
-
873
- // Create decipher using AES-256-GCM
874
- const decipher = crypto.createDecipheriv('aes-256-gcm', secretKey, iv);
875
-
876
- // Set the authentication tag
877
- decipher.setAuthTag(authTag);
878
-
879
- // Decrypt the data
880
- let decrypted;
881
- if (typeof encryptedObj.encryptedData === 'string') {
882
- decrypted = decipher.update(encryptedObj.encryptedData, 'hex', 'utf8');
883
- } else {
884
- decrypted = decipher.update(encryptedObj.encryptedData);
885
- decrypted = decrypted.toString('utf8');
886
- }
887
- decipher.final();
888
-
889
- return decrypted;
890
- }
891
-
892
- /**
893
- * Generates a cryptographically secure random key
894
- *
895
- * This function generates a random key suitable for AES-256 encryption.
896
- *
897
- * @param {number} [length=32] - Length of the key in bytes (32 bytes = 256 bits)
898
- * @returns {Buffer} A cryptographically secure random key
899
- */
900
- function generateKey(length = 32) {
901
- const crypto = require('crypto');
902
- return crypto.randomBytes(length);
903
- }
904
-
905
- module.exports = {
906
- factorial,
907
- fibonacci,
908
- fibonacciRecursive,
909
- max,
910
- isPrime,
911
- HashTable,
912
- parseAndValidateJSON,
913
- processCSV,
914
- processWeatherData,
915
- RESTClient,
916
- executeCommand,
917
- executeJSFile,
918
- encryptData,
919
- decryptData,
920
- generateKey,
921
- };
1
+ /**
2
+ * Utility functions for the Stigmergy CLI
3
+ */
4
+
5
+ const { processWeatherData } = require('./weatherProcessor');
6
+
7
+ /**
8
+ * Simple REST API client
9
+ */
10
+ class RESTClient {
11
+ /**
12
+ * Create a new REST client
13
+ * @param {string} baseURL - The base URL for the API
14
+ * @param {Object} defaultHeaders - Default headers to include in all requests
15
+ */
16
+ constructor(baseURL = '', defaultHeaders = {}) {
17
+ this.baseURL = baseURL;
18
+ this.defaultHeaders = {
19
+ 'Content-Type': 'application/json',
20
+ ...defaultHeaders,
21
+ };
22
+ }
23
+
24
+ /**
25
+ * Make an HTTP request
26
+ * @param {string} method - HTTP method (GET, POST, PUT, DELETE, etc.)
27
+ * @param {string} url - Request URL
28
+ * @param {Object} options - Request options
29
+ * @returns {Promise} Response promise
30
+ */
31
+ async request(method, url, options = {}) {
32
+ const fullURL = this.baseURL + url;
33
+
34
+ const config = {
35
+ method,
36
+ headers: {
37
+ ...this.defaultHeaders,
38
+ ...options.headers,
39
+ },
40
+ ...options,
41
+ };
42
+
43
+ // Handle JSON body
44
+ if (
45
+ options.body &&
46
+ typeof options.body === 'object' &&
47
+ !(options.body instanceof String)
48
+ ) {
49
+ config.body = JSON.stringify(options.body);
50
+ }
51
+
52
+ try {
53
+ const response = await fetch(fullURL, config);
54
+
55
+ // Try to parse JSON response
56
+ let data;
57
+ const contentType = response.headers.get('content-type');
58
+ if (contentType && contentType.includes('application/json')) {
59
+ data = await response.json();
60
+ } else {
61
+ data = await response.text();
62
+ }
63
+
64
+ // Throw error for non-success status codes
65
+ if (!response.ok) {
66
+ throw new Error(
67
+ `HTTP ${response.status}: ${response.statusText} - ${JSON.stringify(data)}`,
68
+ );
69
+ }
70
+
71
+ return {
72
+ status: response.status,
73
+ statusText: response.statusText,
74
+ headers: response.headers,
75
+ data,
76
+ };
77
+ } catch (error) {
78
+ throw new Error(`Request failed: ${error.message}`);
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Make a GET request
84
+ * @param {string} url - Request URL
85
+ * @param {Object} options - Request options
86
+ * @returns {Promise} Response promise
87
+ */
88
+ async get(url, options = {}) {
89
+ return this.request('GET', url, options);
90
+ }
91
+
92
+ /**
93
+ * Make a POST request
94
+ * @param {string} url - Request URL
95
+ * @param {Object} data - Request body data
96
+ * @param {Object} options - Request options
97
+ * @returns {Promise} Response promise
98
+ */
99
+ async post(url, data, options = {}) {
100
+ return this.request('POST', url, { ...options, body: data });
101
+ }
102
+
103
+ /**
104
+ * Make a PUT request
105
+ * @param {string} url - Request URL
106
+ * @param {Object} data - Request body data
107
+ * @param {Object} options - Request options
108
+ * @returns {Promise} Response promise
109
+ */
110
+ async put(url, data, options = {}) {
111
+ return this.request('PUT', url, { ...options, body: data });
112
+ }
113
+
114
+ /**
115
+ * Make a DELETE request
116
+ * @param {string} url - Request URL
117
+ * @param {Object} options - Request options
118
+ * @returns {Promise} Response promise
119
+ */
120
+ async delete(url, options = {}) {
121
+ return this.request('DELETE', url, options);
122
+ }
123
+ }
124
+
125
+ /**
126
+ * HashTable implementation with collision handling using chaining
127
+ */
128
+ class HashTable {
129
+ /**
130
+ * Create a new HashTable
131
+ * @param {number} size - Initial size of the hash table
132
+ */
133
+ constructor(size = 53) {
134
+ this.keyMap = new Array(size);
135
+ }
136
+
137
+ /**
138
+ * Hash function to convert a key to an index
139
+ * @param {string} key - Key to hash
140
+ * @returns {number} Index in the hash table
141
+ */
142
+ _hash(key) {
143
+ let total = 0;
144
+ const WEIRD_PRIME = 31;
145
+ for (let i = 0; i < Math.min(key.length, 100); i++) {
146
+ const char = key[i];
147
+ const value = char.charCodeAt(0) - 96;
148
+ total = (total * WEIRD_PRIME + value) % this.keyMap.length;
149
+ }
150
+ return total;
151
+ }
152
+
153
+ /**
154
+ * Set a key-value pair in the hash table
155
+ * @param {string} key - Key to store
156
+ * @param {*} value - Value to store
157
+ * @returns {HashTable} The hash table instance
158
+ */
159
+ set(key, value) {
160
+ const index = this._hash(key);
161
+ if (!this.keyMap[index]) {
162
+ this.keyMap[index] = [];
163
+ }
164
+ this.keyMap[index].push([key, value]);
165
+ return this;
166
+ }
167
+
168
+ /**
169
+ * Get a value by its key
170
+ * @param {string} key - Key to look up
171
+ * @returns {*} The value associated with the key, or undefined if not found
172
+ */
173
+ get(key) {
174
+ const index = this._hash(key);
175
+ if (this.keyMap[index]) {
176
+ for (let i = 0; i < this.keyMap[index].length; i++) {
177
+ if (this.keyMap[index][i][0] === key) {
178
+ return this.keyMap[index][i][1];
179
+ }
180
+ }
181
+ }
182
+ return undefined;
183
+ }
184
+
185
+ /**
186
+ * Get all keys in the hash table
187
+ * @returns {Array} Array of all keys
188
+ */
189
+ keys() {
190
+ const keysArr = [];
191
+ for (let i = 0; i < this.keyMap.length; i++) {
192
+ if (this.keyMap[i]) {
193
+ for (let j = 0; j < this.keyMap[i].length; j++) {
194
+ if (!keysArr.includes(this.keyMap[i][j][0])) {
195
+ keysArr.push(this.keyMap[i][j][0]);
196
+ }
197
+ }
198
+ }
199
+ }
200
+ return keysArr;
201
+ }
202
+
203
+ /**
204
+ * Get all values in the hash table
205
+ * @returns {Array} Array of all values
206
+ */
207
+ values() {
208
+ const valuesArr = [];
209
+ for (let i = 0; i < this.keyMap.length; i++) {
210
+ if (this.keyMap[i]) {
211
+ for (let j = 0; j < this.keyMap[i].length; j++) {
212
+ if (!valuesArr.includes(this.keyMap[i][j][1])) {
213
+ valuesArr.push(this.keyMap[i][j][1]);
214
+ }
215
+ }
216
+ }
217
+ }
218
+ return valuesArr;
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Calculate the factorial of a number
224
+ * @param {number} n - The number to calculate factorial for
225
+ * @returns {number} The factorial of n
226
+ */
227
+ function factorial(n) {
228
+ if (n < 0) {
229
+ throw new Error('Factorial is not defined for negative numbers');
230
+ }
231
+
232
+ if (n === 0 || n === 1) {
233
+ return 1;
234
+ }
235
+
236
+ let result = 1;
237
+ for (let i = 2; i <= n; i++) {
238
+ result *= i;
239
+ }
240
+
241
+ return result;
242
+ }
243
+
244
+ /**
245
+ * Calculate the nth Fibonacci number using iteration (efficient)
246
+ * @param {number} n - The position in the Fibonacci sequence
247
+ * @returns {number} The nth Fibonacci number
248
+ */
249
+ function fibonacci(n) {
250
+ if (n < 0) {
251
+ throw new Error('Fibonacci is not defined for negative numbers');
252
+ }
253
+
254
+ if (n === 0) return 0;
255
+ if (n === 1) return 1;
256
+
257
+ let a = 0;
258
+ let b = 1;
259
+
260
+ for (let i = 2; i <= n; i++) {
261
+ const temp = a + b;
262
+ a = b;
263
+ b = temp;
264
+ }
265
+
266
+ return b;
267
+ }
268
+
269
+ /**
270
+ * Calculate the nth Fibonacci number using recursion (less efficient)
271
+ * @param {number} n - The position in the Fibonacci sequence
272
+ * @returns {number} The nth Fibonacci number
273
+ */
274
+ function fibonacciRecursive(n) {
275
+ if (n < 0) {
276
+ throw new Error('Fibonacci is not defined for negative numbers');
277
+ }
278
+
279
+ if (n === 0) return 0;
280
+ if (n === 1) return 1;
281
+
282
+ return fibonacciRecursive(n - 1) + fibonacciRecursive(n - 2);
283
+ }
284
+
285
+ /**
286
+ * Check if a number is prime
287
+ * @param {number} n - The number to check for primality
288
+ * @returns {boolean} True if the number is prime, false otherwise
289
+ */
290
+ function isPrime(n) {
291
+ // Handle edge cases
292
+ if (n <= 1) return false;
293
+ if (n <= 3) return true;
294
+ if (n % 2 === 0 || n % 3 === 0) return false;
295
+
296
+ // Check for divisors from 5 up to sqrt(n)
297
+ // Using the fact that all primes > 3 are of the form 6k ± 1
298
+ for (let i = 5; i * i <= n; i += 6) {
299
+ if (n % i === 0 || n % (i + 2) === 0) {
300
+ return false;
301
+ }
302
+ }
303
+
304
+ return true;
305
+ }
306
+
307
+ /**
308
+ * Find the maximum of two numbers
309
+ * @param {number} a - First number
310
+ * @param {number} b - Second number
311
+ * @returns {number} The maximum of a and b
312
+ */
313
+ function max(a, b) {
314
+ return a > b ? a : b;
315
+ }
316
+
317
+ /**
318
+ * Parse JSON data and validate its structure
319
+ * @param {string} jsonString - The JSON string to parse
320
+ * @param {Object} schema - Optional schema to validate against
321
+ * @returns {Object} Parsed and validated JSON data
322
+ * @throws {Error} If JSON is invalid or doesn't match schema
323
+ */
324
+ function parseAndValidateJSON(jsonString, schema = null) {
325
+ // Parse the JSON string
326
+ let parsedData;
327
+ try {
328
+ parsedData = JSON.parse(jsonString);
329
+ } catch (error) {
330
+ throw new Error(`Invalid JSON format: ${error.message}`);
331
+ }
332
+
333
+ // If no schema provided, return parsed data
334
+ if (!schema) {
335
+ return parsedData;
336
+ }
337
+
338
+ // Validate against schema
339
+ validateSchema(parsedData, schema);
340
+
341
+ return parsedData;
342
+ }
343
+
344
+ /**
345
+ * Validate data against a schema
346
+ * @param {*} data - Data to validate
347
+ * @param {Object} schema - Schema to validate against
348
+ * @throws {Error} If data doesn't match schema
349
+ */
350
+ function validateSchema(data, schema) {
351
+ // Check if schema is an object
352
+ if (typeof schema !== 'object' || schema === null) {
353
+ throw new Error('Schema must be a valid object');
354
+ }
355
+
356
+ // Check required fields
357
+ if (schema.required && Array.isArray(schema.required)) {
358
+ for (const field of schema.required) {
359
+ if (!(field in data)) {
360
+ throw new Error(`Required field '${field}' is missing`);
361
+ }
362
+ }
363
+ }
364
+
365
+ // Validate each field
366
+ for (const key in schema.properties) {
367
+ if (!(key in data) && schema.required && schema.required.includes(key)) {
368
+ throw new Error(`Required field '${key}' is missing`);
369
+ }
370
+
371
+ if (key in data) {
372
+ const propertySchema = schema.properties[key];
373
+ const value = data[key];
374
+
375
+ // Check type
376
+ if (propertySchema.type) {
377
+ // Special handling for null values
378
+ if (value === null && propertySchema.nullable) {
379
+ continue; // Accept null values for nullable fields
380
+ }
381
+
382
+ if (propertySchema.type === 'array' && !Array.isArray(value)) {
383
+ throw new Error(`Field '${key}' should be an array`);
384
+ } else if (
385
+ propertySchema.type === 'object' &&
386
+ (typeof value !== 'object' || value === null || Array.isArray(value))
387
+ ) {
388
+ throw new Error(`Field '${key}' should be an object`);
389
+ } else if (
390
+ propertySchema.type !== 'array' &&
391
+ propertySchema.type !== 'object' &&
392
+ typeof value !== propertySchema.type
393
+ ) {
394
+ throw new Error(
395
+ `Field '${key}' should be of type ${propertySchema.type}, got ${typeof value}`,
396
+ );
397
+ }
398
+ }
399
+
400
+ // Check enum values
401
+ if (propertySchema.enum && !propertySchema.enum.includes(value)) {
402
+ throw new Error(
403
+ `Field '${key}' should be one of: ${propertySchema.enum.join(', ')}`,
404
+ );
405
+ }
406
+
407
+ // Check minimum and maximum for numbers
408
+ if (typeof value === 'number') {
409
+ if (
410
+ propertySchema.minimum !== undefined &&
411
+ value < propertySchema.minimum
412
+ ) {
413
+ throw new Error(
414
+ `Field '${key}' should be greater than or equal to ${propertySchema.minimum}`,
415
+ );
416
+ }
417
+ if (
418
+ propertySchema.maximum !== undefined &&
419
+ value > propertySchema.maximum
420
+ ) {
421
+ throw new Error(
422
+ `Field '${key}' should be less than or equal to ${propertySchema.maximum}`,
423
+ );
424
+ }
425
+ }
426
+
427
+ // Check minLength and maxLength for strings
428
+ if (typeof value === 'string') {
429
+ if (
430
+ propertySchema.minLength !== undefined &&
431
+ value.length < propertySchema.minLength
432
+ ) {
433
+ throw new Error(
434
+ `Field '${key}' should have a minimum length of ${propertySchema.minLength}`,
435
+ );
436
+ }
437
+ if (
438
+ propertySchema.maxLength !== undefined &&
439
+ value.length > propertySchema.maxLength
440
+ ) {
441
+ throw new Error(
442
+ `Field '${key}' should have a maximum length of ${propertySchema.maxLength}`,
443
+ );
444
+ }
445
+ }
446
+
447
+ // Check nested objects recursively
448
+ if (propertySchema.type === 'object' && propertySchema.properties) {
449
+ validateSchema(value, propertySchema);
450
+ }
451
+
452
+ // Check array items
453
+ if (
454
+ propertySchema.type === 'array' &&
455
+ propertySchema.items &&
456
+ Array.isArray(value)
457
+ ) {
458
+ // Check minItems and maxItems for arrays
459
+ if (
460
+ propertySchema.minItems !== undefined &&
461
+ value.length < propertySchema.minItems
462
+ ) {
463
+ throw new Error(
464
+ `Array '${key}' should have at least ${propertySchema.minItems} items`,
465
+ );
466
+ }
467
+ if (
468
+ propertySchema.maxItems !== undefined &&
469
+ value.length > propertySchema.maxItems
470
+ ) {
471
+ throw new Error(
472
+ `Array '${key}' should have at most ${propertySchema.maxItems} items`,
473
+ );
474
+ }
475
+
476
+ for (const [index, item] of value.entries()) {
477
+ if (
478
+ propertySchema.items.type &&
479
+ typeof item !== propertySchema.items.type
480
+ ) {
481
+ throw new Error(
482
+ `Item at index ${index} in array '${key}' should be of type ${propertySchema.items.type}, got ${typeof item}`,
483
+ );
484
+ }
485
+
486
+ // Recursively validate object items
487
+ if (
488
+ propertySchema.items.type === 'object' &&
489
+ propertySchema.items.properties
490
+ ) {
491
+ validateSchema(item, propertySchema.items);
492
+ }
493
+ }
494
+ }
495
+ }
496
+ }
497
+ }
498
+
499
+ /**
500
+ * Process CSV data and generate statistics
501
+ * @param {string} csvData - The CSV data as a string
502
+ * @param {Object} options - Options for processing
503
+ * @returns {Object} Statistics about the CSV data
504
+ */
505
+ function processCSV(csvData, options = {}) {
506
+ // Default options
507
+ const opts = {
508
+ delimiter: ',',
509
+ hasHeader: true,
510
+ ...options,
511
+ };
512
+
513
+ // Split CSV data into lines
514
+ const lines = csvData.trim().split('\n');
515
+
516
+ if (lines.length === 0) {
517
+ return { error: 'Empty CSV data' };
518
+ }
519
+
520
+ // Parse header
521
+ let headers = [];
522
+ let startIndex = 0;
523
+
524
+ if (opts.hasHeader) {
525
+ headers = lines[0].split(opts.delimiter).map((h) => h.trim());
526
+ startIndex = 1;
527
+ }
528
+
529
+ // Parse rows
530
+ const rows = [];
531
+ for (let i = startIndex; i < lines.length; i++) {
532
+ if (lines[i].trim()) {
533
+ // Skip empty lines
534
+ const values = lines[i].split(opts.delimiter).map((v) => v.trim());
535
+ const row = {};
536
+
537
+ if (opts.hasHeader) {
538
+ // Map values to headers
539
+ headers.forEach((header, index) => {
540
+ row[header] = values[index] || '';
541
+ });
542
+ } else {
543
+ // Use indices as keys
544
+ values.forEach((value, index) => {
545
+ row[index] = value;
546
+ });
547
+ }
548
+
549
+ rows.push(row);
550
+ }
551
+ }
552
+
553
+ // Generate statistics
554
+ const stats = {
555
+ rowCount: rows.length,
556
+ columnCount: opts.hasHeader
557
+ ? headers.length
558
+ : rows[0]
559
+ ? Object.keys(rows[0]).length
560
+ : 0,
561
+ headers: opts.hasHeader ? headers : [],
562
+ columns: {},
563
+ };
564
+
565
+ // Initialize column statistics
566
+ const columnNames = opts.hasHeader ? headers : Object.keys(rows[0] || {});
567
+ columnNames.forEach((column) => {
568
+ stats.columns[column] = {
569
+ count: 0,
570
+ uniqueValues: new Set(),
571
+ numericValues: [],
572
+ emptyCount: 0,
573
+ };
574
+ });
575
+
576
+ // Process each row
577
+ rows.forEach((row) => {
578
+ columnNames.forEach((column) => {
579
+ const value = row[column];
580
+ const columnStats = stats.columns[column];
581
+
582
+ columnStats.count++;
583
+
584
+ if (value === '' || value === null || value === undefined) {
585
+ columnStats.emptyCount++;
586
+ } else {
587
+ columnStats.uniqueValues.add(value);
588
+
589
+ // Try to parse as number
590
+ const numValue = parseFloat(value);
591
+ if (!isNaN(numValue)) {
592
+ columnStats.numericValues.push(numValue);
593
+ }
594
+ }
595
+ });
596
+ });
597
+
598
+ // Calculate additional statistics
599
+ Object.keys(stats.columns).forEach((column) => {
600
+ const columnStats = stats.columns[column];
601
+ columnStats.uniqueCount = columnStats.uniqueValues.size;
602
+ delete columnStats.uniqueValues; // Remove Set object for cleaner output
603
+
604
+ // Calculate numeric statistics if applicable
605
+ if (columnStats.numericValues.length > 0) {
606
+ const nums = columnStats.numericValues;
607
+ columnStats.numericStats = {
608
+ min: Math.min(...nums),
609
+ max: Math.max(...nums),
610
+ sum: nums.reduce((a, b) => a + b, 0),
611
+ average: nums.reduce((a, b) => a + b, 0) / nums.length,
612
+ };
613
+ }
614
+ delete columnStats.numericValues; // Remove array for cleaner output
615
+ });
616
+
617
+ return stats;
618
+ }
619
+
620
+ /**
621
+ * Execute a command with reliable cross-platform support
622
+ * @param {string} command - The command to execute
623
+ * @param {Array} args - Arguments for the command
624
+ * @param {Object} options - Options for execution
625
+ * @returns {Promise<Object>} Result of the execution
626
+ */
627
+ async function executeCommand(command, args = [], options = {}) {
628
+ const { spawn } = require('child_process');
629
+
630
+ // Default options
631
+ const opts = {
632
+ stdio: 'inherit',
633
+ shell: true,
634
+ timeout: 300000, // 5 minute timeout
635
+ ...options,
636
+ };
637
+
638
+ return new Promise((resolve, reject) => {
639
+ // Don't log the command if it contains sensitive information
640
+ if (process.env.DEBUG === 'true') {
641
+ console.log(`[EXEC] Running: ${command} ${args.join(' ')}`);
642
+ }
643
+
644
+ try {
645
+ // Validate that command is a string and not empty
646
+ if (!command || typeof command !== 'string') {
647
+ reject({
648
+ error: new Error(
649
+ 'Invalid command: command must be a non-empty string',
650
+ ),
651
+ message: 'Invalid command: command must be a non-empty string',
652
+ stdout: '',
653
+ stderr: '',
654
+ });
655
+ return;
656
+ }
657
+
658
+ // Validate that args is an array
659
+ if (!Array.isArray(args)) {
660
+ reject({
661
+ error: new Error('Invalid arguments: args must be an array'),
662
+ message: 'Invalid arguments: args must be an array',
663
+ stdout: '',
664
+ stderr: '',
665
+ });
666
+ return;
667
+ }
668
+
669
+ // Special handling for JS files - ensure they are executed with node
670
+ if (command.endsWith('.js') || command.endsWith('.cjs')) {
671
+ // Prepend 'node' to the command if it's a JS file
672
+ const nodeArgs = [command, ...args];
673
+ command = process.execPath; // Use the same node executable
674
+ args = nodeArgs;
675
+ // Disable shell mode for direct process execution to avoid quoting issues
676
+ opts.shell = false;
677
+ }
678
+
679
+ const child = spawn(command, args, opts);
680
+
681
+ // Add debug logging for Windows command execution
682
+ if (process.platform === 'win32' && process.env.DEBUG === 'true') {
683
+ console.log(`[DEBUG] Spawned process with command: ${command}`);
684
+ console.log(`[DEBUG] Spawned process with args: ${JSON.stringify(args)}`);
685
+ }
686
+
687
+ let stdout = '';
688
+ let stderr = '';
689
+
690
+ if (child.stdout) {
691
+ child.stdout.on('data', (data) => {
692
+ stdout += data.toString();
693
+ });
694
+ }
695
+
696
+ if (child.stderr) {
697
+ child.stderr.on('data', (data) => {
698
+ stderr += data.toString();
699
+ });
700
+ }
701
+
702
+ child.on('close', (code) => {
703
+ resolve({
704
+ code,
705
+ stdout,
706
+ stderr,
707
+ success: code === 0,
708
+ });
709
+ });
710
+
711
+ child.on('error', (error) => {
712
+ // Provide more detailed error information
713
+ let errorMessage = error.message;
714
+ if (error.code === 'ENOENT') {
715
+ errorMessage = `Command not found: ${command}. Please check if the command is installed and in your PATH.`;
716
+ } else if (error.code === 'EACCES') {
717
+ errorMessage = `Permission denied: Cannot execute ${command}. Please check file permissions.`;
718
+ } else if (error.code === 'EISDIR') {
719
+ errorMessage = `Cannot execute directory: ${command}. This might be a file path issue.`;
720
+ }
721
+
722
+ reject({
723
+ error,
724
+ message: `Failed to execute command: ${errorMessage}`,
725
+ stdout,
726
+ stderr,
727
+ });
728
+ });
729
+
730
+ // Handle timeout
731
+ if (opts.timeout) {
732
+ setTimeout(() => {
733
+ child.kill();
734
+ reject({
735
+ error: new Error('Command timeout'),
736
+ message: `Command timed out after ${opts.timeout}ms`,
737
+ stdout,
738
+ stderr,
739
+ });
740
+ }, opts.timeout);
741
+ }
742
+ } catch (error) {
743
+ reject({
744
+ error,
745
+ message: `Failed to spawn command: ${error.message}`,
746
+ });
747
+ }
748
+ });
749
+ }
750
+
751
+ /**
752
+ * Safely execute a JavaScript file
753
+ * @param {string} jsFilePath - Path to the JS file to execute
754
+ * @param {Array} args - Arguments to pass to the JS file
755
+ * @param {Object} options - Execution options
756
+ * @returns {Promise<Object>} Result of the execution
757
+ */
758
+ async function executeJSFile(jsFilePath, args = [], options = {}) {
759
+ const fs = require('fs').promises;
760
+ const path = require('path');
761
+
762
+ try {
763
+ // Validate that the file exists
764
+ await fs.access(jsFilePath);
765
+
766
+ // Validate that it's actually a file (not a directory)
767
+ const stats = await fs.stat(jsFilePath);
768
+ if (!stats.isFile()) {
769
+ throw new Error(`Path is not a file: ${jsFilePath}`);
770
+ }
771
+
772
+ // Validate file extension
773
+ const ext = path.extname(jsFilePath).toLowerCase();
774
+ if (ext !== '.js' && ext !== '.cjs') {
775
+ throw new Error(`File is not a JavaScript file: ${jsFilePath}`);
776
+ }
777
+
778
+ // Execute the JS file with node
779
+ // On Windows, paths with spaces need to be quoted when using shell: true
780
+ const nodePath = process.execPath;
781
+ return await executeCommand(nodePath, [jsFilePath, ...args], options);
782
+ } catch (error) {
783
+ throw new Error(
784
+ `Failed to execute JS file '${jsFilePath}': ${error.message}`,
785
+ );
786
+ }
787
+ }
788
+
789
+ /**
790
+ * Encrypts data using AES-256-GCM authenticated encryption
791
+ *
792
+ * This function provides secure symmetric encryption with authentication.
793
+ * It generates a random initialization vector for each encryption operation
794
+ * and returns the encrypted data along with the IV and authentication tag.
795
+ *
796
+ * @param {string|Buffer} data - The plaintext data to encrypt
797
+ * @param {string|Buffer} secretKey - The secret key for encryption (must be 32 bytes for AES-256)
798
+ * @returns {Object} Object containing encrypted data, IV, and authentication tag
799
+ * @throws {Error} If encryption fails due to invalid inputs or cryptographic errors
800
+ */
801
+ function encryptData(data, secretKey) {
802
+ const crypto = require('crypto');
803
+
804
+ // Validate inputs
805
+ if (!data) {
806
+ throw new Error('Data to encrypt cannot be empty');
807
+ }
808
+
809
+ if (!secretKey) {
810
+ throw new Error('Secret key is required');
811
+ }
812
+
813
+ // Generate a random initialization vector
814
+ const iv = crypto.randomBytes(16);
815
+
816
+ // Create cipher using AES-256-GCM
817
+ const cipher = crypto.createCipheriv('aes-256-gcm', secretKey, iv);
818
+
819
+ // Encrypt the data
820
+ let encrypted;
821
+ if (typeof data === 'string') {
822
+ encrypted = cipher.update(data, 'utf8', 'hex');
823
+ } else {
824
+ encrypted = cipher.update(data);
825
+ encrypted = encrypted.toString('hex');
826
+ }
827
+ cipher.final();
828
+
829
+ // Get the authentication tag
830
+ const authTag = cipher.getAuthTag();
831
+
832
+ // Return encrypted data with IV and auth tag
833
+ return {
834
+ encryptedData: encrypted,
835
+ iv: iv.toString('base64'),
836
+ authTag: authTag.toString('base64'),
837
+ };
838
+ }
839
+
840
+ /**
841
+ * Decrypts data using AES-256-GCM authenticated decryption
842
+ *
843
+ * This function decrypts data that was encrypted with encryptData().
844
+ * It requires the encrypted data object containing the encrypted data,
845
+ * initialization vector, and authentication tag.
846
+ *
847
+ * @param {Object} encryptedObj - Object containing encrypted data, IV, and auth tag
848
+ * @param {string|Buffer} secretKey - The secret key used for encryption
849
+ * @returns {string} The decrypted plaintext data
850
+ * @throws {Error} If decryption fails due to invalid inputs, tampered data, or cryptographic errors
851
+ */
852
+ function decryptData(encryptedObj, secretKey) {
853
+ const crypto = require('crypto');
854
+
855
+ // Validate inputs
856
+ if (
857
+ !encryptedObj ||
858
+ !encryptedObj.encryptedData ||
859
+ !encryptedObj.iv ||
860
+ !encryptedObj.authTag
861
+ ) {
862
+ throw new Error('Invalid encrypted object');
863
+ }
864
+
865
+ if (!secretKey) {
866
+ throw new Error('Secret key is required');
867
+ }
868
+
869
+ // Decode base64 encoded values
870
+ const iv = Buffer.from(encryptedObj.iv, 'base64');
871
+ const authTag = Buffer.from(encryptedObj.authTag, 'base64');
872
+
873
+ // Create decipher using AES-256-GCM
874
+ const decipher = crypto.createDecipheriv('aes-256-gcm', secretKey, iv);
875
+
876
+ // Set the authentication tag
877
+ decipher.setAuthTag(authTag);
878
+
879
+ // Decrypt the data
880
+ let decrypted;
881
+ if (typeof encryptedObj.encryptedData === 'string') {
882
+ decrypted = decipher.update(encryptedObj.encryptedData, 'hex', 'utf8');
883
+ } else {
884
+ decrypted = decipher.update(encryptedObj.encryptedData);
885
+ decrypted = decrypted.toString('utf8');
886
+ }
887
+ decipher.final();
888
+
889
+ return decrypted;
890
+ }
891
+
892
+ /**
893
+ * Generates a cryptographically secure random key
894
+ *
895
+ * This function generates a random key suitable for AES-256 encryption.
896
+ *
897
+ * @param {number} [length=32] - Length of the key in bytes (32 bytes = 256 bits)
898
+ * @returns {Buffer} A cryptographically secure random key
899
+ */
900
+ function generateKey(length = 32) {
901
+ const crypto = require('crypto');
902
+ return crypto.randomBytes(length);
903
+ }
904
+
905
+ module.exports = {
906
+ factorial,
907
+ fibonacci,
908
+ fibonacciRecursive,
909
+ max,
910
+ isPrime,
911
+ HashTable,
912
+ parseAndValidateJSON,
913
+ processCSV,
914
+ processWeatherData,
915
+ RESTClient,
916
+ executeCommand,
917
+ executeJSFile,
918
+ encryptData,
919
+ decryptData,
920
+ generateKey,
921
+ };