igel-qe-core 1.0.14 → 1.0.18

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
@@ -2,7 +2,7 @@
2
2
  import { Command } from "commander";
3
3
  import chalk from "chalk";
4
4
  import { spawn, spawnSync, execSync } from "child_process";
5
- import { existsSync, mkdirSync, writeFileSync, readdirSync, statSync } from "fs";
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSync } from "fs";
6
6
  import { join, dirname } from "path";
7
7
  import { fileURLToPath } from "url";
8
8
  import { setupPlatform } from "./platform.js";
@@ -229,49 +229,79 @@ function isCopilotCliAvailable() {
229
229
  });
230
230
  return check.status === 0 && !check.error;
231
231
  }
232
- function isCopilotAuthenticated(workspaceRoot) {
233
- // A strict timeout prevents the init command from hanging forever when
234
- // Copilot waits for interactive input or a network response.
235
- const probe = spawnSync("copilot", ["-C", workspaceRoot, "-p", "Reply with exactly OK.", "--allow-all-tools", "--no-color", "-s"], {
232
+ function isCopilotAuthenticated() {
233
+ // Use a lightweight auth-status check instead of spawning a full agent session.
234
+ // "copilot auth status" exits 0 when authenticated, non-zero otherwise.
235
+ // Fallback: if the sub-command doesn't exist, treat as unauthenticated.
236
+ const probe = spawnSync("copilot", ["auth", "status"], {
236
237
  encoding: "utf-8",
237
238
  shell: process.platform === "win32",
238
- timeout: 30_000, // 30 s max — return false if Copilot doesn't answer
239
- input: "", // close stdin immediately so it doesn't wait for user input
239
+ timeout: 10_000, // 10 s max — simple status call should be instant
240
240
  });
241
241
  return probe.status === 0 && !probe.error;
242
242
  }
243
243
  function ensureCopilotLogin(workspaceRoot) {
244
244
  if (!isCopilotCliAvailable())
245
245
  return false;
246
- if (isCopilotAuthenticated(workspaceRoot))
246
+ if (isCopilotAuthenticated())
247
247
  return true;
248
248
  console.log(chalk.yellow(" ⚠ GitHub Copilot CLI is not authenticated."));
249
249
  console.log(chalk.yellow(" → Please complete Copilot login to continue context enrichment."));
250
250
  const login = spawnSync("copilot", ["login"], { stdio: "inherit", encoding: "utf-8", shell: process.platform === "win32" });
251
251
  if (login.status !== 0)
252
252
  return false;
253
- return isCopilotAuthenticated(workspaceRoot);
253
+ return isCopilotAuthenticated();
254
254
  }
255
+ // Maximum number of context.md files to enrich per init run.
256
+ // Prevents extremely long waits on repos with many folders.
257
+ const MAX_ENRICHMENT_FILES = 15;
255
258
  function enrichWithCopilotCli(workspaceRoot, contextFiles) {
256
259
  let updated = 0;
257
260
  let failed = 0;
258
- for (const absPath of contextFiles) {
259
- const relPath = absPath.replace(`${workspaceRoot}/`, "");
261
+ // Limit the number of files processed in one init run.
262
+ const filesToProcess = contextFiles.slice(0, MAX_ENRICHMENT_FILES);
263
+ const total = filesToProcess.length;
264
+ for (let i = 0; i < filesToProcess.length; i++) {
265
+ const absPath = filesToProcess[i];
266
+ // Normalise to forward slashes so the prompt is readable on all platforms.
267
+ const relPath = absPath
268
+ .replace(`${workspaceRoot}\\`, "")
269
+ .replace(`${workspaceRoot}/`, "")
270
+ .replace(/\\/g, "/");
271
+ // Show per-file progress so the user knows work is happening.
272
+ process.stdout.write(` [${i + 1}/${total}] Enriching ${relPath}…`);
273
+ // Read the current (template) content so Copilot can see the structure.
274
+ let currentContent = "";
275
+ try {
276
+ currentContent = readFileSync(absPath, "utf-8");
277
+ }
278
+ catch {
279
+ // If we can't read, skip this file.
280
+ process.stdout.write(" ✗ (unreadable)\n");
281
+ failed += 1;
282
+ continue;
283
+ }
284
+ // Build a prompt that asks Copilot to RETURN the improved markdown as plain
285
+ // text to stdout. We write the file ourselves so we know it was updated.
260
286
  const prompt = [
261
- `Update the markdown file at path '${relPath}' in place.`,
262
- "Goal: make it both human-understandable and LLM-ready for automation framework reasoning.",
263
- "Requirements:",
264
- "- Keep heading style consistent.",
265
- "- Include: purpose, architecture role, key assets, dependencies, conventions, risks, and update guidance.",
266
- "- Be concrete and repository-aware.",
267
- "- Do not modify any file other than the target file.",
268
- "- Preserve valid markdown.",
287
+ `You are enriching a context.md file for an automation test framework.`,
288
+ `The file is located at: ${relPath} (relative to the workspace root).`,
289
+ ``,
290
+ `Current content:`,
291
+ `\`\`\`markdown`,
292
+ currentContent,
293
+ `\`\`\``,
294
+ ``,
295
+ `Rewrite the content above to be concrete, human-understandable, and LLM-ready.`,
296
+ `Requirements:`,
297
+ `- Keep the same heading structure (# Context, ## Purpose, ## Key Assets, ## Conventions, ## Risks and Dependencies).`,
298
+ `- Replace placeholder text with specific details inferred from the folder path and framework context.`,
299
+ `- Be concise but informative. Each section: 2–5 bullet points or short sentences.`,
300
+ `- Output ONLY the improved markdown. Do NOT add commentary, code fences, or extra text around it.`,
269
301
  ].join("\n");
270
302
  const result = spawnSync("copilot", [
271
303
  "-C", workspaceRoot,
272
304
  "-p", prompt,
273
- "--allow-all-tools",
274
- "--allow-all-paths",
275
305
  "--no-color",
276
306
  "-s",
277
307
  ], {
@@ -279,11 +309,30 @@ function enrichWithCopilotCli(workspaceRoot, contextFiles) {
279
309
  shell: process.platform === "win32",
280
310
  timeout: 120_000, // 2 min per file — prevents indefinite hangs
281
311
  input: "", // close stdin immediately
312
+ maxBuffer: 4 * 1024 * 1024, // 4 MB stdout buffer
282
313
  });
283
- if (result.status === 0) {
284
- updated += 1;
314
+ if (result.status === 0 && !result.error) {
315
+ const enriched = (result.stdout || "").trim();
316
+ if (enriched.length > 50) {
317
+ // Only write back if Copilot returned meaningful content (>50 chars).
318
+ try {
319
+ writeFileSync(absPath, enriched, "utf-8");
320
+ process.stdout.write(" ✓\n");
321
+ updated += 1;
322
+ }
323
+ catch {
324
+ process.stdout.write(" ✗ (write failed)\n");
325
+ failed += 1;
326
+ }
327
+ }
328
+ else {
329
+ // Copilot ran but returned empty/trivial output.
330
+ process.stdout.write(" ✗ (empty response)\n");
331
+ failed += 1;
332
+ }
285
333
  }
286
334
  else {
335
+ process.stdout.write(" ✗\n");
287
336
  failed += 1;
288
337
  }
289
338
  }
@@ -409,7 +458,9 @@ program
409
458
  // Wrap both the python path and requirements path in quotes so
410
459
  // Windows paths that contain spaces (e.g. "IGEL Automation July 2026")
411
460
  // are treated as a single argument by the shell.
412
- execSync(`"${pyBin}" -m pip install -q -r "${reqPath}"`, {
461
+ // --prefer-binary: use pre-built wheels instead of compiling from source
462
+ // (avoids Rust/Cargo requirement for packages like tokenizers, crawl4ai)
463
+ execSync(`"${pyBin}" -m pip install -q --prefer-binary -r "${reqPath}"`, {
413
464
  cwd: workspaceRoot, stdio: "inherit",
414
465
  });
415
466
  console.log(chalk.green(" ✓ Dependencies installed"));
@@ -436,29 +487,41 @@ program
436
487
  else {
437
488
  console.log(chalk.green(" ✓ Framework context files already up to date"));
438
489
  }
439
- // Step 2d — Copilot CLI enrichment pass (asks login if needed), then DB sync.
490
+ // Step 3d — Copilot CLI enrichment pass.
491
+ // Only run when --with-copilot / --with-all is explicitly requested.
492
+ // Skipped silently on plain `igel-qe init` to avoid unexpected long waits.
493
+ const runEnrichment = options.withCopilot || options.withAll;
440
494
  console.log(chalk.yellow("3d. Enriching context via GitHub Copilot CLI…"));
441
- try {
442
- const allContextFiles = listFrameworkContextFiles(workspaceRoot);
443
- if (allContextFiles.length === 0) {
444
- console.log(chalk.yellow(" ⚠ No context.md files found for Copilot enrichment"));
445
- }
446
- else if (!isCopilotCliAvailable()) {
447
- console.log(chalk.yellow(" ⚠ Copilot CLI not installed; skipping Copilot enrichment"));
448
- }
449
- else if (!ensureCopilotLogin(workspaceRoot)) {
450
- console.log(chalk.yellow(" ⚠ Copilot login was not completed; skipping Copilot enrichment"));
451
- }
452
- else {
453
- const run = enrichWithCopilotCli(workspaceRoot, allContextFiles);
454
- console.log(chalk.green(` Copilot updated ${run.updated} context.md file(s)`));
455
- if (run.failed > 0) {
456
- console.log(chalk.yellow(` ⚠ Copilot failed to update ${run.failed} context.md file(s)`));
495
+ if (!runEnrichment) {
496
+ console.log(chalk.gray(" - Skipped (pass --with-copilot to enable Copilot context enrichment)"));
497
+ }
498
+ else {
499
+ try {
500
+ const allContextFiles = listFrameworkContextFiles(workspaceRoot);
501
+ if (allContextFiles.length === 0) {
502
+ console.log(chalk.yellow(" ⚠ No context.md files found for Copilot enrichment"));
503
+ }
504
+ else if (!isCopilotCliAvailable()) {
505
+ console.log(chalk.yellow(" ⚠ Copilot CLI not installed; skipping Copilot enrichment"));
506
+ }
507
+ else if (!ensureCopilotLogin(workspaceRoot)) {
508
+ console.log(chalk.yellow(" Copilot login was not completed; skipping Copilot enrichment"));
509
+ }
510
+ else {
511
+ const cap = Math.min(allContextFiles.length, MAX_ENRICHMENT_FILES);
512
+ if (allContextFiles.length > MAX_ENRICHMENT_FILES) {
513
+ console.log(chalk.gray(` - ${allContextFiles.length} context files found; enriching first ${MAX_ENRICHMENT_FILES} (re-run to continue)`));
514
+ }
515
+ const run = enrichWithCopilotCli(workspaceRoot, allContextFiles);
516
+ console.log(chalk.green(` ✓ Copilot updated ${run.updated}/${cap} context.md file(s)`));
517
+ if (run.failed > 0) {
518
+ console.log(chalk.yellow(` ⚠ Copilot failed to update ${run.failed} context.md file(s)`));
519
+ }
457
520
  }
458
521
  }
459
- }
460
- catch (e) {
461
- console.log(chalk.yellow(` ⚠ Copilot context enrichment skipped: ${e instanceof Error ? e.message : String(e)}`));
522
+ catch (e) {
523
+ console.log(chalk.yellow(` ⚠ Copilot context enrichment skipped: ${e instanceof Error ? e.message : String(e)}`));
524
+ }
462
525
  }
463
526
  // Step 3 — write MCP config (default: Copilot when no explicit platform flags)
464
527
  console.log(chalk.yellow("4. Configuring MCP server…"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "igel-qe-core",
3
- "version": "1.0.14",
3
+ "version": "1.0.18",
4
4
  "description": "IGEL QE Developer Experience Layer (CLI & MCP)",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -16,6 +16,7 @@
16
16
  },
17
17
  "scripts": {
18
18
  "build": "tsc",
19
+ "prepublishOnly": "tsc",
19
20
  "start:mcp": "node ./dist/mcp/server.js"
20
21
  },
21
22
  "dependencies": {
package/src/cli/index.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  import { Command } from "commander";
4
4
  import chalk from "chalk";
5
5
  import { spawn, spawnSync, execSync } from "child_process";
6
- import { existsSync, mkdirSync, writeFileSync, readdirSync, statSync } from "fs";
6
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSync } from "fs";
7
7
  import { join, dirname } from "path";
8
8
  import { fileURLToPath } from "url";
9
9
  import { setupPlatform } from "./platform.js";
@@ -241,17 +241,17 @@ function isCopilotCliAvailable(): boolean {
241
241
  return check.status === 0 && !check.error;
242
242
  }
243
243
 
244
- function isCopilotAuthenticated(workspaceRoot: string): boolean {
245
- // A strict timeout prevents the init command from hanging forever when
246
- // Copilot waits for interactive input or a network response.
244
+ function isCopilotAuthenticated(): boolean {
245
+ // Use a lightweight auth-status check instead of spawning a full agent session.
246
+ // "copilot auth status" exits 0 when authenticated, non-zero otherwise.
247
+ // Fallback: if the sub-command doesn't exist, treat as unauthenticated.
247
248
  const probe = spawnSync(
248
249
  "copilot",
249
- ["-C", workspaceRoot, "-p", "Reply with exactly OK.", "--allow-all-tools", "--no-color", "-s"],
250
+ ["auth", "status"],
250
251
  {
251
252
  encoding: "utf-8",
252
253
  shell: process.platform === "win32",
253
- timeout: 30_000, // 30 s max — return false if Copilot doesn't answer
254
- input: "", // close stdin immediately so it doesn't wait for user input
254
+ timeout: 10_000, // 10 s max — simple status call should be instant
255
255
  },
256
256
  );
257
257
  return probe.status === 0 && !probe.error;
@@ -259,31 +259,70 @@ function isCopilotAuthenticated(workspaceRoot: string): boolean {
259
259
 
260
260
  function ensureCopilotLogin(workspaceRoot: string): boolean {
261
261
  if (!isCopilotCliAvailable()) return false;
262
- if (isCopilotAuthenticated(workspaceRoot)) return true;
262
+ if (isCopilotAuthenticated()) return true;
263
263
 
264
264
  console.log(chalk.yellow(" ⚠ GitHub Copilot CLI is not authenticated."));
265
265
  console.log(chalk.yellow(" → Please complete Copilot login to continue context enrichment."));
266
266
 
267
267
  const login = spawnSync("copilot", ["login"], { stdio: "inherit", encoding: "utf-8", shell: process.platform === "win32" });
268
268
  if (login.status !== 0) return false;
269
- return isCopilotAuthenticated(workspaceRoot);
269
+ return isCopilotAuthenticated();
270
270
  }
271
271
 
272
- function enrichWithCopilotCli(workspaceRoot: string, contextFiles: string[]): { updated: number; failed: number } {
272
+ // Maximum number of context.md files to enrich per init run.
273
+ // Prevents extremely long waits on repos with many folders.
274
+ const MAX_ENRICHMENT_FILES = 15;
275
+
276
+ function enrichWithCopilotCli(
277
+ workspaceRoot: string,
278
+ contextFiles: string[],
279
+ ): { updated: number; failed: number } {
273
280
  let updated = 0;
274
281
  let failed = 0;
275
282
 
276
- for (const absPath of contextFiles) {
277
- const relPath = absPath.replace(`${workspaceRoot}/`, "");
283
+ // Limit the number of files processed in one init run.
284
+ const filesToProcess = contextFiles.slice(0, MAX_ENRICHMENT_FILES);
285
+ const total = filesToProcess.length;
286
+
287
+ for (let i = 0; i < filesToProcess.length; i++) {
288
+ const absPath = filesToProcess[i];
289
+ // Normalise to forward slashes so the prompt is readable on all platforms.
290
+ const relPath = absPath
291
+ .replace(`${workspaceRoot}\\`, "")
292
+ .replace(`${workspaceRoot}/`, "")
293
+ .replace(/\\/g, "/");
294
+
295
+ // Show per-file progress so the user knows work is happening.
296
+ process.stdout.write(` [${i + 1}/${total}] Enriching ${relPath}…`);
297
+
298
+ // Read the current (template) content so Copilot can see the structure.
299
+ let currentContent = "";
300
+ try {
301
+ currentContent = readFileSync(absPath, "utf-8");
302
+ } catch {
303
+ // If we can't read, skip this file.
304
+ process.stdout.write(" ✗ (unreadable)\n");
305
+ failed += 1;
306
+ continue;
307
+ }
308
+
309
+ // Build a prompt that asks Copilot to RETURN the improved markdown as plain
310
+ // text to stdout. We write the file ourselves so we know it was updated.
278
311
  const prompt = [
279
- `Update the markdown file at path '${relPath}' in place.`,
280
- "Goal: make it both human-understandable and LLM-ready for automation framework reasoning.",
281
- "Requirements:",
282
- "- Keep heading style consistent.",
283
- "- Include: purpose, architecture role, key assets, dependencies, conventions, risks, and update guidance.",
284
- "- Be concrete and repository-aware.",
285
- "- Do not modify any file other than the target file.",
286
- "- Preserve valid markdown.",
312
+ `You are enriching a context.md file for an automation test framework.`,
313
+ `The file is located at: ${relPath} (relative to the workspace root).`,
314
+ ``,
315
+ `Current content:`,
316
+ `\`\`\`markdown`,
317
+ currentContent,
318
+ `\`\`\``,
319
+ ``,
320
+ `Rewrite the content above to be concrete, human-understandable, and LLM-ready.`,
321
+ `Requirements:`,
322
+ `- Keep the same heading structure (# Context, ## Purpose, ## Key Assets, ## Conventions, ## Risks and Dependencies).`,
323
+ `- Replace placeholder text with specific details inferred from the folder path and framework context.`,
324
+ `- Be concise but informative. Each section: 2–5 bullet points or short sentences.`,
325
+ `- Output ONLY the improved markdown. Do NOT add commentary, code fences, or extra text around it.`,
287
326
  ].join("\n");
288
327
 
289
328
  const result = spawnSync(
@@ -291,8 +330,6 @@ function enrichWithCopilotCli(workspaceRoot: string, contextFiles: string[]): {
291
330
  [
292
331
  "-C", workspaceRoot,
293
332
  "-p", prompt,
294
- "--allow-all-tools",
295
- "--allow-all-paths",
296
333
  "--no-color",
297
334
  "-s",
298
335
  ],
@@ -301,12 +338,29 @@ function enrichWithCopilotCli(workspaceRoot: string, contextFiles: string[]): {
301
338
  shell: process.platform === "win32",
302
339
  timeout: 120_000, // 2 min per file — prevents indefinite hangs
303
340
  input: "", // close stdin immediately
341
+ maxBuffer: 4 * 1024 * 1024, // 4 MB stdout buffer
304
342
  },
305
343
  );
306
344
 
307
- if (result.status === 0) {
308
- updated += 1;
345
+ if (result.status === 0 && !result.error) {
346
+ const enriched = (result.stdout || "").trim();
347
+ if (enriched.length > 50) {
348
+ // Only write back if Copilot returned meaningful content (>50 chars).
349
+ try {
350
+ writeFileSync(absPath, enriched, "utf-8");
351
+ process.stdout.write(" ✓\n");
352
+ updated += 1;
353
+ } catch {
354
+ process.stdout.write(" ✗ (write failed)\n");
355
+ failed += 1;
356
+ }
357
+ } else {
358
+ // Copilot ran but returned empty/trivial output.
359
+ process.stdout.write(" ✗ (empty response)\n");
360
+ failed += 1;
361
+ }
309
362
  } else {
363
+ process.stdout.write(" ✗\n");
310
364
  failed += 1;
311
365
  }
312
366
  }
@@ -439,7 +493,9 @@ program
439
493
  // Wrap both the python path and requirements path in quotes so
440
494
  // Windows paths that contain spaces (e.g. "IGEL Automation July 2026")
441
495
  // are treated as a single argument by the shell.
442
- execSync(`"${pyBin}" -m pip install -q -r "${reqPath}"`, {
496
+ // --prefer-binary: use pre-built wheels instead of compiling from source
497
+ // (avoids Rust/Cargo requirement for packages like tokenizers, crawl4ai)
498
+ execSync(`"${pyBin}" -m pip install -q --prefer-binary -r "${reqPath}"`, {
443
499
  cwd: workspaceRoot, stdio: "inherit",
444
500
  });
445
501
  console.log(chalk.green(" ✓ Dependencies installed"));
@@ -467,25 +523,36 @@ program
467
523
  }
468
524
 
469
525
 
470
- // Step 2d — Copilot CLI enrichment pass (asks login if needed), then DB sync.
526
+ // Step 3d — Copilot CLI enrichment pass.
527
+ // Only run when --with-copilot / --with-all is explicitly requested.
528
+ // Skipped silently on plain `igel-qe init` to avoid unexpected long waits.
529
+ const runEnrichment = options.withCopilot || options.withAll;
471
530
  console.log(chalk.yellow("3d. Enriching context via GitHub Copilot CLI…"));
472
- try {
473
- const allContextFiles = listFrameworkContextFiles(workspaceRoot);
474
- if (allContextFiles.length === 0) {
475
- console.log(chalk.yellow(" ⚠ No context.md files found for Copilot enrichment"));
476
- } else if (!isCopilotCliAvailable()) {
477
- console.log(chalk.yellow(" ⚠ Copilot CLI not installed; skipping Copilot enrichment"));
478
- } else if (!ensureCopilotLogin(workspaceRoot)) {
479
- console.log(chalk.yellow(" ⚠ Copilot login was not completed; skipping Copilot enrichment"));
480
- } else {
481
- const run = enrichWithCopilotCli(workspaceRoot, allContextFiles);
482
- console.log(chalk.green(` Copilot updated ${run.updated} context.md file(s)`));
483
- if (run.failed > 0) {
484
- console.log(chalk.yellow(` ⚠ Copilot failed to update ${run.failed} context.md file(s)`));
531
+ if (!runEnrichment) {
532
+ console.log(chalk.gray(" - Skipped (pass --with-copilot to enable Copilot context enrichment)"));
533
+ } else {
534
+ try {
535
+ const allContextFiles = listFrameworkContextFiles(workspaceRoot);
536
+ if (allContextFiles.length === 0) {
537
+ console.log(chalk.yellow(" ⚠ No context.md files found for Copilot enrichment"));
538
+ } else if (!isCopilotCliAvailable()) {
539
+ console.log(chalk.yellow(" ⚠ Copilot CLI not installed; skipping Copilot enrichment"));
540
+ } else if (!ensureCopilotLogin(workspaceRoot)) {
541
+ console.log(chalk.yellow(" Copilot login was not completed; skipping Copilot enrichment"));
542
+ } else {
543
+ const cap = Math.min(allContextFiles.length, MAX_ENRICHMENT_FILES);
544
+ if (allContextFiles.length > MAX_ENRICHMENT_FILES) {
545
+ console.log(chalk.gray(` - ${allContextFiles.length} context files found; enriching first ${MAX_ENRICHMENT_FILES} (re-run to continue)`));
546
+ }
547
+ const run = enrichWithCopilotCli(workspaceRoot, allContextFiles);
548
+ console.log(chalk.green(` ✓ Copilot updated ${run.updated}/${cap} context.md file(s)`));
549
+ if (run.failed > 0) {
550
+ console.log(chalk.yellow(` ⚠ Copilot failed to update ${run.failed} context.md file(s)`));
551
+ }
485
552
  }
553
+ } catch (e: unknown) {
554
+ console.log(chalk.yellow(` ⚠ Copilot context enrichment skipped: ${e instanceof Error ? e.message : String(e)}`));
486
555
  }
487
- } catch (e: unknown) {
488
- console.log(chalk.yellow(` ⚠ Copilot context enrichment skipped: ${e instanceof Error ? e.message : String(e)}`));
489
556
  }
490
557
 
491
558
  // Step 3 — write MCP config (default: Copilot when no explicit platform flags)
@@ -27,18 +27,32 @@ _PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
27
27
  if str(_PROJECT_ROOT) not in sys.path:
28
28
  sys.path.insert(0, str(_PROJECT_ROOT))
29
29
 
30
- from src.db.igel_schema import ensure_igel_tables
31
- from src.connectors.jira_connector import JiraConnector
32
- from src.agents.workflow_engine import WorkflowEngine
33
- from src.intelligence.diff_analyzer import analyze_diff_scope
34
- from src.intelligence.routing_engine import RouteInputs, compute_weighted_score, classify_route
35
- from src.agents.framework_analyzer_agent import FrameworkAnalyzerAgent
36
- from src.agents.orchestrator.orchestrator import Orchestrator
37
- from src.agents.internal.approval_manager import ApprovalError
38
- from src.config import cfg
39
- from src.project.context_projection_builder import ContextProjectionBuilder
40
- from src.project.context_requirement_sync import sync_requirement_context
41
- from src.project.context_enricher import enrich_context_files
30
+ try:
31
+ from src.db.igel_schema import ensure_igel_tables
32
+ from src.connectors.jira_connector import JiraConnector
33
+ from src.agents.workflow_engine import WorkflowEngine
34
+ from src.intelligence.diff_analyzer import analyze_diff_scope
35
+ from src.intelligence.routing_engine import RouteInputs, compute_weighted_score, classify_route
36
+ from src.agents.framework_analyzer_agent import FrameworkAnalyzerAgent
37
+ from src.agents.orchestrator.orchestrator import Orchestrator
38
+ from src.agents.internal.approval_manager import ApprovalError
39
+ from src.config import cfg
40
+ from src.project.context_projection_builder import ContextProjectionBuilder
41
+ from src.project.context_requirement_sync import sync_requirement_context
42
+ from src.project.context_enricher import enrich_context_files
43
+ _DEPS_AVAILABLE = True
44
+ _DEPS_ERROR: str | None = None
45
+ except ImportError as _ie:
46
+ _DEPS_AVAILABLE = False
47
+ _DEPS_ERROR = (
48
+ f"Missing Python dependency: {_ie}.\n"
49
+ "Run: igel-qe init --with-copilot to install required packages."
50
+ )
51
+ # Define stubs so references below don't cause NameErrors
52
+ ensure_igel_tables = JiraConnector = WorkflowEngine = None # type: ignore[assignment,misc]
53
+ analyze_diff_scope = RouteInputs = compute_weighted_score = classify_route = None # type: ignore[assignment]
54
+ FrameworkAnalyzerAgent = Orchestrator = ApprovalError = cfg = None # type: ignore[assignment,misc]
55
+ ContextProjectionBuilder = sync_requirement_context = enrich_context_files = None # type: ignore[assignment]
42
56
 
43
57
  logging.basicConfig(level=logging.ERROR) # Only output strict JSON to stdout
44
58
 
@@ -99,6 +113,28 @@ def main():
99
113
  except json.JSONDecodeError:
100
114
  _err("Invalid JSON args")
101
115
 
116
+ # ── Dependency guard ───────────────────────────────────────────────────────
117
+ # If core Python packages (e.g. psycopg2) are missing, return a structured
118
+ # warning for init_check so the CLI can surface a helpful message rather
119
+ # than crashing with a raw traceback.
120
+ if not _DEPS_AVAILABLE:
121
+ if args.action in ("init_project", "init_check"):
122
+ print(json.dumps({
123
+ "status": "success",
124
+ "ready": False,
125
+ "required_failed": ["python_dependencies"],
126
+ "warnings": [],
127
+ "checks": {
128
+ "python_dependencies": {
129
+ "status": "error",
130
+ "message": _DEPS_ERROR or "Required Python packages not installed. Run: igel-qe init --with-copilot",
131
+ }
132
+ },
133
+ }))
134
+ sys.exit(0)
135
+ else:
136
+ _err(_DEPS_ERROR or "Required Python packages not installed. Run: igel-qe init --with-copilot")
137
+
102
138
  # ── DB init ────────────────────────────────────────────────────────────────
103
139
  if args.action not in ("submit_feedback",):
104
140
  try:
package/src/db/client.py CHANGED
@@ -3,15 +3,31 @@ from __future__ import annotations
3
3
  import logging
4
4
  import os
5
5
 
6
- import psycopg2
7
- import psycopg2.pool
6
+ try:
7
+ import psycopg2
8
+ import psycopg2.pool
9
+ _PSYCOPG2_AVAILABLE = True
10
+ except ImportError:
11
+ psycopg2 = None # type: ignore[assignment]
12
+ _PSYCOPG2_AVAILABLE = False
13
+
8
14
  from contextlib import contextmanager
9
15
 
10
16
  from src.config import cfg
11
17
 
12
18
  logger = logging.getLogger(__name__)
13
19
 
14
- _pool: psycopg2.pool.ThreadedConnectionPool | None = None
20
+ _pool: "psycopg2.pool.ThreadedConnectionPool | None" = None
21
+
22
+
23
+ def _require_psycopg2() -> None:
24
+ """Raise a clear error if psycopg2 is not installed."""
25
+ if not _PSYCOPG2_AVAILABLE:
26
+ raise ImportError(
27
+ "psycopg2 is not installed. Run: pip install psycopg2-binary\n"
28
+ "or re-run: igel-qe init --with-copilot (which will retry the install)"
29
+ )
30
+
15
31
 
16
32
 
17
33
  def _pool_kwargs() -> dict:
@@ -32,6 +48,7 @@ def _pool_kwargs() -> dict:
32
48
 
33
49
 
34
50
  def init_pool(minconn: int = 2, maxconn: int = 10) -> None:
51
+ _require_psycopg2()
35
52
  global _pool
36
53
  if _pool is not None:
37
54
  return
@@ -49,7 +49,8 @@ beautifulsoup4>=4.12.0
49
49
  lxml>=5.0.0
50
50
  duckduckgo-search>=6.0.0,<7.0.0; python_version < "3.9"
51
51
  firecrawl-py>=1.0.0
52
- playwright>=1.40.0,<1.49.0
52
+ # playwright: no upper bound — crawl4ai>=0.4 requires playwright>=1.49
53
+ playwright>=1.49.0
53
54
  crawl4ai>=0.4.0,<0.5.0; python_version >= "3.9"
54
55
 
55
56
  # REST API server (always-running service)