x-fidelity 1.5.0 → 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 CHANGED
@@ -1,3 +1,22 @@
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
+
13
+ ## [1.5.1](https://github.com/zotoio/x-fidelity/compare/v1.5.0...v1.5.1) (2024-07-25)
14
+
15
+
16
+ ### Bug Fixes
17
+
18
+ * **scope:** logging correlation ([ea1c497](https://github.com/zotoio/x-fidelity/commit/ea1c4977cac418c49cd9f6778e7f72458c2c2a61))
19
+
1
20
  # [1.5.0](https://github.com/zotoio/x-fidelity/compare/v1.4.1...v1.5.0) (2024-07-24)
2
21
 
3
22
 
package/README.md CHANGED
@@ -16,6 +16,24 @@ x-fidelity is an advanced CLI tool designed to enforce opinionated framework adh
16
16
  -------------------------------------
17
17
  ```
18
18
 
19
+ ## Quick Start
20
+
21
+ 1. Install x-fidelity:
22
+ ```
23
+ yarn global add x-fidelity
24
+ export PATH="$PATH:$(yarn global bin)"
25
+ ```
26
+
27
+ 2. Run in your project directory:
28
+ ```
29
+ xfidelity
30
+ ```
31
+
32
+ 3. For more options:
33
+ ```
34
+ xfidelity --help
35
+ ```
36
+
19
37
  ## Table of Contents
20
38
 
21
39
  1. [Intent and Purpose](#intent-and-purpose)
@@ -79,14 +97,17 @@ xfidelity
79
97
  Use command-line options for more control:
80
98
 
81
99
  ```sh
82
- xfidelity [-d --dir <directory>] [-c --configServer <url>] [-a --archetype <archetype>] [-m --mode <mode>] [-p --port <port>]
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>]
83
101
  ```
84
102
 
85
103
  - `-d --dir <directory>`: Specify the root directory to analyze (default: current directory)
86
- - `-c --configServer <url>`: URL to fetch the configuration from
104
+ - `-c --configServer <url>`: URL to fetch the configuration from. eg. https://localhost:8888
87
105
  - `-a --archetype <archetype>`: Archetype to use for analysis (default: 'node-fullstack')
88
106
  - `-m --mode <mode>`: Run mode: 'cli' or 'server' (default: 'cli')
89
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
90
111
 
91
112
  Examples:
92
113
 
@@ -94,11 +115,14 @@ Examples:
94
115
  # Use remote config server
95
116
  xfidelity --configServer https://localhost:8888
96
117
 
97
- # Analyze parent directory with java-microservice archetype
98
- 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
99
120
 
100
- # Run in server mode with custom port
101
- 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
102
126
 
103
127
  ```
104
128
 
@@ -147,6 +171,7 @@ interface ArchetypeConfig {
147
171
  rules: string[];
148
172
  operators: string[];
149
173
  facts: string[];
174
+ configUrl?: string;
150
175
  config: {
151
176
  minimumDependencyVersions: Record<string, string>;
152
177
  standardStructure: Record<string, any>;
@@ -156,6 +181,25 @@ interface ArchetypeConfig {
156
181
  }
157
182
  ```
158
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
+
159
203
  ## Extending x-fidelity
160
204
 
161
205
  x-fidelity is designed to be highly extensible:
@@ -195,16 +239,40 @@ export const myNewArchetype: ArchetypeConfig = {
195
239
  To enable AI-powered code analysis:
196
240
 
197
241
  1. Sign up for an [OpenAI API key](https://platform.openai.com).
198
- 2. Set environment variables:
242
+ 2. Set the OPENAI_API_KEY environment variable:
199
243
 
200
244
  ```sh
201
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
202
257
  export OPENAI_MODEL=gpt-4 # Optional, default is gpt-4o
203
258
  ```
204
259
 
205
260
  > [!IMPORTANT]
206
261
  > Be aware of potential costs and data privacy concerns when using OpenAI's API.
207
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
+
208
276
  ## Hosting Config Servers
209
277
 
210
278
  To host a config server for x-fidelity:
@@ -16,10 +16,6 @@ exports.archetypes = {
16
16
  frontend: null,
17
17
  server: null
18
18
  }
19
- // app: {
20
- // frontend: 'required',
21
- // server: 'required'
22
- // }
23
19
  },
24
20
  blacklistPatterns: [
25
21
  '.*\\/\\..*', // dot files
package/dist/core/cli.js CHANGED
@@ -6,14 +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
- process.exit(1);
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
- .option("-d, --dir <directory>", "The checkout directory to analyze", ".")
13
- .option("-a, --archetype <archetype>", "The archetype to use for analysis", "node-fullstack")
19
+ .option("-d, --dir <directory>", "The checkout directory to analyze (default: current directory)", ".")
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")
15
- .option("-m, --mode <mode>", "Run mode: 'cli' or 'server'", "cli")
16
- .option("-p, --port <port>", "Port number for server mode", "8888");
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")
24
+ .option("-m, --mode <mode>", "Run mode: 'cli' or 'server' (default: cli)", "cli")
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");
17
27
  commander_1.program.parse();
18
28
  const options = commander_1.program.opts();
19
29
  exports.options = options;
@@ -35,6 +45,7 @@ directory: ${process.env.PWD}/${options.dir}
35
45
  configServer: ${options.configServer ? options.configServer : 'none'}
36
46
  mode: ${options.mode}
37
47
  port: ${options.mode === 'server' ? options.port : 'N/A'}
48
+ local-config: ${options.localConfig ? options.localConfig : 'none'}
38
49
  for available options run: xfidelity --help
39
50
  =====================================`);
40
51
  logger_1.logger.info(banner);
@@ -8,6 +8,9 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  step((generator = generator.apply(thisArg, _arguments || [])).next());
9
9
  });
