igel-qe-core 1.0.0 → 1.0.2

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/dist/cli/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
3
  import chalk from "chalk";
4
+ import { spawn } from "child_process";
5
+ import { setupPlatform } from "./platform.js";
4
6
  const program = new Command();
5
7
  program
6
8
  .name("igel-qe")
@@ -9,7 +11,10 @@ program
9
11
  program
10
12
  .command("init")
11
13
  .description("Bootstrap the Python AI Platform Layer")
12
- .action(async () => {
14
+ .option("--with-copilot", "Auto-configure for GitHub Copilot in VS Code")
15
+ .option("--with-cline", "Auto-configure for Cline")
16
+ .option("--with-all", "Auto-configure all supported platforms")
17
+ .action(async (options) => {
13
18
  console.log(chalk.blue("Initializing IGEL QE AI Platform..."));
14
19
  const steps = [
15
20
  "1. Checking Python version...",
@@ -26,13 +31,52 @@ program
26
31
  console.log(chalk.green(` ✓ Done`));
27
32
  }
28
33
  console.log(chalk.bold.green("\nIGEL QE AI Platform successfully initialized!"));
29
- console.log(`You can now use GitHub Copilot to invoke MCP tools or run commands via this CLI.`);
34
+ if (options.withCopilot || options.withAll) {
35
+ await setupPlatform('copilot');
36
+ }
37
+ if (options.withCline || options.withAll) {
38
+ await setupPlatform('cline');
39
+ }
40
+ console.log(`You can now use GitHub Copilot or Cline to invoke MCP tools or run commands via this CLI.`);
30
41
  });
31
42
  program
32
43
  .command("sync")
33
44
  .description("Trigger a delta sync of the automation codebase")
34
- .action(() => {
35
- console.log(chalk.blue("Triggering delta sync..."));
36
- // Would IPC call the Python BitbucketSyncAgent
45
+ .option("-c, --commit <hash>", "Bitbucket commit hash to sync", "HEAD")
46
+ .action((options) => {
47
+ console.log(chalk.blue(`Triggering delta sync for commit: ${options.commit}...`));
48
+ import('path').then(path => {
49
+ import('url').then(url => {
50
+ const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
51
+ const projectRoot = path.join(__dirname, '..', '..', '..');
52
+ const pythonProcess = spawn("python", [
53
+ "-m", "knowledge_base.cli.workflow_cli",
54
+ "--action", "sync_bitbucket",
55
+ "--args", JSON.stringify({ commit_hash: options.commit })
56
+ ], {
57
+ cwd: projectRoot,
58
+ env: process.env,
59
+ stdio: 'inherit'
60
+ });
61
+ pythonProcess.on("close", (code) => {
62
+ if (code === 0) {
63
+ console.log(chalk.green("Sync completed successfully."));
64
+ }
65
+ else {
66
+ console.error(chalk.red(`Sync failed with code ${code}`));
67
+ }
68
+ });
69
+ });
70
+ });
71
+ });
72
+ const platformCmd = program
73
+ .command("platform")
74
+ .description("Manage coding agent platform configurations");
75
+ platformCmd
76
+ .command("setup <platform>")
77
+ .description("Setup an MCP configuration for a specific platform (copilot, cline)")
78
+ .action(async (platform) => {
79
+ console.log(chalk.blue(`Setting up configuration for ${platform}...`));
80
+ await setupPlatform(platform);
37
81
  });
38
82
  program.parse();
@@ -0,0 +1,66 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as os from 'os';
4
+ import chalk from 'chalk';
5
+ export async function setupPlatform(platform) {
6
+ const homeDir = os.homedir();
7
+ // The command we want to inject
8
+ const mcpServerConfig = {
9
+ command: process.platform === 'win32' ? 'igel-qe-mcp.cmd' : 'igel-qe-mcp',
10
+ args: []
11
+ };
12
+ try {
13
+ if (platform === 'copilot') {
14
+ // Setup for GitHub Copilot in VS Code
15
+ // VS Code user settings on Windows usually live in AppData/Roaming/Code/User
16
+ let vscodeSettingsPath = path.join(homeDir, 'AppData', 'Roaming', 'Code', 'User', 'settings.json');
17
+ if (process.platform !== 'win32') {
18
+ vscodeSettingsPath = path.join(homeDir, '.config', 'Code', 'User', 'settings.json');
19
+ }
20
+ let settings = {};
21
+ if (fs.existsSync(vscodeSettingsPath)) {
22
+ const raw = fs.readFileSync(vscodeSettingsPath, 'utf8');
23
+ try {
24
+ settings = JSON.parse(raw);
25
+ }
26
+ catch (e) { /* ignore parse error for now */ }
27
+ }
28
+ else {
29
+ fs.mkdirSync(path.dirname(vscodeSettingsPath), { recursive: true });
30
+ }
31
+ if (!settings['github.copilot.chat.mcp.servers']) {
32
+ settings['github.copilot.chat.mcp.servers'] = {};
33
+ }
34
+ settings['github.copilot.chat.mcp.servers']['igel-qe'] = mcpServerConfig;
35
+ fs.writeFileSync(vscodeSettingsPath, JSON.stringify(settings, null, 2));
36
+ console.log(chalk.green(`✓ Successfully configured GitHub Copilot MCP in ${vscodeSettingsPath}`));
37
+ }
38
+ else if (platform === 'cline') {
39
+ // Setup for Cline (Claude Dev)
40
+ let clineConfigPath = path.join(homeDir, 'AppData', 'Roaming', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json');
41
+ if (process.platform !== 'win32') {
42
+ clineConfigPath = path.join(homeDir, '.config', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json');
43
+ }
44
+ let settings = { mcpServers: {} };
45
+ if (fs.existsSync(clineConfigPath)) {
46
+ const raw = fs.readFileSync(clineConfigPath, 'utf8');
47
+ try {
48
+ settings = JSON.parse(raw);
49
+ }
50
+ catch (e) { /* ignore parse error */ }
51
+ }
52
+ else {
53
+ fs.mkdirSync(path.dirname(clineConfigPath), { recursive: true });
54
+ }
55
+ settings.mcpServers['igel-qe'] = mcpServerConfig;
56
+ fs.writeFileSync(clineConfigPath, JSON.stringify(settings, null, 2));
57
+ console.log(chalk.green(`✓ Successfully configured Cline MCP in ${clineConfigPath}`));
58
+ }
59
+ else {
60
+ console.log(chalk.yellow(`Platform '${platform}' is not currently supported for auto-setup.`));
61
+ }
62
+ }
63
+ catch (err) {
64
+ console.error(chalk.red(`Failed to setup ${platform}: ${err.message}`));
65
+ }
66
+ }
@@ -1,6 +1,7 @@
1
1
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
4
+ import { spawn } from "child_process";
4
5
  import path from "path";
5
6
  import { fileURLToPath } from "url";
6
7
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -12,16 +13,42 @@ const server = new Server({
12
13
  tools: {}
13
14
  }
14
15
  });
15
- // Helper to bridge to the Python AI Platform Layer
16
16
  async function callPythonEngine(workflowName, args) {
17
17
  return new Promise((resolve, reject) => {
18
- // In a real implementation, this would call a unified Python IPC script
19
- // For demonstration, we just return a simulated response.
20
- resolve(JSON.stringify({
21
- status: "success",
22
- workflow: workflowName,
23
- message: `Dispatched to Python AI Platform: ${workflowName}`
24
- }));
18
+ // Run the Python CLI in the project root
19
+ // We assume the environment is set up and `python` refers to the venv
20
+ const projectRoot = path.join(__dirname, '..', '..', '..');
21
+ const pythonProcess = spawn("python", [
22
+ "-m", "knowledge_base.cli.workflow_cli",
23
+ "--action", workflowName,
24
+ "--args", JSON.stringify(args)
25
+ ], {
26
+ cwd: projectRoot,
27
+ env: process.env // Inherit the environment (.env vars)
28
+ });
29
+ let stdoutData = "";
30
+ let stderrData = "";
31
+ pythonProcess.stdout.on("data", (data) => {
32
+ stdoutData += data.toString();
33
+ });
34
+ pythonProcess.stderr.on("data", (data) => {
35
+ stderrData += data.toString();
36
+ console.error(`Python stderr: ${data}`); // MCP servers use stderr for logging
37
+ });
38
+ pythonProcess.on("close", (code) => {
39
+ if (code !== 0) {
40
+ // If it crashed entirely, return the stderr as a JSON error
41
+ resolve(JSON.stringify({
42
+ status: "error",
43
+ message: `Python process exited with code ${code}`,
44
+ stderr: stderrData
45
+ }));
46
+ }
47
+ else {
48
+ // Return the exact JSON stdout printed by workflow_cli.py
49
+ resolve(stdoutData.trim());
50
+ }
51
+ });
25
52
  });
26
53
  }
27
54
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
@@ -0,0 +1,93 @@
1
+ import argparse
2
+ import json
3
+ import logging
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from knowledge_base.db.client import get_conn
8
+ from knowledge_base.db.igel_schema import ensure_igel_tables
9
+ from knowledge_base.connectors.jira_connector import JiraConnector
10
+ from knowledge_base.agents.workflow_engine import WorkflowEngine
11
+
12
+ logging.basicConfig(level=logging.ERROR) # Only output strict JSON to stdout
13
+
14
+ def main():
15
+ parser = argparse.ArgumentParser(description="Workflow CLI for Node.js IPC")
16
+ parser.add_argument("--action", required=True, choices=["generate_test_cases", "sync_bitbucket"])
17
+ parser.add_argument("--args", required=True, help="JSON arguments for the action")
18
+
19
+ args = parser.parse_args()
20
+
21
+ try:
22
+ payload = json.loads(args.args)
23
+ except json.JSONDecodeError:
24
+ print(json.dumps({"status": "error", "message": "Invalid JSON args"}))
25
+ sys.exit(1)
26
+
27
+ try:
28
+ ensure_igel_tables()
29
+ except Exception as e:
30
+ print(json.dumps({"status": "error", "message": f"Database Init Failed: {e}"}))
31
+ sys.exit(1)
32
+
33
+ if args.action == "generate_test_cases":
34
+ jira_key = payload.get("jira_key")
35
+ if not jira_key:
36
+ print(json.dumps({"status": "error", "message": "Missing jira_key"}))
37
+ sys.exit(1)
38
+
39
+ try:
40
+ # 1. Fetch from Jira
41
+ connector = JiraConnector()
42
+ req = connector.get_test_requirement(jira_key)
43
+
44
+ # 2. Run Workflow
45
+ repo_path = Path.cwd()
46
+ engine = WorkflowEngine(repo_path)
47
+
48
+ req, scenarios = engine.run_requirement_flow(req)
49
+ test_cases = engine.run_testcase_flow(req, scenarios)
50
+
51
+ # Since we skip Human Review in CLI mode, we auto-approve all for scripting
52
+ for tc in test_cases:
53
+ tc.is_approved = True
54
+
55
+ output_script = repo_path / "tests" / f"test_{jira_key.replace('-', '_').lower()}.py"
56
+ ok, msg = engine.run_script_flow(req, test_cases, output_script)
57
+
58
+ if ok:
59
+ result = {
60
+ "status": "success",
61
+ "jira_key": jira_key,
62
+ "generated_cases": len(test_cases),
63
+ "script_path": str(output_script)
64
+ }
65
+ else:
66
+ result = {
67
+ "status": "error",
68
+ "message": msg
69
+ }
70
+ print(json.dumps(result))
71
+
72
+ except Exception as e:
73
+ print(json.dumps({"status": "error", "message": str(e)}))
74
+ sys.exit(1)
75
+
76
+ elif args.action == "sync_bitbucket":
77
+ commit_hash = payload.get("commit_hash")
78
+ if not commit_hash:
79
+ print(json.dumps({"status": "error", "message": "Missing commit_hash"}))
80
+ sys.exit(1)
81
+
82
+ try:
83
+ from knowledge_base.agents.regression_agent import BitbucketSyncAgent
84
+ repo_path = Path.cwd()
85
+ agent = BitbucketSyncAgent(repo_path)
86
+ agent.sync_changed_files(commit_hash)
87
+ print(json.dumps({"status": "success", "synced_commit": commit_hash}))
88
+ except Exception as e:
89
+ print(json.dumps({"status": "error", "message": str(e)}))
90
+ sys.exit(1)
91
+
92
+ if __name__ == "__main__":
93
+ main()
@@ -219,6 +219,7 @@ class Config:
219
219
  JIRA_URL: str = os.getenv("JIRA_URL", "")
220
220
  JIRA_USERNAME: str = os.getenv("JIRA_USERNAME", "")
221
221
  JIRA_API_TOKEN: str = os.getenv("JIRA_API_TOKEN", "")
222
+ JIRA_OAUTH_TOKEN: str = os.getenv("JIRA_OAUTH_TOKEN", "")
222
223
 
223
224
  CONFLUENCE_ENABLED: bool = _truthy(os.getenv("CONFLUENCE_ENABLED"), False)
224
225
  CONFLUENCE_URL: str = os.getenv("CONFLUENCE_URL", "")
@@ -45,11 +45,20 @@ class JiraConnector(BaseConnector):
45
45
  def connect(self) -> bool:
46
46
  if not self.enabled:
47
47
  return False
48
- if not cfg.JIRA_URL or not cfg.JIRA_USERNAME or not cfg.JIRA_API_TOKEN:
49
- logger.warning("JIRA credentials missing")
48
+ if not cfg.JIRA_URL:
49
+ logger.warning("JIRA_URL missing")
50
+ return False
51
+ if not cfg.JIRA_OAUTH_TOKEN and not (cfg.JIRA_USERNAME and cfg.JIRA_API_TOKEN):
52
+ logger.warning("JIRA credentials missing (no OAuth token or Basic Auth)")
50
53
  return False
51
54
  return True
52
55
 
56
+ def _get_auth_kwargs(self) -> dict:
57
+ """Return httpx kwargs for authentication (Bearer or Basic)."""
58
+ if cfg.JIRA_OAUTH_TOKEN:
59
+ return {"headers": {"Authorization": f"Bearer {cfg.JIRA_OAUTH_TOKEN}"}}
60
+ return {"auth": (cfg.JIRA_USERNAME, cfg.JIRA_API_TOKEN)}
61
+
53
62
  def search(self, query: str, limit: int = 5, timeout: float | None = None) -> list[str]:
54
63
  if not self.connect():
55
64
  return []
@@ -60,7 +69,7 @@ class JiraConnector(BaseConnector):
60
69
  resp = client.get(
61
70
  f"{cfg.JIRA_URL.rstrip('/')}/rest/api/2/search",
62
71
  params={"jql": jql, "maxResults": limit, "fields": "summary"},
63
- auth=(cfg.JIRA_USERNAME, cfg.JIRA_API_TOKEN),
72
+ **self._get_auth_kwargs()
64
73
  )
65
74
  resp.raise_for_status()
66
75
  issues = resp.json().get("issues") or []
@@ -84,7 +93,7 @@ class JiraConnector(BaseConnector):
84
93
  resp = client.get(
85
94
  f"{cfg.JIRA_URL.rstrip('/')}/rest/api/2/search",
86
95
  params={"jql": jql, "maxResults": 50, "fields": "summary,description,status,components,fixVersions,issuetype"},
87
- auth=(cfg.JIRA_USERNAME, cfg.JIRA_API_TOKEN),
96
+ **self._get_auth_kwargs()
88
97
  )
89
98
  resp.raise_for_status()
90
99
  for issue in resp.json().get("issues") or []:
@@ -98,6 +107,78 @@ class JiraConnector(BaseConnector):
98
107
  "issue_type": (fields.get("issuetype") or {}).get("name"),
99
108
  "source_type": "jira",
100
109
  })
110
+ return items
111
+ except Exception as exc:
112
+ logger.warning("JIRA sync failed: %s", exc)
113
+ return []
114
+
115
+ def get_test_requirement(self, jira_key: str) -> 'knowledge_base.models.test_requirement.TestRequirement':
116
+ """Fetch a specific Jira issue and parse its manual Xray test steps."""
117
+ if not self.connect():
118
+ raise Exception("Jira is not configured or disabled.")
119
+
120
+ # We import here to avoid circular imports during init
121
+ from knowledge_base.models.test_requirement import TestRequirement, TestStep
122
+
123
+ try:
124
+ with httpx.Client(timeout=30.0) as client:
125
+ # 1. Fetch the Issue
126
+ resp = client.get(
127
+ f"{cfg.JIRA_URL.rstrip('/')}/rest/api/2/issue/{jira_key}",
128
+ **self._get_auth_kwargs()
129
+ )
130
+ resp.raise_for_status()
131
+ issue = resp.json()
132
+ fields = issue.get("fields", {})
133
+
134
+ summary = fields.get("summary", jira_key)
135
+ description = _jira_text(fields.get("description"))
136
+ components = [c.get("name") for c in fields.get("components", []) if c.get("name")]
137
+ labels = fields.get("labels", [])
138
+
139
+ # 2. Extract Test Steps
140
+ # Depending on the Xray configuration, steps might be in a custom field,
141
+ # or require a separate API call to `/rest/raven/1.0/api/test/{issueId}/step`.
142
+ # We will attempt to fetch via Raven API for standard Xray.
143
+ steps = []
144
+ try:
145
+ step_resp = client.get(
146
+ f"{cfg.JIRA_URL.rstrip('/')}/rest/raven/1.0/api/test/{jira_key}/step",
147
+ **self._get_auth_kwargs()
148
+ )
149
+ if step_resp.status_code == 200:
150
+ xray_steps = step_resp.json()
151
+ for i, s in enumerate(xray_steps):
152
+ # The Xray REST API returns raw HTML or text for action/data/result
153
+ steps.append(TestStep(
154
+ step_number=i + 1,
155
+ action=_jira_text(s.get("action")),
156
+ test_data=_jira_text(s.get("data")),
157
+ expected_result=_jira_text(s.get("result"))
158
+ ))
159
+ except Exception as e:
160
+ logger.warning(f"Failed to fetch Xray steps for {jira_key}: {e}")
161
+
162
+ # If no Xray steps found, provide a fallback stub step for the AI to process based on description
163
+ if not steps:
164
+ steps.append(TestStep(
165
+ step_number=1,
166
+ action="Analyze requirement description",
167
+ expected_result="System behaves as specified in description"
168
+ ))
169
+
170
+ return TestRequirement(
171
+ jira_key=jira_key,
172
+ summary=summary,
173
+ description=description,
174
+ components=components,
175
+ labels=labels,
176
+ steps=steps
177
+ )
178
+
179
+ except Exception as exc:
180
+ logger.error(f"Failed to get test requirement {jira_key}: {exc}")
181
+ raise
101
182
  except Exception as exc:
102
183
  logger.warning("JIRA sync failed: %s", exc)
103
184
  raise
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "igel-qe-core",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "IGEL QE Developer Experience Layer (CLI & MCP)",
5
5
  "type": "module",
6
6
  "publishConfig": {