x-fidelity 1.5.0 → 1.5.1

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,10 @@
1
+ ## [1.5.1](https://github.com/zotoio/x-fidelity/compare/v1.5.0...v1.5.1) (2024-07-25)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **scope:** logging correlation ([ea1c497](https://github.com/zotoio/x-fidelity/commit/ea1c4977cac418c49cd9f6778e7f72458c2c2a61))
7
+
1
8
  # [1.5.0](https://github.com/zotoio/x-fidelity/compare/v1.4.1...v1.5.0) (2024-07-24)
2
9
 
3
10
 
package/README.md CHANGED
@@ -2,6 +2,24 @@
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
+ ## Quick Start
6
+
7
+ 1. Install x-fidelity:
8
+ ```
9
+ yarn global add x-fidelity
10
+ export PATH="$PATH:$(yarn global bin)"
11
+ ```
12
+
13
+ 2. Run in your project directory:
14
+ ```
15
+ xfidelity
16
+ ```
17
+
18
+ 3. For more options:
19
+ ```
20
+ xfidelity --help
21
+ ```
22
+
5
23
  ```
6
24
  =====================================
7
25
  __ __ ________ ______
@@ -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', 'yarnLockfileCheck-global'],
7
7
  operators: ['fileContains', 'outdatedFramework', 'nonStandardDirectoryStructure', 'openaiAnalysisHighSeverity'],
8
8
  facts: ['repoFilesystemFacts', 'repoDependencyFacts', 'openaiAnalysisFacts'],
9
9
  config: {
@@ -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
@@ -9,11 +9,12 @@ if (!logger_1.logger || typeof logger_1.logger.info !== 'function') {
9
9
  process.exit(1);
10
10
  }
11
11
  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")
12
+ .option("-d, --dir <directory>", "The checkout directory to analyze (default: current directory)", ".")
13
+ .option("-a, --archetype <archetype>", "The archetype to use for analysis (default: node-fullstack)", "node-fullstack")
14
14
  .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");
15
+ .option("-m, --mode <mode>", "Run mode: 'cli' or 'server' (default: cli)", "cli")
16
+ .option("-p, --port <port>", "Port number for server mode (default: 8888)", "8888")
17
+ .option("-l, --local-config <path>", "Path to local archetype config and rules");
17
18
  commander_1.program.parse();
18
19
  const options = commander_1.program.opts();
19
20
  exports.options = options;
@@ -35,6 +36,7 @@ directory: ${process.env.PWD}/${options.dir}
35
36
  configServer: ${options.configServer ? options.configServer : 'none'}
36
37
  mode: ${options.mode}
37
38
  port: ${options.mode === 'server' ? options.port : 'N/A'}
39
+ local-config: ${options.localConfig ? options.localConfig : 'none'}
38
40
  for available options run: xfidelity --help
39
41
  =====================================`);
40
42
  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");
@@ -20,10 +23,12 @@ const facts_1 = require("../facts");
20
23
  const rules_1 = require("../rules");
21
24
  const config_1 = require("../utils/config");
22
25
  const telemetry_1 = require("../utils/telemetry");
