fraim-framework 2.0.41 → 2.0.42

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.
@@ -0,0 +1,310 @@
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.saveGitHubTokenToConfig = exports.loadGlobalConfig = exports.addIDECommand = exports.runAddIDE = 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 os_1 = __importDefault(require("os"));
13
+ const ide_detector_1 = require("../setup/ide-detector");
14
+ const mcp_config_generator_1 = require("../setup/mcp-config-generator");
15
+ const loadGlobalConfig = () => {
16
+ const globalConfigPath = path_1.default.join(os_1.default.homedir(), '.fraim', 'config.json');
17
+ if (!fs_1.default.existsSync(globalConfigPath)) {
18
+ return null;
19
+ }
20
+ try {
21
+ const config = JSON.parse(fs_1.default.readFileSync(globalConfigPath, 'utf8'));
22
+ return {
23
+ fraimKey: config.apiKey,
24
+ githubToken: config.githubToken
25
+ };
26
+ }
27
+ catch (e) {
28
+ return null;
29
+ }
30
+ };
31
+ exports.loadGlobalConfig = loadGlobalConfig;
32
+ const promptForGitHubToken = async () => {
33
+ console.log(chalk_1.default.yellow('\nšŸ”‘ GitHub token needed for MCP configuration'));
34
+ console.log(chalk_1.default.gray('This is required for git and GitHub MCP servers to function properly.\n'));
35
+ const tokenResponse = await (0, prompts_1.default)({
36
+ type: 'password',
37
+ name: 'token',
38
+ message: 'Enter your GitHub token',
39
+ validate: (value) => {
40
+ if (!value)
41
+ return 'GitHub token is required';
42
+ if (value.startsWith('ghp_') || value.startsWith('github_pat_'))
43
+ return true;
44
+ return 'Please enter a valid GitHub token (starts with ghp_ or github_pat_)';
45
+ }
46
+ });
47
+ if (!tokenResponse.token) {
48
+ console.log(chalk_1.default.red('GitHub token is required. Exiting.'));
49
+ process.exit(1);
50
+ }
51
+ return tokenResponse.token;
52
+ };
53
+ const saveGitHubTokenToConfig = (githubToken) => {
54
+ const globalConfigPath = path_1.default.join(os_1.default.homedir(), '.fraim', 'config.json');
55
+ if (fs_1.default.existsSync(globalConfigPath)) {
56
+ try {
57
+ const config = JSON.parse(fs_1.default.readFileSync(globalConfigPath, 'utf8'));
58
+ config.githubToken = githubToken;
59
+ fs_1.default.writeFileSync(globalConfigPath, JSON.stringify(config, null, 2));
60
+ console.log(chalk_1.default.green('āœ… GitHub token saved to global config'));
61
+ }
62
+ catch (e) {
63
+ console.log(chalk_1.default.yellow('āš ļø Could not save GitHub token to config'));
64
+ }
65
+ }
66
+ };
67
+ exports.saveGitHubTokenToConfig = saveGitHubTokenToConfig;
68
+ const configureIDEMCP = async (ide, fraimKey, githubToken) => {
69
+ const configPath = (0, ide_detector_1.expandPath)(ide.configPath);
70
+ console.log(chalk_1.default.blue(`šŸ”§ Configuring ${ide.name}...`));
71
+ // Create backup if config exists
72
+ if (fs_1.default.existsSync(configPath)) {
73
+ const backupPath = `${configPath}.fraim-backup-${Date.now()}`;
74
+ fs_1.default.copyFileSync(configPath, backupPath);
75
+ console.log(chalk_1.default.gray(` šŸ“ Backup created: ${backupPath}`));
76
+ }
77
+ // Ensure directory exists
78
+ const configDir = path_1.default.dirname(configPath);
79
+ if (!fs_1.default.existsSync(configDir)) {
80
+ fs_1.default.mkdirSync(configDir, { recursive: true });
81
+ console.log(chalk_1.default.gray(` šŸ“ Created directory: ${configDir}`));
82
+ }
83
+ let existingConfig = {};
84
+ let existingMCPServers = {};
85
+ if (fs_1.default.existsSync(configPath) && ide.configFormat === 'json') {
86
+ try {
87
+ existingConfig = JSON.parse(fs_1.default.readFileSync(configPath, 'utf8'));
88
+ existingMCPServers = existingConfig.mcpServers || {};
89
+ console.log(chalk_1.default.gray(` šŸ“‹ Found existing config with ${Object.keys(existingMCPServers).length} MCP servers`));
90
+ }
91
+ catch (e) {
92
+ console.log(chalk_1.default.yellow(` āš ļø Could not parse existing ${ide.name} config, creating new one`));
93
+ }
94
+ }
95
+ if (ide.configFormat === 'toml') {
96
+ // Handle TOML format (Codex)
97
+ let existingTomlContent = '';
98
+ if (fs_1.default.existsSync(configPath)) {
99
+ existingTomlContent = fs_1.default.readFileSync(configPath, 'utf8');
100
+ console.log(chalk_1.default.gray(` šŸ“‹ Found existing TOML config`));
101
+ }
102
+ const newTomlContent = (0, mcp_config_generator_1.generateMCPConfig)(ide.configType, fraimKey, githubToken);
103
+ let finalContent = existingTomlContent;
104
+ const serversToAdd = ['fraim', 'git', 'github', 'playwright'];
105
+ const addedServers = [];
106
+ for (const server of serversToAdd) {
107
+ if (!existingTomlContent.includes(`[mcp_servers.${server}]`)) {
108
+ const serverRegex = new RegExp(`\\[mcp_servers\\.${server}\\][\\s\\S]*?(?=\\[mcp_servers\\.|$)`, 'g');
109
+ const serverMatch = newTomlContent.match(serverRegex);
110
+ if (serverMatch) {
111
+ finalContent += '\n' + serverMatch[0].trim() + '\n';
112
+ addedServers.push(server);
113
+ }
114
+ }
115
+ else {
116
+ console.log(chalk_1.default.gray(` ā­ļø Skipped ${server} (already exists)`));
117
+ }
118
+ }
119
+ fs_1.default.writeFileSync(configPath, finalContent);
120
+ addedServers.forEach(server => {
121
+ console.log(chalk_1.default.green(` āœ… Added ${server} MCP server`));
122
+ });
123
+ }
124
+ else {
125
+ // Handle JSON format
126
+ const newConfig = (0, mcp_config_generator_1.generateMCPConfig)(ide.configType, fraimKey, githubToken);
127
+ const newMCPServers = newConfig.mcpServers || {};
128
+ // Merge MCP servers intelligently
129
+ const mergedMCPServers = { ...existingMCPServers };
130
+ const addedServers = [];
131
+ const skippedServers = [];
132
+ for (const [serverName, serverConfig] of Object.entries(newMCPServers)) {
133
+ if (!existingMCPServers[serverName]) {
134
+ mergedMCPServers[serverName] = serverConfig;
135
+ addedServers.push(serverName);
136
+ }
137
+ else {
138
+ skippedServers.push(serverName);
139
+ }
140
+ }
141
+ // Merge with existing config
142
+ const mergedConfig = {
143
+ ...existingConfig,
144
+ ...newConfig,
145
+ mcpServers: mergedMCPServers
146
+ };
147
+ // Write updated config
148
+ fs_1.default.writeFileSync(configPath, JSON.stringify(mergedConfig, null, 2));
149
+ addedServers.forEach(server => {
150
+ console.log(chalk_1.default.green(` āœ… Added ${server} MCP server`));
151
+ });
152
+ skippedServers.forEach(server => {
153
+ console.log(chalk_1.default.gray(` ā­ļø Skipped ${server} (already exists)`));
154
+ });
155
+ }
156
+ console.log(chalk_1.default.green(`āœ… Updated ${configPath}`));
157
+ };
158
+ const listSupportedIDEs = () => {
159
+ const allIDEs = (0, ide_detector_1.getAllSupportedIDEs)();
160
+ const detectedIDEs = (0, ide_detector_1.detectInstalledIDEs)();
161
+ console.log(chalk_1.default.blue('šŸ“‹ Supported IDEs:\n'));
162
+ allIDEs.forEach(ide => {
163
+ const isDetected = detectedIDEs.some(detected => detected.name === ide.name);
164
+ const statusIcon = isDetected ? 'āœ…' : 'āŒ';
165
+ const statusText = isDetected ? 'detected' : 'not found';
166
+ console.log(chalk_1.default.white(` ${statusIcon} ${ide.name} (${statusText})`));
167
+ console.log(chalk_1.default.gray(` ${ide.description}`));
168
+ console.log(chalk_1.default.gray(` Config: ${ide.configPath}\n`));
169
+ });
170
+ console.log(chalk_1.default.yellow('šŸ’” Use "fraim add-ide --ide <name>" to configure a specific IDE'));
171
+ console.log(chalk_1.default.yellow(' Example: fraim add-ide --ide claude'));
172
+ };
173
+ const promptForIDESelection = async (availableIDEs) => {
174
+ console.log(chalk_1.default.green(`āœ… Found ${availableIDEs.length} IDEs that can be configured:\n`));
175
+ availableIDEs.forEach((ide, index) => {
176
+ const configExists = fs_1.default.existsSync((0, ide_detector_1.expandPath)(ide.configPath));
177
+ const statusIcon = configExists ? 'šŸ“' : 'šŸ“„';
178
+ const statusText = configExists ? 'has config' : 'new config';
179
+ console.log(chalk_1.default.white(` ${index + 1}. ${ide.name} ${statusIcon} (${statusText})`));
180
+ });
181
+ console.log(chalk_1.default.blue('\nFRAIM will add these MCP servers:'));
182
+ console.log(chalk_1.default.gray(' • fraim (workflows and AI management)'));
183
+ console.log(chalk_1.default.gray(' • git (version control integration)'));
184
+ console.log(chalk_1.default.gray(' • github (GitHub API access)'));
185
+ console.log(chalk_1.default.gray(' • playwright (browser automation)'));
186
+ const response = await (0, prompts_1.default)({
187
+ type: 'text',
188
+ name: 'selection',
189
+ message: 'Configure which IDEs? (Enter \'all\' or numbers like \'1,3\')',
190
+ initial: 'all',
191
+ validate: (value) => {
192
+ if (value.toLowerCase() === 'all')
193
+ return true;
194
+ if (value.toLowerCase() === 'none')
195
+ return true;
196
+ const numbers = value.split(',').map(n => parseInt(n.trim()));
197
+ const valid = numbers.every(n => n >= 1 && n <= availableIDEs.length && !isNaN(n));
198
+ return valid || 'Please enter "all", "none", or valid numbers (e.g., "1,3")';
199
+ }
200
+ });
201
+ if (response.selection.toLowerCase() === 'all') {
202
+ return availableIDEs;
203
+ }
204
+ if (response.selection.toLowerCase() === 'none') {
205
+ return [];
206
+ }
207
+ const selectedIndices = response.selection.split(',').map((n) => parseInt(n.trim()) - 1);
208
+ return selectedIndices.map((i) => availableIDEs[i]).filter(Boolean);
209
+ };
210
+ const runAddIDE = async (options) => {
211
+ if (options.list) {
212
+ listSupportedIDEs();
213
+ return;
214
+ }
215
+ console.log(chalk_1.default.blue('šŸ”§ FRAIM IDE Configuration\n'));
216
+ // Load existing configuration
217
+ const globalConfig = loadGlobalConfig();
218
+ if (!globalConfig || !globalConfig.fraimKey) {
219
+ console.log(chalk_1.default.red('āŒ No FRAIM configuration found.'));
220
+ console.log(chalk_1.default.yellow('šŸ’” Please run "fraim setup" first to configure your FRAIM and GitHub keys.'));
221
+ process.exit(1);
222
+ }
223
+ let githubToken = globalConfig.githubToken;
224
+ if (!githubToken) {
225
+ console.log(chalk_1.default.yellow('āš ļø No GitHub token found in configuration.'));
226
+ githubToken = await promptForGitHubToken();
227
+ saveGitHubTokenToConfig(githubToken);
228
+ }
229
+ console.log(chalk_1.default.green('āœ… Using existing FRAIM configuration\n'));
230
+ // Detect available IDEs
231
+ const detectedIDEs = (0, ide_detector_1.detectInstalledIDEs)();
232
+ if (detectedIDEs.length === 0) {
233
+ console.log(chalk_1.default.yellow('āš ļø No supported IDEs detected on your system.'));
234
+ console.log(chalk_1.default.gray('Supported IDEs: Claude, Antigravity, Kiro, Cursor, VSCode, Codex, Windsurf'));
235
+ console.log(chalk_1.default.blue('\nšŸ’” Install an IDE and run this command again.'));
236
+ return;
237
+ }
238
+ let idesToConfigure;
239
+ if (options.ide) {
240
+ // Configure specific IDE
241
+ const requestedIDE = (0, ide_detector_1.findIDEByName)(options.ide);
242
+ if (!requestedIDE) {
243
+ console.log(chalk_1.default.red(`āŒ IDE "${options.ide}" not supported.`));
244
+ console.log(chalk_1.default.yellow('šŸ’” Use "fraim add-ide --list" to see supported IDEs.'));
245
+ return;
246
+ }
247
+ const detectedIDE = detectedIDEs.find(ide => ide.name === requestedIDE.name);
248
+ if (!detectedIDE) {
249
+ console.log(chalk_1.default.red(`āŒ ${requestedIDE.name} not found on your system.`));
250
+ console.log(chalk_1.default.yellow(`šŸ’” Please install ${requestedIDE.name} and try again.`));
251
+ return;
252
+ }
253
+ idesToConfigure = [detectedIDE];
254
+ }
255
+ else if (options.all) {
256
+ // Configure all detected IDEs
257
+ idesToConfigure = detectedIDEs;
258
+ }
259
+ else {
260
+ // Interactive selection
261
+ idesToConfigure = await promptForIDESelection(detectedIDEs);
262
+ }
263
+ if (idesToConfigure.length === 0) {
264
+ console.log(chalk_1.default.yellow('āš ļø No IDEs selected for configuration.'));
265
+ return;
266
+ }
267
+ console.log(chalk_1.default.blue(`\nšŸš€ Configuring ${idesToConfigure.length} IDE(s)...\n`));
268
+ const results = {
269
+ successful: [],
270
+ failed: []
271
+ };
272
+ for (const ide of idesToConfigure) {
273
+ try {
274
+ await configureIDEMCP(ide, globalConfig.fraimKey, githubToken);
275
+ results.successful.push(ide.name);
276
+ }
277
+ catch (error) {
278
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
279
+ results.failed.push({ ide: ide.name, error: errorMessage });
280
+ console.log(chalk_1.default.red(`āŒ Failed to configure ${ide.name}: ${errorMessage}`));
281
+ }
282
+ }
283
+ // Summary
284
+ console.log(chalk_1.default.green(`\nšŸŽ‰ Configuration Complete:`));
285
+ if (results.successful.length > 0) {
286
+ console.log(chalk_1.default.green(` āœ… Successfully configured: ${results.successful.length} IDE(s)`));
287
+ results.successful.forEach(ide => {
288
+ console.log(chalk_1.default.green(` • ${ide}`));
289
+ });
290
+ }
291
+ if (results.failed.length > 0) {
292
+ console.log(chalk_1.default.red(` āŒ Failed to configure: ${results.failed.length} IDE(s)`));
293
+ results.failed.forEach(({ ide, error }) => {
294
+ console.log(chalk_1.default.red(` • ${ide}: ${error}`));
295
+ });
296
+ }
297
+ if (results.successful.length > 0) {
298
+ console.log(chalk_1.default.blue('\nšŸ”„ Next steps:'));
299
+ console.log(chalk_1.default.cyan(' 1. Restart your configured IDEs'));
300
+ console.log(chalk_1.default.cyan(' 2. Ask your AI agent: "list fraim workflows"'));
301
+ console.log(chalk_1.default.blue('\nšŸ’” Use "fraim test-mcp" to verify the configuration.'));
302
+ }
303
+ };
304
+ exports.runAddIDE = runAddIDE;
305
+ exports.addIDECommand = new commander_1.Command('add-ide')
306
+ .description('Add FRAIM configuration to additional IDEs')
307
+ .option('--ide <name>', 'Configure specific IDE (claude, antigravity, kiro, cursor, vscode, codex, windsurf)')
308
+ .option('--all', 'Configure all detected IDEs')
309
+ .option('--list', 'List all supported IDEs and their detection status')
310
+ .action(exports.runAddIDE);
@@ -66,7 +66,7 @@ const promptForFraimKey = async () => {
66
66
  console.log(chalk_1.default.gray('Please ensure you have a valid FRAIM key and try again.'));
67
67
  process.exit(1);
68
68
  };
