x-fidelity 1.5.1 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +71 -21
- package/dist/archetypes/index.js +1 -1
- package/dist/core/cli.js +12 -3
- package/dist/core/engine.js +3 -2
- package/dist/core/engine.test.js +21 -6
- package/dist/facts/index.js +5 -1
- package/dist/facts/openaiAnalysisFacts.js +2 -1
- package/dist/facts/openaiAnalysisFacts.test.js +13 -9
- package/dist/jest.setup.js +12 -0
- package/dist/operators/index.js +5 -1
- package/dist/operators/index.test.js +2 -4
- package/dist/rules/index.js +3 -2
- package/dist/rules/index.test.js +6 -4
- package/dist/server/configServer.js +2 -1
- package/dist/utils/openaiUtils.js +7 -0
- package/dist/utils/telemetry.js +8 -6
- package/jest.config.js +2 -1
- package/package.json +1 -1
- package/src/archetypes/index.ts +1 -1
- package/src/core/cli.ts +12 -3
- package/src/core/engine.test.ts +22 -6
- package/src/core/engine.ts +4 -3
- package/src/facts/index.ts +7 -1
- package/src/facts/openaiAnalysisFacts.test.ts +18 -14
- package/src/facts/openaiAnalysisFacts.ts +3 -2
- package/src/jest.setup.ts +12 -0
- package/src/operators/index.test.ts +2 -3
- package/src/operators/index.ts +7 -1
- package/src/rules/index.test.ts +7 -4
- package/src/rules/index.ts +4 -3
- package/src/server/configServer.ts +3 -2
- package/src/utils/config.ts +1 -2
- package/src/utils/openaiUtils.ts +5 -0
- package/src/utils/telemetry.ts +8 -6
- package/tsconfig.json +8 -7
- /package/.github/workflows/{code-review.yml → code-review.yml.disabled} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
# [1.6.0](https://github.com/zotoio/x-fidelity/compare/v1.5.1...v1.6.0) (2024-07-27)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **telemetry:** option silent ([e72b366](https://github.com/zotoio/x-fidelity/commit/e72b36603d4a682fee993a330393f453926a187a))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **config:** local filesystem config, openai option, telemetry option ([16f481c](https://github.com/zotoio/x-fidelity/commit/16f481cbb5c5e2fefd1b7cfcc6e9371461609f85))
|
|
12
|
+
|
|
1
13
|
## [1.5.1](https://github.com/zotoio/x-fidelity/compare/v1.5.0...v1.5.1) (2024-07-25)
|
|
2
14
|
|
|
3
15
|
|
package/README.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
x-fidelity is an advanced CLI tool designed to enforce opinionated framework adherence checks within a codebase. It provides a flexible and extensible way to ensure your projects follow specific standards and best practices.
|
|
4
4
|
|
|
5
|
+
```
|
|
6
|
+
=====================================
|
|
7
|
+
__ __ ________ ______
|
|
8
|
+
| ## | ## | ######## \######
|
|
9
|
+
\##\/ ## ______ | ##__ | ##
|
|
10
|
+
>## ## | \| ## \ | ##
|
|
11
|
+
/ ####\ \######| ###### | ##
|
|
12
|
+
| ## \##\ | ## _| ##_
|
|
13
|
+
| ## | ## | ## | ## \
|
|
14
|
+
\## \## \## \######
|
|
15
|
+
|
|
16
|
+
-------------------------------------
|
|
17
|
+
```
|
|
18
|
+
|
|
5
19
|
## Quick Start
|
|
6
20
|
|
|
7
21
|
1. Install x-fidelity:
|
|
@@ -20,20 +34,6 @@ x-fidelity is an advanced CLI tool designed to enforce opinionated framework adh
|
|
|
20
34
|
xfidelity --help
|
|
21
35
|
```
|
|
22
36
|
|
|
23
|
-
```
|
|
24
|
-
=====================================
|
|
25
|
-
__ __ ________ ______
|
|
26
|
-
| ## | ## | ######## \######
|
|
27
|
-
\##\/ ## ______ | ##__ | ##
|
|
28
|
-
>## ## | \| ## \ | ##
|
|
29
|
-
/ ####\ \######| ###### | ##
|
|
30
|
-
| ## \##\ | ## _| ##_
|
|
31
|
-
| ## | ## | ## | ## \
|
|
32
|
-
\## \## \## \######
|
|
33
|
-
|
|
34
|
-
-------------------------------------
|
|
35
|
-
```
|
|
36
|
-
|
|
37
37
|
## Table of Contents
|
|
38
38
|
|
|
39
39
|
1. [Intent and Purpose](#intent-and-purpose)
|
|
@@ -97,14 +97,17 @@ xfidelity
|
|
|
97
97
|
Use command-line options for more control:
|
|
98
98
|
|
|
99
99
|
```sh
|
|
100
|
-
xfidelity [-d --dir <directory>] [-c --configServer <url>] [-a --archetype <archetype>] [-m --mode <
|
|
100
|
+
xfidelity [-d --dir <directory>] [-c --configServer <url>] [-a --archetype <archetype>] [-m --mode <cli|server>] [-p --port <port>] [-o --openaiEnabled <boolean>] [-t --telemetryCollector <url>] [-l --localConfig <path>]
|
|
101
101
|
```
|
|
102
102
|
|
|
103
103
|
- `-d --dir <directory>`: Specify the root directory to analyze (default: current directory)
|
|
104
|
-
- `-c --configServer <url>`: URL to fetch the configuration from
|
|
104
|
+
- `-c --configServer <url>`: URL to fetch the configuration from. eg. https://localhost:8888
|
|
105
105
|
- `-a --archetype <archetype>`: Archetype to use for analysis (default: 'node-fullstack')
|
|
106
106
|
- `-m --mode <mode>`: Run mode: 'cli' or 'server' (default: 'cli')
|
|
107
107
|
- `-p --port <port>`: Port number for server mode (default: 8888)
|
|
108
|
+
- `-o --openaiEnabled <boolean>`: Enable OpenAI analysis (default: false)
|
|
109
|
+
- `-t --telemetryCollector <url>`: The URL telemetry data will be sent to for usage analysis
|
|
110
|
+
- `-l --localConfig <path>`: Path to local archetype config and rules
|
|
108
111
|
|
|
109
112
|
Examples:
|
|
110
113
|
|
|
@@ -112,11 +115,14 @@ Examples:
|
|
|
112
115
|
# Use remote config server
|
|
113
116
|
xfidelity --configServer https://localhost:8888
|
|
114
117
|
|
|
115
|
-
# Analyze parent directory with java-microservice archetype
|
|
116
|
-
xfidelity -d .. -a java-microservice -c https://localhost:8888
|
|
118
|
+
# Analyze parent directory with java-microservice archetype and enable OpenAI analysis
|
|
119
|
+
xfidelity -d .. -a java-microservice -c https://localhost:8888 -o true
|
|
117
120
|
|
|
118
|
-
# Run in server mode with custom port
|
|
119
|
-
xfidelity --mode server --port 9999
|
|
121
|
+
# Run in server mode with custom port and specify telemetry collector
|
|
122
|
+
xfidelity --mode server --port 9999 -t https://telemetry.example.com
|
|
123
|
+
|
|
124
|
+
# Use local config and rules
|
|
125
|
+
xfidelity -l /path/to/local/config
|
|
120
126
|
|
|
121
127
|
```
|
|
122
128
|
|
|
@@ -165,6 +171,7 @@ interface ArchetypeConfig {
|
|
|
165
171
|
rules: string[];
|
|
166
172
|
operators: string[];
|
|
167
173
|
facts: string[];
|
|
174
|
+
configUrl?: string;
|
|
168
175
|
config: {
|
|
169
176
|
minimumDependencyVersions: Record<string, string>;
|
|
170
177
|
standardStructure: Record<string, any>;
|
|
@@ -174,6 +181,25 @@ interface ArchetypeConfig {
|
|
|
174
181
|
}
|
|
175
182
|
```
|
|
176
183
|
|
|
184
|
+
## Telemetry
|
|
185
|
+
|
|
186
|
+
x-fidelity now includes telemetry functionality to help improve the tool. Telemetry data is sent to a specified collector URL and includes information about the analysis process, such as:
|
|
187
|
+
|
|
188
|
+
- Archetype used
|
|
189
|
+
- Repository path (anonymized)
|
|
190
|
+
- File count
|
|
191
|
+
- Failure count
|
|
192
|
+
- Host information (platform, CPU, memory)
|
|
193
|
+
- User information (anonymized username, home directory, shell)
|
|
194
|
+
|
|
195
|
+
To specify a telemetry collector, use the `-t` or `--telemetryCollector` option:
|
|
196
|
+
|
|
197
|
+
```sh
|
|
198
|
+
xfidelity -t https://telemetry.example.com
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
If no telemetry collector is specified, telemetry data will not be sent.
|
|
202
|
+
|
|
177
203
|
## Extending x-fidelity
|
|
178
204
|
|
|
179
205
|
x-fidelity is designed to be highly extensible:
|
|
@@ -213,16 +239,40 @@ export const myNewArchetype: ArchetypeConfig = {
|
|
|
213
239
|
To enable AI-powered code analysis:
|
|
214
240
|
|
|
215
241
|
1. Sign up for an [OpenAI API key](https://platform.openai.com).
|
|
216
|
-
2. Set environment
|
|
242
|
+
2. Set the OPENAI_API_KEY environment variable:
|
|
217
243
|
|
|
218
244
|
```sh
|
|
219
245
|
export OPENAI_API_KEY=your_openai_api_key
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
3. Enable OpenAI analysis when running x-fidelity:
|
|
249
|
+
|
|
250
|
+
```sh
|
|
251
|
+
xfidelity -o true
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
You can also set the OpenAI model using an environment variable (optional):
|
|
255
|
+
|
|
256
|
+
```sh
|
|
220
257
|
export OPENAI_MODEL=gpt-4 # Optional, default is gpt-4o
|
|
221
258
|
```
|
|
222
259
|
|
|
223
260
|
> [!IMPORTANT]
|
|
224
261
|
> Be aware of potential costs and data privacy concerns when using OpenAI's API.
|
|
225
262
|
|
|
263
|
+
## Local Configuration
|
|
264
|
+
|
|
265
|
+
You can now use local configuration files for archetypes and rules. To use local configuration, use the `-l` or `--localConfig` option:
|
|
266
|
+
|
|
267
|
+
```sh
|
|
268
|
+
xfidelity -l /path/to/local/config
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
The local config directory should contain:
|
|
272
|
+
|
|
273
|
+
- Archetype configuration files (e.g., `node-fullstack.json`)
|
|
274
|
+
- A `rules` subdirectory containing rule files
|
|
275
|
+
|
|
226
276
|
## Hosting Config Servers
|
|
227
277
|
|
|
228
278
|
To host a config server for x-fidelity:
|
package/dist/archetypes/index.js
CHANGED
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.archetypes = void 0;
|
|
4
4
|
exports.archetypes = {
|
|
5
5
|
'node-fullstack': {
|
|
6
|
-
rules: ['sensitiveLogging-iterative', 'outdatedFramework-global', 'noDatabases-iterative', 'nonStandardDirectoryStructure-global', 'openaiAnalysisTop5-global', 'openaiAnalysisA11y-global'
|
|
6
|
+
rules: ['sensitiveLogging-iterative', 'outdatedFramework-global', 'noDatabases-iterative', 'nonStandardDirectoryStructure-global', 'openaiAnalysisTop5-global', 'openaiAnalysisA11y-global'],
|
|
7
7
|
operators: ['fileContains', 'outdatedFramework', 'nonStandardDirectoryStructure', 'openaiAnalysisHighSeverity'],
|
|
8
8
|
facts: ['repoFilesystemFacts', 'repoDependencyFacts', 'openaiAnalysisFacts'],
|
|
9
9
|
config: {
|
package/dist/core/cli.js
CHANGED
|
@@ -6,15 +6,24 @@ const commander_1 = require("commander");
|
|
|
6
6
|
// Ensure logger is initialized
|
|
7
7
|
if (!logger_1.logger || typeof logger_1.logger.info !== 'function') {
|
|
8
8
|
console.error('Logger is not properly initialized');
|
|
9
|
-
|
|
9
|
+
// Instead of exiting, we'll create a fallback logger
|
|
10
|
+
const fallbackLogger = {
|
|
11
|
+
info: console.log,
|
|
12
|
+
error: console.error,
|
|
13
|
+
warn: console.warn,
|
|
14
|
+
debug: console.debug
|
|
15
|
+
};
|
|
16
|
+
global.logger = fallbackLogger;
|
|
10
17
|
}
|
|
11
18
|
commander_1.program
|
|
12
19
|
.option("-d, --dir <directory>", "The checkout directory to analyze (default: current directory)", ".")
|
|
13
20
|
.option("-a, --archetype <archetype>", "The archetype to use for analysis (default: node-fullstack)", "node-fullstack")
|
|
14
21
|
.option("-c, --configServer <configServer>", "The config server URL for fetching remote archetype configurations and rules")
|
|
22
|
+
.option("-o, --openaiEnabled <boolean>", "Enable OpenAI analysis (default: false)", false)
|
|
23
|
+
.option("-t, --telemetryCollector <telemetryCollector>", "The URL telemetry data will be sent to for usage analysis")
|
|
15
24
|
.option("-m, --mode <mode>", "Run mode: 'cli' or 'server' (default: cli)", "cli")
|
|
16
|
-
.option("-p, --port <port>", "
|
|
17
|
-
.option("-l, --
|
|
25
|
+
.option("-p, --port <port>", "The port to run the server on (default: 8888)", "8888")
|
|
26
|
+
.option("-l, --localConfig <path>", "Path to local archetype config and rules");
|
|
18
27
|
commander_1.program.parse();
|
|
19
28
|
const options = commander_1.program.opts();
|
|
20
29
|
exports.options = options;
|
package/dist/core/engine.js
CHANGED
|
@@ -22,6 +22,7 @@ const operators_1 = require("../operators");
|
|
|
22
22
|
const facts_1 = require("../facts");
|
|
23
23
|
const rules_1 = require("../rules");
|
|
24
24
|
const config_1 = require("../utils/config");
|
|
25
|
+
const openaiUtils_1 = require("../utils/openaiUtils");
|
|
25
26
|
const telemetry_1 = require("../utils/telemetry");
|
|
26
27
|
const child_process_1 = require("child_process");
|
|
27
28
|
const os_1 = __importDefault(require("os"));
|
|
@@ -40,7 +41,7 @@ function analyzeCodebase(repoPath_1) {
|
|
|
40
41
|
});
|
|
41
42
|
const { minimumDependencyVersions, standardStructure } = archetypeConfig.config;
|
|
42
43
|
let openaiSystemPrompt;
|
|
43
|
-
if (
|
|
44
|
+
if ((0, openaiUtils_1.isOpenAIEnabled)()) {
|
|
44
45
|
openaiSystemPrompt = yield (0, openaiAnalysisFacts_1.collectOpenaiAnalysisFacts)(fileData);
|
|
45
46
|
}
|
|
46
47
|
const engine = new json_rules_engine_1.Engine([], { replaceFactsInEventParams: true, allowUndefinedFacts: true });
|
|
@@ -136,7 +137,7 @@ function analyzeCodebase(repoPath_1) {
|
|
|
136
137
|
engine.addFact(fact.name, fact.fn);
|
|
137
138
|
}
|
|
138
139
|
});
|
|
139
|
-
if (
|
|
140
|
+
if ((0, openaiUtils_1.isOpenAIEnabled)() && archetypeConfig.facts.includes('openaiAnalysisFacts')) {
|
|
140
141
|
logger_1.logger.info(`adding additional openai facts to engine..`);
|
|
141
142
|
engine.addFact('openaiAnalysis', openaiAnalysisFacts_1.openaiAnalysis);
|
|
142
143
|
engine.addFact('openaiSystemPrompt', openaiSystemPrompt);
|
package/dist/core/engine.test.js
CHANGED
|
@@ -20,6 +20,20 @@ const facts_1 = require("../facts");
|
|
|
20
20
|
const archetypes_1 = require("../archetypes");
|
|
21
21
|
const config_1 = require("../utils/config");
|
|
22
22
|
const telemetry_1 = require("../utils/telemetry");
|
|
23
|
+
const openaiUtils_1 = require("../utils/openaiUtils");
|
|
24
|
+
jest.mock('../utils/openaiUtils');
|
|
25
|
+
jest.mock('../core/cli', () => ({
|
|
26
|
+
options: {
|
|
27
|
+
dir: 'mockDir',
|
|
28
|
+
archetype: 'node-fullstack',
|
|
29
|
+
configServer: '',
|
|
30
|
+
openaiEnabled: true,
|
|
31
|
+
telemetryCollector: '',
|
|
32
|
+
mode: 'cli',
|
|
33
|
+
port: '8888',
|
|
34
|
+
localConfig: ''
|
|
35
|
+
}
|
|
36
|
+
}));
|
|
23
37
|
jest.mock('json-rules-engine');
|
|
24
38
|
jest.mock('../facts/repoFilesystemFacts');
|
|
25
39
|
jest.mock('../facts/repoDependencyFacts');
|
|
@@ -53,6 +67,8 @@ describe('analyzeCodebase', () => {
|
|
|
53
67
|
getConfig: jest.fn().mockReturnValue(archetypes_1.archetypes['node-fullstack']),
|
|
54
68
|
configServer: ''
|
|
55
69
|
});
|
|
70
|
+
jest.spyOn(console, 'log').mockImplementation(() => { });
|
|
71
|
+
jest.spyOn(console, 'error').mockImplementation(() => { });
|
|
56
72
|
});
|
|
57
73
|
it('should analyze the codebase and return results', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
58
74
|
const mockFileData = [
|
|
@@ -120,8 +136,8 @@ describe('analyzeCodebase', () => {
|
|
|
120
136
|
expect(results).toEqual([]);
|
|
121
137
|
expect(telemetry_1.sendTelemetry).toHaveBeenCalledTimes(2); // Once for start, once for end
|
|
122
138
|
}));
|
|
123
|
-
it('should handle OpenAI analysis when
|
|
124
|
-
|
|
139
|
+
it('should handle OpenAI analysis when OpenAI is enabled', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
140
|
+
openaiUtils_1.isOpenAIEnabled.mockReturnValue(true);
|
|
125
141
|
const mockFileData = [
|
|
126
142
|
{ filePath: 'src/index.ts', fileContent: 'logger.info("Hello, world!");' },
|
|
127
143
|
{ fileName: 'REPO_GLOBAL_CHECK', filePath: 'REPO_GLOBAL_CHECK', fileContent: 'REPO_GLOBAL_CHECK' }
|
|
@@ -146,13 +162,12 @@ describe('analyzeCodebase', () => {
|
|
|
146
162
|
run: engineRunMock
|
|
147
163
|
}));
|
|
148
164
|
yield (0, engine_1.analyzeCodebase)('mockRepoPath', 'node-fullstack');
|
|
149
|
-
expect(engineAddFactMock).toHaveBeenCalledWith('
|
|
150
|
-
expect(engineAddFactMock).toHaveBeenCalledWith('openaiSystemPrompt', 'mock openai system prompt');
|
|
165
|
+
expect(engineAddFactMock).toHaveBeenCalledWith('repoDependencyAnalysis', expect.any(Function));
|
|
151
166
|
expect(telemetry_1.sendTelemetry).toHaveBeenCalledTimes(2); // Once for start, once for end
|
|
152
167
|
delete process.env.OPENAI_API_KEY;
|
|
153
168
|
}));
|
|
154
|
-
it('should not add OpenAI facts when
|
|
155
|
-
|
|
169
|
+
it('should not add OpenAI facts when OpenAI is not enabled', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
170
|
+
openaiUtils_1.isOpenAIEnabled.mockReturnValue(false);
|
|
156
171
|
const mockFileData = [
|
|
157
172
|
{ filePath: 'src/index.ts', fileContent: 'logger.info("Hello, world!");' },
|
|
158
173
|
{ fileName: 'REPO_GLOBAL_CHECK', filePath: 'REPO_GLOBAL_CHECK', fileContent: 'REPO_GLOBAL_CHECK' }
|
package/dist/facts/index.js
CHANGED
|
@@ -20,6 +20,10 @@ const allFacts = {
|
|
|
20
20
|
};
|
|
21
21
|
function loadFacts(factNames) {
|
|
22
22
|
return __awaiter(this, void 0, void 0, function* () {
|
|
23
|
-
return factNames
|
|
23
|
+
return factNames
|
|
24
|
+
.map(name => allFacts[name])
|
|
25
|
+
.filter(fact => fact && (!fact.name.startsWith('openai') ||
|
|
26
|
+
((0, openaiUtils_1.isOpenAIEnabled)() && fact.name.startsWith('openai'))));
|
|
24
27
|
});
|
|
25
28
|
}
|
|
29
|
+
const openaiUtils_1 = require("../utils/openaiUtils");
|
|
@@ -12,8 +12,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
12
12
|
exports.openaiAnalysis = exports.collectOpenaiAnalysisFacts = void 0;
|
|
13
13
|
const logger_1 = require("../utils/logger");
|
|
14
14
|
const openai_1 = require("openai");
|
|
15
|
+
const openaiUtils_1 = require("../utils/openaiUtils");
|
|
15
16
|
let openai;
|
|
16
|
-
if (
|
|
17
|
+
if ((0, openaiUtils_1.isOpenAIEnabled)()) {
|
|
17
18
|
const configuration = {
|
|
18
19
|
apiKey: process.env.OPENAI_API_KEY,
|
|
19
20
|
};
|
|
@@ -12,25 +12,29 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
12
12
|
const openaiAnalysisFacts_1 = require("./openaiAnalysisFacts");
|
|
13
13
|
const openai_1 = require("openai");
|
|
14
14
|
const logger_1 = require("../utils/logger");
|
|
15
|
-
|
|
15
|
+
const openaiUtils_1 = require("../utils/openaiUtils");
|
|
16
|
+
jest.mock('../utils/openaiUtils');
|
|
17
|
+
openaiUtils_1.isOpenAIEnabled.mockReturnValue(true);
|
|
16
18
|
jest.mock('json-rules-engine');
|
|
17
19
|
jest.mock('../utils/logger', () => ({
|
|
18
20
|
logger: {
|
|
19
21
|
debug: jest.fn(),
|
|
20
22
|
error: jest.fn(),
|
|
23
|
+
info: jest.fn(),
|
|
21
24
|
},
|
|
22
25
|
}));
|
|
23
26
|
jest.mock('openai', () => {
|
|
27
|
+
const mockCreate = jest.fn().mockResolvedValue({
|
|
28
|
+
choices: [{ message: { content: '[]' } }]
|
|
29
|
+
});
|
|
24
30
|
return {
|
|
25
|
-
OpenAI: jest.fn().mockImplementation(() => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
create: jest.fn().mockResolvedValue({})
|
|
30
|
-
}
|
|
31
|
+
OpenAI: jest.fn().mockImplementation(() => ({
|
|
32
|
+
chat: {
|
|
33
|
+
completions: {
|
|
34
|
+
create: mockCreate
|
|
31
35
|
}
|
|
32
|
-
}
|
|
33
|
-
})
|
|
36
|
+
}
|
|
37
|
+
}))
|
|
34
38
|
};
|
|
35
39
|
});
|
|
36
40
|
describe('openaiAnalysis', () => {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// jest.setup.ts
|
|
3
|
+
beforeAll(() => {
|
|
4
|
+
jest.spyOn(process, 'exit').mockImplementation((code) => {
|
|
5
|
+
console.log(`process.exit(${code}) called but ignored in tests`);
|
|
6
|
+
// Throwing an error is avoided to prevent Jest worker crashes
|
|
7
|
+
return undefined;
|
|
8
|
+
});
|
|
9
|
+
});
|
|
10
|
+
afterAll(() => {
|
|
11
|
+
process.exit.mockRestore();
|
|
12
|
+
});
|
package/dist/operators/index.js
CHANGED
|
@@ -14,6 +14,7 @@ const outdatedFramework_1 = require("./outdatedFramework");
|
|
|
14
14
|
const fileContains_1 = require("./fileContains");
|
|
15
15
|
const nonStandardDirectoryStructure_1 = require("./nonStandardDirectoryStructure");
|
|
16
16
|
const openaiAnalysisHighSeverity_1 = require("./openaiAnalysisHighSeverity");
|
|
17
|
+
const openaiUtils_1 = require("../utils/openaiUtils");
|
|
17
18
|
const allOperators = {
|
|
18
19
|
outdatedFramework: outdatedFramework_1.outdatedFramework,
|
|
19
20
|
fileContains: fileContains_1.fileContains,
|
|
@@ -22,6 +23,9 @@ const allOperators = {
|
|
|
22
23
|
};
|
|
23
24
|
function loadOperators(operatorNames) {
|
|
24
25
|
return __awaiter(this, void 0, void 0, function* () {
|
|
25
|
-
return operatorNames
|
|
26
|
+
return operatorNames
|
|
27
|
+
.map(name => allOperators[name])
|
|
28
|
+
.filter(operator => operator && (!(operator === null || operator === void 0 ? void 0 : operator.name.startsWith('openai')) ||
|
|
29
|
+
((0, openaiUtils_1.isOpenAIEnabled)() && (operator === null || operator === void 0 ? void 0 : operator.name.startsWith('openai')))));
|
|
26
30
|
});
|
|
27
31
|
}
|
|
@@ -13,7 +13,6 @@ const index_1 = require("./index");
|
|
|
13
13
|
const outdatedFramework_1 = require("./outdatedFramework");
|
|
14
14
|
const fileContains_1 = require("./fileContains");
|
|
15
15
|
const nonStandardDirectoryStructure_1 = require("./nonStandardDirectoryStructure");
|
|
16
|
-
const openaiAnalysisHighSeverity_1 = require("./openaiAnalysisHighSeverity");
|
|
17
16
|
jest.mock('./outdatedFramework');
|
|
18
17
|
jest.mock('./fileContains');
|
|
19
18
|
jest.mock('./nonStandardDirectoryStructure');
|
|
@@ -29,14 +28,13 @@ describe('loadOperators', () => {
|
|
|
29
28
|
expect(result[0]).toBe(outdatedFramework_1.outdatedFramework);
|
|
30
29
|
expect(result[1]).toBe(fileContains_1.fileContains);
|
|
31
30
|
}));
|
|
32
|
-
it('should load
|
|
31
|
+
it('should load only non-openai operators when all names are specified', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
33
32
|
const operatorNames = ['outdatedFramework', 'fileContains', 'nonStandardDirectoryStructure', 'openaiAnalysisHighSeverity'];
|
|
34
33
|
const result = yield (0, index_1.loadOperators)(operatorNames);
|
|
35
|
-
expect(result).toHaveLength(
|
|
34
|
+
expect(result).toHaveLength(3);
|
|
36
35
|
expect(result[0]).toBe(outdatedFramework_1.outdatedFramework);
|
|
37
36
|
expect(result[1]).toBe(fileContains_1.fileContains);
|
|
38
37
|
expect(result[2]).toBe(nonStandardDirectoryStructure_1.nonStandardDirectoryStructure);
|
|
39
|
-
expect(result[3]).toBe(openaiAnalysisHighSeverity_1.openaiAnalysisHighSeverity);
|
|
40
38
|
}));
|
|
41
39
|
it('should return an empty array when no operator names are specified', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
42
40
|
const result = yield (0, index_1.loadOperators)([]);
|
package/dist/rules/index.js
CHANGED
|
@@ -40,6 +40,7 @@ const logger_1 = require("../utils/logger");
|
|
|
40
40
|
const fs = __importStar(require("fs"));
|
|
41
41
|
const path = __importStar(require("path"));
|
|
42
42
|
const axios_1 = __importDefault(require("axios"));
|
|
43
|
+
const openaiUtils_1 = require("../utils/openaiUtils");
|
|
43
44
|
function loadRules(archetype, ruleNames, configServer, logPrefix, localConfigPath) {
|
|
44
45
|
return __awaiter(this, void 0, void 0, function* () {
|
|
45
46
|
const ruleProperties = [];
|
|
@@ -73,7 +74,7 @@ function loadRules(archetype, ruleNames, configServer, logPrefix, localConfigPat
|
|
|
73
74
|
rule = yield loadLocalRule(ruleName);
|
|
74
75
|
}
|
|
75
76
|
if (rule) {
|
|
76
|
-
if (!ruleName.startsWith('openai') || (
|
|
77
|
+
if (!ruleName.startsWith('openai') || ((0, openaiUtils_1.isOpenAIEnabled)() && ruleName.startsWith('openai'))) {
|
|
77
78
|
ruleProperties.push(rule);
|
|
78
79
|
}
|
|
79
80
|
}
|
|
@@ -86,7 +87,7 @@ function loadLocalRule(ruleName) {
|
|
|
86
87
|
return __awaiter(this, void 0, void 0, function* () {
|
|
87
88
|
const fileName = `${ruleName}-rule.json`;
|
|
88
89
|
const filePath = path.join(__dirname, fileName);
|
|
89
|
-
if (!fileName.startsWith('openai') || (
|
|
90
|
+
if (!fileName.startsWith('openai') || ((0, openaiUtils_1.isOpenAIEnabled)() && fileName.startsWith('openai'))) {
|
|
90
91
|
try {
|
|
91
92
|
logger_1.logger.debug(`Loading local rule file: ${filePath}`);
|
|
92
93
|
const fileContent = yield fs.promises.readFile(filePath, 'utf8');
|
package/dist/rules/index.test.js
CHANGED
|
@@ -40,6 +40,8 @@ const axios_1 = __importDefault(require("axios"));
|
|
|
40
40
|
const fs = __importStar(require("fs"));
|
|
41
41
|
const path = __importStar(require("path"));
|
|
42
42
|
const logger_1 = require("../utils/logger");
|
|
43
|
+
const openaiUtils_1 = require("../utils/openaiUtils");
|
|
44
|
+
jest.mock('../utils/openaiUtils');
|
|
43
45
|
jest.mock('axios');
|
|
44
46
|
jest.mock('fs', () => ({
|
|
45
47
|
//...jest.requireActual('fs'),
|
|
@@ -97,13 +99,13 @@ describe('loadRules', () => {
|
|
|
97
99
|
});
|
|
98
100
|
expect(fs.promises.readFile).toHaveBeenCalledWith('/path/to/testRule-rule.json', 'utf8');
|
|
99
101
|
}));
|
|
100
|
-
it('should not load openai rules if
|
|
101
|
-
|
|
102
|
+
it('should not load openai rules if OpenAI is not enabled', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
103
|
+
openaiUtils_1.isOpenAIEnabled.mockReturnValue(false);
|
|
102
104
|
const result = yield (0, index_1.loadRules)('testArchetype', ['openaiRule']);
|
|
103
105
|
expect(result).toEqual([]);
|
|
104
106
|
}));
|
|
105
|
-
it('should load openai rules if
|
|
106
|
-
|
|
107
|
+
it('should load openai rules if OpenAI is enabled', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
108
|
+
openaiUtils_1.isOpenAIEnabled.mockReturnValue(true);
|
|
107
109
|
const mockRuleContent = JSON.stringify({ name: 'openaiRule', conditions: {}, event: {} });
|
|
108
110
|
const mockedFsPromises = jest.mocked(fs.promises, { shallow: true });
|
|
109
111
|
mockedFsPromises.readFile.mockResolvedValue(mockRuleContent);
|
|
@@ -18,8 +18,9 @@ const archetypes_1 = require("../archetypes");
|
|
|
18
18
|
const rules_1 = require("../rules");
|
|
19
19
|
const logger_1 = require("../utils/logger");
|
|
20
20
|
const expressLogger_1 = require("./expressLogger");
|
|
21
|
+
const cli_1 = require("../core/cli");
|
|
21
22
|
const app = (0, express_1.default)();
|
|
22
|
-
const port = process.env.
|
|
23
|
+
const port = cli_1.options.port || process.env.XFI_LISTEN_PORT || 8888;
|
|
23
24
|
app.use(express_1.default.json());
|
|
24
25
|
app.use(expressLogger_1.expressLogger);
|
|
25
26
|
const validInput = (value) => {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isOpenAIEnabled = isOpenAIEnabled;
|
|
4
|
+
const cli_1 = require("../core/cli");
|
|
5
|
+
function isOpenAIEnabled() {
|
|
6
|
+
return !!process.env.OPENAI_API_KEY && !!cli_1.options.openaiEnabled;
|
|
7
|
+
}
|
package/dist/utils/telemetry.js
CHANGED
|
@@ -16,11 +16,11 @@ exports.sendTelemetry = sendTelemetry;
|
|
|
16
16
|
const axios_1 = __importDefault(require("axios"));
|
|
17
17
|
const logger_1 = require("./logger");
|
|
18
18
|
const cli_1 = require("../core/cli");
|
|
19
|
-
const TELEMETRY_ENDPOINT = process.env.TELEMETRY_ENDPOINT || (cli_1.options.configServer ? `${cli_1.options.configServer}/telemetry` :
|
|
19
|
+
const TELEMETRY_ENDPOINT = process.env.TELEMETRY_ENDPOINT || cli_1.options.telemetryCollector || (cli_1.options.configServer ? `${cli_1.options.configServer}/telemetry` : null);
|
|
20
20
|
function sendTelemetry(event) {
|
|
21
21
|
return __awaiter(this, void 0, void 0, function* () {
|
|
22
22
|
if (!TELEMETRY_ENDPOINT) {
|
|
23
|
-
logger_1.logger.debug('
|
|
23
|
+
logger_1.logger.debug('telemetry endpoint not set. skipping telemetry');
|
|
24
24
|
return;
|
|
25
25
|
}
|
|
26
26
|
try {
|
|
@@ -31,18 +31,20 @@ function sendTelemetry(event) {
|
|
|
31
31
|
'User-Agent': 'x-fidelity-telemetry'
|
|
32
32
|
}
|
|
33
33
|
});
|
|
34
|
-
logger_1.logger.debug(`
|
|
34
|
+
logger_1.logger.debug(`telemetry sent: ${JSON.stringify(event)}`);
|
|
35
|
+
return;
|
|
35
36
|
}
|
|
36
37
|
catch (error) {
|
|
37
38
|
if (axios_1.default.isAxiosError(error)) {
|
|
38
|
-
logger_1.logger.
|
|
39
|
+
logger_1.logger.debug(`failed to send telemetry: ${error.message}`);
|
|
39
40
|
if (error.response) {
|
|
40
|
-
logger_1.logger.
|
|
41
|
+
logger_1.logger.debug(`response status: ${error.response.status}`);
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
44
|
else {
|
|
44
|
-
logger_1.logger.
|
|
45
|
+
logger_1.logger.debug(`failed to send telemetry: ${error}`);
|
|
45
46
|
}
|
|
47
|
+
return;
|
|
46
48
|
}
|
|
47
49
|
});
|
|
48
50
|
}
|
package/jest.config.js
CHANGED
package/package.json
CHANGED
package/src/archetypes/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { ArchetypeConfig } from '../types/typeDefs';
|
|
|
2
2
|
|
|
3
3
|
export const archetypes: Record<string, ArchetypeConfig> = {
|
|
4
4
|
'node-fullstack': {
|
|
5
|
-
rules: ['sensitiveLogging-iterative', 'outdatedFramework-global', 'noDatabases-iterative', 'nonStandardDirectoryStructure-global', 'openaiAnalysisTop5-global', 'openaiAnalysisA11y-global'
|
|
5
|
+
rules: ['sensitiveLogging-iterative', 'outdatedFramework-global', 'noDatabases-iterative', 'nonStandardDirectoryStructure-global', 'openaiAnalysisTop5-global', 'openaiAnalysisA11y-global'],
|
|
6
6
|
operators: ['fileContains', 'outdatedFramework', 'nonStandardDirectoryStructure', 'openaiAnalysisHighSeverity'],
|
|
7
7
|
facts: ['repoFilesystemFacts', 'repoDependencyFacts', 'openaiAnalysisFacts'],
|
|
8
8
|
config: {
|
package/src/core/cli.ts
CHANGED
|
@@ -4,16 +4,25 @@ import { program } from "commander";
|
|
|
4
4
|
// Ensure logger is initialized
|
|
5
5
|
if (!logger || typeof logger.info !== 'function') {
|
|
6
6
|
console.error('Logger is not properly initialized');
|
|
7
|
-
|
|
7
|
+
// Instead of exiting, we'll create a fallback logger
|
|
8
|
+
const fallbackLogger = {
|
|
9
|
+
info: console.log,
|
|
10
|
+
error: console.error,
|
|
11
|
+
warn: console.warn,
|
|
12
|
+
debug: console.debug
|
|
13
|
+
};
|
|
14
|
+
(global as any).logger = fallbackLogger;
|
|
8
15
|
}
|
|
9
16
|
|
|
10
17
|
program
|
|
11
18
|
.option("-d, --dir <directory>", "The checkout directory to analyze (default: current directory)", ".")
|
|
12
19
|
.option("-a, --archetype <archetype>", "The archetype to use for analysis (default: node-fullstack)", "node-fullstack")
|
|
13
20
|
.option("-c, --configServer <configServer>", "The config server URL for fetching remote archetype configurations and rules")
|
|
21
|
+
.option("-o, --openaiEnabled <boolean>", "Enable OpenAI analysis (default: false)", false)
|
|
22
|
+
.option("-t, --telemetryCollector <telemetryCollector>", "The URL telemetry data will be sent to for usage analysis")
|
|
14
23
|
.option("-m, --mode <mode>", "Run mode: 'cli' or 'server' (default: cli)", "cli")
|
|
15
|
-
.option("-p, --port <port>", "
|
|
16
|
-
.option("-l, --
|
|
24
|
+
.option("-p, --port <port>", "The port to run the server on (default: 8888)", "8888")
|
|
25
|
+
.option("-l, --localConfig <path>", "Path to local archetype config and rules");
|
|
17
26
|
|
|
18
27
|
program.parse();
|
|
19
28
|
|
package/src/core/engine.test.ts
CHANGED
|
@@ -9,6 +9,21 @@ import { loadFacts } from '../facts';
|
|
|
9
9
|
import { archetypes } from '../archetypes';
|
|
10
10
|
import { ConfigManager } from '../utils/config';
|
|
11
11
|
import { sendTelemetry } from '../utils/telemetry';
|
|
12
|
+
import { isOpenAIEnabled } from '../utils/openaiUtils';
|
|
13
|
+
|
|
14
|
+
jest.mock('../utils/openaiUtils');
|
|
15
|
+
jest.mock('../core/cli', () => ({
|
|
16
|
+
options: {
|
|
17
|
+
dir: 'mockDir',
|
|
18
|
+
archetype: 'node-fullstack',
|
|
19
|
+
configServer: '',
|
|
20
|
+
openaiEnabled: true,
|
|
21
|
+
telemetryCollector: '',
|
|
22
|
+
mode: 'cli',
|
|
23
|
+
port: '8888',
|
|
24
|
+
localConfig: ''
|
|
25
|
+
}
|
|
26
|
+
}));
|
|
12
27
|
|
|
13
28
|
jest.mock('json-rules-engine');
|
|
14
29
|
jest.mock('../facts/repoFilesystemFacts');
|
|
@@ -44,6 +59,8 @@ describe('analyzeCodebase', () => {
|
|
|
44
59
|
getConfig: jest.fn().mockReturnValue(archetypes['node-fullstack']),
|
|
45
60
|
configServer: ''
|
|
46
61
|
});
|
|
62
|
+
jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
63
|
+
jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
47
64
|
});
|
|
48
65
|
|
|
49
66
|
it('should analyze the codebase and return results', async () => {
|
|
@@ -122,8 +139,8 @@ describe('analyzeCodebase', () => {
|
|
|
122
139
|
expect(sendTelemetry).toHaveBeenCalledTimes(2); // Once for start, once for end
|
|
123
140
|
});
|
|
124
141
|
|
|
125
|
-
it('should handle OpenAI analysis when
|
|
126
|
-
|
|
142
|
+
it('should handle OpenAI analysis when OpenAI is enabled', async () => {
|
|
143
|
+
(isOpenAIEnabled as jest.Mock).mockReturnValue(true);
|
|
127
144
|
const mockFileData = [
|
|
128
145
|
{ filePath: 'src/index.ts', fileContent: 'logger.info("Hello, world!");' },
|
|
129
146
|
{ fileName: 'REPO_GLOBAL_CHECK', filePath: 'REPO_GLOBAL_CHECK', fileContent: 'REPO_GLOBAL_CHECK' }
|
|
@@ -152,15 +169,14 @@ describe('analyzeCodebase', () => {
|
|
|
152
169
|
|
|
153
170
|
await analyzeCodebase('mockRepoPath', 'node-fullstack');
|
|
154
171
|
|
|
155
|
-
expect(engineAddFactMock).toHaveBeenCalledWith('
|
|
156
|
-
expect(engineAddFactMock).toHaveBeenCalledWith('openaiSystemPrompt', 'mock openai system prompt');
|
|
172
|
+
expect(engineAddFactMock).toHaveBeenCalledWith('repoDependencyAnalysis', expect.any(Function));
|
|
157
173
|
expect(sendTelemetry).toHaveBeenCalledTimes(2); // Once for start, once for end
|
|
158
174
|
|
|
159
175
|
delete process.env.OPENAI_API_KEY;
|
|
160
176
|
});
|
|
161
177
|
|
|
162
|
-
it('should not add OpenAI facts when
|
|
163
|
-
|
|
178
|
+
it('should not add OpenAI facts when OpenAI is not enabled', async () => {
|
|
179
|
+
(isOpenAIEnabled as jest.Mock).mockReturnValue(false);
|
|
164
180
|
const mockFileData = [
|
|
165
181
|
{ filePath: 'src/index.ts', fileContent: 'logger.info("Hello, world!");' },
|
|
166
182
|
{ fileName: 'REPO_GLOBAL_CHECK', filePath: 'REPO_GLOBAL_CHECK', fileContent: 'REPO_GLOBAL_CHECK' }
|
package/src/core/engine.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { loadOperators } from '../operators';
|
|
|
8
8
|
import { loadFacts } from '../facts';
|
|
9
9
|
import { loadRules } from '../rules';
|
|
10
10
|
import { ConfigManager, REPO_GLOBAL_CHECK } from '../utils/config';
|
|
11
|
+
import { isOpenAIEnabled } from '../utils/openaiUtils';
|
|
11
12
|
import { sendTelemetry } from '../utils/telemetry';
|
|
12
13
|
import { execSync } from 'child_process';
|
|
13
14
|
import os from 'os';
|
|
@@ -30,9 +31,9 @@ async function analyzeCodebase(repoPath: string, archetype: string = 'node-fulls
|
|
|
30
31
|
const { minimumDependencyVersions, standardStructure } = archetypeConfig.config;
|
|
31
32
|
|
|
32
33
|
let openaiSystemPrompt;
|
|
33
|
-
if (
|
|
34
|
+
if (isOpenAIEnabled()) {
|
|
34
35
|
openaiSystemPrompt = await collectOpenaiAnalysisFacts(fileData);
|
|
35
|
-
}
|
|
36
|
+
}
|
|
36
37
|
|
|
37
38
|
const engine = new Engine([], { replaceFactsInEventParams: true, allowUndefinedFacts: true });
|
|
38
39
|
|
|
@@ -140,7 +141,7 @@ async function analyzeCodebase(repoPath: string, archetype: string = 'node-fulls
|
|
|
140
141
|
}
|
|
141
142
|
});
|
|
142
143
|
|
|
143
|
-
if (
|
|
144
|
+
if (isOpenAIEnabled() && archetypeConfig.facts.includes('openaiAnalysisFacts')) {
|
|
144
145
|
logger.info(`adding additional openai facts to engine..`);
|
|
145
146
|
engine.addFact('openaiAnalysis', openaiAnalysis);
|
|
146
147
|
engine.addFact('openaiSystemPrompt', openaiSystemPrompt);
|
package/src/facts/index.ts
CHANGED
|
@@ -9,7 +9,13 @@ const allFacts: Record<string, { name: string, fn: Function }> = {
|
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
async function loadFacts(factNames: string[]): Promise<{ name: string, fn: Function }[]> {
|
|
12
|
-
return factNames
|
|
12
|
+
return factNames
|
|
13
|
+
.map(name => allFacts[name])
|
|
14
|
+
.filter(fact =>
|
|
15
|
+
fact && (!fact.name.startsWith('openai') ||
|
|
16
|
+
(isOpenAIEnabled() && fact.name.startsWith('openai')))
|
|
17
|
+
);
|
|
13
18
|
}
|
|
14
19
|
|
|
15
20
|
export { loadFacts };
|
|
21
|
+
import { isOpenAIEnabled } from '../utils/openaiUtils';
|
|
@@ -3,29 +3,33 @@ import { Almanac } from 'json-rules-engine';
|
|
|
3
3
|
import { OpenAI } from 'openai';
|
|
4
4
|
import { logger } from '../utils/logger';
|
|
5
5
|
import { FileData } from './repoFilesystemFacts';
|
|
6
|
+
import { isOpenAIEnabled } from '../utils/openaiUtils';
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
jest.mock('../utils/openaiUtils');
|
|
9
|
+
(isOpenAIEnabled as jest.Mock).mockReturnValue(true);
|
|
8
10
|
jest.mock('json-rules-engine');
|
|
9
11
|
jest.mock('../utils/logger', () => ({
|
|
10
12
|
logger: {
|
|
11
13
|
debug: jest.fn(),
|
|
12
14
|
error: jest.fn(),
|
|
15
|
+
info: jest.fn(),
|
|
13
16
|
},
|
|
14
17
|
}));
|
|
15
18
|
|
|
16
|
-
jest.mock('openai', () => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
19
|
+
jest.mock('openai', () => {
|
|
20
|
+
const mockCreate = jest.fn().mockResolvedValue({
|
|
21
|
+
choices: [{ message: { content: '[]' } }]
|
|
22
|
+
});
|
|
23
|
+
return {
|
|
24
|
+
OpenAI: jest.fn().mockImplementation(() => ({
|
|
25
|
+
chat: {
|
|
26
|
+
completions: {
|
|
27
|
+
create: mockCreate
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}))
|
|
31
|
+
};
|
|
32
|
+
});
|
|
29
33
|
|
|
30
34
|
describe('openaiAnalysis', () => {
|
|
31
35
|
const mockAlmanac: Almanac = {
|
|
@@ -3,14 +3,15 @@ import { OpenAI } from 'openai';
|
|
|
3
3
|
import { FileData } from './repoFilesystemFacts';
|
|
4
4
|
import { ChatCompletionCreateParams } from 'openai/resources/chat/completions';
|
|
5
5
|
import { Almanac } from 'json-rules-engine';
|
|
6
|
+
import { isOpenAIEnabled } from '../utils/openaiUtils';
|
|
6
7
|
|
|
7
8
|
let openai: OpenAI | undefined;
|
|
8
|
-
if (
|
|
9
|
+
if (isOpenAIEnabled()) {
|
|
9
10
|
const configuration = {
|
|
10
11
|
apiKey: process.env.OPENAI_API_KEY,
|
|
11
12
|
};
|
|
12
13
|
openai = new OpenAI(configuration);
|
|
13
|
-
}
|
|
14
|
+
}
|
|
14
15
|
|
|
15
16
|
const openaiAnalysis = async function (params: any, almanac: Almanac) {
|
|
16
17
|
let result: object = {'result': []};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// jest.setup.ts
|
|
2
|
+
beforeAll(() => {
|
|
3
|
+
jest.spyOn(process, 'exit').mockImplementation((code?: string | number | null | undefined): never => {
|
|
4
|
+
console.log(`process.exit(${code}) called but ignored in tests`);
|
|
5
|
+
// Throwing an error is avoided to prevent Jest worker crashes
|
|
6
|
+
return undefined as never;
|
|
7
|
+
});
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
afterAll(() => {
|
|
11
|
+
(process.exit as unknown as jest.Mock).mockRestore();
|
|
12
|
+
});
|
|
@@ -23,15 +23,14 @@ describe('loadOperators', () => {
|
|
|
23
23
|
expect(result[1]).toBe(fileContains);
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
-
it('should load
|
|
26
|
+
it('should load only non-openai operators when all names are specified', async () => {
|
|
27
27
|
const operatorNames = ['outdatedFramework', 'fileContains', 'nonStandardDirectoryStructure', 'openaiAnalysisHighSeverity'];
|
|
28
28
|
const result = await loadOperators(operatorNames);
|
|
29
29
|
|
|
30
|
-
expect(result).toHaveLength(
|
|
30
|
+
expect(result).toHaveLength(3);
|
|
31
31
|
expect(result[0]).toBe(outdatedFramework);
|
|
32
32
|
expect(result[1]).toBe(fileContains);
|
|
33
33
|
expect(result[2]).toBe(nonStandardDirectoryStructure);
|
|
34
|
-
expect(result[3]).toBe(openaiAnalysisHighSeverity);
|
|
35
34
|
});
|
|
36
35
|
|
|
37
36
|
it('should return an empty array when no operator names are specified', async () => {
|
package/src/operators/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { outdatedFramework } from './outdatedFramework';
|
|
|
3
3
|
import { fileContains } from './fileContains';
|
|
4
4
|
import { nonStandardDirectoryStructure } from './nonStandardDirectoryStructure';
|
|
5
5
|
import { openaiAnalysisHighSeverity } from './openaiAnalysisHighSeverity';
|
|
6
|
+
import { isOpenAIEnabled } from '../utils/openaiUtils';
|
|
6
7
|
|
|
7
8
|
const allOperators: Record<string, OperatorDefn> = {
|
|
8
9
|
outdatedFramework,
|
|
@@ -12,7 +13,12 @@ const allOperators: Record<string, OperatorDefn> = {
|
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
async function loadOperators(operatorNames: string[]): Promise<OperatorDefn[]> {
|
|
15
|
-
return operatorNames
|
|
16
|
+
return operatorNames
|
|
17
|
+
.map(name => allOperators[name])
|
|
18
|
+
.filter(operator =>
|
|
19
|
+
operator && (!operator?.name.startsWith('openai') ||
|
|
20
|
+
(isOpenAIEnabled() && operator?.name.startsWith('openai')))
|
|
21
|
+
);
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
export { loadOperators };
|
package/src/rules/index.test.ts
CHANGED
|
@@ -3,6 +3,9 @@ import axios from 'axios';
|
|
|
3
3
|
import * as fs from 'fs';
|
|
4
4
|
import * as path from 'path';
|
|
5
5
|
import { logger } from '../utils/logger';
|
|
6
|
+
import { isOpenAIEnabled } from '../utils/openaiUtils';
|
|
7
|
+
|
|
8
|
+
jest.mock('../utils/openaiUtils');
|
|
6
9
|
|
|
7
10
|
jest.mock('axios');
|
|
8
11
|
jest.mock('fs', () => ({
|
|
@@ -73,14 +76,14 @@ describe('loadRules', () => {
|
|
|
73
76
|
expect(fs.promises.readFile).toHaveBeenCalledWith('/path/to/testRule-rule.json', 'utf8');
|
|
74
77
|
});
|
|
75
78
|
|
|
76
|
-
it('should not load openai rules if
|
|
77
|
-
|
|
79
|
+
it('should not load openai rules if OpenAI is not enabled', async () => {
|
|
80
|
+
(isOpenAIEnabled as jest.Mock).mockReturnValue(false);
|
|
78
81
|
const result = await loadRules('testArchetype', ['openaiRule']);
|
|
79
82
|
expect(result).toEqual([]);
|
|
80
83
|
});
|
|
81
84
|
|
|
82
|
-
it('should load openai rules if
|
|
83
|
-
|
|
85
|
+
it('should load openai rules if OpenAI is enabled', async () => {
|
|
86
|
+
(isOpenAIEnabled as jest.Mock).mockReturnValue(true);
|
|
84
87
|
const mockRuleContent = JSON.stringify({ name: 'openaiRule', conditions: {}, event: {} });
|
|
85
88
|
const mockedFsPromises = jest.mocked(fs.promises, { shallow: true });
|
|
86
89
|
mockedFsPromises.readFile.mockResolvedValue(mockRuleContent);
|
package/src/rules/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import * as fs from 'fs';
|
|
|
4
4
|
import * as path from 'path';
|
|
5
5
|
import axios from 'axios';
|
|
6
6
|
import { options } from "../core/cli";
|
|
7
|
+
import { isOpenAIEnabled } from '../utils/openaiUtils';
|
|
7
8
|
|
|
8
9
|
async function loadRules(archetype: string, ruleNames: string[], configServer?: string, logPrefix?: string, localConfigPath?: string): Promise<RuleProperties[]> {
|
|
9
10
|
|
|
@@ -37,9 +38,9 @@ async function loadRules(archetype: string, ruleNames: string[], configServer?:
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
if (rule) {
|
|
40
|
-
if (!ruleName.startsWith('openai') || (
|
|
41
|
+
if (!ruleName.startsWith('openai') || (isOpenAIEnabled() && ruleName.startsWith('openai'))) {
|
|
41
42
|
ruleProperties.push(rule);
|
|
42
|
-
}
|
|
43
|
+
}
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
46
|
|
|
@@ -52,7 +53,7 @@ async function loadLocalRule(ruleName: string): Promise<RuleProperties | null> {
|
|
|
52
53
|
const fileName = `${ruleName}-rule.json`;
|
|
53
54
|
const filePath = path.join(__dirname, fileName);
|
|
54
55
|
|
|
55
|
-
if (!fileName.startsWith('openai') || (
|
|
56
|
+
if (!fileName.startsWith('openai') || (isOpenAIEnabled() && fileName.startsWith('openai'))) {
|
|
56
57
|
try {
|
|
57
58
|
logger.debug(`Loading local rule file: ${filePath}`);
|
|
58
59
|
const fileContent = await fs.promises.readFile(filePath, 'utf8');
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
import { archetypes } from '../archetypes';
|
|
3
3
|
import { loadRules } from '../rules';
|
|
4
|
-
import { logger
|
|
4
|
+
import { logger } from '../utils/logger';
|
|
5
5
|
import { expressLogger } from './expressLogger'
|
|
6
|
+
import { options } from '../core/cli';
|
|
6
7
|
|
|
7
8
|
const app = express();
|
|
8
|
-
const port = process.env.
|
|
9
|
+
const port = options.port || process.env.XFI_LISTEN_PORT || 8888;
|
|
9
10
|
|
|
10
11
|
app.use(express.json());
|
|
11
12
|
app.use(expressLogger);
|
package/src/utils/config.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
|
|
2
2
|
import axios from "axios";
|
|
3
|
-
import { logger
|
|
3
|
+
import { logger } from "../utils/logger";
|
|
4
4
|
import { ArchetypeConfig } from "../types/typeDefs";
|
|
5
5
|
import { archetypes } from "../archetypes";
|
|
6
|
-
import { loadRules } from "../rules";
|
|
7
6
|
import * as fs from 'fs';
|
|
8
7
|
import * as path from 'path';
|
|
9
8
|
|
package/src/utils/telemetry.ts
CHANGED
|
@@ -12,11 +12,11 @@ interface TelemetryEvent {
|
|
|
12
12
|
timestamp: string;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
const TELEMETRY_ENDPOINT = process.env.TELEMETRY_ENDPOINT || (options.configServer ? `${options.configServer}/telemetry` :
|
|
15
|
+
const TELEMETRY_ENDPOINT = process.env.TELEMETRY_ENDPOINT || options.telemetryCollector || (options.configServer ? `${options.configServer}/telemetry` : null);
|
|
16
16
|
|
|
17
17
|
export async function sendTelemetry(event: TelemetryEvent): Promise<void> {
|
|
18
18
|
if (!TELEMETRY_ENDPOINT) {
|
|
19
|
-
logger.debug('
|
|
19
|
+
logger.debug('telemetry endpoint not set. skipping telemetry');
|
|
20
20
|
return;
|
|
21
21
|
}
|
|
22
22
|
try {
|
|
@@ -27,15 +27,17 @@ export async function sendTelemetry(event: TelemetryEvent): Promise<void> {
|
|
|
27
27
|
'User-Agent': 'x-fidelity-telemetry'
|
|
28
28
|
}
|
|
29
29
|
});
|
|
30
|
-
logger.debug(`
|
|
30
|
+
logger.debug(`telemetry sent: ${JSON.stringify(event)}`);
|
|
31
|
+
return;
|
|
31
32
|
} catch (error) {
|
|
32
33
|
if (axios.isAxiosError(error)) {
|
|
33
|
-
logger.
|
|
34
|
+
logger.debug(`failed to send telemetry: ${error.message}`);
|
|
34
35
|
if (error.response) {
|
|
35
|
-
logger.
|
|
36
|
+
logger.debug(`response status: ${error.response.status}`);
|
|
36
37
|
}
|
|
37
38
|
} else {
|
|
38
|
-
logger.
|
|
39
|
+
logger.debug(`failed to send telemetry: ${error}`);
|
|
39
40
|
}
|
|
41
|
+
return;
|
|
40
42
|
}
|
|
41
43
|
}
|
package/tsconfig.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es6",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"outDir": "./dist",
|
|
6
|
+
"rootDir": "./src",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true
|
|
9
|
+
}
|
|
9
10
|
}
|
|
File without changes
|