fraim-framework 2.0.86 ā 2.0.88
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 +30 -0
- package/bin/fraim.js +1 -1
- package/dist/src/cli/commands/add-provider.js +16 -6
- package/dist/src/cli/commands/init-project.js +103 -1
- package/dist/src/cli/commands/login.js +84 -0
- package/dist/src/cli/commands/setup.js +135 -13
- package/dist/src/cli/fraim.js +2 -0
- package/dist/src/cli/internal/device-flow-service.js +83 -0
- package/dist/src/cli/mcp/mcp-server-registry.js +11 -10
- package/dist/src/cli/providers/local-provider-registry.js +22 -1
- package/dist/src/cli/services/device-flow-service.js +83 -0
- package/dist/src/cli/setup/provider-prompts.js +39 -0
- package/dist/src/cli/utils/remote-sync.js +159 -28
- package/dist/src/core/ai-mentor.js +248 -0
- package/dist/src/core/utils/git-utils.js +6 -6
- package/dist/src/core/utils/include-resolver.js +45 -0
- package/dist/src/core/utils/inheritance-parser.js +154 -16
- package/dist/src/core/utils/local-registry-resolver.js +326 -22
- package/dist/src/core/utils/server-startup.js +34 -0
- package/dist/src/core/utils/stub-generator.js +34 -27
- package/dist/src/core/utils/workflow-parser.js +32 -2
- package/dist/src/local-mcp-server/stdio-server.js +240 -284
- package/index.js +26 -5
- package/package.json +15 -5
package/README.md
CHANGED
|
@@ -304,6 +304,36 @@ fraim sync # Sync latest workflows and rules
|
|
|
304
304
|
|
|
305
305
|
**š” Pro Tip**: Use `fraim add-ide` when you install a new IDE after initial setup. It reuses your existing FRAIM and platform tokens, making it much faster than running full setup again.
|
|
306
306
|
|
|
307
|
+
### **š§© Personalized Jobs, Skills, and Rules**
|
|
308
|
+
|
|
309
|
+
Project-specific customization now lives under `.fraim/personalized-employee/`.
|
|
310
|
+
|
|
311
|
+
Recommended layout:
|
|
312
|
+
|
|
313
|
+
```text
|
|
314
|
+
.fraim/
|
|
315
|
+
personalized-employee/
|
|
316
|
+
jobs/
|
|
317
|
+
skills/
|
|
318
|
+
rules/
|
|
319
|
+
templates/
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Use `fraim override` to create a local starting point:
|
|
323
|
+
|
|
324
|
+
```bash
|
|
325
|
+
fraim override --inherit jobs/product-building/feature-implementation.md
|
|
326
|
+
fraim override --copy rules/engineering/architecture-standards.md
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Guidance:
|
|
330
|
+
- Put phased job customizations in `.fraim/personalized-employee/jobs/...`
|
|
331
|
+
- Put reusable local capability snippets in `.fraim/personalized-employee/skills/...`
|
|
332
|
+
- Put broad team conventions in `.fraim/personalized-employee/rules/...`
|
|
333
|
+
- Put local deliverable tweaks in `.fraim/personalized-employee/templates/...`
|
|
334
|
+
- Do not edit synced content under `.fraim/ai-employee/` or `.fraim/ai-manager/`; `fraim sync` will overwrite it
|
|
335
|
+
- Legacy `.fraim/overrides/` is still read for compatibility, but new work should go in `.fraim/personalized-employee/`
|
|
336
|
+
|
|
307
337
|
### **š§ Jira Integration Setup**
|
|
308
338
|
|
|
309
339
|
FRAIM uses the official Model Context Protocol (MCP) server for Jira integration. The setup command automatically configures the correct format.
|
package/bin/fraim.js
CHANGED
|
@@ -42,9 +42,14 @@ const updateIDEConfigs = async (provider, config) => {
|
|
|
42
42
|
const tokenConfig = {
|
|
43
43
|
github: config.tokens?.github,
|
|
44
44
|
gitlab: config.tokens?.gitlab,
|
|
45
|
+
ado: config.tokens?.ado,
|
|
45
46
|
jira: config.tokens?.jira
|
|
46
47
|
};
|
|
47
|
-
const providerConfigs = config.providerConfigs || {}
|
|
48
|
+
const providerConfigs = Object.entries(config.providerConfigs || {}).reduce((acc, [key, value]) => {
|
|
49
|
+
const providerId = key.replace(/Config$/, '');
|
|
50
|
+
acc[providerId] = value;
|
|
51
|
+
return acc;
|
|
52
|
+
}, {});
|
|
48
53
|
for (const ide of detectedIDEs) {
|
|
49
54
|
try {
|
|
50
55
|
const configPath = (0, ide_detector_1.expandPath)(ide.configPath);
|
|
@@ -135,11 +140,14 @@ const runAddProvider = async (provider, options) => {
|
|
|
135
140
|
// Check if provider already configured
|
|
136
141
|
const providerName = await (0, provider_registry_1.getProviderDisplayName)(provider);
|
|
137
142
|
const hasExistingConfig = config.tokens[provider] ||
|
|
138
|
-
(provider === 'jira' && config.providerConfigs?.jiraConfig?.baseUrl)
|
|
143
|
+
(provider === 'jira' && config.providerConfigs?.jiraConfig?.baseUrl) ||
|
|
144
|
+
(provider === 'ado' && config.providerConfigs?.adoConfig?.organization);
|
|
139
145
|
if (hasExistingConfig) {
|
|
140
146
|
const displayInfo = provider === 'jira' && config.providerConfigs?.jiraConfig?.baseUrl
|
|
141
147
|
? ` (${config.providerConfigs.jiraConfig.baseUrl})`
|
|
142
|
-
: ''
|
|
148
|
+
: provider === 'ado' && config.providerConfigs?.adoConfig?.organization
|
|
149
|
+
? ` (${config.providerConfigs.adoConfig.organization})`
|
|
150
|
+
: '';
|
|
143
151
|
console.log(chalk_1.default.yellow(`ā ļø ${providerName} is already configured${displayInfo}`));
|
|
144
152
|
// Skip prompt if FRAIM_FORCE_OVERWRITE is set (for testing)
|
|
145
153
|
if (!process.env.FRAIM_FORCE_OVERWRITE) {
|
|
@@ -163,11 +171,12 @@ const runAddProvider = async (provider, options) => {
|
|
|
163
171
|
if (options.token) {
|
|
164
172
|
providedTokens[provider] = options.token;
|
|
165
173
|
}
|
|
166
|
-
// Handle provider-specific config options (e.g., Jira URL
|
|
167
|
-
if (options.url || options.email) {
|
|
174
|
+
// Handle provider-specific config options (e.g., Jira URL/email or ADO organization)
|
|
175
|
+
if (options.url || options.email || options.organization) {
|
|
168
176
|
providedConfigs[provider] = {
|
|
169
177
|
...(options.url && { baseUrl: options.url.replace(/^https?:\/\//, '').replace(/\/$/, '') }),
|
|
170
|
-
...(options.email && { email: options.email })
|
|
178
|
+
...(options.email && { email: options.email }),
|
|
179
|
+
...(options.organization && { organization: options.organization })
|
|
171
180
|
};
|
|
172
181
|
}
|
|
173
182
|
// Get credentials using generic prompt system
|
|
@@ -209,6 +218,7 @@ exports.addProviderCommand = new commander_1.Command('add-provider')
|
|
|
209
218
|
.description(`Add or update a provider after initial setup`)
|
|
210
219
|
.argument('<provider>', `Provider to add`)
|
|
211
220
|
.option('--token <token>', 'Provider token (will prompt if not provided)')
|
|
221
|
+
.option('--organization <organization>', 'Organization (for providers that require it)')
|
|
212
222
|
.option('--email <email>', 'Email (for providers that require it)')
|
|
213
223
|
.option('--url <url>', 'Instance URL (for providers that require it)')
|
|
214
224
|
.option('--no-validate', 'Skip token validation')
|
|
@@ -10,6 +10,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
10
10
|
const os_1 = __importDefault(require("os"));
|
|
11
11
|
const chalk_1 = __importDefault(require("chalk"));
|
|
12
12
|
const prompts_1 = __importDefault(require("prompts"));
|
|
13
|
+
const child_process_1 = require("child_process");
|
|
13
14
|
const sync_1 = require("./sync");
|
|
14
15
|
const platform_detection_1 = require("../utils/platform-detection");
|
|
15
16
|
const version_utils_1 = require("../utils/version-utils");
|
|
@@ -21,6 +22,11 @@ const promptForJiraProjectKey = async (jiraBaseUrl) => {
|
|
|
21
22
|
console.log(chalk_1.default.blue('\nš« Jira Project Configuration'));
|
|
22
23
|
console.log(chalk_1.default.gray(`Jira instance: ${jiraBaseUrl}`));
|
|
23
24
|
console.log(chalk_1.default.gray('Enter the Jira project key for this repository (e.g., TEAM, PROJ, DEV)\n'));
|
|
25
|
+
if (process.env.FRAIM_NON_INTERACTIVE) {
|
|
26
|
+
const defaultKey = process.env.FRAIM_JIRA_PROJECT_KEY || 'PROJ';
|
|
27
|
+
console.log(chalk_1.default.yellow(`\nā¹ļø Non-interactive mode: using Jira project key "${defaultKey}"`));
|
|
28
|
+
return defaultKey;
|
|
29
|
+
}
|
|
24
30
|
const response = await (0, prompts_1.default)({
|
|
25
31
|
type: 'text',
|
|
26
32
|
name: 'projectKey',
|
|
@@ -60,6 +66,77 @@ const checkGlobalSetup = () => {
|
|
|
60
66
|
return { exists: true, mode: 'integrated', tokens: {} };
|
|
61
67
|
}
|
|
62
68
|
};
|
|
69
|
+
/**
|
|
70
|
+
* Install GitHub workflow files to .github/workflows/
|
|
71
|
+
* Gracefully handles cases where gh CLI is not available
|
|
72
|
+
*/
|
|
73
|
+
const installGitHubWorkflows = (projectRoot) => {
|
|
74
|
+
const workflowsDir = path_1.default.join(projectRoot, '.github', 'workflows');
|
|
75
|
+
// Find registry directory (works in both dev and installed package)
|
|
76
|
+
const registryDir = fs_1.default.existsSync(path_1.default.join(__dirname, '..', '..', '..', 'registry'))
|
|
77
|
+
? path_1.default.join(__dirname, '..', '..', '..', 'registry')
|
|
78
|
+
: path_1.default.join(__dirname, '..', '..', 'registry');
|
|
79
|
+
const sourceDir = path_1.default.join(registryDir, 'github', 'workflows');
|
|
80
|
+
// Create .github/workflows directory if it doesn't exist
|
|
81
|
+
if (!fs_1.default.existsSync(workflowsDir)) {
|
|
82
|
+
fs_1.default.mkdirSync(workflowsDir, { recursive: true });
|
|
83
|
+
}
|
|
84
|
+
const workflowFiles = ['phase-change.yml', 'status-change.yml', 'sync-on-pr-review.yml'];
|
|
85
|
+
workflowFiles.forEach((file) => {
|
|
86
|
+
const sourcePath = path_1.default.join(sourceDir, file);
|
|
87
|
+
const destPath = path_1.default.join(workflowsDir, file);
|
|
88
|
+
if (fs_1.default.existsSync(sourcePath)) {
|
|
89
|
+
fs_1.default.copyFileSync(sourcePath, destPath);
|
|
90
|
+
console.log(chalk_1.default.green(`Installed workflow: .github/workflows/${file}`));
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
console.log(chalk_1.default.yellow(`Warning: Workflow not found in registry: ${file}`));
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Create GitHub labels using gh CLI
|
|
99
|
+
* Gracefully handles cases where gh CLI is not available
|
|
100
|
+
*/
|
|
101
|
+
const createGitHubLabels = (projectRoot) => {
|
|
102
|
+
// Check if gh CLI is available
|
|
103
|
+
try {
|
|
104
|
+
(0, child_process_1.execSync)('gh --version', { stdio: 'ignore' });
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
console.log(chalk_1.default.yellow('GitHub CLI (gh) not found. Skipping label creation.'));
|
|
108
|
+
console.log(chalk_1.default.gray('Install gh CLI to enable automatic label creation: https://cli.github.com/'));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
// Read labels from labels.json
|
|
112
|
+
const labelsPath = path_1.default.join(__dirname, '..', '..', '..', 'labels.json');
|
|
113
|
+
if (!fs_1.default.existsSync(labelsPath)) {
|
|
114
|
+
console.log(chalk_1.default.yellow('labels.json not found. Skipping label creation.'));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
const labels = JSON.parse(fs_1.default.readFileSync(labelsPath, 'utf8'));
|
|
119
|
+
labels.forEach((label) => {
|
|
120
|
+
try {
|
|
121
|
+
// Try to create the label, ignore if it already exists
|
|
122
|
+
(0, child_process_1.execSync)(`gh label create "${label.name}" --color "${label.color}" --description "${label.description}"`, { cwd: projectRoot, stdio: 'ignore' });
|
|
123
|
+
console.log(chalk_1.default.green(`Created label: ${label.name}`));
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
// Label might already exist, which is fine
|
|
127
|
+
if (error.message && error.message.includes('already exists')) {
|
|
128
|
+
console.log(chalk_1.default.gray(`Label already exists: ${label.name}`));
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
console.log(chalk_1.default.yellow(`Could not create label ${label.name}: ${error.message}`));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
console.log(chalk_1.default.yellow(`Error reading labels.json: ${error.message}`));
|
|
138
|
+
}
|
|
139
|
+
};
|
|
63
140
|
const runInitProject = async () => {
|
|
64
141
|
console.log(chalk_1.default.blue('Initializing FRAIM project...'));
|
|
65
142
|
const globalSetup = checkGlobalSetup();
|
|
@@ -170,6 +247,13 @@ const runInitProject = async () => {
|
|
|
170
247
|
if ((0, fraim_gitignore_1.ensureFraimSyncedContentIgnored)(projectRoot)) {
|
|
171
248
|
console.log(chalk_1.default.green('Updated .gitignore with FRAIM synced content ignore rules'));
|
|
172
249
|
}
|
|
250
|
+
// Install GitHub workflows and create labels for GitHub repositories
|
|
251
|
+
const detection = (0, platform_detection_1.detectPlatformFromGit)();
|
|
252
|
+
if (detection.provider === 'github') {
|
|
253
|
+
console.log(chalk_1.default.blue('\nSetting up GitHub workflows and labels...'));
|
|
254
|
+
installGitHubWorkflows(projectRoot);
|
|
255
|
+
createGitHubLabels(projectRoot);
|
|
256
|
+
}
|
|
173
257
|
if (!process.env.FRAIM_SKIP_SYNC) {
|
|
174
258
|
await (0, sync_1.runSync)({});
|
|
175
259
|
}
|
|
@@ -180,7 +264,25 @@ const runInitProject = async () => {
|
|
|
180
264
|
console.log(chalk_1.default.green(`${status} project Codex config at ${codexLocalResult.path}`));
|
|
181
265
|
}
|
|
182
266
|
console.log(chalk_1.default.green('\nFRAIM project initialized!'));
|
|
183
|
-
|
|
267
|
+
if (!process.env.FRAIM_NON_INTERACTIVE) {
|
|
268
|
+
const response = await (0, prompts_1.default)({
|
|
269
|
+
type: 'confirm',
|
|
270
|
+
name: 'runOnboarding',
|
|
271
|
+
message: 'Would you like to set up your AI employee for success? Use the "ai-manager/jobs/project-setup/project-onboarding" job now.',
|
|
272
|
+
initial: true
|
|
273
|
+
});
|
|
274
|
+
if (response.runOnboarding) {
|
|
275
|
+
console.log(chalk_1.default.blue('\nš Proactive Onboarding: Project Success Setup'));
|
|
276
|
+
console.log(chalk_1.default.gray('Simply tell your AI agent: "run the project-onboarding job under ai-manager/project-setup"'));
|
|
277
|
+
console.log(chalk_1.default.gray('This will help the AI understand your project goals, tech stack, and industry context.'));
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
console.log(chalk_1.default.cyan('\nTip: You can always ask your AI agent to "list fraim jobs" to see what\'s available.'));
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
console.log(chalk_1.default.cyan('\nWould you like to set up your AI employee for success? Use the "ai-manager/jobs/project-setup/project-onboarding" job now.'));
|
|
285
|
+
}
|
|
184
286
|
};
|
|
185
287
|
exports.runInitProject = runInitProject;
|
|
186
288
|
exports.initProjectCommand = new commander_1.Command('init-project')
|
|
@@ -0,0 +1,84 @@
|
|
|
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.loginCommand = void 0;
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const device_flow_service_1 = require("../internal/device-flow-service");
|
|
10
|
+
const provider_client_1 = require("../api/provider-client");
|
|
11
|
+
const script_sync_utils_1 = require("../utils/script-sync-utils");
|
|
12
|
+
const fs_1 = __importDefault(require("fs"));
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
const version_utils_1 = require("../utils/version-utils");
|
|
15
|
+
const auto_mcp_setup_1 = require("../setup/auto-mcp-setup");
|
|
16
|
+
exports.loginCommand = new commander_1.Command('login')
|
|
17
|
+
.description('Login to platforms (GitHub, etc.)');
|
|
18
|
+
exports.loginCommand
|
|
19
|
+
.command('github')
|
|
20
|
+
.description('Login to GitHub using Device Flow')
|
|
21
|
+
.action(async () => {
|
|
22
|
+
// Load the token to global config
|
|
23
|
+
const globalConfigDir = (0, script_sync_utils_1.getUserFraimDir)();
|
|
24
|
+
const globalConfigPath = path_1.default.join(globalConfigDir, 'config.json');
|
|
25
|
+
let config = {};
|
|
26
|
+
if (fs_1.default.existsSync(globalConfigPath)) {
|
|
27
|
+
config = JSON.parse(fs_1.default.readFileSync(globalConfigPath, 'utf8'));
|
|
28
|
+
}
|
|
29
|
+
if (!config.apiKey) {
|
|
30
|
+
console.error(chalk_1.default.red('\nā No FRAIM API key found. Please run "fraim setup" first.'));
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
const client = new provider_client_1.ProviderClient(config.apiKey);
|
|
34
|
+
let githubConfig;
|
|
35
|
+
try {
|
|
36
|
+
const providers = await client.getAllProviders();
|
|
37
|
+
githubConfig = providers.find(p => p.id === 'github')?.deviceFlowConfig;
|
|
38
|
+
if (!githubConfig) {
|
|
39
|
+
console.error(chalk_1.default.red('\nā Device flow configuration for GitHub not found on server.'));
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
console.error(chalk_1.default.red(`\nā Failed to fetch provider configuration: ${error.message}`));
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
const deviceFlow = new device_flow_service_1.DeviceFlowService(githubConfig);
|
|
48
|
+
try {
|
|
49
|
+
const token = await deviceFlow.login();
|
|
50
|
+
config.tokens = {
|
|
51
|
+
...(config.tokens || {}),
|
|
52
|
+
github: token
|
|
53
|
+
};
|
|
54
|
+
config.version = config.version || (0, version_utils_1.getFraimVersion)();
|
|
55
|
+
config.updatedAt = new Date().toISOString();
|
|
56
|
+
fs_1.default.writeFileSync(globalConfigPath, JSON.stringify(config, null, 2));
|
|
57
|
+
console.log(chalk_1.default.green('\nā
GitHub token saved to global configuration.'));
|
|
58
|
+
// Trigger MCP reconfiguration
|
|
59
|
+
if (config.apiKey) {
|
|
60
|
+
console.log(chalk_1.default.blue('š Updating MCP server configurations...'));
|
|
61
|
+
const mcpTokens = {};
|
|
62
|
+
Object.entries(config.tokens).forEach(([id, t]) => {
|
|
63
|
+
mcpTokens[id] = t;
|
|
64
|
+
});
|
|
65
|
+
// Re-use existing provider configs if any
|
|
66
|
+
const providerConfigsMap = {};
|
|
67
|
+
if (config.providerConfigs) {
|
|
68
|
+
Object.entries(config.providerConfigs).forEach(([key, val]) => {
|
|
69
|
+
const providerId = key.replace('Config', '');
|
|
70
|
+
providerConfigsMap[providerId] = val;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
await (0, auto_mcp_setup_1.autoConfigureMCP)(config.apiKey, mcpTokens, undefined, providerConfigsMap);
|
|
74
|
+
console.log(chalk_1.default.green('ā
MCP servers updated with new GitHub token.'));
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
console.log(chalk_1.default.yellow('ā ļø No FRAIM API key found. Run "fraim setup" to complete configuration.'));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
console.error(chalk_1.default.red(`\nā Login failed: ${error.message}`));
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
@@ -245,7 +245,7 @@ const runSetup = async (options) => {
|
|
|
245
245
|
let providedConfigs = {};
|
|
246
246
|
if (isUpdate) {
|
|
247
247
|
// Update existing setup - add platforms
|
|
248
|
-
console.log(chalk_1.default.blue('š
|
|
248
|
+
console.log(chalk_1.default.blue('š Existing FRAIM configuration detected.\n'));
|
|
249
249
|
try {
|
|
250
250
|
const existingConfig = JSON.parse(fs_1.default.readFileSync(globalConfigPath, 'utf8'));
|
|
251
251
|
// Allow updating FRAIM key even without provider changes
|
|
@@ -258,11 +258,141 @@ const runSetup = async (options) => {
|
|
|
258
258
|
// Check if this is just a key update or a provider update
|
|
259
259
|
const isKeyUpdate = options.key && options.key !== existingConfig.apiKey;
|
|
260
260
|
const isProviderUpdate = requestedProviders.length > 0;
|
|
261
|
-
//
|
|
261
|
+
// If no specific changes requested, offer interactive update
|
|
262
262
|
if (!isKeyUpdate && !isProviderUpdate) {
|
|
263
|
-
console.log(chalk_1.default.
|
|
264
|
-
console.log(chalk_1.default.gray(
|
|
265
|
-
|
|
263
|
+
console.log(chalk_1.default.gray(' Current configuration:'));
|
|
264
|
+
console.log(chalk_1.default.gray(` ⢠Mode: ${existingConfig.mode || 'integrated'}`));
|
|
265
|
+
// Show existing tokens
|
|
266
|
+
const { getProvider } = await Promise.resolve().then(() => __importStar(require('../providers/provider-registry')));
|
|
267
|
+
if (existingConfig.tokens && Object.keys(existingConfig.tokens).length > 0) {
|
|
268
|
+
const providerNames = await Promise.all(Object.keys(existingConfig.tokens).map(async (id) => {
|
|
269
|
+
const provider = await getProvider(id);
|
|
270
|
+
return provider?.displayName || id;
|
|
271
|
+
}));
|
|
272
|
+
console.log(chalk_1.default.gray(` ⢠Providers: ${providerNames.join(', ')}`));
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
console.log(chalk_1.default.gray(' ⢠Providers: none'));
|
|
276
|
+
}
|
|
277
|
+
console.log();
|
|
278
|
+
// Ask user what they want to do
|
|
279
|
+
const response = await (0, prompts_1.default)({
|
|
280
|
+
type: 'select',
|
|
281
|
+
name: 'action',
|
|
282
|
+
message: 'What would you like to do?',
|
|
283
|
+
choices: [
|
|
284
|
+
{
|
|
285
|
+
title: 'Add a provider',
|
|
286
|
+
value: 'add-provider',
|
|
287
|
+
description: 'Add a new platform integration (GitHub, GitLab, etc.)'
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
title: 'Update FRAIM key',
|
|
291
|
+
value: 'update-key',
|
|
292
|
+
description: 'Change your FRAIM API key'
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
title: 'Reconfigure from scratch',
|
|
296
|
+
value: 'reconfigure',
|
|
297
|
+
description: 'Start fresh setup (will preserve existing config as backup)'
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
title: 'Cancel',
|
|
301
|
+
value: 'cancel',
|
|
302
|
+
description: 'Exit without making changes'
|
|
303
|
+
}
|
|
304
|
+
],
|
|
305
|
+
initial: 0
|
|
306
|
+
});
|
|
307
|
+
if (!response.action || response.action === 'cancel') {
|
|
308
|
+
console.log(chalk_1.default.gray('\nSetup cancelled. No changes made.'));
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (response.action === 'add-provider') {
|
|
312
|
+
console.log(chalk_1.default.blue('\nš¦ Adding a provider...\n'));
|
|
313
|
+
const providerClient = new provider_client_1.ProviderClient(fraimKey, process.env.FRAIM_REMOTE_URL || undefined);
|
|
314
|
+
// Prompt for which provider to add
|
|
315
|
+
const providersToAdd = await (0, provider_prompts_1.promptForProviders)(providerClient);
|
|
316
|
+
requestedProviders = providersToAdd;
|
|
317
|
+
}
|
|
318
|
+
else if (response.action === 'update-key') {
|
|
319
|
+
console.log(chalk_1.default.blue('\nš Updating FRAIM key...\n'));
|
|
320
|
+
fraimKey = await promptForFraimKey();
|
|
321
|
+
}
|
|
322
|
+
else if (response.action === 'reconfigure') {
|
|
323
|
+
console.log(chalk_1.default.blue('\nš Starting fresh setup...\n'));
|
|
324
|
+
// Backup existing config
|
|
325
|
+
const backupPath = globalConfigPath + '.backup.' + Date.now();
|
|
326
|
+
fs_1.default.copyFileSync(globalConfigPath, backupPath);
|
|
327
|
+
console.log(chalk_1.default.gray(` Backed up existing config to: ${path_1.default.basename(backupPath)}\n`));
|
|
328
|
+
// Treat as fresh setup
|
|
329
|
+
fraimKey = options.key || await promptForFraimKey();
|
|
330
|
+
console.log(chalk_1.default.green('ā
FRAIM key accepted\n'));
|
|
331
|
+
mode = options.mode ? parseModeOption(options.mode) : await promptForMode();
|
|
332
|
+
const parsed = await parseLegacyOptions(options, fraimKey);
|
|
333
|
+
requestedProviders = parsed.requestedProviders;
|
|
334
|
+
providedTokens = parsed.providedTokens;
|
|
335
|
+
providedConfigs = parsed.providedConfigs;
|
|
336
|
+
// Clear existing tokens/configs for fresh start
|
|
337
|
+
Object.keys(tokens).forEach(key => delete tokens[key]);
|
|
338
|
+
Object.keys(configs).forEach(key => delete configs[key]);
|
|
339
|
+
// Continue with fresh setup flow
|
|
340
|
+
const providerClient = new provider_client_1.ProviderClient(fraimKey, process.env.FRAIM_REMOTE_URL || undefined);
|
|
341
|
+
if (mode === 'integrated') {
|
|
342
|
+
let providersToSetup = requestedProviders;
|
|
343
|
+
if (providersToSetup.length === 0) {
|
|
344
|
+
providersToSetup = await (0, provider_prompts_1.promptForProviders)(providerClient);
|
|
345
|
+
}
|
|
346
|
+
for (const providerId of providersToSetup) {
|
|
347
|
+
try {
|
|
348
|
+
const creds = await (0, provider_prompts_1.promptForProviderCredentials)(providerClient, providerId, providedTokens[providerId], providedConfigs[providerId]);
|
|
349
|
+
tokens[providerId] = creds.token;
|
|
350
|
+
if (creds.config) {
|
|
351
|
+
configs[providerId] = creds.config;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
catch (e) {
|
|
355
|
+
const providerName = await (0, provider_registry_1.getProviderDisplayName)(providerId);
|
|
356
|
+
console.log(chalk_1.default.red(`ā Failed to get ${providerName} credentials`));
|
|
357
|
+
process.exit(1);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
else if (mode === 'split') {
|
|
362
|
+
// Split mode setup
|
|
363
|
+
console.log(chalk_1.default.blue('\nš Split Mode Configuration'));
|
|
364
|
+
console.log(chalk_1.default.gray('Configure separate platforms for code hosting and issue tracking.\n'));
|
|
365
|
+
const codeRepoProvider = await (0, provider_prompts_1.promptForSingleProvider)(providerClient, 'code');
|
|
366
|
+
const creds1 = await (0, provider_prompts_1.promptForProviderCredentials)(providerClient, codeRepoProvider, providedTokens[codeRepoProvider], providedConfigs[codeRepoProvider]);
|
|
367
|
+
tokens[codeRepoProvider] = creds1.token;
|
|
368
|
+
if (creds1.config) {
|
|
369
|
+
configs[codeRepoProvider] = creds1.config;
|
|
370
|
+
}
|
|
371
|
+
const issueProvider = await (0, provider_prompts_1.promptForSingleProvider)(providerClient, 'issues');
|
|
372
|
+
if (!tokens[issueProvider]) {
|
|
373
|
+
const creds2 = await (0, provider_prompts_1.promptForProviderCredentials)(providerClient, issueProvider, providedTokens[issueProvider], providedConfigs[issueProvider]);
|
|
374
|
+
tokens[issueProvider] = creds2.token;
|
|
375
|
+
if (creds2.config) {
|
|
376
|
+
configs[issueProvider] = creds2.config;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
// Save and configure MCP
|
|
381
|
+
console.log(chalk_1.default.blue('\nš¾ Saving global configuration...'));
|
|
382
|
+
saveGlobalConfig(fraimKey, mode, tokens, configs);
|
|
383
|
+
console.log(chalk_1.default.blue('\nš Configuring MCP servers...'));
|
|
384
|
+
const mcpTokens = {};
|
|
385
|
+
Object.entries(tokens).forEach(([id, token]) => {
|
|
386
|
+
mcpTokens[id] = token;
|
|
387
|
+
});
|
|
388
|
+
const providerConfigsMap = {};
|
|
389
|
+
Object.entries(configs).forEach(([providerId, config]) => {
|
|
390
|
+
providerConfigsMap[providerId] = config;
|
|
391
|
+
});
|
|
392
|
+
await (0, auto_mcp_setup_1.autoConfigureMCP)(fraimKey, mcpTokens, options.ide ? [options.ide] : undefined, providerConfigsMap);
|
|
393
|
+
console.log(chalk_1.default.green('\nšÆ Reconfiguration complete!'));
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
266
396
|
}
|
|
267
397
|
mode = existingConfig.mode || 'integrated';
|
|
268
398
|
// Preserve existing tokens
|
|
@@ -276,14 +406,6 @@ const runSetup = async (options) => {
|
|
|
276
406
|
configs[providerId] = value;
|
|
277
407
|
});
|
|
278
408
|
}
|
|
279
|
-
console.log(chalk_1.default.gray(` Current mode: ${mode}`));
|
|
280
|
-
// Show existing tokens
|
|
281
|
-
const { getProvider } = await Promise.resolve().then(() => __importStar(require('../providers/provider-registry')));
|
|
282
|
-
const providerNames = await Promise.all(Object.keys(tokens).map(async (id) => {
|
|
283
|
-
const provider = await getProvider(id);
|
|
284
|
-
return provider?.displayName || id;
|
|
285
|
-
}));
|
|
286
|
-
console.log(chalk_1.default.gray(` Existing tokens: ${providerNames.join(', ') || 'none'}\n`));
|
|
287
409
|
}
|
|
288
410
|
catch (e) {
|
|
289
411
|
console.log(chalk_1.default.red('ā Failed to read existing configuration'));
|
package/dist/src/cli/fraim.js
CHANGED
|
@@ -47,6 +47,7 @@ const add_ide_1 = require("./commands/add-ide");
|
|
|
47
47
|
const add_provider_1 = require("./commands/add-provider");
|
|
48
48
|
const override_1 = require("./commands/override");
|
|
49
49
|
const list_overridable_1 = require("./commands/list-overridable");
|
|
50
|
+
const login_1 = require("./commands/login");
|
|
50
51
|
const fs_1 = __importDefault(require("fs"));
|
|
51
52
|
const path_1 = __importDefault(require("path"));
|
|
52
53
|
const program = new commander_1.Command();
|
|
@@ -83,6 +84,7 @@ program.addCommand(add_ide_1.addIDECommand);
|
|
|
83
84
|
program.addCommand(add_provider_1.addProviderCommand);
|
|
84
85
|
program.addCommand(override_1.overrideCommand);
|
|
85
86
|
program.addCommand(list_overridable_1.listOverridableCommand);
|
|
87
|
+
program.addCommand(login_1.loginCommand);
|
|
86
88
|
// Wait for async command initialization before parsing
|
|
87
89
|
(async () => {
|
|
88
90
|
// Import the initialization promise from setup command
|
|
@@ -0,0 +1,83 @@
|
|
|
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.DeviceFlowService = void 0;
|
|
7
|
+
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
class DeviceFlowService {
|
|
10
|
+
constructor(config) {
|
|
11
|
+
this.config = config;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Start the Device Flow Login
|
|
15
|
+
*/
|
|
16
|
+
async login() {
|
|
17
|
+
console.log(chalk_1.default.blue('\nš Starting Authentication...'));
|
|
18
|
+
try {
|
|
19
|
+
// 1. Request device and user codes
|
|
20
|
+
const deviceCode = await this.requestDeviceCode();
|
|
21
|
+
console.log(chalk_1.default.yellow('\nACTION REQUIRED:'));
|
|
22
|
+
console.log(`1. Go to: ${chalk_1.default.cyan.underline(deviceCode.verification_uri)}`);
|
|
23
|
+
console.log(`2. Enter the code: ${chalk_1.default.bold.green(deviceCode.user_code)}`);
|
|
24
|
+
console.log(chalk_1.default.gray(`\nWaiting for authorization (expires in ${Math.floor(deviceCode.expires_in / 60)} minutes)...`));
|
|
25
|
+
// 2. Poll for the access token
|
|
26
|
+
const token = await this.pollForToken(deviceCode.device_code, deviceCode.interval);
|
|
27
|
+
console.log(chalk_1.default.green('\nā
Authentication Successful!'));
|
|
28
|
+
return token;
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
console.error(chalk_1.default.red(`\nā Authentication failed: ${error.message}`));
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async requestDeviceCode() {
|
|
36
|
+
const response = await axios_1.default.post(this.config.authUrl, {
|
|
37
|
+
client_id: this.config.clientId,
|
|
38
|
+
scope: this.config.scope
|
|
39
|
+
}, {
|
|
40
|
+
headers: { Accept: 'application/json' }
|
|
41
|
+
});
|
|
42
|
+
return response.data;
|
|
43
|
+
}
|
|
44
|
+
async pollForToken(deviceCode, interval) {
|
|
45
|
+
let currentInterval = interval * 1000;
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
const poll = async () => {
|
|
48
|
+
try {
|
|
49
|
+
const response = await axios_1.default.post(this.config.tokenUrl, {
|
|
50
|
+
client_id: this.config.clientId,
|
|
51
|
+
device_code: deviceCode,
|
|
52
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
|
|
53
|
+
}, {
|
|
54
|
+
headers: { Accept: 'application/json' }
|
|
55
|
+
});
|
|
56
|
+
if (response.data.access_token) {
|
|
57
|
+
resolve(response.data.access_token);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (response.data.error) {
|
|
61
|
+
const error = response.data.error;
|
|
62
|
+
if (error === 'authorization_pending') {
|
|
63
|
+
// Keep polling
|
|
64
|
+
setTimeout(poll, currentInterval);
|
|
65
|
+
}
|
|
66
|
+
else if (error === 'slow_down') {
|
|
67
|
+
currentInterval += 5000;
|
|
68
|
+
setTimeout(poll, currentInterval);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
reject(new Error(response.data.error_description || error));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
reject(error);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
setTimeout(poll, currentInterval);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
exports.DeviceFlowService = DeviceFlowService;
|
|
@@ -99,19 +99,20 @@ function buildStdioServer(mcpConfig, token, config) {
|
|
|
99
99
|
if (!mcpConfig.command) {
|
|
100
100
|
throw new Error('Stdio MCP server requires command');
|
|
101
101
|
}
|
|
102
|
+
const resolveTemplate = (template) => {
|
|
103
|
+
let value = template.replace('{token}', token);
|
|
104
|
+
if (config) {
|
|
105
|
+
for (const [configKey, configValue] of Object.entries(config)) {
|
|
106
|
+
value = value.replace(`{config.${configKey}}`, String(configValue));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return value;
|
|
110
|
+
};
|
|
102
111
|
// Build environment variables from template
|
|
103
112
|
const env = {};
|
|
104
113
|
if (mcpConfig.envTemplate) {
|
|
105
114
|
for (const [key, template] of Object.entries(mcpConfig.envTemplate)) {
|
|
106
|
-
let value = template;
|
|
107
|
-
// Replace {token} placeholder
|
|
108
|
-
value = value.replace('{token}', token);
|
|
109
|
-
// Replace {config.key} placeholders
|
|
110
|
-
if (config) {
|
|
111
|
-
for (const [configKey, configValue] of Object.entries(config)) {
|
|
112
|
-
value = value.replace(`{config.${configKey}}`, String(configValue));
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
+
let value = resolveTemplate(template);
|
|
115
116
|
// Handle URL normalization for Jira
|
|
116
117
|
if (key === 'JIRA_URL' && value && !value.startsWith('http')) {
|
|
117
118
|
value = `https://${value}`;
|
|
@@ -121,7 +122,7 @@ function buildStdioServer(mcpConfig, token, config) {
|
|
|
121
122
|
}
|
|
122
123
|
return {
|
|
123
124
|
command: mcpConfig.command,
|
|
124
|
-
args: mcpConfig.args || [],
|
|
125
|
+
args: (mcpConfig.args || []).map(resolveTemplate),
|
|
125
126
|
env
|
|
126
127
|
};
|
|
127
128
|
}
|