fraim 2.0.100

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.
Files changed (70) hide show
  1. package/README.md +445 -0
  2. package/bin/fraim.js +23 -0
  3. package/dist/src/cli/api/get-provider-client.js +41 -0
  4. package/dist/src/cli/api/provider-client.js +107 -0
  5. package/dist/src/cli/commands/add-ide.js +430 -0
  6. package/dist/src/cli/commands/add-provider.js +233 -0
  7. package/dist/src/cli/commands/doctor.js +149 -0
  8. package/dist/src/cli/commands/init-project.js +301 -0
  9. package/dist/src/cli/commands/list-overridable.js +184 -0
  10. package/dist/src/cli/commands/list.js +57 -0
  11. package/dist/src/cli/commands/login.js +84 -0
  12. package/dist/src/cli/commands/mcp.js +15 -0
  13. package/dist/src/cli/commands/migrate-project-fraim.js +42 -0
  14. package/dist/src/cli/commands/override.js +177 -0
  15. package/dist/src/cli/commands/setup.js +651 -0
  16. package/dist/src/cli/commands/sync.js +162 -0
  17. package/dist/src/cli/commands/test-mcp.js +171 -0
  18. package/dist/src/cli/doctor/check-runner.js +199 -0
  19. package/dist/src/cli/doctor/checks/global-setup-checks.js +220 -0
  20. package/dist/src/cli/doctor/checks/ide-config-checks.js +250 -0
  21. package/dist/src/cli/doctor/checks/mcp-connectivity-checks.js +381 -0
  22. package/dist/src/cli/doctor/checks/project-setup-checks.js +282 -0
  23. package/dist/src/cli/doctor/checks/scripts-checks.js +157 -0
  24. package/dist/src/cli/doctor/checks/workflow-checks.js +251 -0
  25. package/dist/src/cli/doctor/reporters/console-reporter.js +96 -0
  26. package/dist/src/cli/doctor/reporters/json-reporter.js +11 -0
  27. package/dist/src/cli/doctor/types.js +6 -0
  28. package/dist/src/cli/fraim.js +100 -0
  29. package/dist/src/cli/internal/device-flow-service.js +83 -0
  30. package/dist/src/cli/mcp/ide-formats.js +243 -0
  31. package/dist/src/cli/mcp/mcp-server-builder.js +48 -0
  32. package/dist/src/cli/mcp/mcp-server-registry.js +160 -0
  33. package/dist/src/cli/mcp/types.js +3 -0
  34. package/dist/src/cli/providers/local-provider-registry.js +166 -0
  35. package/dist/src/cli/providers/provider-registry.js +230 -0
  36. package/dist/src/cli/setup/auto-mcp-setup.js +331 -0
  37. package/dist/src/cli/setup/codex-local-config.js +37 -0
  38. package/dist/src/cli/setup/first-run.js +242 -0
  39. package/dist/src/cli/setup/ide-detector.js +179 -0
  40. package/dist/src/cli/setup/mcp-config-generator.js +192 -0
  41. package/dist/src/cli/setup/provider-prompts.js +339 -0
  42. package/dist/src/cli/utils/agent-adapters.js +126 -0
  43. package/dist/src/cli/utils/digest-utils.js +47 -0
  44. package/dist/src/cli/utils/fraim-gitignore.js +40 -0
  45. package/dist/src/cli/utils/platform-detection.js +258 -0
  46. package/dist/src/cli/utils/project-bootstrap.js +93 -0
  47. package/dist/src/cli/utils/remote-sync.js +315 -0
  48. package/dist/src/cli/utils/script-sync-utils.js +221 -0
  49. package/dist/src/cli/utils/version-utils.js +32 -0
  50. package/dist/src/core/ai-mentor.js +230 -0
  51. package/dist/src/core/config-loader.js +114 -0
  52. package/dist/src/core/config-writer.js +75 -0
  53. package/dist/src/core/types.js +23 -0
  54. package/dist/src/core/utils/git-utils.js +95 -0
  55. package/dist/src/core/utils/include-resolver.js +92 -0
  56. package/dist/src/core/utils/inheritance-parser.js +288 -0
  57. package/dist/src/core/utils/job-parser.js +176 -0
  58. package/dist/src/core/utils/local-registry-resolver.js +616 -0
  59. package/dist/src/core/utils/object-utils.js +11 -0
  60. package/dist/src/core/utils/project-fraim-migration.js +103 -0
  61. package/dist/src/core/utils/project-fraim-paths.js +38 -0
  62. package/dist/src/core/utils/provider-utils.js +18 -0
  63. package/dist/src/core/utils/server-startup.js +34 -0
  64. package/dist/src/core/utils/stub-generator.js +147 -0
  65. package/dist/src/core/utils/workflow-parser.js +174 -0
  66. package/dist/src/local-mcp-server/learning-context-builder.js +229 -0
  67. package/dist/src/local-mcp-server/stdio-server.js +1698 -0
  68. package/dist/src/local-mcp-server/usage-collector.js +264 -0
  69. package/index.js +85 -0
  70. package/package.json +139 -0
