fraim-framework 2.0.80 → 2.0.82

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/README.md CHANGED
@@ -306,9 +306,11 @@ FRAIM uses the official Model Context Protocol (MCP) server for Jira integration
306
306
  ```json
307
307
  {
308
308
  "jira": {
309
- "command": "npx",
310
- "args": ["-y", "@modelcontextprotocol/server-jira"],
309
+ "command": "uvx",
310
+ "args": ["mcp-atlassian"],
311
311
  "env": {
312
+ "JIRA_URL": "https://mycompany.atlassian.net",
313
+ "JIRA_USERNAME": "user@mycompany.com",
312
314
  "JIRA_API_TOKEN": "your-token-here"
313
315
  }
314
316
  }
@@ -316,9 +318,9 @@ FRAIM uses the official Model Context Protocol (MCP) server for Jira integration
316
318
  ```
317
319
 
318
320
  **⚠️ Common Issues**:
319
- - **Old configuration format**: If you see `https://mcp.atlassian.com/v1/sse` in your config, this is incorrect. Run `fraim setup --jira` to update.
321
+ - **Old package name**: If you see `@modelcontextprotocol/server-jira` in your config, this package doesn't exist. Run `fraim setup --jira` to update to the correct `mcp-atlassian` package.
320
322
  - **Token format**: Jira API tokens typically start with `ATATT3`. If your token doesn't match this format, verify you created an API token (not a personal access token).
321
- - **First run slow**: The first time `npx` runs the Jira MCP server, it downloads the package. This is normal and only happens once.
323
+ - **First run slow**: The first time `uvx` runs the Jira MCP server, it downloads the package. This is normal and only happens once.
322
324
 
323
325
  **Troubleshooting**:
