fraim-framework 2.0.47 → 2.0.48

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.
Files changed (32) hide show
  1. package/dist/registry/providers/ado.json +19 -0
  2. package/dist/registry/providers/github.json +19 -0
  3. package/dist/src/ai-manager/ai-manager.js +8 -1
  4. package/dist/src/cli/commands/init-project.js +5 -4
  5. package/dist/src/cli/commands/init.js +8 -7
  6. package/dist/src/cli/setup/first-run.js +116 -29
  7. package/dist/src/fraim/config-loader.js +58 -23
  8. package/dist/src/fraim/issue-tracking/ado-provider.js +304 -0
  9. package/dist/src/fraim/issue-tracking/factory.js +63 -0
  10. package/dist/src/fraim/issue-tracking/github-provider.js +200 -0
  11. package/dist/src/fraim/issue-tracking/types.js +7 -0
  12. package/dist/src/fraim/issue-tracking-config.js +83 -0
  13. package/dist/src/fraim/issues.js +25 -23
  14. package/dist/src/fraim/setup-wizard.js +5 -3
  15. package/dist/src/fraim/template-processor.js +156 -30
  16. package/dist/src/fraim/types.js +21 -23
  17. package/dist/src/fraim-mcp-server.js +192 -31
  18. package/dist/src/utils/git-utils.js +38 -3
  19. package/dist/src/utils/platform-detection.js +213 -0
  20. package/dist/tests/test-cli.js +6 -10
  21. package/dist/tests/test-debug-session.js +130 -0
  22. package/dist/tests/test-enhanced-session-init.js +184 -0
  23. package/dist/tests/test-first-run-interactive.js +1 -0
  24. package/dist/tests/test-first-run-journey.js +274 -54
  25. package/dist/tests/test-fraim-issues.js +1 -1
  26. package/dist/tests/test-genericization.js +5 -25
  27. package/dist/tests/test-mcp-issue-integration.js +6 -2
  28. package/dist/tests/test-mcp-template-processing.js +156 -0
  29. package/dist/tests/test-modular-issue-tracking.js +161 -0
  30. package/dist/tests/test-package-size.js +7 -0
  31. package/dist/tests/test-workflow-discovery.js +242 -0
  32. package/package.json +1 -1
@@ -43,10 +43,10 @@ const cors_1 = __importDefault(require("cors"));
43
43
  const fs_1 = require("fs");
44
44
  const path_1 = require("path");
45
45
  const git_utils_1 = require("./utils/git-utils");
46
- const config_loader_1 = require("./fraim/config-loader");
47
46
  const db_service_1 = require("./fraim/db-service");
48
47
  const ai_manager_1 = require("./ai-manager/ai-manager");
49
48
  const issues_1 = require("./fraim/issues");
49
+ const template_processor_1 = require("./fraim/template-processor");
50
50
  const crypto_1 = require("crypto");
51
51
  const dotenv = __importStar(require("dotenv"));
52
52
  // Load environment variables
