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.
@@ -9,11 +9,36 @@ const fs_1 = __importDefault(require("fs"));
9
9
  const path_1 = __importDefault(require("path"));
10
10
  const os_1 = __importDefault(require("os"));
11
11
  const chalk_1 = __importDefault(require("chalk"));
12
+ const prompts_1 = __importDefault(require("prompts"));
12
13
  const sync_1 = require("./sync");
13
14
  const platform_detection_1 = require("../utils/platform-detection");
14
15
  const version_utils_1 = require("../utils/version-utils");
15
16
  const ide_detector_1 = require("../setup/ide-detector");
16
17
  const codex_local_config_1 = require("../setup/codex-local-config");
18
+ const promptForJiraProjectKey = async (jiraBaseUrl) => {
19
+ console.log(chalk_1.default.blue('\nšŸŽ« Jira Project Configuration'));
20
+ console.log(chalk_1.default.gray(`Jira instance: ${jiraBaseUrl}`));
21
+ console.log(chalk_1.default.gray('Enter the Jira project key for this repository (e.g., TEAM, PROJ, DEV)\n'));
22
+ const response = await (0, prompts_1.default)({
23
+ type: 'text',
24
+ name: 'projectKey',
25
+ message: 'Jira Project Key:',
26
+ validate: (value) => {
27
+ if (!value || value.trim().length === 0) {
28
+ return 'Project key is required';
29
+ }
30
+ if (!/^[A-Z][A-Z0-9]*$/.test(value.trim())) {
31
+ return 'Project key must start with a letter and contain only uppercase letters and numbers';
32
+ }
33
+ return true;
34
+ }
35
+ });
36
+ if (!response.projectKey) {
37
+ console.log(chalk_1.default.red('\nāŒ Jira project key is required for split mode'));
38
+ process.exit(1);
39
+ }
40
+ return response.projectKey.trim().toUpperCase();
41
+ };
17
42
  const checkGlobalSetup = () => {
18
43
  const fraimUserDir = process.env.FRAIM_USER_DIR || path_1.default.join(os_1.default.homedir(), '.fraim');
19
44
  const globalConfigPath = path_1.default.join(fraimUserDir, 'config.json');
@@ -25,7 +50,8 @@ const checkGlobalSetup = () => {
25
50
  return {
26
51
  exists: true,
27
52
  mode: config.mode || 'integrated',
28
- tokens: config.tokens || {}
53
+ tokens: config.tokens || {},
54
+ jiraConfig: config.jiraConfig
29
55
  };
30
56
  }
31
57
  catch {
@@ -69,25 +95,45 @@ const runInitProject = async () => {
69
95
  else {
70
96
  const detection = (0, platform_detection_1.detectPlatformFromGit)();
71
97
  if (detection.provider !== 'unknown' && detection.repository) {
72
- const issueTracking = detection.provider === 'github'
73
- ? {
74
- provider: 'github',
75
- owner: detection.repository.owner,
76
- name: detection.repository.name
77
- }
78
- : detection.provider === 'ado'
98
+ // Determine issue tracking configuration
99
+ let issueTracking;
100
+ // In split mode with Jira configured, use Jira for issue tracking
101
+ if (preferredMode === 'split' && globalSetup.tokens?.jira && globalSetup.jiraConfig?.baseUrl) {
102
+ // Prompt for Jira project key (project-specific)
103
+ const projectKey = process.env.FRAIM_JIRA_PROJECT_KEY ||
104
+ await promptForJiraProjectKey(globalSetup.jiraConfig.baseUrl);
105
+ issueTracking = {
106
+ provider: 'jira',
107
+ baseUrl: globalSetup.jiraConfig.baseUrl,
108
+ projectKey: projectKey,
109
+ email: globalSetup.jiraConfig.email
110
+ };
111
+ console.log(chalk_1.default.blue(` Code Repository: ${detection.provider.toUpperCase()}`));
112
+ console.log(chalk_1.default.blue(` Issue Tracking: JIRA (${projectKey})`));
113
+ }
114
+ else {
115
+ // Integrated mode: use same provider for both code and issues
116
+ issueTracking = detection.provider === 'github'
79
117
  ? {
80
- provider: 'ado',
81
- organization: detection.repository.organization,
82
- project: detection.repository.project,
118
+ provider: 'github',
119
+ owner: detection.repository.owner,
83
120
  name: detection.repository.name
84
121
  }
85
- : {
86
- provider: 'gitlab',
87
- namespace: detection.repository.namespace,
88
- name: detection.repository.name,
89
- projectPath: detection.repository.projectPath
90
- };
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()}`));
136
+ }
91
137
  config = {
92
138
  version: (0, version_utils_1.getFraimVersion)(),
93
139
  project: {
@@ -97,7 +143,6 @@ const runInitProject = async () => {
97
143
  issueTracking,
98
144
  customizations: {}
99
145
  };
100
- console.log(chalk_1.default.blue(` Platform: ${detection.provider.toUpperCase()}`));
101
146
  if (detection.provider === 'github') {
102
147
  console.log(chalk_1.default.gray(` Repository: ${detection.repository.owner}/${detection.repository.name}`));
103
148
  }
@@ -132,7 +177,9 @@ const runInitProject = async () => {
132
177
  console.log(chalk_1.default.green(`Created .fraim/${dir}`));
133
178
  }
134
179
  });
135
- await (0, sync_1.runSync)({});
180
+ if (!process.env.FRAIM_SKIP_SYNC) {
181
+ await (0, sync_1.runSync)({});
182
+ }
136
183
  const codexAvailable = (0, ide_detector_1.detectInstalledIDEs)().some((ide) => ide.configType === 'codex');
137
184
  if (codexAvailable) {
138
185
  const codexLocalResult = (0, codex_local_config_1.ensureCodexLocalConfig)(projectRoot);
@@ -88,7 +88,7 @@ const runSync = async (options) => {
88
88
  skipUpdates: true
89
89
  });
90
90
  if (result.success) {
91
- console.log(chalk_1.default.green(`āœ… Successfully synced ${result.workflowsSynced} workflows, ${result.scriptsSynced} scripts, and ${result.coachingSynced} coaching files from local server`));
91
+ console.log(chalk_1.default.green(`āœ… Successfully synced ${result.workflowsSynced} workflows, ${result.scriptsSynced} scripts, ${result.coachingSynced} coaching files, and ${result.docsSynced} docs from local server`));
92
92
  return;
93
93
  }
94
94
  console.error(chalk_1.default.red(`āŒ Local sync failed: ${result.error}`));
@@ -119,7 +119,7 @@ const runSync = async (options) => {
119
119
  }
120
120
  process.exit(1);
121
121
  }
122
- console.log(chalk_1.default.green(`āœ… Successfully synced ${result.workflowsSynced} workflows, ${result.scriptsSynced} scripts, and ${result.coachingSynced} coaching files from remote`));
122
+ console.log(chalk_1.default.green(`āœ… Successfully synced ${result.workflowsSynced} workflows, ${result.scriptsSynced} scripts, ${result.coachingSynced} coaching files, and ${result.docsSynced} docs from remote`));
123
123
  // Update version in config.json
124
124
  const configPath = path_1.default.join(fraimDir, 'config.json');
125
125
  if (fs_1.default.existsSync(configPath)) {
@@ -0,0 +1,196 @@
1
+ "use strict";
2
+ /**
3
+ * Check runner for FRAIM doctor command
4
+ * Handles parallel execution, timeouts, and error handling
5
+ * Issue #144: Enhanced doctor command
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.runChecks = runChecks;
9
+ const CHECK_TIMEOUT = 2000; // 2 seconds per check
10
+ const TOTAL_TIMEOUT = 10000; // 10 seconds total
11
+ // Simple logger for doctor command (optional, falls back to no-op)
12
+ const logger = {
13
+ debug: (...args) => {
14
+ if (process.env.DEBUG)
15
+ console.debug('[DOCTOR]', ...args);
16
+ },
17
+ info: (...args) => {
18
+ if (process.env.VERBOSE)
19
+ console.log('[DOCTOR]', ...args);
20
+ },
21
+ warn: (...args) => {
22
+ console.warn('[DOCTOR]', ...args);
23
+ },
24
+ error: (...args) => {
25
+ console.error('[DOCTOR]', ...args);
26
+ }
27
+ };
28
+ // Simple metric tracker (no-op for now, can be enhanced later)
29
+ const trackMetric = (name, value) => {
30
+ if (process.env.DEBUG) {
31
+ console.debug(`[METRIC] ${name}: ${value}`);
32
+ }
33
+ };
34
+ /**
35
+ * Run a single check with timeout
36
+ */
37
+ async function runCheckWithTimeout(check, timeout = CHECK_TIMEOUT) {
38
+ const checkStartTime = Date.now();
39
+ try {
40
+ logger.debug(`Running check: ${check.name}`, { category: check.category });
41
+ const result = await Promise.race([
42
+ check.run(),
43
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), timeout))
44
+ ]);
45
+ const checkDuration = Date.now() - checkStartTime;
46
+ logger.debug(`Check completed: ${check.name}`, {
47
+ status: result.status,
48
+ duration: checkDuration
49
+ });
50
+ return result;
51
+ }
52
+ catch (error) {
53
+ const checkDuration = Date.now() - checkStartTime;
54
+ if (error.message === 'Timeout') {
55
+ logger.warn(`Check timed out: ${check.name}`, { duration: checkDuration });
56
+ return {
57
+ status: 'warning',
58
+ message: `${check.name} timed out`,
59
+ suggestion: 'Check may be slow or unresponsive'
60
+ };
61
+ }
62
+ logger.error(`Check failed: ${check.name}`, { error: error.message, duration: checkDuration });
63
+ return {
64
+ status: 'error',
65
+ message: `${check.name} failed: ${error.message}`,
66
+ suggestion: 'See error details above',
67
+ details: { error: error.message }
68
+ };
69
+ }
70
+ }
71
+ /**
72
+ * Filter checks based on command options
73
+ */
74
+ function filterChecks(checks, options) {
75
+ // If no specific flags, run all checks
76
+ if (!options.testMcp && !options.testConfig && !options.testWorkflows) {
77
+ return checks;
78
+ }
79
+ return checks.filter(check => {
80
+ if (options.testMcp && check.category === 'mcpConnectivity')
81
+ return true;
82
+ if (options.testConfig && (check.category === 'globalSetup' || check.category === 'projectSetup' || check.category === 'ideConfiguration'))
83
+ return true;
84
+ if (options.testWorkflows && check.category === 'workflows')
85
+ return true;
86
+ return false;
87
+ });
88
+ }
89
+ /**
90
+ * Run all checks with parallel execution and timeout
91
+ */
92
+ async function runChecks(checks, options, version) {
93
+ const startTime = Date.now();
94
+ // Filter checks based on options
95
+ const filteredChecks = filterChecks(checks, options);
96
+ logger.info(`Running ${filteredChecks.length} checks`, {
97
+ total: checks.length,
98
+ filtered: filteredChecks.length,
99
+ options
100
+ });
101
+ // Group checks by category for logging
102
+ const categoryCounts = {};
103
+ for (const check of filteredChecks) {
104
+ categoryCounts[check.category] = (categoryCounts[check.category] || 0) + 1;
105
+ }
106
+ for (const [category, count] of Object.entries(categoryCounts)) {
107
+ logger.info(`Category: ${category}`, { checkCount: count });
108
+ }
109
+ // Run checks in parallel with overall timeout
110
+ const checkPromises = filteredChecks.map(check => runCheckWithTimeout(check).then(result => ({ check, result })));
111
+ let checkResults;
112
+ try {
113
+ checkResults = await Promise.race([
114
+ Promise.all(checkPromises),
115
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Total timeout exceeded')), TOTAL_TIMEOUT))
116
+ ]);
117
+ }
118
+ catch (error) {
119
+ logger.error('Total timeout exceeded for all checks');
120
+ // If total timeout exceeded, return partial results
121
+ checkResults = [];
122
+ }
123
+ // Group results by category
124
+ const categories = {
125
+ globalSetup: { checks: [] },
126
+ projectSetup: { checks: [] },
127
+ workflows: { checks: [] },
128
+ ideConfiguration: { checks: [] },
129
+ mcpConnectivity: { checks: [] },
130
+ scripts: { checks: [] }
131
+ };
132
+ let passed = 0;
133
+ let warnings = 0;
134
+ let errors = 0;
135
+ for (const { check, result } of checkResults) {
136
+ const categoryResult = {
137
+ name: check.name,
138
+ status: result.status,
139
+ message: result.message,
140
+ suggestion: result.suggestion,
141
+ command: result.command,
142
+ details: result.details,
143
+ latency: result.latency
144
+ };
145
+ categories[check.category].checks.push(categoryResult);
146
+ if (result.status === 'passed')
147
+ passed++;
148
+ else if (result.status === 'warning')
149
+ warnings++;
150
+ else if (result.status === 'error')
151
+ errors++;
152
+ }
153
+ const duration = Date.now() - startTime;
154
+ // Track category durations
155
+ for (const [category, categoryResult] of Object.entries(categories)) {
156
+ if (categoryResult.checks.length > 0) {
157
+ trackMetric(`doctor.category.${category}.duration`, duration);
158
+ trackMetric(`doctor.category.${category}.checks`, categoryResult.checks.length);
159
+ }
160
+ }
161
+ // Track MCP latencies
162
+ for (const { result } of checkResults) {
163
+ if (result.latency !== undefined) {
164
+ trackMetric('doctor.mcp.latency', result.latency);
165
+ }
166
+ }
167
+ logger.info('All checks completed', {
168
+ duration,
169
+ passed,
170
+ warnings,
171
+ errors,
172
+ total: checkResults.length
173
+ });
174
+ // Generate suggestions from errors and warnings
175
+ const suggestions = checkResults
176
+ .filter(({ result }) => result.status !== 'passed' && result.suggestion)
177
+ .map(({ result }, index) => ({
178
+ priority: result.status === 'error' ? index + 1 : index + 100,
179
+ message: result.suggestion,
180
+ command: result.command,
181
+ reason: result.message
182
+ }))
183
+ .sort((a, b) => a.priority - b.priority);
184
+ return {
185
+ timestamp: new Date().toISOString(),
186
+ version,
187
+ summary: {
188
+ passed,
189
+ warnings,
190
+ errors,
191
+ total: checkResults.length
192
+ },
193
+ categories: categories,
194
+ suggestions: suggestions.length > 0 ? suggestions : undefined
195
+ };
196
+ }
@@ -0,0 +1,221 @@
1
+ "use strict";
2
+ /**
3
+ * Global setup checks for FRAIM doctor command
4
+ * Validates global configuration and API keys
5
+ * Issue #144: Enhanced doctor command
6
+ */
7
+ var __importDefault = (this && this.__importDefault) || function (mod) {
8
+ return (mod && mod.__esModule) ? mod : { "default": mod };
9
+ };
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.getGlobalSetupChecks = getGlobalSetupChecks;
12
+ const fs_1 = __importDefault(require("fs"));
13
+ const path_1 = __importDefault(require("path"));
14
+ const os_1 = __importDefault(require("os"));
15
+ const GLOBAL_CONFIG_PATH = path_1.default.join(os_1.default.homedir(), '.fraim', 'config.json');
16
+ /**
17
+ * Check if global config exists
18
+ */
19
+ function checkGlobalConfigExists() {
20
+ return {
21
+ name: 'Global config exists',
22
+ category: 'globalSetup',
23
+ critical: true,
24
+ run: async () => {
25
+ if (fs_1.default.existsSync(GLOBAL_CONFIG_PATH)) {
26
+ return {
27
+ status: 'passed',
28
+ message: 'Global config exists',
29
+ details: { path: GLOBAL_CONFIG_PATH }
30
+ };
31
+ }
32
+ return {
33
+ status: 'error',
34
+ message: 'Global config missing',
35
+ suggestion: 'Run fraim setup to create global configuration',
36
+ command: 'fraim setup'
37
+ };
38
+ }
39
+ };
40
+ }
41
+ /**
42
+ * Check if mode is valid
43
+ */
44
+ function checkModeValid() {
45
+ return {
46
+ name: 'Mode valid',
47
+ category: 'globalSetup',
48
+ critical: false,
49
+ run: async () => {
50
+ try {
51
+ if (!fs_1.default.existsSync(GLOBAL_CONFIG_PATH)) {
52
+ return {
53
+ status: 'error',
54
+ message: 'Cannot check mode - global config missing'
55
+ };
56
+ }
57
+ const config = JSON.parse(fs_1.default.readFileSync(GLOBAL_CONFIG_PATH, 'utf8'));
58
+ const mode = config.mode || 'conversational';
59
+ const validModes = ['conversational', 'integrated', 'split'];
60
+ if (validModes.includes(mode)) {
61
+ return {
62
+ status: 'passed',
63
+ message: `Mode: ${mode}`,
64
+ details: { mode }
65
+ };
66
+ }
67
+ return {
68
+ status: 'error',
69
+ message: `Invalid mode: ${mode}`,
70
+ suggestion: 'Mode must be conversational, integrated, or split',
71
+ details: { mode, validModes }
72
+ };
73
+ }
74
+ catch (error) {
75
+ return {
76
+ status: 'error',
77
+ message: 'Failed to read mode from config',
78
+ details: { error: error.message }
79
+ };
80
+ }
81
+ }
82
+ };
83
+ }
84
+ /**
85
+ * Check if API key is configured
86
+ */
87
+ function checkApiKeyConfigured() {
88
+ return {
89
+ name: 'API key configured',
90
+ category: 'globalSetup',
91
+ critical: false,
92
+ run: async () => {
93
+ try {
94
+ if (!fs_1.default.existsSync(GLOBAL_CONFIG_PATH)) {
95
+ return {
96
+ status: 'error',
97
+ message: 'Cannot check API key - global config missing'
98
+ };
99
+ }
100
+ const config = JSON.parse(fs_1.default.readFileSync(GLOBAL_CONFIG_PATH, 'utf8'));
101
+ if (config.apiKey) {
102
+ const maskedKey = config.apiKey.substring(0, 10) + '...';
103
+ return {
104
+ status: 'passed',
105
+ message: `API key configured (${maskedKey})`,
106
+ details: { keyPrefix: config.apiKey.substring(0, 10) }
107
+ };
108
+ }
109
+ return {
110
+ status: 'warning',
111
+ message: 'API key not configured',
112
+ suggestion: 'Add apiKey to global config for remote features',
113
+ details: { configPath: GLOBAL_CONFIG_PATH }
114
+ };
115
+ }
116
+ catch (error) {
117
+ return {
118
+ status: 'error',
119
+ message: 'Failed to read API key from config',
120
+ details: { error: error.message }
121
+ };
122
+ }
123
+ }
124
+ };
125
+ }
126
+ /**
127
+ * Check if GitHub token is configured
128
+ */
129
+ function checkGitHubTokenConfigured() {
130
+ return {
131
+ name: 'GitHub token configured',
132
+ category: 'globalSetup',
133
+ critical: false,
134
+ run: async () => {
135
+ try {
136
+ if (!fs_1.default.existsSync(GLOBAL_CONFIG_PATH)) {
137
+ return {
138
+ status: 'error',
139
+ message: 'Cannot check GitHub token - global config missing'
140
+ };
141
+ }
142
+ const config = JSON.parse(fs_1.default.readFileSync(GLOBAL_CONFIG_PATH, 'utf8'));
143
+ // Check if repository provider is GitHub
144
+ if (config.repository?.provider === 'github') {
145
+ // Token would be in IDE MCP configs, not global config
146
+ return {
147
+ status: 'passed',
148
+ message: 'GitHub provider configured',
149
+ details: { provider: 'github' }
150
+ };
151
+ }
152
+ return {
153
+ status: 'passed',
154
+ message: 'GitHub not configured (not required)',
155
+ details: { provider: config.repository?.provider || 'none' }
156
+ };
157
+ }
158
+ catch (error) {
159
+ return {
160
+ status: 'error',
161
+ message: 'Failed to check GitHub configuration',
162
+ details: { error: error.message }
163
+ };
164
+ }
165
+ }
166
+ };
167
+ }
168
+ /**
169
+ * Check if remote URL is configured
170
+ */
171
+ function checkRemoteUrlConfigured() {
172
+ return {
173
+ name: 'Remote URL configured',
174
+ category: 'globalSetup',
175
+ critical: false,
176
+ run: async () => {
177
+ try {
178
+ if (!fs_1.default.existsSync(GLOBAL_CONFIG_PATH)) {
179
+ return {
180
+ status: 'error',
181
+ message: 'Cannot check remote URL - global config missing'
182
+ };
183
+ }
184
+ const config = JSON.parse(fs_1.default.readFileSync(GLOBAL_CONFIG_PATH, 'utf8'));
185
+ if (config.remoteUrl) {
186
+ return {
187
+ status: 'passed',
188
+ message: `Remote URL: ${config.remoteUrl}`,
189
+ details: { remoteUrl: config.remoteUrl }
190
+ };
191
+ }
192
+ return {
193
+ status: 'warning',
194
+ message: 'Remote URL not configured',
195
+ suggestion: 'Add remoteUrl to global config for remote sync',
196
+ command: 'fraim sync',
197
+ details: { configPath: GLOBAL_CONFIG_PATH }
198
+ };
199
+ }
200
+ catch (error) {
201
+ return {
202
+ status: 'error',
203
+ message: 'Failed to read remote URL from config',
204
+ details: { error: error.message }
205
+ };
206
+ }
207
+ }
208
+ };
209
+ }
210
+ /**
211
+ * Get all global setup checks
212
+ */
213
+ function getGlobalSetupChecks() {
214
+ return [
215
+ checkGlobalConfigExists(),
216
+ checkModeValid(),
217
+ checkApiKeyConfigured(),
218
+ checkGitHubTokenConfigured(),
219
+ checkRemoteUrlConfigured()
220
+ ];
221
+ }