stigmergy 1.0.94 → 1.0.95
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/bin/stigmergy +26 -12
- package/docs/HASH_TABLE.md +83 -0
- package/docs/WEATHER_PROCESSOR_API.md +230 -0
- package/docs/best_practices.md +135 -0
- package/docs/development_guidelines.md +392 -0
- package/docs/requirements_specification.md +148 -0
- package/docs/system_design.md +314 -0
- package/docs/tdd_implementation_plan.md +384 -0
- package/docs/test_report.md +49 -0
- package/examples/calculator-example.js +72 -0
- package/examples/json-validation-example.js +64 -0
- package/examples/rest-client-example.js +52 -0
- package/package.json +21 -17
- package/scripts/post-deployment-config.js +9 -2
- package/src/auth.js +171 -0
- package/src/auth_command.js +195 -0
- package/src/calculator.js +220 -0
- package/src/core/cli_help_analyzer.js +625 -562
- package/src/core/cli_parameter_handler.js +121 -0
- package/src/core/cli_tools.js +89 -0
- package/src/core/error_handler.js +307 -0
- package/src/core/memory_manager.js +76 -0
- package/src/core/smart_router.js +133 -0
- package/src/deploy.js +50 -0
- package/src/main_english.js +642 -719
- package/src/main_fixed.js +1035 -0
- package/src/utils.js +529 -0
- package/src/weatherProcessor.js +205 -0
- package/test/calculator.test.js +215 -0
- package/test/collision-test.js +26 -0
- package/test/csv-processing-test.js +36 -0
- package/test/e2e/claude-cli-test.js +128 -0
- package/test/e2e/collaboration-test.js +75 -0
- package/test/e2e/comprehensive-test.js +431 -0
- package/test/e2e/error-handling-test.js +90 -0
- package/test/e2e/individual-tool-test.js +143 -0
- package/test/e2e/other-cli-test.js +130 -0
- package/test/e2e/qoder-cli-test.js +128 -0
- package/test/e2e/run-e2e-tests.js +73 -0
- package/test/e2e/test-data.js +88 -0
- package/test/e2e/test-utils.js +222 -0
- package/test/hash-table-demo.js +33 -0
- package/test/hash-table-test.js +26 -0
- package/test/json-validation-test.js +164 -0
- package/test/rest-client-test.js +56 -0
- package/test/unit/calculator-full.test.js +191 -0
- package/test/unit/calculator-simple.test.js +96 -0
- package/test/unit/calculator.test.js +97 -0
- package/test/unit/cli_parameter_handler.test.js +116 -0
- package/test/weather-processor.test.js +104 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Stigmergy CLI - 测试报告
|
|
2
|
+
|
|
3
|
+
## 概述
|
|
4
|
+
本次测试验证了Stigmergy CLI系统的核心模块,包括智能路由、CLI帮助分析和内存管理模块。
|
|
5
|
+
|
|
6
|
+
## 测试结果
|
|
7
|
+
|
|
8
|
+
### 测试套件执行情况
|
|
9
|
+
- **通过**: 3/3 测试套件
|
|
10
|
+
- **失败**: 0/3 测试套件
|
|
11
|
+
- **总测试数**: 20个测试用例
|
|
12
|
+
- **通过率**: 100%
|
|
13
|
+
|
|
14
|
+
### 详细测试结果
|
|
15
|
+
|
|
16
|
+
#### 1. SmartRouter 测试套件
|
|
17
|
+
- **总测试数**: 11个测试用例
|
|
18
|
+
- **通过**: 11/11
|
|
19
|
+
- **测试内容**:
|
|
20
|
+
- 构造函数测试
|
|
21
|
+
- 关键字提取功能测试
|
|
22
|
+
- 智能路由决策测试
|
|
23
|
+
|
|
24
|
+
#### 2. CLIHelpAnalyzer 测试套件
|
|
25
|
+
- **总测试数**: 6个测试用例
|
|
26
|
+
- **通过**: 6/6
|
|
27
|
+
- **测试内容**:
|
|
28
|
+
- 构造函数测试
|
|
29
|
+
- 初始化功能测试
|
|
30
|
+
- 缓存过期检查测试
|
|
31
|
+
- 文件存在性检查测试
|
|
32
|
+
|
|
33
|
+
#### 3. MemoryManager 测试套件
|
|
34
|
+
- **总测试数**: 3个测试用例
|
|
35
|
+
- **通过**: 3/3
|
|
36
|
+
- **测试内容**:
|
|
37
|
+
- 构造函数测试
|
|
38
|
+
- 全局内存获取测试
|
|
39
|
+
- 内存文件读写测试
|
|
40
|
+
|
|
41
|
+
## 代码覆盖率
|
|
42
|
+
当前测试覆盖率尚未达到预设阈值:
|
|
43
|
+
- **语句覆盖率**: 7.51% (目标: 80%)
|
|
44
|
+
- **分支覆盖率**: 7.84% (目标: 75%)
|
|
45
|
+
- **函数覆盖率**: 12.39% (目标: 85%)
|
|
46
|
+
- **行覆盖率**: 7.6% (目标: 80%)
|
|
47
|
+
|
|
48
|
+
## 结论
|
|
49
|
+
所有单元测试均已通过,验证了核心模块的基本功能。下一步需要增加更多测试用例以提高代码覆盖率。
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example usage of the Calculator class
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const Calculator = require('../src/calculator');
|
|
6
|
+
|
|
7
|
+
// Create a new calculator instance
|
|
8
|
+
const calc = new Calculator();
|
|
9
|
+
|
|
10
|
+
// Basic operations
|
|
11
|
+
console.log('=== Basic Operations ===');
|
|
12
|
+
console.log(`Addition: 5 + 3 = ${calc.add(5, 3)}`);
|
|
13
|
+
console.log(`Subtraction: 10 - 4 = ${calc.subtract(10, 4)}`);
|
|
14
|
+
console.log(`Multiplication: 6 * 7 = ${calc.multiply(6, 7)}`);
|
|
15
|
+
console.log(`Division: 15 / 3 = ${calc.divide(15, 3)}`);
|
|
16
|
+
console.log(`Power: 2^8 = ${calc.power(2, 8)}`);
|
|
17
|
+
console.log(`Square Root: √64 = ${calc.sqrt(64)}`);
|
|
18
|
+
console.log(`Factorial: 5! = ${calc.factorial(5)}`);
|
|
19
|
+
console.log(`Percentage: 25 is what percent of 200 = ${calc.percentage(25, 200)}%`);
|
|
20
|
+
|
|
21
|
+
// Chained calculations
|
|
22
|
+
console.log('\n=== Chained Calculations ===');
|
|
23
|
+
const result1 = calc.chain(10)
|
|
24
|
+
.add(5)
|
|
25
|
+
.multiply(2)
|
|
26
|
+
.subtract(10)
|
|
27
|
+
.divide(4)
|
|
28
|
+
.equals();
|
|
29
|
+
|
|
30
|
+
console.log(`((10 + 5) * 2 - 10) / 4 = ${result1}`);
|
|
31
|
+
|
|
32
|
+
// More complex chained calculation
|
|
33
|
+
const result2 = calc.chain(2)
|
|
34
|
+
.power(3)
|
|
35
|
+
.add(1)
|
|
36
|
+
.multiply(2)
|
|
37
|
+
.sqrt()
|
|
38
|
+
.equals();
|
|
39
|
+
|
|
40
|
+
console.log(`√(((2^3) + 1) * 2) = ${result2}`);
|
|
41
|
+
|
|
42
|
+
// Getting intermediate values
|
|
43
|
+
console.log('\n=== Intermediate Values ===');
|
|
44
|
+
const chain = calc.chain(100)
|
|
45
|
+
.subtract(20) // 80
|
|
46
|
+
.divide(4) // 20
|
|
47
|
+
.add(5); // 25
|
|
48
|
+
|
|
49
|
+
console.log(`Intermediate result: ${chain.value()}`);
|
|
50
|
+
|
|
51
|
+
const finalResult = chain.multiply(2).equals(); // 50
|
|
52
|
+
console.log(`Final result: ${finalResult}`);
|
|
53
|
+
|
|
54
|
+
// Error handling examples
|
|
55
|
+
console.log('\n=== Error Handling ===');
|
|
56
|
+
try {
|
|
57
|
+
calc.divide(10, 0);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.log(`Error caught: ${error.message}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
calc.sqrt(-1);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.log(`Error caught: ${error.message}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
calc.chain(10).divide(0);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.log(`Error caught: ${error.message}`);
|
|
72
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Example usage of parseAndValidateJSON function
|
|
2
|
+
|
|
3
|
+
const { parseAndValidateJSON } = require('../src/utils');
|
|
4
|
+
|
|
5
|
+
// Example 1: Basic JSON parsing
|
|
6
|
+
console.log('=== Example 1: Basic JSON Parsing ===');
|
|
7
|
+
try {
|
|
8
|
+
const jsonData = '{"name": "Alice", "age": 25, "city": "New York"}';
|
|
9
|
+
const parsed = parseAndValidateJSON(jsonData);
|
|
10
|
+
console.log('Parsed data:', parsed);
|
|
11
|
+
} catch (error) {
|
|
12
|
+
console.error('Error:', error.message);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Example 2: JSON parsing with schema validation
|
|
16
|
+
console.log('\n=== Example 2: JSON Parsing with Schema Validation ===');
|
|
17
|
+
try {
|
|
18
|
+
const jsonData = '{"id": 123, "name": "Bob", "email": "bob@example.com", "active": true}';
|
|
19
|
+
|
|
20
|
+
// Define a schema for validation
|
|
21
|
+
const userSchema = {
|
|
22
|
+
required: ['id', 'name', 'email'],
|
|
23
|
+
properties: {
|
|
24
|
+
id: { type: 'number' },
|
|
25
|
+
name: { type: 'string' },
|
|
26
|
+
email: { type: 'string' },
|
|
27
|
+
active: { type: 'boolean' }
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const validatedData = parseAndValidateJSON(jsonData, userSchema);
|
|
32
|
+
console.log('Validated data:', validatedData);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error('Error:', error.message);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Example 3: Handling invalid JSON
|
|
38
|
+
console.log('\n=== Example 3: Handling Invalid JSON ===');
|
|
39
|
+
try {
|
|
40
|
+
const invalidJson = '{"name": "Charlie", "age":}';
|
|
41
|
+
const result = parseAndValidateJSON(invalidJson);
|
|
42
|
+
console.log('Result:', result);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error('Error:', error.message);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Example 4: Handling schema validation errors
|
|
48
|
+
console.log('\n=== Example 4: Handling Schema Validation Errors ===');
|
|
49
|
+
try {
|
|
50
|
+
const jsonData = '{"name": "David", "age": "thirty"}'; // age should be a number
|
|
51
|
+
|
|
52
|
+
const schema = {
|
|
53
|
+
required: ['name', 'age'],
|
|
54
|
+
properties: {
|
|
55
|
+
name: { type: 'string' },
|
|
56
|
+
age: { type: 'number' }
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const result = parseAndValidateJSON(jsonData, schema);
|
|
61
|
+
console.log('Result:', result);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error('Error:', error.message);
|
|
64
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Example usage of the RESTClient class
|
|
2
|
+
|
|
3
|
+
const { RESTClient } = require('../src/utils');
|
|
4
|
+
|
|
5
|
+
// Create a new REST client instance
|
|
6
|
+
const client = new RESTClient('https://jsonplaceholder.typicode.com');
|
|
7
|
+
|
|
8
|
+
// Example 1: GET request
|
|
9
|
+
async function getPost() {
|
|
10
|
+
try {
|
|
11
|
+
const response = await client.get('/posts/1');
|
|
12
|
+
console.log('Post Title:', response.data.title);
|
|
13
|
+
} catch (error) {
|
|
14
|
+
console.error('Error fetching post:', error.message);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Example 2: POST request
|
|
19
|
+
async function createPost() {
|
|
20
|
+
try {
|
|
21
|
+
const newPost = {
|
|
22
|
+
title: 'My New Post',
|
|
23
|
+
body: 'This is the content of my new post',
|
|
24
|
+
userId: 1
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const response = await client.post('/posts', newPost);
|
|
28
|
+
console.log('Created Post ID:', response.data.id);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error('Error creating post:', error.message);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Example 3: Using custom headers
|
|
35
|
+
async function getRequestWithHeaders() {
|
|
36
|
+
try {
|
|
37
|
+
const clientWithAuth = new RESTClient('https://jsonplaceholder.typicode.com', {
|
|
38
|
+
'Authorization': 'Bearer your-token-here',
|
|
39
|
+
'X-Custom-Header': 'custom-value'
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const response = await clientWithAuth.get('/users/1');
|
|
43
|
+
console.log('User Name:', response.data.name);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error('Error fetching user:', error.message);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Run the examples
|
|
50
|
+
getPost();
|
|
51
|
+
createPost();
|
|
52
|
+
getRequestWithHeaders();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stigmergy",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.95",
|
|
4
4
|
"description": "Stigmergy CLI - Multi-Agents Cross-AI CLI Tools Collaboration System",
|
|
5
5
|
"main": "src/main_english.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,9 +8,12 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node src/main_english.js",
|
|
11
|
-
"test": "
|
|
12
|
-
"test:
|
|
13
|
-
"test:
|
|
11
|
+
"test": "jest",
|
|
12
|
+
"test:unit": "jest tests/unit",
|
|
13
|
+
"test:integration": "jest tests/integration",
|
|
14
|
+
"test:e2e": "jest tests/e2e",
|
|
15
|
+
"coverage": "jest --coverage",
|
|
16
|
+
"test:watch": "jest --watch",
|
|
14
17
|
"build": "node scripts/build.js",
|
|
15
18
|
"deploy": "node src/deploy.js",
|
|
16
19
|
"status": "node src/index.js status",
|
|
@@ -69,29 +72,30 @@
|
|
|
69
72
|
"node": ">=16.0.0"
|
|
70
73
|
},
|
|
71
74
|
"dependencies": {
|
|
75
|
+
"chalk": "^4.1.2",
|
|
76
|
+
"child_process": "^1.0.2",
|
|
77
|
+
"chokidar": "^3.5.3",
|
|
72
78
|
"commander": "^12.0.0",
|
|
79
|
+
"crypto": "^1.0.1",
|
|
80
|
+
"events": "^3.3.0",
|
|
81
|
+
"figures": "^3.2.0",
|
|
82
|
+
"fs-extra": "^11.1.1",
|
|
83
|
+
"glob": "^10.3.10",
|
|
73
84
|
"inquirer": "^8.2.6",
|
|
74
|
-
"chalk": "^4.1.2",
|
|
75
85
|
"js-yaml": "^4.1.0",
|
|
76
86
|
"node-fetch": "^2.6.7",
|
|
77
|
-
"
|
|
78
|
-
"fs-extra": "^11.1.1",
|
|
79
|
-
"path": "^0.12.7",
|
|
87
|
+
"ora": "^5.4.1",
|
|
80
88
|
"os": "^0.1.2",
|
|
81
|
-
"
|
|
82
|
-
"events": "^3.3.0",
|
|
83
|
-
"util": "^0.12.5",
|
|
89
|
+
"path": "^0.12.7",
|
|
84
90
|
"semver": "^7.5.4",
|
|
85
|
-
"glob": "^10.3.10",
|
|
86
|
-
"chokidar": "^3.5.3",
|
|
87
91
|
"table": "^6.8.1",
|
|
88
|
-
"
|
|
89
|
-
"ora": "^5.4.1"
|
|
92
|
+
"util": "^0.12.5"
|
|
90
93
|
},
|
|
91
94
|
"devDependencies": {
|
|
92
95
|
"eslint": "^8.50.0",
|
|
93
|
-
"
|
|
94
|
-
"nodemon": "^3.0.1"
|
|
96
|
+
"jest": "^30.2.0",
|
|
97
|
+
"nodemon": "^3.0.1",
|
|
98
|
+
"prettier": "^3.0.3"
|
|
95
99
|
},
|
|
96
100
|
"config": {
|
|
97
101
|
"encoding": "ansi",
|
|
@@ -63,9 +63,16 @@ class PostDeploymentConfigurer {
|
|
|
63
63
|
// Check if an installation script exists
|
|
64
64
|
async checkInstallScript(toolName) {
|
|
65
65
|
const tool = CLI_TOOLS[toolName];
|
|
66
|
-
if (!tool) return false;
|
|
66
|
+
if (!tool) return { exists: false, path: '' };
|
|
67
|
+
|
|
68
|
+
// Mapping for tool names that don't match their adapter directory names
|
|
69
|
+
const toolNameToAdapterDir = {
|
|
70
|
+
'qodercli': 'qoder',
|
|
71
|
+
'qwencode': 'qwen'
|
|
72
|
+
};
|
|
73
|
+
const adapterDirName = toolNameToAdapterDir[toolName] || toolName;
|
|
74
|
+
const scriptPath = path.join(this.stigmergyAssetsDir, adapterDirName, tool.installScript);
|
|
67
75
|
|
|
68
|
-
const scriptPath = path.join(this.stigmergyAssetsDir, toolName, tool.installScript);
|
|
69
76
|
try {
|
|
70
77
|
await fs.access(scriptPath);
|
|
71
78
|
return { exists: true, path: scriptPath };
|
package/src/auth.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication module for the Stigmergy CLI system.
|
|
3
|
+
* Provides user authentication functionality including password hashing and token management.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Custom exception for authentication failures.
|
|
10
|
+
*/
|
|
11
|
+
class AuthenticationError extends Error {
|
|
12
|
+
constructor(message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = 'AuthenticationError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Handles user authentication operations including registration, login, and session management.
|
|
20
|
+
*/
|
|
21
|
+
class UserAuthenticator {
|
|
22
|
+
/**
|
|
23
|
+
* Create a new UserAuthenticator instance.
|
|
24
|
+
*/
|
|
25
|
+
constructor() {
|
|
26
|
+
// In production, this would be stored in a secure database
|
|
27
|
+
this._users = new Map();
|
|
28
|
+
this._sessions = new Map();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Register a new user with the provided username and password.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} username - The user's username
|
|
35
|
+
* @param {string} password - The user's password
|
|
36
|
+
* @returns {boolean} True if registration successful, false if username already exists
|
|
37
|
+
* @throws {Error} If username or password is invalid
|
|
38
|
+
*/
|
|
39
|
+
registerUser(username, password) {
|
|
40
|
+
if (!username || !password) {
|
|
41
|
+
throw new Error('Username and password cannot be empty');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (username.length < 3) {
|
|
45
|
+
throw new Error('Username must be at least 3 characters long');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (password.length < 8) {
|
|
49
|
+
throw new Error('Password must be at least 8 characters long');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (this._users.has(username)) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Hash the password with a salt
|
|
57
|
+
const salt = crypto.randomBytes(16).toString('hex');
|
|
58
|
+
const passwordHash = this._hashPassword(password, salt);
|
|
59
|
+
|
|
60
|
+
this._users.set(username, {
|
|
61
|
+
passwordHash: passwordHash,
|
|
62
|
+
salt: salt
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Authenticate a user with the provided credentials.
|
|
70
|
+
*
|
|
71
|
+
* @param {string} username - The user's username
|
|
72
|
+
* @param {string} password - The user's password
|
|
73
|
+
* @returns {string} Session token if authentication is successful
|
|
74
|
+
* @throws {AuthenticationError} If authentication fails
|
|
75
|
+
*/
|
|
76
|
+
authenticateUser(username, password) {
|
|
77
|
+
if (!this._users.has(username)) {
|
|
78
|
+
throw new AuthenticationError('Invalid username or password');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const userData = this._users.get(username);
|
|
82
|
+
const passwordHash = this._hashPassword(password, userData.salt);
|
|
83
|
+
|
|
84
|
+
if (passwordHash !== userData.passwordHash) {
|
|
85
|
+
throw new AuthenticationError('Invalid username or password');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Generate session token
|
|
89
|
+
const sessionToken = crypto.randomBytes(32).toString('base64url');
|
|
90
|
+
this._sessions.set(sessionToken, {
|
|
91
|
+
username: username,
|
|
92
|
+
createdAt: Date.now()
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return sessionToken;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Validate a session token and return the associated username.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} sessionToken - The session token to validate
|
|
102
|
+
* @returns {string|null} Username if session is valid, null otherwise
|
|
103
|
+
*/
|
|
104
|
+
validateSession(sessionToken) {
|
|
105
|
+
if (!this._sessions.has(sessionToken)) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// In production, you would check session expiration here
|
|
110
|
+
return this._sessions.get(sessionToken).username;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Invalidate a session token.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} sessionToken - The session token to invalidate
|
|
117
|
+
* @returns {boolean} True if session was invalidated, false if token was not found
|
|
118
|
+
*/
|
|
119
|
+
logout(sessionToken) {
|
|
120
|
+
if (this._sessions.has(sessionToken)) {
|
|
121
|
+
this._sessions.delete(sessionToken);
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Hash a password with the provided salt using PBKDF2.
|
|
129
|
+
*
|
|
130
|
+
* @param {string} password - The password to hash
|
|
131
|
+
* @param {string} salt - The salt to use for hashing
|
|
132
|
+
* @returns {string} The hashed password
|
|
133
|
+
* @private
|
|
134
|
+
*/
|
|
135
|
+
_hashPassword(password, salt) {
|
|
136
|
+
return crypto.pbkdf2Sync(
|
|
137
|
+
password,
|
|
138
|
+
salt,
|
|
139
|
+
100000, // iterations
|
|
140
|
+
32, // key length
|
|
141
|
+
'sha256' // digest
|
|
142
|
+
).toString('hex');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Helper function to authenticate a user and return a session token.
|
|
148
|
+
*
|
|
149
|
+
* @param {UserAuthenticator} authenticator - The authenticator instance
|
|
150
|
+
* @param {string} username - The user's username
|
|
151
|
+
* @param {string} password - The user's password
|
|
152
|
+
* @returns {[boolean, string]} A tuple containing success status and message/token
|
|
153
|
+
*/
|
|
154
|
+
function authenticateAndGetToken(authenticator, username, password) {
|
|
155
|
+
try {
|
|
156
|
+
const token = authenticator.authenticateUser(username, password);
|
|
157
|
+
return [true, token];
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (error instanceof AuthenticationError) {
|
|
160
|
+
return [false, error.message];
|
|
161
|
+
} else {
|
|
162
|
+
return [false, `Authentication error: ${error.message}`];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
module.exports = {
|
|
168
|
+
UserAuthenticator,
|
|
169
|
+
AuthenticationError,
|
|
170
|
+
authenticateAndGetToken
|
|
171
|
+
};
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication command handler for the Stigmergy CLI.
|
|
3
|
+
* Provides CLI commands for user registration, login, and session management.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { UserAuthenticator, authenticateAndGetToken } = require('./auth');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get the path to the authentication data file.
|
|
12
|
+
* @returns {string} Path to the auth data file
|
|
13
|
+
*/
|
|
14
|
+
function getAuthDataPath() {
|
|
15
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
16
|
+
const configDir = path.join(homeDir, '.stigmergy');
|
|
17
|
+
|
|
18
|
+
// Create config directory if it doesn't exist
|
|
19
|
+
if (!fs.existsSync(configDir)) {
|
|
20
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return path.join(configDir, 'auth.json');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Load authentication data from file.
|
|
28
|
+
* @param {UserAuthenticator} authenticator - The authenticator instance
|
|
29
|
+
*/
|
|
30
|
+
function loadAuthData(authenticator) {
|
|
31
|
+
try {
|
|
32
|
+
const authFile = getAuthDataPath();
|
|
33
|
+
if (fs.existsSync(authFile)) {
|
|
34
|
+
const data = JSON.parse(fs.readFileSync(authFile, 'utf8'));
|
|
35
|
+
|
|
36
|
+
// Load users
|
|
37
|
+
if (data.users) {
|
|
38
|
+
for (const [username, userData] of Object.entries(data.users)) {
|
|
39
|
+
authenticator._users.set(username, userData);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Load sessions
|
|
44
|
+
if (data.sessions) {
|
|
45
|
+
for (const [token, sessionData] of Object.entries(data.sessions)) {
|
|
46
|
+
authenticator._sessions.set(token, sessionData);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.warn('[WARN] Could not load authentication data:', error.message);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Save authentication data to file.
|
|
57
|
+
* @param {UserAuthenticator} authenticator - The authenticator instance
|
|
58
|
+
*/
|
|
59
|
+
function saveAuthData(authenticator) {
|
|
60
|
+
try {
|
|
61
|
+
const authFile = getAuthDataPath();
|
|
62
|
+
|
|
63
|
+
// Convert Maps to objects for JSON serialization
|
|
64
|
+
const users = {};
|
|
65
|
+
for (const [username, userData] of authenticator._users.entries()) {
|
|
66
|
+
users[username] = userData;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const sessions = {};
|
|
70
|
+
for (const [token, sessionData] of authenticator._sessions.entries()) {
|
|
71
|
+
sessions[token] = sessionData;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const data = {
|
|
75
|
+
users,
|
|
76
|
+
sessions
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
fs.writeFileSync(authFile, JSON.stringify(data, null, 2));
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.warn('[WARN] Could not save authentication data:', error.message);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Handle user registration.
|
|
87
|
+
* @param {string} username - The username to register
|
|
88
|
+
* @param {string} password - The password for the user
|
|
89
|
+
*/
|
|
90
|
+
function handleRegister(username, password) {
|
|
91
|
+
const authenticator = new UserAuthenticator();
|
|
92
|
+
loadAuthData(authenticator);
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const success = authenticator.registerUser(username, password);
|
|
96
|
+
if (success) {
|
|
97
|
+
saveAuthData(authenticator);
|
|
98
|
+
console.log(`[SUCCESS] User '${username}' registered successfully`);
|
|
99
|
+
} else {
|
|
100
|
+
console.log(`[ERROR] Username '${username}' already exists`);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.log(`[ERROR] Registration failed: ${error.message}`);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Handle user login.
|
|
111
|
+
* @param {string} username - The username to login
|
|
112
|
+
* @param {string} password - The password for the user
|
|
113
|
+
*/
|
|
114
|
+
function handleLogin(username, password) {
|
|
115
|
+
const authenticator = new UserAuthenticator();
|
|
116
|
+
loadAuthData(authenticator);
|
|
117
|
+
|
|
118
|
+
const [success, result] = authenticateAndGetToken(authenticator, username, password);
|
|
119
|
+
|
|
120
|
+
if (success) {
|
|
121
|
+
const token = result;
|
|
122
|
+
saveAuthData(authenticator);
|
|
123
|
+
|
|
124
|
+
// Also save the token to a session file for easy access
|
|
125
|
+
const sessionFile = path.join(path.dirname(getAuthDataPath()), 'session.token');
|
|
126
|
+
fs.writeFileSync(sessionFile, token);
|
|
127
|
+
|
|
128
|
+
console.log(`[SUCCESS] Login successful`);
|
|
129
|
+
console.log(`Session token: ${token}`);
|
|
130
|
+
} else {
|
|
131
|
+
console.log(`[ERROR] Login failed: ${result}`);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Handle user logout.
|
|
138
|
+
*/
|
|
139
|
+
function handleLogout() {
|
|
140
|
+
const authenticator = new UserAuthenticator();
|
|
141
|
+
loadAuthData(authenticator);
|
|
142
|
+
|
|
143
|
+
// Read the current session token
|
|
144
|
+
const sessionFile = path.join(path.dirname(getAuthDataPath()), 'session.token');
|
|
145
|
+
|
|
146
|
+
if (!fs.existsSync(sessionFile)) {
|
|
147
|
+
console.log(`[ERROR] No active session found`);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const token = fs.readFileSync(sessionFile, 'utf8').trim();
|
|
152
|
+
|
|
153
|
+
const success = authenticator.logout(token);
|
|
154
|
+
if (success) {
|
|
155
|
+
saveAuthData(authenticator);
|
|
156
|
+
fs.unlinkSync(sessionFile); // Remove the session file
|
|
157
|
+
console.log(`[SUCCESS] Logged out successfully`);
|
|
158
|
+
} else {
|
|
159
|
+
console.log(`[ERROR] Logout failed: Invalid session`);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Check if user is authenticated.
|
|
166
|
+
*/
|
|
167
|
+
function handleStatus() {
|
|
168
|
+
const authenticator = new UserAuthenticator();
|
|
169
|
+
loadAuthData(authenticator);
|
|
170
|
+
|
|
171
|
+
// Read the current session token
|
|
172
|
+
const sessionFile = path.join(path.dirname(getAuthDataPath()), 'session.token');
|
|
173
|
+
|
|
174
|
+
if (!fs.existsSync(sessionFile)) {
|
|
175
|
+
console.log(`[INFO] No active session`);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const token = fs.readFileSync(sessionFile, 'utf8').trim();
|
|
180
|
+
const username = authenticator.validateSession(token);
|
|
181
|
+
|
|
182
|
+
if (username) {
|
|
183
|
+
console.log(`[INFO] Authenticated as: ${username}`);
|
|
184
|
+
} else {
|
|
185
|
+
console.log(`[INFO] Session expired or invalid`);
|
|
186
|
+
fs.unlinkSync(sessionFile); // Remove the invalid session file
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
module.exports = {
|
|
191
|
+
handleRegister,
|
|
192
|
+
handleLogin,
|
|
193
|
+
handleLogout,
|
|
194
|
+
handleStatus
|
|
195
|
+
};
|