69
- const saveGlobalConfig = (fraimKey) => {
69
+ const saveGlobalConfig = (fraimKey, githubToken) => {
70
70
  const globalConfigDir = path_1.default.join(os_1.default.homedir(), '.fraim');
71
71
  const globalConfigPath = path_1.default.join(globalConfigDir, 'config.json');
72
72
  if (!fs_1.default.existsSync(globalConfigDir)) {
@@ -75,6 +75,7 @@ const saveGlobalConfig = (fraimKey) => {
75
75
  const config = {
76
76
  version: (0, version_utils_1.getFraimVersion)(),
77
77
  apiKey: fraimKey,
78
+ githubToken: githubToken,
78
79
  configuredAt: new Date().toISOString(),
79
80
  userPreferences: {
80
81
  autoSync: true,
@@ -149,7 +150,7 @@ const runSetup = async (options) => {
149
150
  }
150
151
  // Save global configuration
151
152
  console.log(chalk_1.default.blue('šŸ’¾ Saving global configuration...'));
152
- saveGlobalConfig(fraimKey);
153
+ saveGlobalConfig(fraimKey, githubToken);
153
154
  // Sync global scripts
154
155
  syncGlobalScripts();
155
156
  // Configure IDEs
@@ -13,6 +13,7 @@ const wizard_1 = require("./commands/wizard");
13
13
  const setup_1 = require("./commands/setup");
14
14
  const init_project_1 = require("./commands/init-project");
15
15
  const test_mcp_1 = require("./commands/test-mcp");
16
+ const add_ide_1 = require("./commands/add-ide");
16
17
  const fs_1 = __importDefault(require("fs"));
17
18
  const path_1 = __importDefault(require("path"));
18
19
  const program = new commander_1.Command();
@@ -48,4 +49,5 @@ program.addCommand(wizard_1.wizardCommand);
48
49
  program.addCommand(setup_1.setupCommand);
49
50
  program.addCommand(init_project_1.initProjectCommand);
50
51
  program.addCommand(test_mcp_1.testMCPCommand);
52
+ program.addCommand(add_ide_1.addIDECommand);
51
53
  program.parse(process.argv);
@@ -993,7 +993,7 @@ The AI Manager provides detailed review instructions including:
993
993
  - Grading guidelines and reporting format
994
994
 
995
995
  Use this when you believe your work is complete and ready for review.
996
- Currently supports: spec phase (more phases coming soon).`,
996
+ Currently supports: spec, implement phases (more phases coming soon).`,
997
997
  inputSchema: {
998
998
  type: 'object',
999
999
  properties: {
@@ -0,0 +1,283 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ const node_test_1 = require("node:test");
40
+ const node_assert_1 = __importDefault(require("node:assert"));
41
+ const fs_1 = __importDefault(require("fs"));
42
+ const path_1 = __importDefault(require("path"));
43
+ const os_1 = __importDefault(require("os"));
44
+ // Test the add-ide command functionality without interactive prompts
45
+ (0, node_test_1.test)('add-ide command - list option shows supported IDEs', async () => {
46
+ // Import the module dynamically to avoid issues with mocking
47
+ const { runAddIDE } = await Promise.resolve().then(() => __importStar(require('../src/cli/commands/add-ide')));
48
+ let consoleOutput = [];
49
+ const originalConsoleLog = console.log;
50
+ console.log = (...args) => {
51
+ consoleOutput.push(args.join(' '));
52
+ };
53
+ try {
54
+ await runAddIDE({ list: true });
55
+ // Check that output contains supported IDEs
56
+ const output = consoleOutput.join('\n');
57
+ (0, node_assert_1.default)(output.includes('Supported IDEs'), 'Should show supported IDEs header');
58
+ (0, node_assert_1.default)(output.includes('Claude'), 'Should list Claude');
59
+ (0, node_assert_1.default)(output.includes('Antigravity'), 'Should list Antigravity');
60
+ (0, node_assert_1.default)(output.includes('Kiro'), 'Should list Kiro');
61
+ }
62
+ finally {
63
+ console.log = originalConsoleLog;
64
+ }
65
+ });
66
+ (0, node_test_1.test)('add-ide command - validates IDE names', async () => {
67
+ // Test the findIDEByName function directly to avoid interactive prompts
68
+ const { findIDEByName } = await Promise.resolve().then(() => __importStar(require('../src/cli/setup/ide-detector')));
69
+ // Test valid IDE names
70
+ (0, node_assert_1.default)(findIDEByName('claude'), 'Should find Claude');
71
+ (0, node_assert_1.default)(findIDEByName('antigravity'), 'Should find Antigravity');
72
+ (0, node_assert_1.default)(findIDEByName('kiro'), 'Should find Kiro');
73
+ (0, node_assert_1.default)(findIDEByName('cursor'), 'Should find Cursor');
74
+ // Test case insensitive matching
75
+ (0, node_assert_1.default)(findIDEByName('CLAUDE'), 'Should find Claude case insensitive');
76
+ (0, node_assert_1.default)(findIDEByName('AntiGravity'), 'Should find Antigravity case insensitive');
77
+ // Test invalid IDE name
78
+ (0, node_assert_1.default)(!findIDEByName('nonexistent-ide'), 'Should not find nonexistent IDE');
79
+ });
80
+ (0, node_test_1.test)('add-ide command - detects IDEs correctly', async () => {
81
+ const { detectInstalledIDEs, getAllSupportedIDEs } = await Promise.resolve().then(() => __importStar(require('../src/cli/setup/ide-detector')));
82
+ const allIDEs = getAllSupportedIDEs();
83
+ const detectedIDEs = detectInstalledIDEs();
84
+ // Should have all supported IDEs defined
85
+ (0, node_assert_1.default)(allIDEs.length >= 7, 'Should have at least 7 supported IDEs');
86
+ // Detected IDEs should be a subset of all IDEs
87
+ (0, node_assert_1.default)(detectedIDEs.length <= allIDEs.length, 'Detected IDEs should not exceed total supported');
88
+ // Each detected IDE should have required properties
89
+ detectedIDEs.forEach(ide => {
90
+ (0, node_assert_1.default)(ide.name, 'IDE should have a name');
91
+ (0, node_assert_1.default)(ide.configPath, 'IDE should have a config path');
92
+ (0, node_assert_1.default)(['json', 'toml'].includes(ide.configFormat), 'IDE should have valid config format');
93
+ });
94
+ });
95
+ (0, node_test_1.test)('add-ide command - loadGlobalConfig scenarios', async () => {
96
+ const { loadGlobalConfig } = await Promise.resolve().then(() => __importStar(require('../src/cli/commands/add-ide')));
97
+ const tempHomeDir = path_1.default.join(os_1.default.tmpdir(), '.fraim-test-add-ide-' + Date.now());
98
+ const originalHomedir = os_1.default.homedir;
99
+ os_1.default.homedir = () => tempHomeDir;
100
+ try {
101
+ // Test 1: No global config exists
102
+ const result1 = loadGlobalConfig();
103
+ node_assert_1.default.strictEqual(result1, null, 'Should return null when no config exists');
104
+ // Test 2: Valid config with both keys
105
+ const globalConfigDir = path_1.default.join(tempHomeDir, '.fraim');
106
+ const globalConfigPath = path_1.default.join(globalConfigDir, 'config.json');
107
+ fs_1.default.mkdirSync(globalConfigDir, { recursive: true });
108
+ const validConfig = {
109
+ version: '2.0.27',
110
+ apiKey: 'fraim_test_key',
111
+ githubToken: 'ghp_test_token',
112
+ configuredAt: new Date().toISOString()
113
+ };
114
+ fs_1.default.writeFileSync(globalConfigPath, JSON.stringify(validConfig, null, 2));
115
+ const result2 = loadGlobalConfig();
116
+ (0, node_assert_1.default)(result2, 'Should return config object');
117
+ node_assert_1.default.strictEqual(result2.fraimKey, 'fraim_test_key', 'Should extract FRAIM key');
118
+ node_assert_1.default.strictEqual(result2.githubToken, 'ghp_test_token', 'Should extract GitHub token');
119
+ // Test 3: Config missing GitHub token
120
+ const configWithoutGithub = {
121
+ version: '2.0.27',
122
+ apiKey: 'fraim_test_key_no_github',
123
+ configuredAt: new Date().toISOString()
124
+ };
125
+ fs_1.default.writeFileSync(globalConfigPath, JSON.stringify(configWithoutGithub, null, 2));
126
+ const result3 = loadGlobalConfig();
127
+ (0, node_assert_1.default)(result3, 'Should return config object even without GitHub token');
128
+ node_assert_1.default.strictEqual(result3.fraimKey, 'fraim_test_key_no_github', 'Should extract FRAIM key');
129
+ node_assert_1.default.strictEqual(result3.githubToken, undefined, 'GitHub token should be undefined');
130
+ }
131
+ finally {
132
+ // Cleanup
133
+ fs_1.default.rmSync(tempHomeDir, { recursive: true, force: true });
134
+ os_1.default.homedir = originalHomedir;
135
+ }
136
+ });
137
+ (0, node_test_1.test)('add-ide command - saveGitHubTokenToConfig functionality', async () => {
138
+ const { saveGitHubTokenToConfig } = await Promise.resolve().then(() => __importStar(require('../src/cli/commands/add-ide')));
139
+ const tempHomeDir = path_1.default.join(os_1.default.tmpdir(), '.fraim-test-save-token-' + Date.now());
140
+ const originalHomedir = os_1.default.homedir;
141
+ os_1.default.homedir = () => tempHomeDir;
142
+ try {
143
+ const globalConfigDir = path_1.default.join(tempHomeDir, '.fraim');
144
+ const globalConfigPath = path_1.default.join(globalConfigDir, 'config.json');
145
+ // Create initial config without GitHub token
146
+ fs_1.default.mkdirSync(globalConfigDir, { recursive: true });
147
+ const initialConfig = {
148
+ version: '2.0.27',
149
+ apiKey: 'fraim_test_key',
150
+ configuredAt: new Date().toISOString(),
151
+ userPreferences: {
152
+ autoSync: true,
153
+ backupConfigs: true
154
+ }
155
+ };
156
+ fs_1.default.writeFileSync(globalConfigPath, JSON.stringify(initialConfig, null, 2));
157
+ // Mock console.log to capture output
158
+ let consoleOutput = [];
159
+ const originalConsoleLog = console.log;
160
+ console.log = (...args) => {
161
+ consoleOutput.push(args.join(' '));
162
+ };
163
+ try {
164
+ // Save GitHub token
165
+ saveGitHubTokenToConfig('ghp_newly_saved_token');
166
+ // Verify token was saved
167
+ const updatedConfig = JSON.parse(fs_1.default.readFileSync(globalConfigPath, 'utf8'));
168
+ node_assert_1.default.strictEqual(updatedConfig.githubToken, 'ghp_newly_saved_token', 'GitHub token should be saved');
169
+ node_assert_1.default.strictEqual(updatedConfig.apiKey, 'fraim_test_key', 'FRAIM key should be preserved');
170
+ (0, node_assert_1.default)(updatedConfig.userPreferences, 'User preferences should be preserved');
171
+ // Check console output
172
+ const output = consoleOutput.join('\n');
173
+ (0, node_assert_1.default)(output.includes('GitHub token saved'), 'Should show success message');
174
+ }
175
+ finally {
176
+ console.log = originalConsoleLog;
177
+ }
178
+ }
179
+ finally {
180
+ // Cleanup
181
+ fs_1.default.rmSync(tempHomeDir, { recursive: true, force: true });
182
+ os_1.default.homedir = originalHomedir;
183
+ }
184
+ });
185
+ (0, node_test_1.test)('add-ide command - error handling for missing setup', async () => {
186
+ const { runAddIDE } = await Promise.resolve().then(() => __importStar(require('../src/cli/commands/add-ide')));
187
+ const tempHomeDir = path_1.default.join(os_1.default.tmpdir(), '.fraim-test-no-setup-' + Date.now());
188
+ const originalHomedir = os_1.default.homedir;
189
+ // Mock process.exit to prevent actual exit
190
+ let exitCode = null;
191
+ const originalExit = process.exit;
192
+ process.exit = ((code) => {
193
+ exitCode = code || 0;
194
+ throw new Error(`Process exit called with code ${code}`);
195
+ });
196
+ // Mock console.log to capture output
197
+ let consoleOutput = [];
198
+ const originalConsoleLog = console.log;
199
+ console.log = (...args) => {
200
+ consoleOutput.push(args.join(' '));
201
+ };
202
+ os_1.default.homedir = () => tempHomeDir;
203
+ try {
204
+ // Try to run add-ide without any global config
205
+ await runAddIDE({});
206
+ node_assert_1.default.fail('Should have exited due to missing config');
207
+ }
208
+ catch (error) {
209
+ // Expected to throw due to process.exit mock
210
+ node_assert_1.default.strictEqual(exitCode, 1, 'Should exit with code 1');
211
+ const output = consoleOutput.join('\n');
212
+ (0, node_assert_1.default)(output.includes('No FRAIM configuration found'), 'Should show missing config error');
213
+ (0, node_assert_1.default)(output.includes('fraim setup'), 'Should suggest running setup');
214
+ }
215
+ finally {
216
+ // Restore mocks
217
+ process.exit = originalExit;
218
+ console.log = originalConsoleLog;
219
+ os_1.default.homedir = originalHomedir;
220
+ // Cleanup
221
+ fs_1.default.rmSync(tempHomeDir, { recursive: true, force: true });
222
+ }
223
+ });
224
+ (0, node_test_1.test)('add-ide command - handles invalid IDE name gracefully', async () => {
225
+ const { runAddIDE } = await Promise.resolve().then(() => __importStar(require('../src/cli/commands/add-ide')));
226
+ const tempHomeDir = path_1.default.join(os_1.default.tmpdir(), '.fraim-test-invalid-ide-' + Date.now());
227
+ const originalHomedir = os_1.default.homedir;
228
+ os_1.default.homedir = () => tempHomeDir;
229
+ // Mock process.exit to prevent actual exit
230
+ let exitCalled = false;
231
+ const originalExit = process.exit;
232
+ process.exit = ((code) => {
233
+ exitCalled = true;
234
+ throw new Error(`Process exit called with code ${code}`);
235
+ });
236
+ try {
237
+ // Create valid global config
238
+ const globalConfigDir = path_1.default.join(tempHomeDir, '.fraim');
239
+ const globalConfigPath = path_1.default.join(globalConfigDir, 'config.json');
240
+ fs_1.default.mkdirSync(globalConfigDir, { recursive: true });
241
+ const validConfig = {
242
+ version: '2.0.27',
243
+ apiKey: 'fraim_test_key',
244
+ githubToken: 'ghp_test_token',
245
+ configuredAt: new Date().toISOString()
246
+ };
247
+ fs_1.default.writeFileSync(globalConfigPath, JSON.stringify(validConfig, null, 2));
248
+ // Create fake IDE directory to make detectInstalledIDEs return at least one IDE
249
+ const fakeClaudeDir = path_1.default.join(tempHomeDir, '.claude.json');
250
+ fs_1.default.writeFileSync(fakeClaudeDir, '{}'); // Create fake Claude config file
251
+ // Mock console.log to capture output
252
+ let consoleOutput = [];
253
+ const originalConsoleLog = console.log;
254
+ console.log = (...args) => {
255
+ consoleOutput.push(args.join(' '));
256
+ };
257
+ try {
258
+ // Try to configure invalid IDE
259
+ await runAddIDE({ ide: 'nonexistent-ide' });
260
+ const output = consoleOutput.join('\n');
261
+ // Strip ANSI color codes for testing
262
+ const cleanOutput = output.replace(/\u001b\[[0-9;]*m/g, '');
263
+ (0, node_assert_1.default)(cleanOutput.includes('not supported.'), 'Should show IDE not supported error');
264
+ (0, node_assert_1.default)(cleanOutput.includes('fraim add-ide --list'), 'Should suggest list command');
265
+ }
266
+ catch (error) {
267
+ // If process.exit was called, that's not what we're testing
268
+ if (exitCalled) {
269
+ node_assert_1.default.fail('Process.exit was called - config loading failed');
270
+ }
271
+ throw error;
272
+ }
273
+ finally {
274
+ console.log = originalConsoleLog;
275
+ }
276
+ }
277
+ finally {
278
+ // Cleanup
279
+ process.exit = originalExit;
280
+ fs_1.default.rmSync(tempHomeDir, { recursive: true, force: true });
281
+ os_1.default.homedir = originalHomedir;
282
+ }
283
+ });
@@ -29,6 +29,22 @@ const ai_manager_1 = require("../src/ai-manager/ai-manager");
29
29
  (0, node_assert_1.default)(instructions.includes('iterationCount'));
30
30
  (0, node_assert_1.default)(instructions.includes('Maximum 3 iterations'));
31
31
  });
32
+ (0, node_test_1.test)('should generate implement workflow instructions', () => {
33
+ const context = {
34
+ workflowType: 'implement',
35
+ issueNumber: '456',
36
+ phase: 'implementation'
37
+ };
38
+ const instructions = aiManager.generateReviewInstructions(context);
39
+ (0, node_assert_1.default)(typeof instructions === 'string');
40
+ (0, node_assert_1.default)(instructions.includes('AI Manager Review Instructions'));
41
+ (0, node_assert_1.default)(instructions.includes('IMPLEMENT'));
42
+ (0, node_assert_1.default)(instructions.includes('456'));
43
+ (0, node_assert_1.default)(instructions.includes('design completeness'));
44
+ (0, node_assert_1.default)(instructions.includes('test quality'));
45
+ (0, node_assert_1.default)(instructions.includes('architecture standards'));
46
+ (0, node_assert_1.default)(instructions.includes('iterationCount'));
47
+ });
32
48
  (0, node_test_1.test)('should throw error for unknown workflow type', () => {
33
49
  const context = {
34
50
  workflowType: 'unknown',