@@ -0,0 +1,162 @@
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 () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.syncCommand = exports.runSync = void 0;
40
+ const commander_1 = require("commander");
41
+ const fs_1 = __importDefault(require("fs"));
42
+ const path_1 = __importDefault(require("path"));
43
+ const chalk_1 = __importDefault(require("chalk"));
44
+ const config_loader_1 = require("../../core/config-loader");
45
+ const version_utils_1 = require("../utils/version-utils");
46
+ const script_sync_utils_1 = require("../utils/script-sync-utils");
47
+ const git_utils_1 = require("../../core/utils/git-utils");
48
+ const agent_adapters_1 = require("../utils/agent-adapters");
49
+ const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
50
+ /**
51
+ * Load API key from user-level config (~/.fraim/config.json)
52
+ */
53
+ function loadUserApiKey() {
54
+ const userConfigPath = path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'config.json');
55
+ if (!fs_1.default.existsSync(userConfigPath)) {
56
+ return undefined;
57
+ }
58
+ try {
59
+ const userConfig = JSON.parse(fs_1.default.readFileSync(userConfigPath, 'utf8'));
60
+ return userConfig.apiKey;
61
+ }
62
+ catch {
63
+ console.warn(chalk_1.default.yellow('Failed to read user-level config for API key'));
64
+ return undefined;
65
+ }
66
+ }
67
+ function updateVersionInConfig(fraimDir) {
68
+ const configPath = path_1.default.join(fraimDir, 'config.json');
69
+ if (!fs_1.default.existsSync(configPath)) {
70
+ return;
71
+ }
72
+ try {
73
+ const currentConfig = JSON.parse(fs_1.default.readFileSync(configPath, 'utf8'));
74
+ const newVersion = (0, version_utils_1.getFraimVersion)();
75
+ if (currentConfig.version !== newVersion) {
76
+ currentConfig.version = newVersion;
77
+ fs_1.default.writeFileSync(configPath, JSON.stringify(currentConfig, null, 2));
78
+ console.log(chalk_1.default.green(`Updated FRAIM version to ${newVersion} in config.`));
79
+ }
80
+ }
81
+ catch {
82
+ console.warn(chalk_1.default.yellow('Could not update version in config.json.'));
83
+ }
84
+ }
85
+ const runSync = async (options) => {
86
+ const projectRoot = process.cwd();
87
+ const config = (0, config_loader_1.loadFraimConfig)();
88
+ const fraimDir = (0, project_fraim_paths_1.getWorkspaceFraimDir)(projectRoot);
89
+ const isNpx = process.env.npm_config_prefix === undefined || process.env.npm_lifecycle_event === 'npx';
90
+ const isGlobal = !isNpx && (process.env.npm_config_global === 'true' || process.env.npm_config_prefix);
91
+ if (isGlobal && !options.skipUpdates) {
92
+ console.log(chalk_1.default.yellow('You are running a global installation of FRAIM.'));
93
+ console.log(chalk_1.default.gray('Updates are not automatic in this mode.'));
94
+ console.log(chalk_1.default.cyan('Recommended: Use "npx fraim-framework@latest sync" instead.\n'));
95
+ }
96
+ const { syncFromRemote } = await Promise.resolve().then(() => __importStar(require('../utils/remote-sync')));
97
+ if (options.local) {
98
+ console.log(chalk_1.default.blue('Syncing FRAIM jobs from local server...'));
99
+ const localPort = process.env.FRAIM_MCP_PORT ? parseInt(process.env.FRAIM_MCP_PORT) : (0, git_utils_1.getPort)();
100
+ const result = await syncFromRemote({
101
+ remoteUrl: `http://localhost:${localPort}`,
102
+ apiKey: 'local-dev',
103
+ projectRoot,
104
+ skipUpdates: true
105
+ });
106
+ if (result.success) {
107
+ console.log(chalk_1.default.green(`Successfully synced ${result.employeeJobsSynced} ai-employee jobs, ${result.managerJobsSynced} ai-manager jobs, ${result.skillsSynced} skills, ${result.rulesSynced} rules, ${result.scriptsSynced} scripts, and ${result.docsSynced} docs from local server`));
108
+ updateVersionInConfig(fraimDir);
109
+ const adapterUpdates = (0, agent_adapters_1.ensureAgentAdapterFiles)(projectRoot);
110
+ if (adapterUpdates.length > 0) {
111
+ console.log(chalk_1.default.green(`Updated FRAIM agent adapter files: ${adapterUpdates.join(', ')}`));
112
+ }
113
+ return;
114
+ }
115
+ console.error(chalk_1.default.red(`Local sync failed: ${result.error}`));
116
+ console.error(chalk_1.default.yellow('Make sure the FRAIM MCP server is running locally (npm run dev).'));
117
+ process.exit(1);
118
+ }
119
+ let apiKey = loadUserApiKey() || config.apiKey || process.env.FRAIM_API_KEY;
120
+ if (!apiKey) {
121
+ if (process.env.TEST_MODE === 'true') {
122
+ console.log(chalk_1.default.yellow('TEST_MODE: No API key configured. Using test placeholder key.'));
123
+ apiKey = 'test-mode-key';
124
+ }
125
+ else {
126
+ console.error(chalk_1.default.red('No API key configured. Cannot sync.'));
127
+ console.error(chalk_1.default.yellow(`Set FRAIM_API_KEY in your environment, or add apiKey to ~/.fraim/config.json or ${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)('config.json')}`));
128
+ console.error(chalk_1.default.yellow('Or use --local to sync from a locally running FRAIM server.'));
129
+ process.exit(1);
130
+ }
131
+ }
132
+ console.log(chalk_1.default.blue('Syncing FRAIM jobs from remote server...'));
133
+ const result = await syncFromRemote({
134
+ remoteUrl: config.remoteUrl,
135
+ apiKey,
136
+ projectRoot,
137
+ skipUpdates: options.skipUpdates || false
138
+ });
139
+ if (!result.success) {
140
+ console.error(chalk_1.default.red(`Remote sync failed: ${result.error}`));
141
+ console.error(chalk_1.default.yellow('Check your API key and network connection.'));
142
+ if (process.env.TEST_MODE === 'true') {
143
+ console.log(chalk_1.default.yellow('TEST_MODE: Continuing without remote sync (server may be unavailable).'));
144
+ updateVersionInConfig(fraimDir);
145
+ return;
146
+ }
147
+ process.exit(1);
148
+ }
149
+ console.log(chalk_1.default.green(`Successfully synced ${result.employeeJobsSynced} ai-employee jobs, ${result.managerJobsSynced} ai-manager jobs, ${result.skillsSynced} skills, ${result.rulesSynced} rules, ${result.scriptsSynced} scripts, and ${result.docsSynced} docs from remote`));
150
+ updateVersionInConfig(fraimDir);
151
+ const adapterUpdates = (0, agent_adapters_1.ensureAgentAdapterFiles)(projectRoot);
152
+ if (adapterUpdates.length > 0) {
153
+ console.log(chalk_1.default.green(`Updated FRAIM agent adapter files: ${adapterUpdates.join(', ')}`));
154
+ }
155
+ };
156
+ exports.runSync = runSync;
157
+ exports.syncCommand = new commander_1.Command('sync')
158
+ .description('Sync job stubs from the framework registry')
159
+ .option('-f, --force', 'Force sync even if digest matches')
160
+ .option('--skip-updates', 'Skip checking for CLI updates (legacy)')
161
+ .option('--local', 'Sync from local development server (port derived from git branch)')
162
+ .action(exports.runSync);
@@ -0,0 +1,171 @@
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 () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.testMCPCommand = exports.runTestMCP = void 0;
40
+ const commander_1 = require("commander");
41
+ const chalk_1 = __importDefault(require("chalk"));
42
+ const fs_1 = __importDefault(require("fs"));
43
+ const path_1 = __importDefault(require("path"));
44
+ const ide_detector_1 = require("../setup/ide-detector");
45
+ const script_sync_utils_1 = require("../utils/script-sync-utils");
46
+ const testIDEConfig = async (ide) => {
47
+ const result = {
48
+ ide: ide.name,
49
+ configExists: false,
50
+ configValid: false,
51
+ mcpServers: [],
52
+ errors: []
53
+ };
54
+ const configPath = (0, ide_detector_1.expandPath)(ide.configPath);
55
+ if (!fs_1.default.existsSync(configPath)) {
56
+ result.errors.push('Config file does not exist');
57
+ return result;
58
+ }
59
+ result.configExists = true;
60
+ try {
61
+ if (ide.configFormat === 'json') {
62
+ const configContent = fs_1.default.readFileSync(configPath, 'utf8');
63
+ const config = JSON.parse(configContent);
64
+ const servers = ide.configType === 'vscode' ? config.servers : config.mcpServers;
65
+ if (servers) {
66
+ result.configValid = true;
67
+ result.mcpServers = Object.keys(servers);
68
+ }
69
+ else {
70
+ const expectedKey = ide.configType === 'vscode' ? 'servers' : 'mcpServers';
71
+ result.errors.push(`No ${expectedKey} section found`);
72
+ }
73
+ }
74
+ else if (ide.configFormat === 'toml') {
75
+ const configContent = fs_1.default.readFileSync(configPath, 'utf8');
76
+ // Simple TOML parsing for MCP servers
77
+ const serverMatches = configContent.match(/\[mcp_servers\.(\w+)\]/g);
78
+ if (serverMatches) {
79
+ result.configValid = true;
80
+ result.mcpServers = serverMatches.map(match => match.replace(/\[mcp_servers\.(\w+)\]/, '$1'));
81
+ }
82
+ else {
83
+ result.errors.push('No mcp_servers sections found');
84
+ }
85
+ }
86
+ }
87
+ catch (error) {
88
+ result.errors.push(`Failed to parse config: ${error instanceof Error ? error.message : 'Unknown error'}`);
89
+ }
90
+ return result;
91
+ };
92
+ const checkGlobalSetup = () => {
93
+ const globalConfigPath = path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'config.json');
94
+ return fs_1.default.existsSync(globalConfigPath);
95
+ };
96
+ const runTestMCP = async () => {
97
+ console.log(chalk_1.default.blue('🔍 Testing MCP configuration...\n'));
98
+ // Check global setup
99
+ if (!checkGlobalSetup()) {
100
+ console.log(chalk_1.default.red('❌ Global FRAIM setup not found.'));
101
+ console.log(chalk_1.default.yellow('Please run: fraim setup --key=<your-fraim-key>'));
102
+ return;
103
+ }
104
+ console.log(chalk_1.default.green('✅ Global FRAIM setup found'));
105
+ // Detect IDEs
106
+ const detectedIDEs = (0, ide_detector_1.detectInstalledIDEs)();
107
+ if (detectedIDEs.length === 0) {
108
+ console.log(chalk_1.default.yellow('⚠️ No supported IDEs detected.'));
109
+ return;
110
+ }
111
+ console.log(chalk_1.default.blue(`\n🔍 Testing ${detectedIDEs.length} detected IDEs...\n`));
112
+ const results = await Promise.all(detectedIDEs.map(ide => testIDEConfig(ide)));
113
+ let totalConfigured = 0;
114
+ let totalWithFRAIM = 0;
115
+ for (const result of results) {
116
+ console.log(chalk_1.default.white(`📱 ${result.ide}`));
117
+ if (!result.configExists) {
118
+ console.log(chalk_1.default.red(' ❌ No MCP config found'));
119
+ console.log(chalk_1.default.gray(` 💡 Run: fraim setup --ide=${result.ide.toLowerCase()}`));
120
+ }
121
+ else if (!result.configValid) {
122
+ console.log(chalk_1.default.yellow(' ⚠️ Config exists but invalid'));
123
+ result.errors.forEach(error => {
124
+ console.log(chalk_1.default.red(` ❌ ${error}`));
125
+ });
126
+ }
127
+ else {
128
+ totalConfigured++;
129
+ console.log(chalk_1.default.green(` ✅ MCP config valid (${result.mcpServers.length} servers)`));
130
+ // Check for essential servers
131
+ const { BASE_MCP_SERVERS } = await Promise.resolve().then(() => __importStar(require('../mcp/mcp-server-registry')));
132
+ const essentialServers = BASE_MCP_SERVERS.map(s => s.id); // fraim, git, playwright
133
+ const hasEssential = essentialServers.filter(server => result.mcpServers.includes(server));
134
+ if (hasEssential.includes('fraim')) {
135
+ totalWithFRAIM++;
136
+ console.log(chalk_1.default.green(' ✅ FRAIM server configured'));
137
+ }
138
+ else {
139
+ console.log(chalk_1.default.yellow(' ⚠️ FRAIM server missing'));
140
+ }
141
+ if (hasEssential.length > 1) {
142
+ console.log(chalk_1.default.green(` ✅ ${hasEssential.length - 1} additional servers: ${hasEssential.filter(s => s !== 'fraim').join(', ')}`));
143
+ }
144
+ const missingEssential = essentialServers.filter(server => !result.mcpServers.includes(server));
145
+ if (missingEssential.length > 0) {
146
+ console.log(chalk_1.default.yellow(` ⚠️ Missing servers: ${missingEssential.join(', ')}`));
147
+ }
148
+ }
149
+ console.log(); // Empty line
150
+ }
151
+ // Summary
152
+ console.log(chalk_1.default.blue('📊 Summary:'));
153
+ console.log(chalk_1.default.green(` ✅ ${totalConfigured}/${detectedIDEs.length} IDEs have valid MCP configs`));
154
+ console.log(chalk_1.default.green(` ✅ ${totalWithFRAIM}/${detectedIDEs.length} IDEs have FRAIM configured`));
155
+ if (totalWithFRAIM === 0) {
156
+ console.log(chalk_1.default.red('\n❌ No IDEs have FRAIM configured!'));
157
+ console.log(chalk_1.default.yellow('💡 Run: fraim setup --key=<your-fraim-key>'));
158
+ }
159
+ else if (totalWithFRAIM < detectedIDEs.length) {
160
+ console.log(chalk_1.default.yellow(`\n⚠️ ${detectedIDEs.length - totalWithFRAIM} IDEs missing FRAIM configuration`));
161
+ console.log(chalk_1.default.yellow('💡 Run: fraim setup to configure remaining IDEs'));
162
+ }
163
+ else {
164
+ console.log(chalk_1.default.green('\n🎉 All detected IDEs have FRAIM configured!'));
165
+ console.log(chalk_1.default.blue('💡 Try running: fraim init-project in any project'));
166
+ }
167
+ };
168
+ exports.runTestMCP = runTestMCP;
169
+ exports.testMCPCommand = new commander_1.Command('test-mcp')
170
+ .description('Test MCP server configurations for all detected IDEs')
171
+ .action(exports.runTestMCP);
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+ /**
3
+ * Check runner for FRAIM doctor command
4
+ * Handles parallel execution, timeouts, and error handling
5
+ * Issue #144: Enhanced doctor command
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.runChecks = runChecks;
9
+ const CHECK_TIMEOUT = 2000; // 2 seconds per check
10
+ const MCP_CHECK_TIMEOUT = 10000; // 10 seconds for MCP connectivity checks
11
+ const TOTAL_TIMEOUT = 30000; // 30 seconds total
12
+ // Simple logger for doctor command (optional, falls back to no-op)
13
+ const logger = {
14
+ debug: (...args) => {
15
+ if (process.env.DEBUG)
16
+ console.debug('[DOCTOR]', ...args);
17
+ },
18
+ info: (...args) => {
19
+ if (process.env.VERBOSE)
20
+ console.log('[DOCTOR]', ...args);
21
+ },
22
+ warn: (...args) => {
23
+ console.warn('[DOCTOR]', ...args);
24
+ },
25
+ error: (...args) => {
26
+ console.error('[DOCTOR]', ...args);
27
+ }
28
+ };
29
+ // Simple metric tracker (no-op for now, can be enhanced later)
30
+ const trackMetric = (name, value) => {
31
+ if (process.env.DEBUG) {
32
+ console.debug(`[METRIC] ${name}: ${value}`);
33
+ }
34
+ };
35
+ /**
36
+ * Run a single check with timeout
37
+ */
38
+ async function runCheckWithTimeout(check, timeout = CHECK_TIMEOUT) {
39
+ const checkStartTime = Date.now();
40
+ // Use longer timeout for MCP connectivity checks
41
+ const effectiveTimeout = check.category === 'mcpConnectivity' ? MCP_CHECK_TIMEOUT : timeout;
42
+ try {
43
+ logger.debug(`Running check: ${check.name}`, { category: check.category });
44
+ const result = await Promise.race([
45
+ check.run(),
46
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), effectiveTimeout))
47
+ ]);
48
+ const checkDuration = Date.now() - checkStartTime;
49
+ logger.debug(`Check completed: ${check.name}`, {
50
+ status: result.status,
51
+ duration: checkDuration
52
+ });
53
+ return result;
54
+ }
55
+ catch (error) {
56
+ const checkDuration = Date.now() - checkStartTime;
57
+ if (error.message === 'Timeout') {
58
+ logger.warn(`Check timed out: ${check.name}`, { duration: checkDuration });
59
+ return {
60
+ status: 'warning',
61
+ message: `${check.name} timed out`,
62
+ suggestion: 'Check may be slow or unresponsive'
63
+ };
64
+ }
65
+ logger.error(`Check failed: ${check.name}`, { error: error.message, duration: checkDuration });
66
+ return {
67
+ status: 'error',
68
+ message: `${check.name} failed: ${error.message}`,
69
+ suggestion: 'See error details above',
70
+ details: { error: error.message }
71
+ };
72
+ }
73
+ }
74
+ /**
75
+ * Filter checks based on command options
76
+ */
77
+ function filterChecks(checks, options) {
78
+ // If no specific flags, run all checks
79
+ if (!options.testMcp && !options.testConfig && !options.testJobs) {
80
+ return checks;
81
+ }
82
+ return checks.filter(check => {
83
+ if (options.testMcp && check.category === 'mcpConnectivity')
84
+ return true;
85
+ if (options.testConfig && (check.category === 'globalSetup' || check.category === 'projectSetup' || check.category === 'ideConfiguration'))
86
+ return true;
87
+ if (options.testJobs && check.category === 'jobs')
88
+ return true;
89
+ return false;
90
+ });
91
+ }
92
+ /**
93
+ * Run all checks with parallel execution and timeout
94
+ */
95
+ async function runChecks(checks, options, version) {
96
+ const startTime = Date.now();
97
+ // Filter checks based on options
98
+ const filteredChecks = filterChecks(checks, options);
99
+ logger.info(`Running ${filteredChecks.length} checks`, {
100
+ total: checks.length,
101
+ filtered: filteredChecks.length,
102
+ options
103
+ });
104
+ // Group checks by category for logging
105
+ const categoryCounts = {};
106
+ for (const check of filteredChecks) {
107
+ categoryCounts[check.category] = (categoryCounts[check.category] || 0) + 1;
108
+ }
109
+ for (const [category, count] of Object.entries(categoryCounts)) {
110
+ logger.info(`Category: ${category}`, { checkCount: count });
111
+ }
112
+ // Run checks in parallel with overall timeout
113
+ const checkPromises = filteredChecks.map(check => runCheckWithTimeout(check).then(result => ({ check, result })));
114
+ let checkResults;
115
+ try {
116
+ checkResults = await Promise.race([
117
+ Promise.all(checkPromises),
118
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Total timeout exceeded')), TOTAL_TIMEOUT))
119
+ ]);
120
+ }
121
+ catch (error) {
122
+ logger.error('Total timeout exceeded for all checks');
123
+ // If total timeout exceeded, return partial results
124
+ checkResults = [];
125
+ }
126
+ // Group results by category
127
+ const categories = {
128
+ globalSetup: { checks: [] },
129
+ projectSetup: { checks: [] },
130
+ jobs: { checks: [] },
131
+ ideConfiguration: { checks: [] },
132
+ mcpConnectivity: { checks: [] },
133
+ scripts: { checks: [] }
134
+ };
135
+ let passed = 0;
136
+ let warnings = 0;
137
+ let errors = 0;
138
+ for (const { check, result } of checkResults) {
139
+ const categoryResult = {
140
+ name: check.name,
141
+ status: result.status,
142
+ message: result.message,
143
+ suggestion: result.suggestion,
144
+ command: result.command,
145
+ details: result.details,
146
+ latency: result.latency
147
+ };
148
+ categories[check.category].checks.push(categoryResult);
149
+ if (result.status === 'passed')
150
+ passed++;
151
+ else if (result.status === 'warning')
152
+ warnings++;
153
+ else if (result.status === 'error')
154
+ errors++;
155
+ }
156
+ const duration = Date.now() - startTime;
157
+ // Track category durations
158
+ for (const [category, categoryResult] of Object.entries(categories)) {
159
+ if (categoryResult.checks.length > 0) {
160
+ trackMetric(`doctor.category.${category}.duration`, duration);
161
+ trackMetric(`doctor.category.${category}.checks`, categoryResult.checks.length);
162
+ }
163
+ }
164
+ // Track MCP latencies
165
+ for (const { result } of checkResults) {
166
+ if (result.latency !== undefined) {
167
+ trackMetric('doctor.mcp.latency', result.latency);
168
+ }
169
+ }
170
+ logger.info('All checks completed', {
171
+ duration,
172
+ passed,
173
+ warnings,
174
+ errors,
175
+ total: checkResults.length
176
+ });
177
+ // Generate suggestions from errors and warnings
178
+ const suggestions = checkResults
179
+ .filter(({ result }) => result.status !== 'passed' && result.suggestion)
180
+ .map(({ result }, index) => ({
181
+ priority: result.status === 'error' ? index + 1 : index + 100,
182
+ message: result.suggestion,
183
+ command: result.command,
184
+ reason: result.message
185
+ }))
186
+ .sort((a, b) => a.priority - b.priority);
187
+ return {
188
+ timestamp: new Date().toISOString(),
189
+ version,
190
+ summary: {
191
+ passed,
192
+ warnings,
193
+ errors,
194
+ total: checkResults.length
195
+ },
196
+ categories: categories,
197
+ suggestions: suggestions // Always include array, even if empty
198
+ };
199
+ }