@@ -170,8 +170,6 @@ class FraimMCPServer {
170
170
  // Initialize database service
171
171
  this.dbService = new db_service_1.FraimDbService();
172
172
  this.sessionManager = new SessionManager(this.dbService);
173
- // Load FRAIM configuration
174
- this.config = (0, config_loader_1.loadFraimConfig)();
175
173
  // Find registry directory (check dist first for production, then source)
176
174
  this.registryPath = this.findRegistryPath();
177
175
  // Build file index from filesystem (includes registry and .fraim)
@@ -238,7 +236,7 @@ class FraimMCPServer {
238
236
  const clientVersion = this.getClientVersion();
239
237
  if (clientVersion && clientVersion !== this.serverVersion) {
240
238
  // Add a notice header for agents/developers to see
241
- res.setHeader('X-FRAIM-Version-Notice', `Version mismatch: Project has ${clientVersion}, Server has ${this.serverVersion}. Run 'npm install @fraim/framework@latest'`);
239
+ res.setHeader('X-FRAIM-Version-Notice', `Update available: Project has ${clientVersion}, Server has ${this.serverVersion}. Run 'fraim sync' to get latest workflows and features. This will not cause issues, but you're missing all the latest capabilities of FRAIM :)`);
242
240
  // If it's an /mcp request, we can inject it into the response later in the handler
243
241
  // For now, storing it in the request object
244
242
  req.versionMismatch = {
@@ -283,11 +281,20 @@ class FraimMCPServer {
283
281
  if (req.path === '/health' || req.path.startsWith('/admin')) {
284
282
  return next();
285
283
  }
286
- // Skip auth in test mode
284
+ const apiKey = req.headers['x-api-key'] || req.query['api-key'];
285
+ // In test mode, still extract API key if provided but don't validate it
287
286
  if (process.env.NODE_ENV === 'test') {
287
+ if (apiKey) {
288
+ // Set mock API key data for testing
289
+ req.apiKeyData = {
290
+ key: apiKey,
291
+ userId: 'test-user',
292
+ orgId: 'test-org',
293
+ isActive: true
294
+ };
295
+ }
288
296
  return next();
289
297
  }
290
- const apiKey = req.headers['x-api-key'] || req.query['api-key'];
291
298
  if (!apiKey) {
292
299
  console.error(`[FRAIM AUTH] Missing API key for ${req.method} ${req.path}`);
293
300
  res.status(401).json({ error: 'Unauthorized', message: 'Missing API key' });
@@ -412,8 +419,12 @@ class FraimMCPServer {
412
419
  buildFileIndex() {
413
420
  this.fileIndex.clear();
414
421
  // 1. Index Global registry (packaged with the server)
415
- // __dirname is src/ (dev) or dist/src/ (prod)
416
- const globalRegistryPath = (0, path_1.join)(__dirname, '..', 'registry');
422
+ // In development, prefer source registry over dist registry
423
+ let globalRegistryPath = (0, path_1.join)(__dirname, '..', 'registry');
424
+ if (!(0, fs_1.existsSync)(globalRegistryPath)) {
425
+ // Fallback to dist registry for production
426
+ globalRegistryPath = (0, path_1.join)(__dirname, '..', '..', 'registry');
427
+ }
417
428
  if ((0, fs_1.existsSync)(globalRegistryPath)) {
418
429
  console.log(`🌍 Indexing global registry from ${globalRegistryPath}`);
419
430
  this.indexDirectory(globalRegistryPath, '');
@@ -428,15 +439,13 @@ class FraimMCPServer {
428
439
  const fraimBasePath = (0, path_1.join)(process.cwd(), '.fraim');
429
440
  if ((0, fs_1.existsSync)(fraimBasePath)) {
430
441
  console.log(`🎨 Indexing project-specific .fraim customizations`);
431
- const customizations = this.config.customizations || {};
432
442
  // Index custom rules
433
443
  const rulesPath = (0, path_1.join)(process.cwd(), '.fraim/rules');
434
444
  if ((0, fs_1.existsSync)(rulesPath)) {
435
445
  this.indexDirectory(rulesPath, 'fraim/rules');
436
446
  }
437
- // Index custom workflows
438
- const workflowsPathStr = customizations.workflowsPath || '.fraim/workflows';
439
- const workflowsPath = (0, path_1.join)(process.cwd(), workflowsPathStr);
447
+ // Index custom workflows (use default path)
448
+ const workflowsPath = (0, path_1.join)(process.cwd(), '.fraim/workflows');
440
449
  if ((0, fs_1.existsSync)(workflowsPath)) {
441
450
  this.indexDirectory(workflowsPath, 'fraim/workflows');
442
451
  }
@@ -797,7 +806,7 @@ class FraimMCPServer {
797
806
  }
798
807
  if (req.versionMismatch) {
799
808
  const { client, server } = req.versionMismatch;
800
- const notice = `⚠️ [FRAIM VERSION MISMATCH] Project uses @fraim/framework@${client}, but the server is at ${server}.\n👉 Action: Run 'npm install @fraim/framework@latest' and 'fraim sync' to stay in sync.`;
809
+ const notice = `ℹ️ **FRAIM Update Available**\n\nYour project is using FRAIM ${client}, but version ${server} is available with new features and improvements.\n\n**To update:** Run \`fraim sync\` to get the latest workflows and templates.\n\n---\n\n`;
801
810
  if (result && Array.isArray(result.content)) {
802
811
  result.content.unshift({
803
812
  type: 'text',
@@ -860,23 +869,87 @@ Call this before fetching any workflow or file to understand the system.`,
860
869
  {
861
870
  name: 'fraim_connect',
862
871
  description: `Handshake to start a FRAIM session. MUST be called at the start of every session.
863
- Registers your environment (machine/repo) for telemetry and unlocks other tools.`,
872
+ Registers your environment (machine/repo/agent) for telemetry and unlocks other tools.
873
+
874
+ **Agent Information**: REQUIRED - Provide your actual agent name and model (validated against known agents).
875
+ **Machine Information**: Use os.hostname() and process.platform - required for telemetry.
876
+ **Repository Information**: FRAIM will auto-detect from .fraim/config.json if available, or provide git remote URL.`,
864
877
  inputSchema: {
865
878
  type: 'object',
866
879
  properties: {
880
+ agent: {
881
+ type: 'object',
882
+ description: 'Agent identification and capabilities',
883
+ properties: {
884
+ name: {
885
+ type: 'string',
886
+ description: 'Agent name (e.g., "Claude", "Cursor", "Kiro", "Windsurf", "Antigravity")',
887
+ examples: ['Claude', 'Cursor', 'Kiro', 'Windsurf', 'Antigravity']
888
+ },
889
+ model: {
890
+ type: 'string',
891
+ description: 'Model name/version (e.g., "claude-3.5-sonnet", "gpt-4", "cursor-small")',
892
+ examples: ['claude-3.5-sonnet', 'gpt-4', 'cursor-small', 'kiro-agent']
893
+ },
894
+ version: {
895
+ type: 'string',
896
+ description: 'Agent version if available',
897
+ examples: ['1.0.0', '2024.12.1']
898
+ }
899
+ },
900
+ required: ['name', 'model'],
901
+ additionalProperties: true
902
+ },
867
903
  machine: {
868
904
  type: 'object',
869
- description: 'Machine specs (hostname, platform, memory, cpus)',
905
+ description: 'Machine specs - use os.hostname(), process.platform, os.totalmem(), os.cpus().length',
906
+ properties: {
907
+ hostname: {
908
+ type: 'string',
909
+ description: 'Machine hostname from os.hostname()'
910
+ },
911
+ platform: {
912
+ type: 'string',
913
+ description: 'Platform from process.platform (win32, darwin, linux)'
914
+ },
915
+ memory: {
916
+ type: 'number',
917
+ description: 'Total memory in bytes from os.totalmem()'
918
+ },
919
+ cpus: {
920
+ type: 'number',
921
+ description: 'CPU count from os.cpus().length'
922
+ }
923
+ },
924
+ required: ['hostname', 'platform'],
870
925
  additionalProperties: true
871
926
  },
872
927
  repo: {
873
928
  type: 'object',
874
- description: 'Repository context (owner, name, url)',
929
+ description: 'Repository context - use git remote -v or check .git/config for URL',
930
+ properties: {
931
+ url: {
932
+ type: 'string',
933
+ description: 'Git repository URL from git remote -v'
934
+ },
935
+ owner: {
936
+ type: 'string',
937
+ description: 'Repository owner (extracted from URL)'
938
+ },
939
+ name: {
940
+ type: 'string',
941
+ description: 'Repository name (extracted from URL)'
942
+ },
943
+ branch: {
944
+ type: 'string',
945
+ description: 'Current branch from git branch --show-current'
946
+ }
947
+ },
875
948
  required: ['url'],
876
949
  additionalProperties: true
877
950
  }
878
951
  },
879
- required: ['machine', 'repo']
952
+ required: ['agent', 'machine', 'repo']
880
953
  }
881
954
  },
882
955
  {
@@ -1054,13 +1127,14 @@ This is your single point of contact for workflow guidance with evidence validat
1054
1127
  }
1055
1128
  async handleToolCall(params, context = {}) {
1056
1129
  const { name: toolName, arguments: toolArgs } = params;
1130
+ console.log(`🔧 MCP Server: handleToolCall called with tool: ${toolName}, args:`, JSON.stringify(toolArgs, null, 2));
1057
1131
  switch (toolName) {
1058
1132
  case 'get_fraim_init':
1059
1133
  return await this.handleGetInit();
1060
1134
  case 'get_fraim_file':
1061
1135
  return await this.handleGetFile(toolArgs.path);
1062
1136
  case 'get_fraim_workflow':
1063
- return await this.handleGetWorkflow(toolArgs.workflow);
1137
+ return await this.handleGetWorkflow(toolArgs.workflow, context.apiKey);
1064
1138
  case 'list_fraim_workflows':
1065
1139
  return await this.handleListWorkflows();
1066
1140
  case 'fraim_get_local_config':
@@ -1246,20 +1320,30 @@ This is your single point of contact for workflow guidance with evidence validat
1246
1320
  throw new Error(`Failed to read file: ${error instanceof Error ? error.message : 'Unknown error'}`);
1247
1321
  }
1248
1322
  }
1249
- async handleGetWorkflow(workflowName) {
1323
+ async handleGetWorkflow(workflowName, apiKey) {
1250
1324
  // Normalize workflow name (remove .md if present)
1251
1325
  const normalizedName = workflowName.replace(/\.md$/, '');
1252
1326
  // Try to find the workflow file
1253
1327
  const workflowPath = `workflows/${normalizedName}.md`;
1254
1328
  const metadata = this.fileIndex.get(workflowPath);
1255
1329
  if (!metadata) {
1256
- // Try alternative category paths
1257
- const categories = ['product-building', 'customer-development', 'business-development', 'marketing', 'performance', 'quality-assurance', 'reviewer', 'startup-credits', 'bootstrap', 'deploy'];
1258
- for (const cat of categories) {
1330
+ // Try alternative category paths - dynamically discover categories from file index
1331
+ const workflowCategories = new Set();
1332
+ // Extract all unique workflow categories from the file index
1333
+ for (const [path, fileMetadata] of this.fileIndex) {
1334
+ if (fileMetadata.type === 'workflow' && path.startsWith('workflows/') && path.includes('/')) {
1335
+ const pathParts = path.split('/');
1336
+ if (pathParts.length >= 3) { // workflows/category/file.md
1337
+ workflowCategories.add(pathParts[1]);
1338
+ }
1339
+ }
1340
+ }
1341
+ // Search through all discovered categories
1342
+ for (const cat of workflowCategories) {
1259
1343
  const altPath = `workflows/${cat}/${normalizedName}.md`;
1260
1344
  const altMetadata = this.fileIndex.get(altPath);
1261
1345
  if (altMetadata) {
1262
- return this.returnWorkflowFile(altMetadata);
1346
+ return this.returnWorkflowFile(altMetadata, apiKey);
1263
1347
  }
1264
1348
  }
1265
1349
  // Workflow not found
@@ -1273,22 +1357,48 @@ This is your single point of contact for workflow guidance with evidence validat
1273
1357
  }]
1274
1358
  };
1275
1359
  }
1276
- return this.returnWorkflowFile(metadata);
1360
+ return this.returnWorkflowFile(metadata, apiKey);
1277
1361
  }
1278
1362
  /**
1279
1363
  * Return a workflow file with its content
1280
1364
  */
1281
- async returnWorkflowFile(metadata) {
1365
+ async returnWorkflowFile(metadata, apiKey) {
1282
1366
  try {
1283
- const content = (0, fs_1.readFileSync)(metadata.fullPath, 'utf-8');
1367
+ const rawContent = (0, fs_1.readFileSync)(metadata.fullPath, 'utf-8');
1368
+ // Get repository info from session if available
1369
+ let repositoryInfo = null;
1370
+ if (apiKey) {
1371
+ try {
1372
+ const session = await this.dbService.getActiveSessionByApiKey(apiKey);
1373
+ if (session?.repo) {
1374
+ repositoryInfo = {
1375
+ owner: session.repo.owner,
1376
+ name: session.repo.name,
1377
+ organization: session.repo.organization,
1378
+ project: session.repo.project,
1379
+ url: session.repo.url,
1380
+ provider: this.detectProviderFromUrl(session.repo.url)
1381
+ };
1382
+ }
1383
+ }
1384
+ catch (e) {
1385
+ // Ignore session lookup errors, continue without repository info
1386
+ }
1387
+ }
1388
+ // Process templates with provider-specific actions
1389
+ const templateEngine = (0, template_processor_1.getTemplateEngine)(this.registryPath);
1390
+ const processedContent = templateEngine.processWorkflow(rawContent, repositoryInfo);
1284
1391
  // Build response
1285
1392
  let response = `# Workflow: ${metadata.name.replace('.md', '')}\n\n`;
1286
1393
  response += `**Path:** \`${metadata.path}\`\n`;
1287
1394
  if (metadata.category) {
1288
1395
  response += `**Category:** ${metadata.category}\n`;
1289
1396
  }
1397
+ // Add provider info if available
1398
+ const provider = repositoryInfo?.provider || 'GITHUB';
1399
+ response += `**Platform:** ${provider.toUpperCase()}\n`;
1290
1400
  response += `\n---\n\n`;
1291
- response += content;
1401
+ response += processedContent;
1292
1402
  return {
1293
1403
  content: [{
1294
1404
  type: 'text',
@@ -1484,16 +1594,41 @@ If \`.fraim/config.json\` doesn't exist:
1484
1594
  if (!apiKey) {
1485
1595
  throw new Error('No API Key found in context for fraim_connect');
1486
1596
  }
1597
+ // Validate required agent information
1598
+ if (!args.agent?.name || !args.agent?.model) {
1599
+ throw new Error('Agent information is required. Please provide agent.name and agent.model in your fraim_connect call.');
1600
+ }
1601
+ // Validate agent information to prevent BS
1602
+ const validAgentNames = ['Claude', 'Cursor', 'Kiro', 'Windsurf', 'Antigravity', 'ChatGPT', 'Copilot'];
1603
+ if (!validAgentNames.some(valid => args.agent.name.toLowerCase().includes(valid.toLowerCase()))) {
1604
+ throw new Error(`Invalid agent name "${args.agent.name}". Please use one of: ${validAgentNames.join(', ')}`);
1605
+ }
1606
+ // Validate machine information
1607
+ if (!args.machine?.hostname || !args.machine?.platform) {
1608
+ throw new Error('Machine information is required. Please provide machine.hostname and machine.platform. Use os.hostname() and process.platform.');
1609
+ }
1610
+ // Validate repo information - check if .fraim/config.json exists and use it
1611
+ let repoInfo = args.repo || {};
1612
+ // Note: MCP server cannot access client-side .fraim/config.json
1613
+ // Repository info must be provided by the client in the fraim_connect call
1614
+ if (!repoInfo.url) {
1615
+ throw new Error('Repository information is required. Please provide repo.url in the fraim_connect call.');
1616
+ }
1487
1617
  // 1. Generate Session ID
1488
1618
  const sessionId = (0, crypto_1.randomUUID)();
1489
1619
  // userId passed from context
1490
1620
  const finalUserId = userId || 'unknown';
1491
- // 2. Create Session in DB
1621
+ // 2. Create Session in DB with enhanced information
1492
1622
  const session = {
1493
1623
  sessionId,
1494
1624
  userId: finalUserId,
1495
- machine: args.machine || {},
1496
- repo: args.repo || {},
1625
+ agent: {
1626
+ name: args.agent.name,
1627
+ model: args.agent.model,
1628
+ version: args.agent.version
1629
+ },
1630
+ machine: args.machine,
1631
+ repo: repoInfo,
1497
1632
  startTime: new Date(),
1498
1633
  lastActive: new Date()
1499
1634
  };
@@ -1503,15 +1638,32 @@ If \`.fraim/config.json\` doesn't exist:
1503
1638
  await this.dbService.createSession(session);
1504
1639
  // 3. Register in SessionManager
1505
1640
  this.sessionManager.registerSession(apiKey, sessionId);
1641
+ // 4. Generate informative response
1642
+ const agentInfo = `${args.agent.name}${args.agent.model ? ` (${args.agent.model})` : ''}${args.agent.version ? ` v${args.agent.version}` : ''}`;
1643
+ const machineInfo = `${args.machine.hostname || 'unknown'} (${args.machine.platform || 'unknown'})`;
1644
+ const repoInfoDisplay = repoInfo.name || repoInfo.url || 'unknown';
1506
1645
  return {
1507
1646
  content: [{
1508
1647
  type: 'text',
1509
- text: `✅ Connected! Session ID: ${sessionId}. Telemetry active.`
1648
+ text: `✅ **FRAIM Session Connected!**
1649
+
1650
+ **Session ID**: ${sessionId}
1651
+ **Agent**: ${agentInfo}
1652
+ **Machine**: ${machineInfo}
1653
+ **Repository**: ${repoInfoDisplay}
1654
+
1655
+ 🔄 Telemetry active. All FRAIM tools are now unlocked.
1656
+
1657
+ **Next Steps**:
1658
+ - Use \`get_fraim_init\` to see available workflows
1659
+ - Use \`list_fraim_workflows\` to browse by category
1660
+ - Use \`get_fraim_workflow({ workflow: "name" })\` to start working`
1510
1661
  }],
1511
1662
  sessionId: sessionId
1512
1663
  };
1513
1664
  }
1514
1665
  async handleSeekCoachingOnNextStep(args) {
1666
+ console.log('🔧 MCP Server: handleSeekCoachingOnNextStep called with args:', JSON.stringify(args, null, 2));
1515
1667
  try {
1516
1668
  const response = await this.aiCoach.handleCoachingRequest({
1517
1669
  workflowType: args.workflowType,
@@ -1521,6 +1673,7 @@ If \`.fraim/config.json\` doesn't exist:
1521
1673
  evidence: args.evidence,
1522
1674
  findings: args.findings
1523
1675
  });
1676
+ console.log('✅ MCP Server: AI Coach returned response, length:', response.length);
1524
1677
  return {
1525
1678
  content: [{
1526
1679
  type: 'text',
@@ -1557,6 +1710,14 @@ If \`.fraim/config.json\` doesn't exist:
1557
1710
  throw error;
1558
1711
  }
1559
1712
  }
1713
+ detectProviderFromUrl(url) {
1714
+ if (!url)
1715
+ return 'github';
1716
+ if (url.includes('dev.azure.com') || url.includes('visualstudio.com')) {
1717
+ return 'ado';
1718
+ }
1719
+ return 'github';
1720
+ }
1560
1721
  }
1561
1722
  exports.FraimMCPServer = FraimMCPServer;
1562
1723
  // Start the server if this file is run directly
@@ -4,6 +4,7 @@ exports.getPort = getPort;
4
4
  exports.determineDatabaseName = determineDatabaseName;
5
5
  exports.getCurrentGitBranch = getCurrentGitBranch;
6
6
  exports.determineSchema = determineSchema;
7
+ exports.getDefaultBranch = getDefaultBranch;
7
8
  exports.getGitRemoteInfo = getGitRemoteInfo;
8
9
  const child_process_1 = require("child_process");
9
10
  /**
@@ -47,7 +48,10 @@ function determineDatabaseName() {
47
48
  */
48
49
  function getCurrentGitBranch() {
49
50
  try {
50
- return (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD').toString().trim();
51
+ return (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD', {
52
+ timeout: 2000, // 2 second timeout
53
+ stdio: 'pipe'
54
+ }).toString().trim();
51
55
  }
52
56
  catch (e) {
53
57
  return 'master';
@@ -63,12 +67,42 @@ function determineSchema(branchName) {
63
67
  }
64
68
  return 'prod';
65
69
  }
70
+ /**
71
+ * Gets the default branch name from git remote
72
+ */
73
+ function getDefaultBranch() {
74
+ try {
75
+ // Try to get the default branch from remote HEAD
76
+ const remoteHead = (0, child_process_1.execSync)('git symbolic-ref refs/remotes/origin/HEAD', {
77
+ timeout: 2000, // 2 second timeout
78
+ stdio: 'pipe'
79
+ }).toString().trim();
80
+ const match = remoteHead.match(/refs\/remotes\/origin\/(.+)$/);
81
+ if (match) {
82
+ return match[1];
83
+ }
84
+ }
85
+ catch (e) {
86
+ // If that fails, try to get it from the current branch
87
+ try {
88
+ return getCurrentGitBranch();
89
+ }
90
+ catch (e2) {
91
+ // Fall back to common defaults
92
+ }
93
+ }
94
+ // Default fallback
95
+ return 'main';
96
+ }
66
97
  /**
67
98
  * Gets the GitHub remote info (owner and repo name)
68
99
  */
69
100
  function getGitRemoteInfo() {
70
101
  try {
71
- const remoteUrl = (0, child_process_1.execSync)('git remote get-url origin').toString().trim();
102
+ const remoteUrl = (0, child_process_1.execSync)('git remote get-url origin', {
103
+ timeout: 2000, // 2 second timeout
104
+ stdio: 'pipe'
105
+ }).toString().trim();
72
106
  // Match both HTTPS and SSH formats
73
107
  // HTTPS: https://github.com/owner/repo.git OR https://github.com/owner/repo
74
108
  // SSH: git@github.com:owner/repo.git OR git@github.com:owner/repo
@@ -76,7 +110,8 @@ function getGitRemoteInfo() {
76
110
  if (match) {
77
111
  return {
78
112
  owner: match[1],
79
- repo: match[2]
113
+ repo: match[2],
114
+ defaultBranch: getDefaultBranch()
80
115
  };
81
116
  }
82
117
  }
@@ -0,0 +1,213 @@
1
+ "use strict";
2
+ /**
3
+ * Platform Detection Utilities
4
+ *
5
+ * Detects development platform (GitHub, ADO) from git remote URLs
6
+ * and provides repository information extraction.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.detectPlatformFromGit = detectPlatformFromGit;
10
+ exports.getGitRemoteUrl = getGitRemoteUrl;
11
+ exports.detectPlatformFromUrl = detectPlatformFromUrl;
12
+ exports.validateRepositoryConfig = validateRepositoryConfig;
13
+ exports.getCurrentBranch = getCurrentBranch;
14
+ exports.isGitRepository = isGitRepository;
15
+ const child_process_1 = require("child_process");
16
+ /**
17
+ * Detect platform from git remote URL
18
+ */
19
+ function detectPlatformFromGit() {
20
+ try {
21
+ const remoteUrl = getGitRemoteUrl();
22
+ if (!remoteUrl) {
23
+ return { provider: 'unknown', confidence: 'low' };
24
+ }
25
+ return detectPlatformFromUrl(remoteUrl);
26
+ }
27
+ catch (error) {
28
+ console.warn('⚠️ Failed to detect platform from git:', error);
29
+ return { provider: 'unknown', confidence: 'low' };
30
+ }
31
+ }
32
+ /**
33
+ * Get git remote URL
34
+ */
35
+ function getGitRemoteUrl() {
36
+ try {
37
+ // Try origin first
38
+ const originUrl = (0, child_process_1.execSync)('git remote get-url origin', {
39
+ encoding: 'utf-8',
40
+ stdio: ['ignore', 'pipe', 'ignore']
41
+ }).trim();
42
+ if (originUrl)
43
+ return originUrl;
44
+ }
45
+ catch (e) {
46
+ // Ignore error, try alternative methods
47
+ }
48
+ try {
49
+ // Try any remote
50
+ const remotes = (0, child_process_1.execSync)('git remote', {
51
+ encoding: 'utf-8',
52
+ stdio: ['ignore', 'pipe', 'ignore']
53
+ }).trim().split('\n');
54
+ if (remotes.length > 0 && remotes[0]) {
55
+ const firstRemoteUrl = (0, child_process_1.execSync)(`git remote get-url ${remotes[0]}`, {
56
+ encoding: 'utf-8',
57
+ stdio: ['ignore', 'pipe', 'ignore']
58
+ }).trim();
59
+ return firstRemoteUrl;
60
+ }
61
+ }
62
+ catch (e) {
63
+ // Ignore error
64
+ }
65
+ return null;
66
+ }
67
+ /**
68
+ * Detect platform from URL
69
+ */
70
+ function detectPlatformFromUrl(url) {
71
+ const normalizedUrl = url.toLowerCase();
72
+ // GitHub detection
73
+ if (normalizedUrl.includes('github.com')) {
74
+ const repository = extractGitHubInfo(url);
75
+ return {
76
+ provider: 'github',
77
+ repository,
78
+ confidence: 'high'
79
+ };
80
+ }
81
+ // ADO detection
82
+ if (normalizedUrl.includes('dev.azure.com') ||
83
+ normalizedUrl.includes('visualstudio.com') ||
84
+ normalizedUrl.includes('azure.com')) {
85
+ const repository = extractAdoInfo(url);
86
+ return {
87
+ provider: 'ado',
88
+ repository,
89
+ confidence: 'high'
90
+ };
91
+ }
92
+ return { provider: 'unknown', confidence: 'low' };
93
+ }
94
+ /**
95
+ * Extract GitHub repository information from URL
96
+ */
97
+ function extractGitHubInfo(url) {
98
+ // Handle both HTTPS and SSH URLs
99
+ // HTTPS: https://github.com/owner/repo.git
100
+ // SSH: git@github.com:owner/repo.git
101
+ let match;
102
+ // HTTPS format
103
+ match = url.match(/github\.com[\/:]([^\/]+)\/([^\/\.]+)/i);
104
+ if (match) {
105
+ return {
106
+ provider: 'github',
107
+ owner: match[1],
108
+ name: match[2],
109
+ url: url,
110
+ defaultBranch: 'main'
111
+ };
112
+ }
113
+ // Fallback - just mark as GitHub
114
+ return {
115
+ provider: 'github',
116
+ url: url,
117
+ defaultBranch: 'main'
118
+ };
119
+ }
120
+ /**
121
+ * Extract ADO repository information from URL
122
+ */
123
+ function extractAdoInfo(url) {
124
+ // ADO URL formats:
125
+ // https://dev.azure.com/organization/project/_git/repository
126
+ // https://organization.visualstudio.com/project/_git/repository
127
+ let match;
128
+ // dev.azure.com format
129
+ match = url.match(/dev\.azure\.com\/([^\/]+)\/([^\/]+)\/_git\/([^\/\.]+)/i);
130
+ if (match) {
131
+ return {
132
+ provider: 'ado',
133
+ organization: match[1],
134
+ project: match[2],
135
+ name: match[3],
136
+ url: url,
137
+ defaultBranch: 'main'
138
+ };
139
+ }
140
+ // visualstudio.com format
141
+ match = url.match(/([^\.]+)\.visualstudio\.com\/([^\/]+)\/_git\/([^\/\.]+)/i);
142
+ if (match) {
143
+ return {
144
+ provider: 'ado',
145
+ organization: match[1],
146
+ project: match[2],
147
+ name: match[3],
148
+ url: url,
149
+ defaultBranch: 'main'
150
+ };
151
+ }
152
+ // Fallback - just mark as ADO
153
+ return {
154
+ provider: 'ado',
155
+ url: url,
156
+ defaultBranch: 'main'
157
+ };
158
+ }
159
+ /**
160
+ * Validate repository configuration
161
+ */
162
+ function validateRepositoryConfig(config) {
163
+ const errors = [];
164
+ if (!config.provider) {
165
+ errors.push('Provider is required');
166
+ }
167
+ if (config.provider === 'github') {
168
+ if (!config.owner)
169
+ errors.push('GitHub owner is required');
170
+ if (!config.name)
171
+ errors.push('GitHub repository name is required');
172
+ }
173
+ if (config.provider === 'ado') {
174
+ if (!config.organization)
175
+ errors.push('ADO organization is required');
176
+ if (!config.project)
177
+ errors.push('ADO project is required');
178
+ if (!config.name)
179
+ errors.push('ADO repository name is required');
180
+ }
181
+ return {
182
+ valid: errors.length === 0,
183
+ errors
184
+ };
185
+ }
186
+ /**
187
+ * Get current git branch
188
+ */
189
+ function getCurrentBranch() {
190
+ try {
191
+ return (0, child_process_1.execSync)('git branch --show-current', {
192
+ encoding: 'utf-8',
193
+ stdio: ['ignore', 'pipe', 'ignore']
194
+ }).trim();
195
+ }
196
+ catch (e) {
197
+ return null;
198
+ }
199
+ }
200
+ /**
201
+ * Check if we're in a git repository
202
+ */
203
+ function isGitRepository() {
204
+ try {
205
+ (0, child_process_1.execSync)('git rev-parse --git-dir', {
206
+ stdio: ['ignore', 'ignore', 'ignore']
207
+ });
208
+ return true;
209
+ }
210
+ catch (e) {
211
+ return false;
212
+ }
213
+ }