324
326
  ```bash
@@ -0,0 +1,383 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.addProviderCommand = exports.runAddProvider = void 0;
7
+ const commander_1 = require("commander");
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const prompts_1 = __importDefault(require("prompts"));
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const token_validator_1 = require("../setup/token-validator");
13
+ const mcp_config_generator_1 = require("../setup/mcp-config-generator");
14
+ const ide_detector_1 = require("../setup/ide-detector");
15
+ const script_sync_utils_1 = require("../utils/script-sync-utils");
16
+ const mcp_config_generator_2 = require("../setup/mcp-config-generator");
17
+ const loadGlobalConfig = () => {
18
+ const globalConfigPath = path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'config.json');
19
+ if (!fs_1.default.existsSync(globalConfigPath)) {
20
+ return null;
21
+ }
22
+ try {
23
+ return JSON.parse(fs_1.default.readFileSync(globalConfigPath, 'utf8'));
24
+ }
25
+ catch (e) {
26
+ return null;
27
+ }
28
+ };
29
+ const saveGlobalConfig = (config) => {
30
+ const globalConfigPath = path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'config.json');
31
+ fs_1.default.writeFileSync(globalConfigPath, JSON.stringify(config, null, 2));
32
+ };
33
+ const promptForGitHubToken = async () => {
34
+ console.log(chalk_1.default.blue('\n🔑 GitHub Token'));
35
+ console.log(chalk_1.default.gray('Create a token at: https://github.com/settings/tokens'));
36
+ console.log(chalk_1.default.gray('Required scopes: repo, read:org, read:user\n'));
37
+ const response = await (0, prompts_1.default)({
38
+ type: 'password',
39
+ name: 'token',
40
+ message: 'Enter your GitHub token (ghp_...):',
41
+ validate: (value) => {
42
+ if (!value)
43
+ return 'Token is required';
44
+ if (!(0, token_validator_1.isValidTokenFormat)(value, 'github')) {
45
+ return 'Invalid GitHub token format (should start with ghp_)';
46
+ }
47
+ return true;
48
+ }
49
+ });
50
+ if (!response.token) {
51
+ throw new Error('GitHub token is required');
52
+ }
53
+ return response.token;
54
+ };
55
+ const promptForGitLabToken = async () => {
56
+ console.log(chalk_1.default.blue('\n🔑 GitLab Token'));
57
+ console.log(chalk_1.default.gray('Create a token at: https://gitlab.com/-/profile/personal_access_tokens'));
58
+ console.log(chalk_1.default.gray('Required scopes: api, read_api, read_repository\n'));
59
+ const response = await (0, prompts_1.default)({
60
+ type: 'password',
61
+ name: 'token',
62
+ message: 'Enter your GitLab token (glpat-...):',
63
+ validate: (value) => {
64
+ if (!value)
65
+ return 'Token is required';
66
+ if (!(0, token_validator_1.isValidTokenFormat)(value, 'gitlab')) {
67
+ return 'Invalid GitLab token format (should start with glpat-)';
68
+ }
69
+ return true;
70
+ }
71
+ });
72
+ if (!response.token) {
73
+ throw new Error('GitLab token is required');
74
+ }
75
+ return response.token;
76
+ };
77
+ const promptForADOToken = async () => {
78
+ console.log(chalk_1.default.blue('\n🔑 Azure DevOps Token'));
79
+ console.log(chalk_1.default.gray('Create a token at: https://dev.azure.com/<org>/_usersSettings/tokens'));
80
+ console.log(chalk_1.default.gray('Required scopes: Code (Read & Write), Work Items (Read & Write)\n'));
81
+ const response = await (0, prompts_1.default)({
82
+ type: 'password',
83
+ name: 'token',
84
+ message: 'Enter your Azure DevOps PAT:',
85
+ validate: (value) => {
86
+ if (!value)
87
+ return 'Token is required';
88
+ if (value.length < 20)
89
+ return 'Token seems too short';
90
+ return true;
91
+ }
92
+ });
93
+ if (!response.token) {
94
+ throw new Error('Azure DevOps token is required');
95
+ }
96
+ return response.token;
97
+ };
98
+ const promptForJiraCredentials = async (options) => {
99
+ // If all options provided, skip prompts (non-interactive mode)
100
+ if (options.url && options.email && options.token) {
101
+ const baseUrl = options.url.replace(/^https?:\/\//, '').replace(/\/$/, '');
102
+ // Validate token format
103
+ if (!(0, token_validator_1.isValidTokenFormat)(options.token, 'jira')) {
104
+ throw new Error('Invalid Jira token format');
105
+ }
106
+ return {
107
+ baseUrl,
108
+ email: options.email,
109
+ token: options.token
110
+ };
111
+ }
112
+ console.log(chalk_1.default.blue('\n🔑 Jira Credentials'));
113
+ console.log(chalk_1.default.gray('Create a token at: https://id.atlassian.com/manage-profile/security/api-tokens\n'));
114
+ // Prompt for base URL
115
+ const urlResponse = await (0, prompts_1.default)({
116
+ type: 'text',
117
+ name: 'url',
118
+ message: 'Jira instance URL (e.g., company.atlassian.net):',
119
+ initial: options.url,
120
+ validate: (value) => {
121
+ if (!value)
122
+ return 'URL is required';
123
+ // Remove protocol if provided
124
+ const cleaned = value.replace(/^https?:\/\//, '').replace(/\/$/, '');
125
+ if (!cleaned.includes('.'))
126
+ return 'Invalid URL format';
127
+ return true;
128
+ }
129
+ });
130
+ if (!urlResponse.url) {
131
+ throw new Error('Jira URL is required');
132
+ }
133
+ const baseUrl = urlResponse.url.replace(/^https?:\/\//, '').replace(/\/$/, '');
134
+ // Prompt for email
135
+ const emailResponse = await (0, prompts_1.default)({
136
+ type: 'text',
137
+ name: 'email',
138
+ message: 'Jira account email:',
139
+ initial: options.email,
140
+ validate: (value) => {
141
+ if (!value)
142
+ return 'Email is required';
143
+ if (!value.includes('@'))
144
+ return 'Invalid email format';
145
+ return true;
146
+ }
147
+ });
148
+ if (!emailResponse.email) {
149
+ throw new Error('Jira email is required');
150
+ }
151
+ // Prompt for token
152
+ const tokenResponse = await (0, prompts_1.default)({
153
+ type: 'password',
154
+ name: 'token',
155
+ message: 'Jira API token:',
156
+ validate: (value) => {
157
+ if (!value)
158
+ return 'Token is required';
159
+ if (!(0, token_validator_1.isValidTokenFormat)(value, 'jira')) {
160
+ return 'Invalid Jira token format';
161
+ }
162
+ return true;
163
+ }
164
+ });
165
+ if (!tokenResponse.token) {
166
+ throw new Error('Jira token is required');
167
+ }
168
+ return {
169
+ baseUrl,
170
+ email: emailResponse.email,
171
+ token: tokenResponse.token
172
+ };
173
+ };
174
+ const updateIDEConfigs = async (provider, config) => {
175
+ const detectedIDEs = (0, ide_detector_1.detectInstalledIDEs)();
176
+ if (detectedIDEs.length === 0) {
177
+ console.log(chalk_1.default.yellow('⚠️ No IDEs detected. MCP configs not updated.'));
178
+ return;
179
+ }
180
+ console.log(chalk_1.default.blue(`\n🔧 Updating ${detectedIDEs.length} IDE configuration(s)...\n`));
181
+ const tokenConfig = {
182
+ github: config.tokens?.github,
183
+ gitlab: config.tokens?.gitlab,
184
+ jira: config.tokens?.jira
185
+ };
186
+ const jiraConfig = config.jiraConfig ? {
187
+ baseUrl: config.jiraConfig.baseUrl,
188
+ email: config.jiraConfig.email
189
+ } : undefined;
190
+ for (const ide of detectedIDEs) {
191
+ try {
192
+ const configPath = (0, ide_detector_1.expandPath)(ide.configPath);
193
+ if (ide.configFormat === 'json') {
194
+ // JSON-based config (Claude Desktop, Cursor, etc.)
195
+ let ideConfig = {};
196
+ if (fs_1.default.existsSync(configPath)) {
197
+ ideConfig = JSON.parse(fs_1.default.readFileSync(configPath, 'utf8'));
198
+ }
199
+ const mcpConfig = (0, mcp_config_generator_1.generateMCPConfig)(ide.configType, config.apiKey, tokenConfig, jiraConfig);
200
+ const serversKey = ide.configType === 'vscode' ? 'servers' : 'mcpServers';
201
+ if (!ideConfig[serversKey]) {
202
+ ideConfig[serversKey] = {};
203
+ }
204
+ // Add the new provider's MCP server
205
+ const providerServerKey = provider === 'ado' ? 'ado' : provider;
206
+ if (mcpConfig[serversKey] && mcpConfig[serversKey][providerServerKey]) {
207
+ ideConfig[serversKey][providerServerKey] = mcpConfig[serversKey][providerServerKey];
208
+ fs_1.default.writeFileSync(configPath, JSON.stringify(ideConfig, null, 2));
209
+ console.log(chalk_1.default.green(`✅ Updated ${ide.name} config`));
210
+ }
211
+ }
212
+ else if (ide.configFormat === 'toml') {
213
+ // TOML-based config (Codex, Zed)
214
+ const mcpConfigToml = (0, mcp_config_generator_1.generateMCPConfig)(ide.configType, config.apiKey, tokenConfig, jiraConfig);
215
+ if (fs_1.default.existsSync(configPath)) {
216
+ const existingContent = fs_1.default.readFileSync(configPath, 'utf8');
217
+ // Merge only the new provider's server
218
+ const serversToMerge = [provider === 'ado' ? 'ado' : provider];
219
+ const mergeResult = (0, mcp_config_generator_2.mergeTomlMCPServers)(existingContent, mcpConfigToml, serversToMerge);
220
+ fs_1.default.writeFileSync(configPath, mergeResult.content);
221
+ console.log(chalk_1.default.green(`✅ Updated ${ide.name} config`));
222
+ }
223
+ else {
224
+ console.log(chalk_1.default.yellow(`⚠️ ${ide.name} config not found, skipping`));
225
+ }
226
+ }
227
+ }
228
+ catch (error) {
229
+ console.log(chalk_1.default.red(`❌ Failed to update ${ide.name}: ${error instanceof Error ? error.message : 'Unknown error'}`));
230
+ }
231
+ }
232
+ };
233
+ const offerModeSwitch = async (config) => {
234
+ if (config.mode === 'split') {
235
+ console.log(chalk_1.default.gray('\nAlready in split mode.'));
236
+ return;
237
+ }
238
+ console.log(chalk_1.default.blue('\n🔄 Mode Switch Available'));
239
+ console.log(chalk_1.default.gray('You can now use split mode (separate platforms for code and issues).'));
240
+ console.log(chalk_1.default.gray('Example: GitHub for code, Jira for issue tracking\n'));
241
+ // Skip prompt if FRAIM_AUTO_SWITCH_MODE is set (for testing)
242
+ const shouldSwitch = process.env.FRAIM_AUTO_SWITCH_MODE === 'true';
243
+ if (!shouldSwitch && !process.env.FRAIM_AUTO_SWITCH_MODE) {
244
+ const response = await (0, prompts_1.default)({
245
+ type: 'confirm',
246
+ name: 'switchMode',
247
+ message: 'Switch to split mode?',
248
+ initial: false
249
+ });
250
+ if (response.switchMode) {
251
+ config.mode = 'split';
252
+ saveGlobalConfig(config);
253
+ console.log(chalk_1.default.green('✅ Switched to split mode'));
254
+ console.log(chalk_1.default.gray('Run "fraim init-project" in your projects to use split mode'));
255
+ }
256
+ }
257
+ else if (shouldSwitch) {
258
+ config.mode = 'split';
259
+ saveGlobalConfig(config);
260
+ console.log(chalk_1.default.green('✅ Switched to split mode'));
261
+ console.log(chalk_1.default.gray('Run "fraim init-project" in your projects to use split mode'));
262
+ }
263
+ };
264
+ const runAddProvider = async (provider, options) => {
265
+ console.log(chalk_1.default.blue(`\n🔧 Adding ${provider.toUpperCase()} provider...\n`));
266
+ // Check global setup exists
267
+ const config = loadGlobalConfig();
268
+ if (!config) {
269
+ console.log(chalk_1.default.red('❌ Global FRAIM setup not found.'));
270
+ console.log(chalk_1.default.yellow('Please run: fraim setup --key=<your-fraim-key>'));
271
+ process.exit(1);
272
+ }
273
+ // Initialize tokens object if it doesn't exist
274
+ if (!config.tokens) {
275
+ config.tokens = {};
276
+ }
277
+ // Check if provider already configured
278
+ if (provider === 'jira') {
279
+ if (config.tokens.jira && config.jiraConfig?.baseUrl) {
280
+ console.log(chalk_1.default.yellow(`⚠️ Jira is already configured (${config.jiraConfig.baseUrl})`));
281
+ // Skip prompt if FRAIM_FORCE_OVERWRITE is set (for testing)
282
+ if (!process.env.FRAIM_FORCE_OVERWRITE) {
283
+ const response = await (0, prompts_1.default)({
284
+ type: 'confirm',
285
+ name: 'overwrite',
286
+ message: 'Overwrite existing Jira configuration?',
287
+ initial: false
288
+ });
289
+ if (!response.overwrite) {
290
+ console.log(chalk_1.default.gray('Cancelled.'));
291
+ process.exit(0);
292
+ }
293
+ }
294
+ }
295
+ }
296
+ else {
297
+ if (config.tokens[provider]) {
298
+ console.log(chalk_1.default.yellow(`⚠️ ${provider.toUpperCase()} token already exists`));
299
+ // Skip prompt if FRAIM_FORCE_OVERWRITE is set (for testing)
300
+ if (!process.env.FRAIM_FORCE_OVERWRITE) {
301
+ const response = await (0, prompts_1.default)({
302
+ type: 'confirm',
303
+ name: 'overwrite',
304
+ message: `Overwrite existing ${provider.toUpperCase()} token?`,
305
+ initial: false
306
+ });
307
+ if (!response.overwrite) {
308
+ console.log(chalk_1.default.gray('Cancelled.'));
309
+ process.exit(0);
310
+ }
311
+ }
312
+ }
313
+ }
314
+ // Get credentials based on provider
315
+ try {
316
+ if (provider === 'github') {
317
+ const token = options.token || await promptForGitHubToken();
318
+ // Validate token if requested
319
+ if (options.validate !== false) {
320
+ console.log(chalk_1.default.blue('🔍 Validating GitHub token...'));
321
+ const isValid = await (0, token_validator_1.validateGitHubToken)(token);
322
+ if (!isValid) {
323
+ console.log(chalk_1.default.red('❌ Invalid GitHub token'));
324
+ process.exit(1);
325
+ }
326
+ console.log(chalk_1.default.green('✅ Token validated'));
327
+ }
328
+ config.tokens.github = token;
329
+ }
330
+ else if (provider === 'gitlab') {
331
+ const token = options.token || await promptForGitLabToken();
332
+ config.tokens.gitlab = token;
333
+ }
334
+ else if (provider === 'ado') {
335
+ const token = options.token || await promptForADOToken();
336
+ config.tokens.ado = token;
337
+ }
338
+ else if (provider === 'jira') {
339
+ const credentials = await promptForJiraCredentials(options);
340
+ config.tokens.jira = credentials.token;
341
+ config.jiraConfig = {
342
+ baseUrl: credentials.baseUrl,
343
+ email: credentials.email
344
+ };
345
+ }
346
+ // Save updated config
347
+ saveGlobalConfig(config);
348
+ console.log(chalk_1.default.green(`\n✅ ${provider.toUpperCase()} credentials saved to global config`));
349
+ // Update IDE configs
350
+ await updateIDEConfigs(provider, config);
351
+ // Offer mode switch if adding Jira
352
+ if (provider === 'jira') {
353
+ await offerModeSwitch(config);
354
+ }
355
+ console.log(chalk_1.default.green('\n🎉 Provider added successfully!'));
356
+ console.log(chalk_1.default.cyan('\n💡 Next steps:'));
357
+ console.log(chalk_1.default.gray(' 1. Restart your IDE(s) to load new MCP servers'));
358
+ if (provider === 'jira' && config.mode === 'split') {
359
+ console.log(chalk_1.default.gray(' 2. Run "fraim init-project" in your projects to use split mode'));
360
+ }
361
+ }
362
+ catch (error) {
363
+ console.log(chalk_1.default.red(`\n❌ Failed to add provider: ${error instanceof Error ? error.message : 'Unknown error'}`));
364
+ process.exit(1);
365
+ }
366
+ };
367
+ exports.runAddProvider = runAddProvider;
368
+ exports.addProviderCommand = new commander_1.Command('add-provider')
369
+ .description('Add or update a provider (GitHub, GitLab, ADO, Jira) after initial setup')
370
+ .argument('<provider>', 'Provider to add: github, gitlab, ado, or jira')
371
+ .option('--token <token>', 'Provider token (will prompt if not provided)')
372
+ .option('--email <email>', 'Email (Jira only)')
373
+ .option('--url <url>', 'Instance URL (Jira only)')
374
+ .option('--no-validate', 'Skip token validation (GitHub only)')
375
+ .action(async (provider, options) => {
376
+ const validProviders = ['github', 'gitlab', 'ado', 'jira'];
377
+ if (!validProviders.includes(provider)) {
378
+ console.log(chalk_1.default.red(`❌ Invalid provider: ${provider}`));
379
+ console.log(chalk_1.default.yellow(`Valid providers: ${validProviders.join(', ')}`));
380
+ process.exit(1);
381
+ }
382
+ await (0, exports.runAddProvider)(provider, options);
383
+ });
@@ -1,129 +1,146 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.doctorCommand = void 0;
4
+ exports.getAllChecks = getAllChecks;
7
5
  const commander_1 = require("commander");
8
- const fs_1 = __importDefault(require("fs"));
9
- const path_1 = __importDefault(require("path"));
10
- const chalk_1 = __importDefault(require("chalk"));
11
- const inheritance_parser_1 = require("../../core/utils/inheritance-parser");
12
- exports.doctorCommand = new commander_1.Command('doctor')
13
- .description('Validate FRAIM installation and configuration')
14
- .action(async () => {
15
- const projectRoot = process.cwd();
16
- const fraimDir = path_1.default.join(projectRoot, '.fraim');
17
- let issuesFound = 0;
18
- console.log(chalk_1.default.blue('🩺 FRAIM Doctor - Checking health...\n'));
19
- // 1. Check .fraim directory
20
- if (!fs_1.default.existsSync(fraimDir)) {
21
- console.log(chalk_1.default.red('❌ Missing .fraim/ directory. Run "fraim setup" or "fraim init-project".'));
22
- issuesFound++;
23
- }
24
- else {
25
- console.log(chalk_1.default.green('✅ .fraim/ directory exists.'));
6
+ const fs_1 = require("fs");
7
+ const path_1 = require("path");
8
+ const check_runner_1 = require("../doctor/check-runner");
9
+ const console_reporter_1 = require("../doctor/reporters/console-reporter");
10
+ const json_reporter_1 = require("../doctor/reporters/json-reporter");
11
+ const global_setup_checks_1 = require("../doctor/checks/global-setup-checks");
12
+ const project_setup_checks_1 = require("../doctor/checks/project-setup-checks");
13
+ const workflow_checks_1 = require("../doctor/checks/workflow-checks");
14
+ const ide_config_checks_1 = require("../doctor/checks/ide-config-checks");
15
+ const mcp_connectivity_checks_1 = require("../doctor/checks/mcp-connectivity-checks");
16
+ const scripts_checks_1 = require("../doctor/checks/scripts-checks");
17
+ // Read version from package.json
18
+ const getFramVersion = () => {
19
+ try {
20
+ const packageJsonPath = (0, path_1.join)(__dirname, '../../../package.json');
21
+ const packageJson = JSON.parse((0, fs_1.readFileSync)(packageJsonPath, 'utf-8'));
22
+ return packageJson.version;
26
23
  }
27
- // 2. Check workflows subdirectory
28
- const workflowsDir = path_1.default.join(fraimDir, 'workflows');
29
- if (!fs_1.default.existsSync(workflowsDir)) {
30
- console.log(chalk_1.default.red('❌ Missing .fraim/workflows/ directory.'));
31
- issuesFound++;
24
+ catch (error) {
25
+ return 'unknown';
32
26
  }
33
- else {
34
- const stubs = fs_1.default.readdirSync(workflowsDir).filter(f => f.endsWith('.md'));
35
- console.log(chalk_1.default.green(`✅ .fraim/workflows/ exists (${stubs.length} stubs found).`));
27
+ };
28
+ const FRAIM_VERSION = getFramVersion();
29
+ // Simple logger for doctor command
30
+ const logger = {
31
+ info: (...args) => {
32
+ if (process.env.VERBOSE)
33
+ console.log('[DOCTOR]', ...args);
34
+ },
35
+ warn: (...args) => {
36
+ console.warn('[DOCTOR]', ...args);
37
+ },
38
+ error: (...args) => {
39
+ console.error('[DOCTOR]', ...args);
36
40
  }
37
- // 3. Check config.json
38
- const configPath = path_1.default.join(fraimDir, 'config.json');
39
- if (!fs_1.default.existsSync(configPath)) {
40
- console.log(chalk_1.default.red('❌ Missing .fraim/config.json.'));
41
- issuesFound++;
41
+ };
42
+ // Simple metric tracker (no-op for now)
43
+ const trackMetric = (name, value) => {
44
+ if (process.env.DEBUG) {
45
+ console.debug(`[METRIC] ${name}: ${value}`);
42
46
  }
43
- else {
44
- try {
45
- JSON.parse(fs_1.default.readFileSync(configPath, 'utf8'));
46
- console.log(chalk_1.default.green('✅ .fraim/config.json is valid JSON.'));
47
- }
48
- catch (e) {
49
- console.log(chalk_1.default.red('❌ .fraim/config.json is corrupted.'));
50
- issuesFound++;
47
+ };
48
+ /**
49
+ * Get all checks for doctor command
50
+ * Exported for testing
51
+ */
52
+ function getAllChecks() {
53
+ return [
54
+ ...(0, global_setup_checks_1.getGlobalSetupChecks)(),
55
+ ...(0, project_setup_checks_1.getProjectSetupChecks)(),
56
+ ...(0, workflow_checks_1.getWorkflowChecks)(),
57
+ ...(0, ide_config_checks_1.getIDEConfigChecks)(),
58
+ ...(0, mcp_connectivity_checks_1.getMCPConnectivityChecks)(),
59
+ ...(0, scripts_checks_1.getScriptsChecks)()
60
+ ];
61
+ }
62
+ exports.doctorCommand = new commander_1.Command('doctor')
63
+ .description('Validate FRAIM installation and configuration')
64
+ .option('--test-mcp', 'Test only MCP server connectivity')
65
+ .option('--test-config', 'Validate only configuration files')
66
+ .option('--test-workflows', 'Check only workflow status')
67
+ .option('--verbose', 'Show detailed output including successful checks')
68
+ .option('--json', 'Output results as JSON')
69
+ .action(async (cmdOptions) => {
70
+ const startTime = Date.now();
71
+ const options = {
72
+ testMcp: cmdOptions.testMcp,
73
+ testConfig: cmdOptions.testConfig,
74
+ testWorkflows: cmdOptions.testWorkflows,
75
+ verbose: cmdOptions.verbose,
76
+ json: cmdOptions.json
77
+ };
78
+ // Log command start
79
+ logger.info('Doctor command started', {
80
+ flags: {
81
+ testMcp: options.testMcp || false,
82
+ testConfig: options.testConfig || false,
83
+ testWorkflows: options.testWorkflows || false,
84
+ verbose: options.verbose || false,
85
+ json: options.json || false
51
86
  }
52
- }
53
- // 4. Check sync status (remote or local)
54
- // With Issue #83, stubs are no longer in the package - they're fetched from remote server
55
- // Check if config has remoteUrl configured
87
+ });
88
+ // Track execution count
89
+ trackMetric('doctor.execution.count', 1);
90
+ // Track flag usage
91
+ if (options.testMcp)
92
+ trackMetric('doctor.flags.test_mcp', 1);
93
+ if (options.testConfig)
94
+ trackMetric('doctor.flags.test_config', 1);
95
+ if (options.testWorkflows)
96
+ trackMetric('doctor.flags.test_workflows', 1);
97
+ if (options.verbose)
98
+ trackMetric('doctor.flags.verbose', 1);
99
+ if (options.json)
100
+ trackMetric('doctor.flags.json', 1);
56
101
  try {
57
- const config = JSON.parse(fs_1.default.readFileSync(configPath, 'utf8'));
58
- if (config.remoteUrl && config.apiKey) {
59
- console.log(chalk_1.default.green('✅ Remote sync configured (remoteUrl and apiKey present).'));
102
+ // Collect all checks
103
+ const checks = getAllChecks();
104
+ // Run checks
105
+ const result = await (0, check_runner_1.runChecks)(checks, options, FRAIM_VERSION);
106
+ const duration = Date.now() - startTime;
107
+ // Log results
108
+ logger.info('Doctor command completed', {
109
+ duration,
110
+ summary: result.summary
111
+ });
112
+ // Track metrics
113
+ trackMetric('doctor.execution.duration', duration);
114
+ trackMetric('doctor.checks.passed', result.summary.passed);
115
+ trackMetric('doctor.checks.warnings', result.summary.warnings);
116
+ trackMetric('doctor.checks.errors', result.summary.errors);
117
+ trackMetric('doctor.checks.total', result.summary.total);
118
+ // Log warnings and errors
119
+ if (result.summary.warnings > 0) {
120
+ logger.warn(`Doctor found ${result.summary.warnings} warning(s)`);
60
121
  }
61
- else {
62
- console.log(chalk_1.default.yellow('⚠️ Remote sync not configured. Run "fraim sync" to fetch workflows from server.'));
63
- console.log(chalk_1.default.gray(' Add remoteUrl and apiKey to .fraim/config.json for remote sync.'));
122
+ if (result.summary.errors > 0) {
123
+ logger.error(`Doctor found ${result.summary.errors} error(s)`);
64
124
  }
65
- }
66
- catch (e) {
67
- // Config check already failed above, skip this check
68
- }
69
- if (issuesFound === 0) {
70
- console.log(chalk_1.default.green('\n✨ Everything looks great! Your project is FRAIM-ready.'));
71
- }
72
- else {
73
- console.log(chalk_1.default.red(`\n🩹 Found ${issuesFound} issues. See recommendations above.`));
74
- }
75
- // 5. Check for overrides
76
- const overridesDir = path_1.default.join(fraimDir, 'overrides');
77
- if (fs_1.default.existsSync(overridesDir)) {
78
- console.log(chalk_1.default.blue('\n📝 Override Diagnostics:\n'));
79
- const overrides = [];
80
- const scanDir = (dir, base = '') => {
81
- const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
82
- for (const entry of entries) {
83
- const relativePath = path_1.default.join(base, entry.name);
84
- if (entry.isDirectory()) {
85
- scanDir(path_1.default.join(dir, entry.name), relativePath);
86
- }
87
- else {
88
- overrides.push(relativePath.replace(/\\/g, '/'));
89
- }
90
- }
91
- };
92
- scanDir(overridesDir);
93
- if (overrides.length === 0) {
94
- console.log(chalk_1.default.gray(' No active overrides found.'));
125
+ // Output results
126
+ if (options.json) {
127
+ console.log((0, json_reporter_1.formatJsonOutput)(result));
95
128
  }
96
129
  else {
97
- console.log(chalk_1.default.green(` Found ${overrides.length} active override(s):\n`));
98
- const parser = new inheritance_parser_1.InheritanceParser();
99
- for (const override of overrides) {
100
- const overridePath = path_1.default.join(overridesDir, override);
101
- const content = fs_1.default.readFileSync(overridePath, 'utf-8');
102
- const parseResult = parser.parse(content);
103
- if (parseResult.hasImports) {
104
- console.log(chalk_1.default.white(` 📄 ${override}`));
105
- console.log(chalk_1.default.gray(` Inherits from: ${parseResult.imports.join(', ')}`));
106
- // Validate import syntax
107
- let hasErrors = false;
108
- for (const importPath of parseResult.imports) {
109
- try {
110
- parser.sanitizePath(importPath);
111
- }
112
- catch (error) {
113
- console.log(chalk_1.default.red(` ⚠️ Invalid import: ${error.message}`));
114
- hasErrors = true;
115
- }
116
- }
117
- if (!hasErrors) {
118
- console.log(chalk_1.default.green(` ✅ Syntax valid`));
119
- }
120
- }
121
- else {
122
- console.log(chalk_1.default.white(` 📄 ${override}`));
123
- console.log(chalk_1.default.gray(` Full override (no inheritance)`));
124
- }
125
- console.log('');
126
- }
130
+ console.log((0, console_reporter_1.formatConsoleOutput)(result, options));
131
+ }
132
+ // Set exit code
133
+ if (result.summary.errors > 0) {
134
+ process.exit(2);
127
135
  }
136
+ else if (result.summary.warnings > 0) {
137
+ process.exit(1);
138
+ }
139
+ }
140
+ catch (error) {
141
+ const duration = Date.now() - startTime;
142
+ logger.error('Doctor command failed', { error, duration });
143
+ trackMetric('doctor.execution.errors', 1);
144
+ throw error;
128
145
  }
129
146
  });