10
10
  };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
11
14
  Object.defineProperty(exports, "__esModule", { value: true });
12
15
  exports.analyzeCodebase = analyzeCodebase;
13
16
  const logger_1 = require("../utils/logger");
@@ -19,11 +22,14 @@ const operators_1 = require("../operators");
19
22
  const facts_1 = require("../facts");
20
23
  const rules_1 = require("../rules");
21
24
  const config_1 = require("../utils/config");
25
+ const openaiUtils_1 = require("../utils/openaiUtils");
22
26
  const telemetry_1 = require("../utils/telemetry");
27
+ const child_process_1 = require("child_process");
28
+ const os_1 = __importDefault(require("os"));
23
29
  function analyzeCodebase(repoPath_1) {
24
- return __awaiter(this, arguments, void 0, function* (repoPath, archetype = 'node-fullstack', configServer = '') {
30
+ return __awaiter(this, arguments, void 0, function* (repoPath, archetype = 'node-fullstack', configServer = '', localConfigPath = '') {
25
31
  const configManager = config_1.ConfigManager.getInstance();
26
- yield configManager.initialize(archetype, configServer);
32
+ yield configManager.initialize(archetype, configServer, localConfigPath);
27
33
  const archetypeConfig = configManager.getConfig();
28
34
  const installedDependencyVersions = yield (0, repoDependencyFacts_1.getDependencyVersionFacts)(archetypeConfig);
29
35
  const fileData = yield (0, repoFilesystemFacts_1.collectRepoFileData)(repoPath, archetypeConfig);
@@ -34,16 +40,46 @@ function analyzeCodebase(repoPath_1) {
34
40
  fileContent: config_1.REPO_GLOBAL_CHECK
35
41
  });
36
42
  const { minimumDependencyVersions, standardStructure } = archetypeConfig.config;
37
- const openaiSystemPrompt = yield (0, openaiAnalysisFacts_1.collectOpenaiAnalysisFacts)(fileData);
43
+ let openaiSystemPrompt;
44
+ if ((0, openaiUtils_1.isOpenAIEnabled)()) {
45
+ openaiSystemPrompt = yield (0, openaiAnalysisFacts_1.collectOpenaiAnalysisFacts)(fileData);
46
+ }
38
47
  const engine = new json_rules_engine_1.Engine([], { replaceFactsInEventParams: true, allowUndefinedFacts: true });
48
+ // Get GitHub repository URL
49
+ let repoUrl = '';
50
+ try {
51
+ repoUrl = (0, child_process_1.execSync)('git config --get remote.origin.url', { cwd: repoPath }).toString().trim();
52
+ }
53
+ catch (error) {
54
+ logger_1.logger.warn('Unable to get GitHub repository URL');
55
+ }
56
+ // Get host information
57
+ const hostInfo = {
58
+ platform: os_1.default.platform(),
59
+ release: os_1.default.release(),
60
+ type: os_1.default.type(),
61
+ arch: os_1.default.arch(),
62
+ cpus: os_1.default.cpus().length,
63
+ totalMemory: os_1.default.totalmem(),
64
+ freeMemory: os_1.default.freemem()
65
+ };
66
+ // Get user information
67
+ const userInfo = {
68
+ username: os_1.default.userInfo().username,
69
+ homedir: os_1.default.userInfo().homedir,
70
+ shell: os_1.default.userInfo().shell
71
+ };
39
72
  // Send telemetry for analysis start
40
73
  yield (0, telemetry_1.sendTelemetry)({
41
74
  eventType: 'analysisStart',
42
75
  metadata: {
43
76
  archetype,
44
77
  repoPath,
78
+ repoUrl,
45
79
  fileCount: fileData.length,
46
- configServer: configServer || 'none'
80
+ configServer: configServer || 'none',
81
+ hostInfo,
82
+ userInfo
47
83
  },
48
84
  timestamp: new Date().toISOString()
49
85
  });
@@ -59,7 +95,7 @@ function analyzeCodebase(repoPath_1) {
59
95
  });
60
96
  // Add rules to engine
61
97
  logger_1.logger.info(`### loading json rules..`);
62
- const rules = yield (0, rules_1.loadRules)(archetype, archetypeConfig.rules, configManager.configServer, logger_1.logPrefix);
98
+ const rules = yield (0, rules_1.loadRules)(archetype, archetypeConfig.rules, configManager.configServer, logger_1.logPrefix, configManager.localConfigPath);
63
99
  logger_1.logger.debug(rules);
64
100
  rules.forEach((rule) => {
65
101
  try {
@@ -101,7 +137,7 @@ function analyzeCodebase(repoPath_1) {
101
137
  engine.addFact(fact.name, fact.fn);
102
138
  }
103
139
  });
104
- if (process.env.OPENAI_API_KEY && archetypeConfig.facts.includes('openaiAnalysisFacts')) {
140
+ if ((0, openaiUtils_1.isOpenAIEnabled)() && archetypeConfig.facts.includes('openaiAnalysisFacts')) {
105
141
  logger_1.logger.info(`adding additional openai facts to engine..`);
106
142
  engine.addFact('openaiAnalysis', openaiAnalysisFacts_1.openaiAnalysis);
107
143
  engine.addFact('openaiSystemPrompt', openaiSystemPrompt);
@@ -154,9 +190,12 @@ function analyzeCodebase(repoPath_1) {
154
190
  metadata: {
155
191
  archetype,
156
192
  repoPath,
193
+ repoUrl,
157
194
  fileCount: fileData.length,
158
195
  failureCount: failures.length,
159
- fatalityCount: fatalities.length
196
+ fatalityCount: fatalities.length,
197
+ hostInfo,
198
+ userInfo
160
199
  },
161
200
  timestamp: new Date().toISOString()
162
201
  });
@@ -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 = [
@@ -80,7 +96,7 @@ describe('analyzeCodebase', () => {
80
96
  const results = yield (0, engine_1.analyzeCodebase)('mockRepoPath', 'node-fullstack');
81
97
  expect(repoFilesystemFacts_1.collectRepoFileData).toHaveBeenCalledWith('mockRepoPath', expect.any(Object));
82
98
  expect(repoDependencyFacts_1.getDependencyVersionFacts).toHaveBeenCalledWith(expect.any(Object));
83
- expect(rules_1.loadRules).toHaveBeenCalledWith('node-fullstack', ['mockRule'], '', expect.any(String));
99
+ expect(rules_1.loadRules).toHaveBeenCalledWith('node-fullstack', ['mockRule'], '', expect.any(String), undefined);
84
100
  expect(operators_1.loadOperators).toHaveBeenCalledWith(['mockOperator']);
85
101
  expect(facts_1.loadFacts).toHaveBeenCalledWith(['mockFact']);
86
102
  expect(engineRunMock).toHaveBeenCalledTimes(mockFileData.length);
@@ -113,15 +129,15 @@ describe('analyzeCodebase', () => {
113
129
  const results = yield (0, engine_1.analyzeCodebase)('mockRepoPath', 'node-fullstack');
114
130
  expect(repoFilesystemFacts_1.collectRepoFileData).toHaveBeenCalledWith('mockRepoPath', expect.any(Object));
115
131
  expect(repoDependencyFacts_1.getDependencyVersionFacts).toHaveBeenCalled();
116
- expect(rules_1.loadRules).toHaveBeenCalledWith('node-fullstack', ['mockRule'], '', expect.any(String));
132
+ expect(rules_1.loadRules).toHaveBeenCalledWith('node-fullstack', ['mockRule'], '', expect.any(String), undefined);
117
133
  expect(operators_1.loadOperators).toHaveBeenCalledWith(['mockOperator']);
118
134
  expect(facts_1.loadFacts).toHaveBeenCalledWith(['mockFact']);
119
135
  expect(engineRunMock).toHaveBeenCalledTimes(mockFileData.length);
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 OPENAI_API_KEY is set', () => __awaiter(void 0, void 0, void 0, function* () {
124
- process.env.OPENAI_API_KEY = 'test-key';
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('openaiAnalysis', expect.any(Function));
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 OPENAI_API_KEY is not set', () => __awaiter(void 0, void 0, void 0, function* () {
155
- delete process.env.OPENAI_API_KEY;
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' }
@@ -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.map(name => allFacts[name]).filter(Boolean);
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 (process.env.OPENAI_API_KEY) {
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
- process.env.OPENAI_API_KEY = 'mock-key';
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
- return {
27
- chat: {
28
- completions: {
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
+ });
@@ -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.map(name => allOperators[name]).filter(Boolean);
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 all operators when all names are specified', () => __awaiter(void 0, void 0, void 0, function* () {
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(4);
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)([]);
@@ -40,7 +40,8 @@ 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
- function loadRules(archetype, ruleNames, configServer, logPrefix) {
43
+ const openaiUtils_1 = require("../utils/openaiUtils");
44
+ function loadRules(archetype, ruleNames, configServer, logPrefix, localConfigPath) {
44
45
  return __awaiter(this, void 0, void 0, function* () {
45
46
  const ruleProperties = [];
46
47
  for (const ruleName of ruleNames) {
@@ -63,11 +64,17 @@ function loadRules(archetype, ruleNames, configServer, logPrefix) {
63
64
  rule = yield loadLocalRule(ruleName);
64
65
  }
65
66
  }
67
+ else if (localConfigPath) {
68
+ rule = yield loadLocalConfigRule(ruleName, localConfigPath);
69
+ }
70
+ else if (localConfigPath) {
71
+ rule = yield loadLocalConfigRule(ruleName, localConfigPath);
72
+ }
66
73
  else {
67
74
  rule = yield loadLocalRule(ruleName);
68
75
  }
69
76
  if (rule) {
70
- if (!ruleName.startsWith('openai') || (process.env.OPENAI_API_KEY && ruleName.startsWith('openai'))) {
77
+ if (!ruleName.startsWith('openai') || ((0, openaiUtils_1.isOpenAIEnabled)() && ruleName.startsWith('openai'))) {
71
78
  ruleProperties.push(rule);
72
79
  }
73
80
  }
@@ -80,7 +87,7 @@ function loadLocalRule(ruleName) {
80
87
  return __awaiter(this, void 0, void 0, function* () {
81
88
  const fileName = `${ruleName}-rule.json`;
82
89
  const filePath = path.join(__dirname, fileName);
83
- if (!fileName.startsWith('openai') || (process.env.OPENAI_API_KEY && fileName.startsWith('openai'))) {
90
+ if (!fileName.startsWith('openai') || ((0, openaiUtils_1.isOpenAIEnabled)() && fileName.startsWith('openai'))) {
84
91
  try {
85
92
  logger_1.logger.debug(`Loading local rule file: ${filePath}`);
86
93
  const fileContent = yield fs.promises.readFile(filePath, 'utf8');
@@ -95,3 +102,22 @@ function loadLocalRule(ruleName) {
95
102
  return null;
96
103
  });
97
104
  }
105
+ function loadLocalConfigRule(ruleName, localConfigPath) {
106
+ return __awaiter(this, void 0, void 0, function* () {
107
+ const fileName = `${ruleName}-rule.json`;
108
+ const filePath = path.join(localConfigPath, 'rules', fileName);
109
+ if (!fileName.startsWith('openai') || (process.env.OPENAI_API_KEY && fileName.startsWith('openai'))) {
110
+ try {
111
+ logger_1.logger.debug(`Loading local config rule file: ${filePath}`);
112
+ const fileContent = yield fs.promises.readFile(filePath, 'utf8');
113
+ return JSON.parse(fileContent);
114
+ }
115
+ catch (error) {
116
+ logger_1.logger.error(`Error loading local config rule file: ${fileName}`);
117
+ logger_1.logger.error(error);
118
+ return null;
119
+ }
120
+ }
121
+ return null;
122
+ });
123
+ }
@@ -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 OPENAI_API_KEY is not set', () => __awaiter(void 0, void 0, void 0, function* () {
101
- delete process.env.OPENAI_API_KEY;
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 OPENAI_API_KEY is set', () => __awaiter(void 0, void 0, void 0, function* () {
106
- process.env.OPENAI_API_KEY = 'test-key';
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.XFI_SERVER_PORT || 8888;
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) => {