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 +6 -4
- package/dist/src/cli/commands/add-provider.js +383 -0
- package/dist/src/cli/commands/doctor.js +129 -112
- package/dist/src/cli/commands/init-project.js +66 -19
- package/dist/src/cli/commands/sync.js +2 -2
- package/dist/src/cli/doctor/check-runner.js +196 -0
- package/dist/src/cli/doctor/checks/global-setup-checks.js +221 -0
- package/dist/src/cli/doctor/checks/ide-config-checks.js +229 -0
- package/dist/src/cli/doctor/checks/mcp-connectivity-checks.js +203 -0
- package/dist/src/cli/doctor/checks/project-setup-checks.js +282 -0
- package/dist/src/cli/doctor/checks/scripts-checks.js +157 -0
- package/dist/src/cli/doctor/checks/workflow-checks.js +247 -0
- package/dist/src/cli/doctor/reporters/console-reporter.js +96 -0
- package/dist/src/cli/doctor/reporters/json-reporter.js +11 -0
- package/dist/src/cli/doctor/types.js +6 -0
- package/dist/src/cli/fraim.js +2 -2
- package/dist/src/cli/setup/mcp-config-generator.js +23 -21
- package/dist/src/cli/utils/remote-sync.js +22 -2
- package/package.json +3 -2
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": "
|
|
310
|
-
"args": ["-
|
|
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
|
|
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 `
|
|
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 =
|
|
9
|
-
const path_1 =
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
//
|
|
54
|
-
|
|
55
|
-
//
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
});
|