26
+ const child_process_1 = require("child_process");
27
+ const os_1 = __importDefault(require("os"));
23
28
  function analyzeCodebase(repoPath_1) {
24
- return __awaiter(this, arguments, void 0, function* (repoPath, archetype = 'node-fullstack', configServer = '') {
29
+ return __awaiter(this, arguments, void 0, function* (repoPath, archetype = 'node-fullstack', configServer = '', localConfigPath = '') {
25
30
  const configManager = config_1.ConfigManager.getInstance();
26
- yield configManager.initialize(archetype, configServer);
31
+ yield configManager.initialize(archetype, configServer, localConfigPath);
27
32
  const archetypeConfig = configManager.getConfig();
28
33
  const installedDependencyVersions = yield (0, repoDependencyFacts_1.getDependencyVersionFacts)(archetypeConfig);
29
34
  const fileData = yield (0, repoFilesystemFacts_1.collectRepoFileData)(repoPath, archetypeConfig);
@@ -34,16 +39,46 @@ function analyzeCodebase(repoPath_1) {
34
39
  fileContent: config_1.REPO_GLOBAL_CHECK
35
40
  });
36
41
  const { minimumDependencyVersions, standardStructure } = archetypeConfig.config;
37
- const openaiSystemPrompt = yield (0, openaiAnalysisFacts_1.collectOpenaiAnalysisFacts)(fileData);
42
+ let openaiSystemPrompt;
43
+ if (process.env.OPENAI_API_KEY) {
44
+ openaiSystemPrompt = yield (0, openaiAnalysisFacts_1.collectOpenaiAnalysisFacts)(fileData);
45
+ }
38
46
  const engine = new json_rules_engine_1.Engine([], { replaceFactsInEventParams: true, allowUndefinedFacts: true });
47
+ // Get GitHub repository URL
48
+ let repoUrl = '';
49
+ try {
50
+ repoUrl = (0, child_process_1.execSync)('git config --get remote.origin.url', { cwd: repoPath }).toString().trim();
51
+ }
52
+ catch (error) {
53
+ logger_1.logger.warn('Unable to get GitHub repository URL');
54
+ }
55
+ // Get host information
56
+ const hostInfo = {
57
+ platform: os_1.default.platform(),
58
+ release: os_1.default.release(),
59
+ type: os_1.default.type(),
60
+ arch: os_1.default.arch(),
61
+ cpus: os_1.default.cpus().length,
62
+ totalMemory: os_1.default.totalmem(),
63
+ freeMemory: os_1.default.freemem()
64
+ };
65
+ // Get user information
66
+ const userInfo = {
67
+ username: os_1.default.userInfo().username,
68
+ homedir: os_1.default.userInfo().homedir,
69
+ shell: os_1.default.userInfo().shell
70
+ };
39
71
  // Send telemetry for analysis start
40
72
  yield (0, telemetry_1.sendTelemetry)({
41
73
  eventType: 'analysisStart',
42
74
  metadata: {
43
75
  archetype,
44
76
  repoPath,
77
+ repoUrl,
45
78
  fileCount: fileData.length,
46
- configServer: configServer || 'none'
79
+ configServer: configServer || 'none',
80
+ hostInfo,
81
+ userInfo
47
82
  },
48
83
  timestamp: new Date().toISOString()
49
84
  });
@@ -59,7 +94,7 @@ function analyzeCodebase(repoPath_1) {
59
94
  });
60
95
  // Add rules to engine
61
96
  logger_1.logger.info(`### loading json rules..`);
62
- const rules = yield (0, rules_1.loadRules)(archetype, archetypeConfig.rules, configManager.configServer, logger_1.logPrefix);
97
+ const rules = yield (0, rules_1.loadRules)(archetype, archetypeConfig.rules, configManager.configServer, logger_1.logPrefix, configManager.localConfigPath);
63
98
  logger_1.logger.debug(rules);
64
99
  rules.forEach((rule) => {
65
100
  try {
@@ -154,9 +189,12 @@ function analyzeCodebase(repoPath_1) {
154
189
  metadata: {
155
190
  archetype,
156
191
  repoPath,
192
+ repoUrl,
157
193
  fileCount: fileData.length,
158
194
  failureCount: failures.length,
159
- fatalityCount: fatalities.length
195
+ fatalityCount: fatalities.length,
196
+ hostInfo,
197
+ userInfo
160
198
  },
161
199
  timestamp: new Date().toISOString()
162
200
  });
@@ -80,7 +80,7 @@ describe('analyzeCodebase', () => {
80
80
  const results = yield (0, engine_1.analyzeCodebase)('mockRepoPath', 'node-fullstack');
81
81
  expect(repoFilesystemFacts_1.collectRepoFileData).toHaveBeenCalledWith('mockRepoPath', expect.any(Object));
82
82
  expect(repoDependencyFacts_1.getDependencyVersionFacts).toHaveBeenCalledWith(expect.any(Object));
83
- expect(rules_1.loadRules).toHaveBeenCalledWith('node-fullstack', ['mockRule'], '', expect.any(String));
83
+ expect(rules_1.loadRules).toHaveBeenCalledWith('node-fullstack', ['mockRule'], '', expect.any(String), undefined);
84
84
  expect(operators_1.loadOperators).toHaveBeenCalledWith(['mockOperator']);
85
85
  expect(facts_1.loadFacts).toHaveBeenCalledWith(['mockFact']);
86
86
  expect(engineRunMock).toHaveBeenCalledTimes(mockFileData.length);
@@ -113,7 +113,7 @@ describe('analyzeCodebase', () => {
113
113
  const results = yield (0, engine_1.analyzeCodebase)('mockRepoPath', 'node-fullstack');
114
114
  expect(repoFilesystemFacts_1.collectRepoFileData).toHaveBeenCalledWith('mockRepoPath', expect.any(Object));
115
115
  expect(repoDependencyFacts_1.getDependencyVersionFacts).toHaveBeenCalled();
116
- expect(rules_1.loadRules).toHaveBeenCalledWith('node-fullstack', ['mockRule'], '', expect.any(String));
116
+ expect(rules_1.loadRules).toHaveBeenCalledWith('node-fullstack', ['mockRule'], '', expect.any(String), undefined);
117
117
  expect(operators_1.loadOperators).toHaveBeenCalledWith(['mockOperator']);
118
118
  expect(facts_1.loadFacts).toHaveBeenCalledWith(['mockFact']);
119
119
  expect(engineRunMock).toHaveBeenCalledTimes(mockFileData.length);
@@ -40,7 +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
- function loadRules(archetype, ruleNames, configServer, logPrefix) {
43
+ function loadRules(archetype, ruleNames, configServer, logPrefix, localConfigPath) {
44
44
  return __awaiter(this, void 0, void 0, function* () {
45
45
  const ruleProperties = [];
46
46
  for (const ruleName of ruleNames) {
@@ -63,6 +63,12 @@ function loadRules(archetype, ruleNames, configServer, logPrefix) {
63
63
  rule = yield loadLocalRule(ruleName);
64
64
  }
65
65
  }
66
+ else if (localConfigPath) {
67
+ rule = yield loadLocalConfigRule(ruleName, localConfigPath);
68
+ }
69
+ else if (localConfigPath) {
70
+ rule = yield loadLocalConfigRule(ruleName, localConfigPath);
71
+ }
66
72
  else {
67
73
  rule = yield loadLocalRule(ruleName);
68
74
  }
@@ -95,3 +101,22 @@ function loadLocalRule(ruleName) {
95
101
  return null;
96
102
  });
97
103
  }
104
+ function loadLocalConfigRule(ruleName, localConfigPath) {
105
+ return __awaiter(this, void 0, void 0, function* () {
106
+ const fileName = `${ruleName}-rule.json`;
107
+ const filePath = path.join(localConfigPath, 'rules', fileName);
108
+ if (!fileName.startsWith('openai') || (process.env.OPENAI_API_KEY && fileName.startsWith('openai'))) {
109
+ try {
110
+ logger_1.logger.debug(`Loading local config rule file: ${filePath}`);
111
+ const fileContent = yield fs.promises.readFile(filePath, 'utf8');
112
+ return JSON.parse(fileContent);
113
+ }
114
+ catch (error) {
115
+ logger_1.logger.error(`Error loading local config rule file: ${fileName}`);
116
+ logger_1.logger.error(error);
117
+ return null;
118
+ }
119
+ }
120
+ return null;
121
+ });
122
+ }
@@ -1,4 +1,27 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
2
25
  var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
26
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
27
  return new (P || (P = Promise))(function (resolve, reject) {
@@ -16,12 +39,15 @@ exports.ConfigManager = exports.REPO_GLOBAL_CHECK = void 0;
16
39
  const axios_1 = __importDefault(require("axios"));
17
40
  const logger_1 = require("../utils/logger");
18
41
  const archetypes_1 = require("../archetypes");
42
+ const fs = __importStar(require("fs"));
43
+ const path = __importStar(require("path"));
19
44
  exports.REPO_GLOBAL_CHECK = 'REPO_GLOBAL_CHECK';
20
45
  class ConfigManager {
21
46
  constructor() {
22
47
  this.config = archetypes_1.archetypes['node-fullstack'];
23
48
  this.rules = [];
24
49
  this.configServer = '';
50
+ this.localConfigPath = '';
25
51
  }
26
52
  static getInstance() {
27
53
  if (!ConfigManager.instance) {
@@ -30,10 +56,26 @@ class ConfigManager {
30
56
  return ConfigManager.instance;
31
57
  }
32
58
  initialize() {
33
- return __awaiter(this, arguments, void 0, function* (archetype = 'node-fullstack', configServer) {
59
+ return __awaiter(this, arguments, void 0, function* (archetype = 'node-fullstack', configServer, localConfigPath) {
34
60
  this.config = archetypes_1.archetypes[archetype] || archetypes_1.archetypes['node-fullstack'];
35
61
  this.configServer = configServer || '';
62
+ this.localConfigPath = localConfigPath || '';
36
63
  logger_1.logger.debug(`Initializing config manager for archetype: ${archetype}`);
64
+ if (this.localConfigPath) {
65
+ try {
66
+ const localConfig = yield this.loadLocalConfig(archetype);
67
+ this.config = Object.assign(Object.assign({}, this.config), localConfig);
68
+ logger_1.logger.debug(`Local config loaded successfully ${JSON.stringify(this.config)}`);
69
+ }
70
+ catch (error) {
71
+ if (error instanceof Error) {
72
+ logger_1.logger.error(`Error loading local config: ${error.message}`);
73
+ }
74
+ else {
75
+ logger_1.logger.error('Error loading local config: Unknown error');
76
+ }
77
+ }
78
+ }
37
79
  if (this.configServer) {
38
80
  try {
39
81
  const configUrl = `${this.configServer}/archetypes/${archetype}`;
@@ -53,6 +95,25 @@ class ConfigManager {
53
95
  }
54
96
  });
55
97
  }
98
+ loadLocalConfig(archetype) {
99
+ return __awaiter(this, void 0, void 0, function* () {
100
+ const configPath = path.join(this.localConfigPath, `${archetype}.json`);
101
+ try {
102
+ const configContent = yield fs.promises.readFile(configPath, 'utf8');
103
+ const localConfig = JSON.parse(configContent);
104
+ return Object.assign(Object.assign({}, archetypes_1.archetypes[archetype]), localConfig);
105
+ }
106
+ catch (error) {
107
+ if (error instanceof Error) {
108
+ logger_1.logger.error(`Error loading local config: ${error.message}`);
109
+ }
110
+ else {
111
+ logger_1.logger.error('Error loading local config: Unknown error');
112
+ }
113
+ return archetypes_1.archetypes[archetype];
114
+ }
115
+ });
116
+ }
56
117
  getConfig() {
57
118
  return this.config;
58
119
  }
@@ -15,15 +15,34 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.sendTelemetry = sendTelemetry;
16
16
  const axios_1 = __importDefault(require("axios"));
17
17
  const logger_1 = require("./logger");
18
- const TELEMETRY_ENDPOINT = process.env.TELEMETRY_ENDPOINT || 'https://example.com/telemetry';
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
20
  function sendTelemetry(event) {
20
21
  return __awaiter(this, void 0, void 0, function* () {
22
+ if (!TELEMETRY_ENDPOINT) {
23
+ logger_1.logger.debug('Telemetry endpoint not set. Skipping telemetry');
24
+ return;
25
+ }
21
26
  try {
22
- yield axios_1.default.post(TELEMETRY_ENDPOINT, event);
27
+ yield axios_1.default.post(TELEMETRY_ENDPOINT, event, {
28
+ timeout: 5000, // 5 seconds timeout
29
+ headers: {
30
+ 'Content-Type': 'application/json',
31
+ 'User-Agent': 'x-fidelity-telemetry'
32
+ }
33
+ });
23
34
  logger_1.logger.debug(`Telemetry sent: ${JSON.stringify(event)}`);
24
35
  }
25
36
  catch (error) {
26
- logger_1.logger.debug(`Failed to send telemetry: ${error}`);
37
+ if (axios_1.default.isAxiosError(error)) {
38
+ logger_1.logger.error(`Failed to send telemetry: ${error.message}`);
39
+ if (error.response) {
40
+ logger_1.logger.error(`Response status: ${error.response.status}`);
41
+ }
42
+ }
43
+ else {
44
+ logger_1.logger.error(`Failed to send telemetry: ${error}`);
45
+ }
27
46
  }
28
47
  });
29
48
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x-fidelity",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "cli for opinionated framework adherence checks",
5
5
  "main": "dist/xfidelity",
6
6
  "bin": {
@@ -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', 'yarnLockfileCheck-global'],
6
6
  operators: ['fileContains', 'outdatedFramework', 'nonStandardDirectoryStructure', 'openaiAnalysisHighSeverity'],
7
7
  facts: ['repoFilesystemFacts', 'repoDependencyFacts', 'openaiAnalysisFacts'],
8
8
  config: {
@@ -15,10 +15,6 @@ export const archetypes: Record<string, ArchetypeConfig> = {
15
15
  frontend: null,
16
16
  server: null
17
17
  }
18
- // app: {
19
- // frontend: 'required',
20
- // server: 'required'
21
- // }
22
18
  },
23
19
  blacklistPatterns: [
24
20
  '.*\\/\\..*', // dot files
package/src/core/cli.ts CHANGED
@@ -8,11 +8,12 @@ if (!logger || typeof logger.info !== 'function') {
8
8
  }
9
9
 
10
10
  program
11
- .option("-d, --dir <directory>", "The checkout directory to analyze", ".")
12
- .option("-a, --archetype <archetype>", "The archetype to use for analysis", "node-fullstack")
11
+ .option("-d, --dir <directory>", "The checkout directory to analyze (default: current directory)", ".")
12
+ .option("-a, --archetype <archetype>", "The archetype to use for analysis (default: node-fullstack)", "node-fullstack")
13
13
  .option("-c, --configServer <configServer>", "The config server URL for fetching remote archetype configurations and rules")
14
- .option("-m, --mode <mode>", "Run mode: 'cli' or 'server'", "cli")
15
- .option("-p, --port <port>", "Port number for server mode", "8888");
14
+ .option("-m, --mode <mode>", "Run mode: 'cli' or 'server' (default: cli)", "cli")
15
+ .option("-p, --port <port>", "Port number for server mode (default: 8888)", "8888")
16
+ .option("-l, --local-config <path>", "Path to local archetype config and rules");
16
17
 
17
18
  program.parse();
18
19
 
@@ -36,6 +37,7 @@ directory: ${process.env.PWD}/${options.dir}
36
37
  configServer: ${options.configServer ? options.configServer : 'none'}
37
38
  mode: ${options.mode}
38
39
  port: ${options.mode === 'server' ? options.port : 'N/A'}
40
+ local-config: ${options.localConfig ? options.localConfig : 'none'}
39
41
  for available options run: xfidelity --help
40
42
  =====================================`);
41
43
 
@@ -76,7 +76,7 @@ describe('analyzeCodebase', () => {
76
76
 
77
77
  expect(collectRepoFileData).toHaveBeenCalledWith('mockRepoPath', expect.any(Object));
78
78
  expect(getDependencyVersionFacts).toHaveBeenCalledWith(expect.any(Object));
79
- expect(loadRules).toHaveBeenCalledWith('node-fullstack', ['mockRule'], '', expect.any(String));
79
+ expect(loadRules).toHaveBeenCalledWith('node-fullstack', ['mockRule'], '', expect.any(String), undefined);
80
80
  expect(loadOperators).toHaveBeenCalledWith(['mockOperator']);
81
81
  expect(loadFacts).toHaveBeenCalledWith(['mockFact']);
82
82
  expect(engineRunMock).toHaveBeenCalledTimes(mockFileData.length);
@@ -114,7 +114,7 @@ describe('analyzeCodebase', () => {
114
114
 
115
115
  expect(collectRepoFileData).toHaveBeenCalledWith('mockRepoPath', expect.any(Object));
116
116
  expect(getDependencyVersionFacts).toHaveBeenCalled();
117
- expect(loadRules).toHaveBeenCalledWith('node-fullstack', ['mockRule'], '', expect.any(String));
117
+ expect(loadRules).toHaveBeenCalledWith('node-fullstack', ['mockRule'], '', expect.any(String), undefined);
118
118
  expect(loadOperators).toHaveBeenCalledWith(['mockOperator']);
119
119
  expect(loadFacts).toHaveBeenCalledWith(['mockFact']);
120
120
  expect(engineRunMock).toHaveBeenCalledTimes(mockFileData.length);
@@ -9,10 +9,12 @@ import { loadFacts } from '../facts';
9
9
  import { loadRules } from '../rules';
10
10
  import { ConfigManager, REPO_GLOBAL_CHECK } from '../utils/config';
11
11
  import { sendTelemetry } from '../utils/telemetry';
12
+ import { execSync } from 'child_process';
13
+ import os from 'os';
12
14
 
13
- async function analyzeCodebase(repoPath: string, archetype: string = 'node-fullstack', configServer: string = ''): Promise<any[]> {
15
+ async function analyzeCodebase(repoPath: string, archetype: string = 'node-fullstack', configServer: string = '', localConfigPath: string = ''): Promise<any[]> {
14
16
  const configManager = ConfigManager.getInstance();
15
- await configManager.initialize(archetype, configServer);
17
+ await configManager.initialize(archetype, configServer, localConfigPath);
16
18
  const archetypeConfig = configManager.getConfig();
17
19
 
18
20
  const installedDependencyVersions = await getDependencyVersionFacts(archetypeConfig);
@@ -26,18 +28,51 @@ async function analyzeCodebase(repoPath: string, archetype: string = 'node-fulls
26
28
  });
27
29
 
28
30
  const { minimumDependencyVersions, standardStructure } = archetypeConfig.config;
29
- const openaiSystemPrompt = await collectOpenaiAnalysisFacts(fileData);
31
+
32
+ let openaiSystemPrompt;
33
+ if (process.env.OPENAI_API_KEY) {
34
+ openaiSystemPrompt = await collectOpenaiAnalysisFacts(fileData);
35
+ }
30
36
 
31
37
  const engine = new Engine([], { replaceFactsInEventParams: true, allowUndefinedFacts: true });
32
38
 
39
+ // Get GitHub repository URL
40
+ let repoUrl = '';
41
+ try {
42
+ repoUrl = execSync('git config --get remote.origin.url', { cwd: repoPath }).toString().trim();
43
+ } catch (error) {
44
+ logger.warn('Unable to get GitHub repository URL');
45
+ }
46
+
47
+ // Get host information
48
+ const hostInfo = {
49
+ platform: os.platform(),
50
+ release: os.release(),
51
+ type: os.type(),
52
+ arch: os.arch(),
53
+ cpus: os.cpus().length,
54
+ totalMemory: os.totalmem(),
55
+ freeMemory: os.freemem()
56
+ };
57
+
58
+ // Get user information
59
+ const userInfo = {
60
+ username: os.userInfo().username,
61
+ homedir: os.userInfo().homedir,
62
+ shell: os.userInfo().shell
63
+ };
64
+
33
65
  // Send telemetry for analysis start
34
66
  await sendTelemetry({
35
67
  eventType: 'analysisStart',
36
68
  metadata: {
37
69
  archetype,
38
70
  repoPath,
71
+ repoUrl,
39
72
  fileCount: fileData.length,
40
- configServer: configServer || 'none'
73
+ configServer: configServer || 'none',
74
+ hostInfo,
75
+ userInfo
41
76
  },
42
77
  timestamp: new Date().toISOString()
43
78
  });
@@ -54,7 +89,7 @@ async function analyzeCodebase(repoPath: string, archetype: string = 'node-fulls
54
89
 
55
90
  // Add rules to engine
56
91
  logger.info(`### loading json rules..`);
57
- const rules: RuleProperties[] = await loadRules(archetype, archetypeConfig.rules, configManager.configServer, logPrefix);
92
+ const rules: RuleProperties[] = await loadRules(archetype, archetypeConfig.rules, configManager.configServer, logPrefix, configManager.localConfigPath);
58
93
  logger.debug(rules);
59
94
 
60
95
  rules.forEach((rule) => {
@@ -165,9 +200,12 @@ async function analyzeCodebase(repoPath: string, archetype: string = 'node-fulls
165
200
  metadata: {
166
201
  archetype,
167
202
  repoPath,
203
+ repoUrl,
168
204
  fileCount: fileData.length,
169
205
  failureCount: failures.length,
170
- fatalityCount: fatalities.length
206
+ fatalityCount: fatalities.length,
207
+ hostInfo,
208
+ userInfo
171
209
  },
172
210
  timestamp: new Date().toISOString()
173
211
  });
@@ -5,7 +5,7 @@ import * as path from 'path';
5
5
  import axios from 'axios';
6
6
  import { options } from "../core/cli";
7
7
 
8
- async function loadRules(archetype: any, ruleNames: string[], configServer?: string, logPrefix?: string): Promise<RuleProperties[]> {
8
+ async function loadRules(archetype: string, ruleNames: string[], configServer?: string, logPrefix?: string, localConfigPath?: string): Promise<RuleProperties[]> {
9
9
 
10
10
  const ruleProperties: RuleProperties[] = [];
11
11
 
@@ -28,6 +28,10 @@ async function loadRules(archetype: any, ruleNames: string[], configServer?: str
28
28
  // If remote fetch fails, fall back to local file
29
29
  rule = await loadLocalRule(ruleName);
30
30
  }
31
+ } else if (localConfigPath) {
32
+ rule = await loadLocalConfigRule(ruleName, localConfigPath);
33
+ } else if (localConfigPath) {
34
+ rule = await loadLocalConfigRule(ruleName, localConfigPath);
31
35
  } else {
32
36
  rule = await loadLocalRule(ruleName);
33
37
  }
@@ -63,3 +67,20 @@ async function loadLocalRule(ruleName: string): Promise<RuleProperties | null> {
63
67
  }
64
68
 
65
69
  export { loadRules };
70
+ async function loadLocalConfigRule(ruleName: string, localConfigPath: string): Promise<RuleProperties | null> {
71
+ const fileName = `${ruleName}-rule.json`;
72
+ const filePath = path.join(localConfigPath, 'rules', fileName);
73
+
74
+ if (!fileName.startsWith('openai') || (process.env.OPENAI_API_KEY && fileName.startsWith('openai'))) {
75
+ try {
76
+ logger.debug(`Loading local config rule file: ${filePath}`);
77
+ const fileContent = await fs.promises.readFile(filePath, 'utf8');
78
+ return JSON.parse(fileContent);
79
+ } catch (error) {
80
+ logger.error(`Error loading local config rule file: ${fileName}`);
81
+ logger.error(error);
82
+ return null;
83
+ }
84
+ }
85
+ return null;
86
+ }
@@ -4,6 +4,8 @@ import { logger, logPrefix } from "../utils/logger";
4
4
  import { ArchetypeConfig } from "../types/typeDefs";
5
5
  import { archetypes } from "../archetypes";
6
6
  import { loadRules } from "../rules";
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
7
9
 
8
10
  export const REPO_GLOBAL_CHECK = 'REPO_GLOBAL_CHECK';
9
11
 
@@ -12,11 +14,13 @@ export class ConfigManager {
12
14
  private config: ArchetypeConfig;
13
15
  private rules: any[];
14
16
  public configServer: string;
17
+ public localConfigPath: string;
15
18
 
16
19
  private constructor() {
17
20
  this.config = archetypes['node-fullstack'];
18
21
  this.rules = [];
19
22
  this.configServer = '';
23
+ this.localConfigPath = '';
20
24
  }
21
25
 
22
26
  public static getInstance(): ConfigManager {
@@ -26,10 +30,29 @@ export class ConfigManager {
26
30
  return ConfigManager.instance;
27
31
  }
28
32
 
29
- public async initialize(archetype: string = 'node-fullstack', configServer?: string): Promise<void> {
33
+ public async initialize(archetype: string = 'node-fullstack', configServer?: string, localConfigPath?: string): Promise<void> {
30
34
  this.config = archetypes[archetype] || archetypes['node-fullstack'];
31
35
  this.configServer = configServer || '';
36
+ this.localConfigPath = localConfigPath || '';
32
37
  logger.debug(`Initializing config manager for archetype: ${archetype}`);
38
+
39
+ if (this.localConfigPath) {
40
+ try {
41
+ const localConfig = await this.loadLocalConfig(archetype);
42
+ this.config = {
43
+ ...this.config,
44
+ ...localConfig
45
+ };
46
+ logger.debug(`Local config loaded successfully ${JSON.stringify(this.config)}`);
47
+ } catch (error) {
48
+ if (error instanceof Error) {
49
+ logger.error(`Error loading local config: ${error.message}`);
50
+ } else {
51
+ logger.error('Error loading local config: Unknown error');
52
+ }
53
+ }
54
+ }
55
+
33
56
  if (this.configServer) {
34
57
  try {
35
58
  const configUrl = `${this.configServer}/archetypes/${archetype}`;
@@ -48,7 +71,25 @@ export class ConfigManager {
48
71
  }
49
72
  }
50
73
  }
74
+ }
51
75
 
76
+ private async loadLocalConfig(archetype: string): Promise<ArchetypeConfig> {
77
+ const configPath = path.join(this.localConfigPath, `${archetype}.json`);
78
+ try {
79
+ const configContent = await fs.promises.readFile(configPath, 'utf8');
80
+ const localConfig = JSON.parse(configContent);
81
+ return {
82
+ ...archetypes[archetype],
83
+ ...localConfig
84
+ };
85
+ } catch (error) {
86
+ if (error instanceof Error) {
87
+ logger.error(`Error loading local config: ${error.message}`);
88
+ } else {
89
+ logger.error('Error loading local config: Unknown error');
90
+ }
91
+ return archetypes[archetype];
92
+ }
52
93
  }
53
94
 
54
95
  public getConfig(): ArchetypeConfig {
@@ -1,5 +1,6 @@
1
1
  import axios from 'axios';
2
2
  import { logger } from './logger';
3
+ import { options } from "../core/cli";
3
4
 
4
5
  interface TelemetryEvent {
5
6
  eventType: string;
@@ -11,13 +12,30 @@ interface TelemetryEvent {
11
12
  timestamp: string;
12
13
  }
13
14
 
14
- const TELEMETRY_ENDPOINT = process.env.TELEMETRY_ENDPOINT || 'https://example.com/telemetry';
15
+ const TELEMETRY_ENDPOINT = process.env.TELEMETRY_ENDPOINT || (options.configServer ? `${options.configServer}/telemetry` : '');
15
16
 
16
17
  export async function sendTelemetry(event: TelemetryEvent): Promise<void> {
18
+ if (!TELEMETRY_ENDPOINT) {
19
+ logger.debug('Telemetry endpoint not set. Skipping telemetry');
20
+ return;
21
+ }
17
22
  try {
18
- await axios.post(TELEMETRY_ENDPOINT, event);
23
+ await axios.post(TELEMETRY_ENDPOINT, event, {
24
+ timeout: 5000, // 5 seconds timeout
25
+ headers: {
26
+ 'Content-Type': 'application/json',
27
+ 'User-Agent': 'x-fidelity-telemetry'
28
+ }
29
+ });
19
30
  logger.debug(`Telemetry sent: ${JSON.stringify(event)}`);
20
31
  } catch (error) {
21
- logger.debug(`Failed to send telemetry: ${error}`);
32
+ if (axios.isAxiosError(error)) {
33
+ logger.error(`Failed to send telemetry: ${error.message}`);
34
+ if (error.response) {
35
+ logger.error(`Response status: ${error.response.status}`);
36
+ }
37
+ } else {
38
+ logger.error(`Failed to send telemetry: ${error}`);
39
+ }
22
40
  }
23
41
  }