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.
- package/README.md +32 -17
- package/STIGMERGY.md +16 -7
- package/docs/MULTI_USER_WIKI_COLLABORATION_SYSTEM.md +523 -0
- package/docs/PROMPT_BASED_SKILLS_SYSTEM_DESIGN.md +458 -0
- package/docs/SKILL_IMPLEMENTATION_CONSTRAINTS_AND_ALIGNMENT.md +423 -0
- package/docs/TECHNICAL_FEASIBILITY_ANALYSIS.md +308 -0
- package/examples/multilingual-hook-demo.js +125 -0
- package/package.json +14 -17
- package/scripts/dependency-analyzer.js +101 -0
- package/scripts/generate-cli-docs.js +64 -0
- package/scripts/postuninstall.js +46 -0
- package/scripts/preuninstall.js +75 -0
- package/scripts/run-layered-tests.js +3 -3
- package/src/adapters/claude/install_claude_integration.js +17 -17
- package/src/adapters/codebuddy/install_codebuddy_integration.js +13 -13
- package/src/adapters/codex/install_codex_integration.js +27 -27
- package/src/adapters/copilot/install_copilot_integration.js +46 -46
- package/src/adapters/gemini/install_gemini_integration.js +10 -10
- package/src/adapters/iflow/install_iflow_integration.js +7 -7
- package/src/adapters/qoder/install_qoder_integration.js +12 -12
- package/src/adapters/qwen/install_qwen_integration.js +17 -17
- package/src/auth.js +173 -173
- package/src/auth_command.js +208 -208
- package/src/calculator.js +313 -313
- package/src/cli/router.js +151 -7
- package/src/core/cache_cleaner.js +767 -767
- package/src/core/cli_help_analyzer.js +680 -680
- package/src/core/cli_parameter_handler.js +132 -132
- package/src/core/cli_tools.js +89 -89
- package/src/core/coordination/index.js +16 -16
- package/src/core/coordination/nodejs/AdapterManager.js +102 -102
- package/src/core/coordination/nodejs/CLCommunication.js +132 -132
- package/src/core/coordination/nodejs/CLIIntegrationManager.js +272 -272
- package/src/core/coordination/nodejs/HealthChecker.js +76 -76
- package/src/core/coordination/nodejs/HookDeploymentManager.js +463 -274
- package/src/core/coordination/nodejs/StatisticsCollector.js +71 -71
- package/src/core/coordination/nodejs/index.js +90 -90
- package/src/core/coordination/nodejs/utils/Logger.js +29 -29
- package/src/core/enhanced_installer.js +479 -479
- package/src/core/enhanced_uninstaller.js +638 -638
- package/src/core/error_handler.js +406 -406
- package/src/core/installer.js +32 -32
- package/src/core/memory_manager.js +83 -83
- package/src/core/multilingual/language-pattern-manager.js +172 -0
- package/src/core/rest_client.js +160 -160
- package/src/core/smart_router.js +261 -249
- package/src/core/upgrade_manager.js +48 -20
- package/src/data_encryption.js +143 -143
- package/src/data_structures.js +440 -440
- package/src/deploy.js +55 -55
- package/src/index.js +30 -30
- package/src/test/cli-availability-checker.js +194 -194
- package/src/test/test-environment.js +289 -289
- package/src/utils/helpers.js +35 -35
- package/src/utils.js +921 -921
- package/src/weatherProcessor.js +228 -228
- package/test/multilingual/hook-deployment.test.js +91 -0
- package/test/multilingual/language-pattern-manager.test.js +140 -0
- 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
|
+
};
|