fraim-framework 2.0.82 → 2.0.83

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.
@@ -9,11 +9,13 @@ const chalk_1 = __importDefault(require("chalk"));
9
9
  const prompts_1 = __importDefault(require("prompts"));
10
10
  const fs_1 = __importDefault(require("fs"));
11
11
  const path_1 = __importDefault(require("path"));
12
- const token_validator_1 = require("../setup/token-validator");
13
12
  const mcp_config_generator_1 = require("../setup/mcp-config-generator");
14
13
  const ide_detector_1 = require("../setup/ide-detector");
15
14
  const script_sync_utils_1 = require("../utils/script-sync-utils");
16
15
  const mcp_config_generator_2 = require("../setup/mcp-config-generator");
16
+ const provider_registry_1 = require("../providers/provider-registry");
17
+ const provider_prompts_1 = require("../setup/provider-prompts");
18
+ const get_provider_client_1 = require("../api/get-provider-client");
17
19
  const loadGlobalConfig = () => {
18
20
  const globalConfigPath = path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'config.json');
19
21
  if (!fs_1.default.existsSync(globalConfigPath)) {
@@ -30,147 +32,6 @@ const saveGlobalConfig = (config) => {
30
32
  const globalConfigPath = path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'config.json');
31
33
  fs_1.default.writeFileSync(globalConfigPath, JSON.stringify(config, null, 2));
32
34
  };
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
35
  const updateIDEConfigs = async (provider, config) => {
175
36
  const detectedIDEs = (0, ide_detector_1.detectInstalledIDEs)();
176
37
  if (detectedIDEs.length === 0) {
@@ -183,10 +44,7 @@ const updateIDEConfigs = async (provider, config) => {
183
44
  gitlab: config.tokens?.gitlab,
184
45
  jira: config.tokens?.jira
185
46
  };
186
- const jiraConfig = config.jiraConfig ? {
187
- baseUrl: config.jiraConfig.baseUrl,
188
- email: config.jiraConfig.email
189
- } : undefined;
47
+ const providerConfigs = config.providerConfigs || {};
190
48
  for (const ide of detectedIDEs) {
191
49
  try {
192
50
  const configPath = (0, ide_detector_1.expandPath)(ide.configPath);
@@ -196,7 +54,7 @@ const updateIDEConfigs = async (provider, config) => {
196
54
  if (fs_1.default.existsSync(configPath)) {
197
55
  ideConfig = JSON.parse(fs_1.default.readFileSync(configPath, 'utf8'));
198
56
  }
199
- const mcpConfig = (0, mcp_config_generator_1.generateMCPConfig)(ide.configType, config.apiKey, tokenConfig, jiraConfig);
57
+ const mcpConfig = await (0, mcp_config_generator_1.generateMCPConfig)(ide.configType, config.apiKey, tokenConfig, providerConfigs);
200
58
  const serversKey = ide.configType === 'vscode' ? 'servers' : 'mcpServers';
201
59
  if (!ideConfig[serversKey]) {
202
60
  ideConfig[serversKey] = {};
@@ -211,7 +69,7 @@ const updateIDEConfigs = async (provider, config) => {
211
69
  }
212
70
  else if (ide.configFormat === 'toml') {
213
71
  // TOML-based config (Codex, Zed)
214
- const mcpConfigToml = (0, mcp_config_generator_1.generateMCPConfig)(ide.configType, config.apiKey, tokenConfig, jiraConfig);
72
+ const mcpConfigToml = await (0, mcp_config_generator_1.generateMCPConfig)(ide.configType, config.apiKey, tokenConfig, providerConfigs);
215
73
  if (fs_1.default.existsSync(configPath)) {
216
74
  const existingContent = fs_1.default.readFileSync(configPath, 'utf8');
217
75
  // Merge only the new provider's server
@@ -275,81 +133,63 @@ const runAddProvider = async (provider, options) => {
275
133
  config.tokens = {};
276
134
  }
277
135
  // 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
- }
136
+ const providerName = await (0, provider_registry_1.getProviderDisplayName)(provider);
137
+ const hasExistingConfig = config.tokens[provider] ||
138
+ (provider === 'jira' && config.providerConfigs?.jiraConfig?.baseUrl);
139
+ if (hasExistingConfig) {
140
+ const displayInfo = provider === 'jira' && config.providerConfigs?.jiraConfig?.baseUrl
141
+ ? ` (${config.providerConfigs.jiraConfig.baseUrl})`
142
+ : '';
143
+ console.log(chalk_1.default.yellow(`āš ļø ${providerName} is already configured${displayInfo}`));
144
+ // Skip prompt if FRAIM_FORCE_OVERWRITE is set (for testing)
145
+ if (!process.env.FRAIM_FORCE_OVERWRITE) {
146
+ const response = await (0, prompts_1.default)({
147
+ type: 'confirm',
148
+ name: 'overwrite',
149
+ message: `Overwrite existing ${providerName} configuration?`,
150
+ initial: false
151
+ });
152
+ if (!response.overwrite) {
153
+ console.log(chalk_1.default.gray('Cancelled.'));
154
+ process.exit(0);
293
155
  }
294
156
  }
295
157
  }
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
158
+ // Get credentials using generic prompt system
315
159
  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
160
+ // Build provided tokens/configs from CLI options
161
+ const providedTokens = {};
162
+ const providedConfigs = {};
163
+ if (options.token) {
164
+ providedTokens[provider] = options.token;
165
+ }
166
+ // Handle provider-specific config options (e.g., Jira URL and email)
167
+ if (options.url || options.email) {
168
+ providedConfigs[provider] = {
169
+ ...(options.url && { baseUrl: options.url.replace(/^https?:\/\//, '').replace(/\/$/, '') }),
170
+ ...(options.email && { email: options.email })
344
171
  };
345
172
  }
173
+ // Get credentials using generic prompt system
174
+ const client = (0, get_provider_client_1.getProviderClient)();
175
+ const creds = await (0, provider_prompts_1.promptForProviderCredentials)(client, provider, providedTokens[provider], providedConfigs[provider]);
176
+ // Save token
177
+ config.tokens[provider] = creds.token;
178
+ // Save provider-specific config if present
179
+ if (creds.config) {
180
+ if (!config.providerConfigs) {
181
+ config.providerConfigs = {};
182
+ }
183
+ config.providerConfigs[`${provider}Config`] = creds.config;
184
+ }
346
185
  // Save updated config
347
186
  saveGlobalConfig(config);
348
- console.log(chalk_1.default.green(`\nāœ… ${provider.toUpperCase()} credentials saved to global config`));
187
+ console.log(chalk_1.default.green(`\nāœ… ${providerName} credentials saved to global config`));
349
188
  // Update IDE configs
350
189
  await updateIDEConfigs(provider, config);
351
- // Offer mode switch if adding Jira
352
- if (provider === 'jira') {
190
+ // Offer mode switch if adding a provider with issues capability
191
+ const providerDef = await (0, provider_registry_1.getProvider)(provider);
192
+ if (providerDef?.capabilities.includes('issues') && provider === 'jira') {
353
193
  await offerModeSwitch(config);
354
194
  }
355
195
  console.log(chalk_1.default.green('\nšŸŽ‰ Provider added successfully!'));
@@ -366,17 +206,17 @@ const runAddProvider = async (provider, options) => {
366
206
  };
367
207
  exports.runAddProvider = runAddProvider;
368
208
  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')
209
+ .description(`Add or update a provider after initial setup`)
210
+ .argument('<provider>', `Provider to add`)
371
211
  .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)')
212
+ .option('--email <email>', 'Email (for providers that require it)')
213
+ .option('--url <url>', 'Instance URL (for providers that require it)')
214
+ .option('--no-validate', 'Skip token validation')
375
215
  .action(async (provider, options) => {
376
- const validProviders = ['github', 'gitlab', 'ado', 'jira'];
377
- if (!validProviders.includes(provider)) {
216
+ if (!(await (0, provider_registry_1.isValidProviderId)(provider))) {
217
+ const allProviderIds = await (0, provider_registry_1.getAllProviderIds)();
378
218
  console.log(chalk_1.default.red(`āŒ Invalid provider: ${provider}`));
379
- console.log(chalk_1.default.yellow(`Valid providers: ${validProviders.join(', ')}`));
219
+ console.log(chalk_1.default.yellow(`Valid providers: ${allProviderIds.join(', ')}`));
380
220
  process.exit(1);
381
221
  }
382
222
  await (0, exports.runAddProvider)(provider, options);
@@ -129,13 +129,16 @@ exports.doctorCommand = new commander_1.Command('doctor')
129
129
  else {
130
130
  console.log((0, console_reporter_1.formatConsoleOutput)(result, options));
131
131
  }
132
- // Set exit code
132
+ // Set exit code based on results
133
133
  if (result.summary.errors > 0) {
134
134
  process.exit(2);
135
135
  }
136
136
  else if (result.summary.warnings > 0) {
137
137
  process.exit(1);
138
138
  }
139
+ else {
140
+ process.exit(0);
141
+ }
139
142
  }
140
143
  catch (error) {
141
144
  const duration = Date.now() - startTime;
@@ -15,6 +15,7 @@ const platform_detection_1 = require("../utils/platform-detection");
15
15
  const version_utils_1 = require("../utils/version-utils");
16
16
  const ide_detector_1 = require("../setup/ide-detector");
17
17
  const codex_local_config_1 = require("../setup/codex-local-config");
18
+ const provider_registry_1 = require("../providers/provider-registry");
18
19
  const promptForJiraProjectKey = async (jiraBaseUrl) => {
19
20
  console.log(chalk_1.default.blue('\nšŸŽ« Jira Project Configuration'));
20
21
  console.log(chalk_1.default.gray(`Jira instance: ${jiraBaseUrl}`));
@@ -51,7 +52,7 @@ const checkGlobalSetup = () => {
51
52
  exists: true,
52
53
  mode: config.mode || 'integrated',
53
54
  tokens: config.tokens || {},
54
- jiraConfig: config.jiraConfig
55
+ providerConfigs: config.providerConfigs || {}
55
56
  };
56
57
  }
57
58
  catch {
@@ -98,41 +99,27 @@ const runInitProject = async () => {
98
99
  // Determine issue tracking configuration
99
100
  let issueTracking;
100
101
  // In split mode with Jira configured, use Jira for issue tracking
101
- if (preferredMode === 'split' && globalSetup.tokens?.jira && globalSetup.jiraConfig?.baseUrl) {
102
+ const jiraConfig = globalSetup.providerConfigs?.jiraConfig;
103
+ if (preferredMode === 'split' && globalSetup.tokens?.jira && jiraConfig?.baseUrl) {
102
104
  // Prompt for Jira project key (project-specific)
103
105
  const projectKey = process.env.FRAIM_JIRA_PROJECT_KEY ||
104
- await promptForJiraProjectKey(globalSetup.jiraConfig.baseUrl);
106
+ await promptForJiraProjectKey(jiraConfig.baseUrl);
105
107
  issueTracking = {
106
108
  provider: 'jira',
107
- baseUrl: globalSetup.jiraConfig.baseUrl,
109
+ baseUrl: jiraConfig.baseUrl,
108
110
  projectKey: projectKey,
109
- email: globalSetup.jiraConfig.email
111
+ email: jiraConfig.email
110
112
  };
111
113
  console.log(chalk_1.default.blue(` Code Repository: ${detection.provider.toUpperCase()}`));
112
114
  console.log(chalk_1.default.blue(` Issue Tracking: JIRA (${projectKey})`));
113
115
  }
114
116
  else {
115
- // Integrated mode: use same provider for both code and issues
116
- issueTracking = detection.provider === 'github'
117
- ? {
118
- provider: 'github',
119
- owner: detection.repository.owner,
120
- name: detection.repository.name
121
- }
122
- : detection.provider === 'ado'
123
- ? {
124
- provider: 'ado',
125
- organization: detection.repository.organization,
126
- project: detection.repository.project,
127
- name: detection.repository.name
128
- }
129
- : {
130
- provider: 'gitlab',
131
- namespace: detection.repository.namespace,
132
- name: detection.repository.name,
133
- projectPath: detection.repository.projectPath
134
- };
135
- console.log(chalk_1.default.blue(` Platform: ${detection.provider.toUpperCase()}`));
117
+ issueTracking = {
118
+ provider: detection.provider,
119
+ ...detection.repository
120
+ };
121
+ const providerDef = await (0, provider_registry_1.getProvider)(detection.provider);
122
+ console.log(chalk_1.default.blue(` Platform: ${providerDef?.displayName || detection.provider.toUpperCase()}`));
136
123
  }
137
124
  config = {
138
125
  version: (0, version_utils_1.getFraimVersion)(),
@@ -143,17 +130,19 @@ const runInitProject = async () => {
143
130
  issueTracking,
144
131
  customizations: {}
145
132
  };
146
- if (detection.provider === 'github') {
147
- console.log(chalk_1.default.gray(` Repository: ${detection.repository.owner}/${detection.repository.name}`));
133
+ // Display repository info based on available fields
134
+ const repo = detection.repository;
135
+ if (repo.owner && repo.name) {
136
+ console.log(chalk_1.default.gray(` Repository: ${repo.owner}/${repo.name}`));
148
137
  }
149
- else if (detection.provider === 'ado') {
150
- console.log(chalk_1.default.gray(` Organization: ${detection.repository.organization}`));
151
- console.log(chalk_1.default.gray(` Project: ${detection.repository.project}`));
152
- console.log(chalk_1.default.gray(` Repository: ${detection.repository.name}`));
138
+ else if (repo.organization && repo.project && repo.name) {
139
+ console.log(chalk_1.default.gray(` Organization: ${repo.organization}`));
140
+ console.log(chalk_1.default.gray(` Project: ${repo.project}`));
141
+ console.log(chalk_1.default.gray(` Repository: ${repo.name}`));
153
142
  }
154
- else if (detection.provider === 'gitlab') {
155
- console.log(chalk_1.default.gray(` Namespace: ${detection.repository.namespace || '(none)'}`));
156
- console.log(chalk_1.default.gray(` Repository: ${detection.repository.name}`));
143
+ else if (repo.namespace && repo.name) {
144
+ console.log(chalk_1.default.gray(` Namespace: ${repo.namespace || '(none)'}`));
145
+ console.log(chalk_1.default.gray(` Repository: ${repo.name}`));
157
146
  }
158
147
  }
159
148
  else {