boomtick-cli 0.2.1__py3-none-any.whl
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.
- boomtick_cli-0.2.1.dist-info/METADATA +20 -0
- boomtick_cli-0.2.1.dist-info/RECORD +103 -0
- boomtick_cli-0.2.1.dist-info/WHEEL +5 -0
- boomtick_cli-0.2.1.dist-info/entry_points.txt +3 -0
- boomtick_cli-0.2.1.dist-info/top_level.txt +1 -0
- dev_tools/__init__.py +1 -0
- dev_tools/cli-schema.json +2028 -0
- dev_tools/cli.py +1542 -0
- dev_tools/config.py +235 -0
- dev_tools/constants.py +14 -0
- dev_tools/daemon.py +146 -0
- dev_tools/dist/config.js +108 -0
- dev_tools/dist/index.js +10 -0
- dev_tools/dist/lib/error_utils.js +8 -0
- dev_tools/dist/lib/git.js +31 -0
- dev_tools/dist/lib/result.js +21 -0
- dev_tools/dist/lib/shell.js +91 -0
- dev_tools/dist/lib/shell.test.js +10 -0
- dev_tools/dist/lib/td-cli.js +27 -0
- dev_tools/dist/lib/test-utils.js +20 -0
- dev_tools/dist/mcp/definitions.js +242 -0
- dev_tools/dist/mcp/server.js +317 -0
- dev_tools/dist/mcp/tools.js +18 -0
- dev_tools/dist/tools/contract.js +2069 -0
- dev_tools/dist/tools/ddgs.search.js +50 -0
- dev_tools/dist/tools/ddgs.search.test.js +67 -0
- dev_tools/dist/tools/ddgs_search.py +23 -0
- dev_tools/dist/tools/github.checkout_branch.js +20 -0
- dev_tools/dist/tools/github.comment_triage_summary.js +25 -0
- dev_tools/dist/tools/github.create_issue.js +28 -0
- dev_tools/dist/tools/github.create_issue.test.js +54 -0
- dev_tools/dist/tools/github.create_pull_request.js +44 -0
- dev_tools/dist/tools/github.get_merge_conflict_files.js +26 -0
- dev_tools/dist/tools/github.get_pr.js +18 -0
- dev_tools/dist/tools/github.get_pr_diff.js +24 -0
- dev_tools/dist/tools/github.issue_comment.js +24 -0
- dev_tools/dist/tools/github.issue_update.js +29 -0
- dev_tools/dist/tools/github.issue_view.js +28 -0
- dev_tools/dist/tools/github.open_replacement_pr.js +41 -0
- dev_tools/dist/tools/github.search_open_prs.js +31 -0
- dev_tools/dist/tools/github.search_open_prs.test.js +51 -0
- dev_tools/dist/tools/jules/cancel-session.js +20 -0
- dev_tools/dist/tools/jules/create-session.js +20 -0
- dev_tools/dist/tools/jules/create-session.test.js +99 -0
- dev_tools/dist/tools/jules/get-messages.js +19 -0
- dev_tools/dist/tools/jules/get-messages.test.js +45 -0
- dev_tools/dist/tools/jules/get-pr.js +14 -0
- dev_tools/dist/tools/jules/get-pr.test.js +37 -0
- dev_tools/dist/tools/jules/get-session.js +10 -0
- dev_tools/dist/tools/jules/list-sessions.js +43 -0
- dev_tools/dist/tools/jules/send-message.js +22 -0
- dev_tools/dist/tools/jules/send-message.test.js +56 -0
- dev_tools/dist/tools/jules/shared.js +26 -0
- dev_tools/dist/tools/jules/trigger-feedback.js +16 -0
- dev_tools/dist/tools/jules/trigger-feedback.test.js +42 -0
- dev_tools/dist/tools/repo.commit_patch.js +33 -0
- dev_tools/dist/tools/repo.create_branch.js +21 -0
- dev_tools/dist/tools/repo.create_branch.test.js +42 -0
- dev_tools/dist/tools/repo.create_repair_branch.js +38 -0
- dev_tools/dist/tools/repo.get_changed_files.js +21 -0
- dev_tools/dist/tools/repo.get_command_schema.js +27 -0
- dev_tools/dist/tools/repo.get_package_scripts.js +35 -0
- dev_tools/dist/tools/repo.get_package_scripts.test.js +39 -0
- dev_tools/dist/tools/repo.get_route_map.js +37 -0
- dev_tools/dist/tools/repo.logs.js +16 -0
- dev_tools/dist/tools/repo.read_agent_context.js +20 -0
- dev_tools/dist/tools/repo.read_ci_logs.js +18 -0
- dev_tools/dist/tools/repo.run_lighthouse.js +37 -0
- dev_tools/dist/tools/repo.run_playwright.js +29 -0
- dev_tools/dist/tools/repo.run_tests.js +43 -0
- dev_tools/dist/tools/types.js +1 -0
- dev_tools/get_ai_context.py +63 -0
- dev_tools/handlers/__init__.py +1 -0
- dev_tools/handlers/command_handler.py +69 -0
- dev_tools/mcp_server.py +36 -0
- dev_tools/models.py +354 -0
- dev_tools/orchestrator.py +2841 -0
- dev_tools/pr_overlap.py +139 -0
- dev_tools/resources/__init__.py +1 -0
- dev_tools/resources/build-repo-context.py +182 -0
- dev_tools/resources/prompt_constants.json +5 -0
- dev_tools/resources/review_template.md +41 -0
- dev_tools/resources/ux-audit.config.json +15 -0
- dev_tools/resources/visual_guidelines.json +3 -0
- dev_tools/review_read_pass.py +232 -0
- dev_tools/schema_gen.py +55 -0
- dev_tools/schema_utils.py +125 -0
- dev_tools/scope_check.py +85 -0
- dev_tools/services/__init__.py +1 -0
- dev_tools/services/ai_service.py +816 -0
- dev_tools/services/dependency_graph.py +123 -0
- dev_tools/services/github.py +783 -0
- dev_tools/services/jules.py +181 -0
- dev_tools/services/repair_service.py +199 -0
- dev_tools/services/vector_store.py +82 -0
- dev_tools/services/vision_service.py +91 -0
- dev_tools/td_cli.py +28 -0
- dev_tools/utils/__init__.py +1035 -0
- dev_tools/utils/git.py +68 -0
- dev_tools/ux_report.py +213 -0
- dev_tools/verify_infra.py +91 -0
- dev_tools/verify_versions.py +191 -0
- dev_tools/version_utils.py +175 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { config } from "../config.js";
|
|
5
|
+
export const GetPackageScriptsInputSchema = z.object({
|
|
6
|
+
filter: z.string().optional(),
|
|
7
|
+
});
|
|
8
|
+
export async function getPackageScriptsHandler(args) {
|
|
9
|
+
GetPackageScriptsInputSchema.parse(args);
|
|
10
|
+
const packageJsonPath = path.join(config.repoPath, "package.json");
|
|
11
|
+
try {
|
|
12
|
+
const content = await fs.readFile(packageJsonPath, "utf-8");
|
|
13
|
+
let pkg;
|
|
14
|
+
try {
|
|
15
|
+
pkg = JSON.parse(content);
|
|
16
|
+
}
|
|
17
|
+
catch (e) {
|
|
18
|
+
throw new Error(`Malformed package.json: ${e instanceof Error ? e.message : String(e)}`);
|
|
19
|
+
}
|
|
20
|
+
let scripts = pkg.scripts || {};
|
|
21
|
+
if (args.filter) {
|
|
22
|
+
const filtered = {};
|
|
23
|
+
for (const [name, cmd] of Object.entries(scripts)) {
|
|
24
|
+
if (name.includes(args.filter)) {
|
|
25
|
+
filtered[name] = cmd;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
scripts = filtered;
|
|
29
|
+
}
|
|
30
|
+
return { scripts };
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
throw new Error(`Failed to read package.json: ${error instanceof Error ? error.message : String(error)}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { getPackageScriptsHandler } from "./repo.get_package_scripts.js";
|
|
3
|
+
import fs from "fs/promises";
|
|
4
|
+
vi.mock("fs/promises");
|
|
5
|
+
describe("repo.get_package_scripts", () => {
|
|
6
|
+
it("should return scripts from package.json", async () => {
|
|
7
|
+
const mockPkg = {
|
|
8
|
+
scripts: { test: "vitest" }
|
|
9
|
+
};
|
|
10
|
+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockPkg));
|
|
11
|
+
const result = await getPackageScriptsHandler({});
|
|
12
|
+
expect(result.scripts.test).toBe("vitest");
|
|
13
|
+
});
|
|
14
|
+
it("should handle missing scripts", async () => {
|
|
15
|
+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify({}));
|
|
16
|
+
const result = await getPackageScriptsHandler({});
|
|
17
|
+
expect(result.scripts).toEqual({});
|
|
18
|
+
});
|
|
19
|
+
it("should throw error when file not found", async () => {
|
|
20
|
+
vi.mocked(fs.readFile).mockRejectedValue(new Error("File not found"));
|
|
21
|
+
await expect(getPackageScriptsHandler({})).rejects.toThrow("Failed to read package.json: File not found");
|
|
22
|
+
});
|
|
23
|
+
it("should filter scripts by pattern", async () => {
|
|
24
|
+
const mockPkg = {
|
|
25
|
+
scripts: {
|
|
26
|
+
test: "vitest",
|
|
27
|
+
build: "vite build",
|
|
28
|
+
"test:ui": "vitest --ui"
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockPkg));
|
|
32
|
+
const result = await getPackageScriptsHandler({ filter: "test" });
|
|
33
|
+
expect(result.scripts).toEqual({
|
|
34
|
+
test: "vitest",
|
|
35
|
+
"test:ui": "vitest --ui"
|
|
36
|
+
});
|
|
37
|
+
expect(result.scripts.build).toBeUndefined();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { runCommand } from "../lib/shell.js";
|
|
3
|
+
import { config } from "../config.js";
|
|
4
|
+
import path from "path";
|
|
5
|
+
export const GetRouteMapInputSchema = z.object({
|
|
6
|
+
includeStatic: z.boolean().optional(),
|
|
7
|
+
});
|
|
8
|
+
export async function getRouteMapHandler(args) {
|
|
9
|
+
GetRouteMapInputSchema.parse(args);
|
|
10
|
+
// Logic based on tech-dancer repo structure: src/config/routes.ts and content/
|
|
11
|
+
const routesPath = path.join(config.repoPath, "src/config/routes.ts");
|
|
12
|
+
// For simplicity in MVP, we'll try to find routes by listing content files
|
|
13
|
+
// and reading the main routes file if it exists.
|
|
14
|
+
let routeMap = {};
|
|
15
|
+
try {
|
|
16
|
+
const gitFiles = await runCommand("git", ["ls-files", "content/"]);
|
|
17
|
+
const contentFiles = gitFiles.stdout.trim().split("\n");
|
|
18
|
+
for (const file of contentFiles) {
|
|
19
|
+
if (file.endsWith(".md")) {
|
|
20
|
+
const slug = path.basename(file, ".md");
|
|
21
|
+
if (file.includes("/posts/")) {
|
|
22
|
+
routeMap[`/blog/${slug}`] = file;
|
|
23
|
+
}
|
|
24
|
+
else if (file.includes("/resources/")) {
|
|
25
|
+
routeMap[`/resources/${slug}`] = file;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
// If no content files, that's fine
|
|
32
|
+
}
|
|
33
|
+
if (args.includeStatic) {
|
|
34
|
+
routeMap["/favicon.ico"] = "public/favicon.ico";
|
|
35
|
+
}
|
|
36
|
+
return { routeMap };
|
|
37
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { runTDCli } from "../lib/td-cli.js";
|
|
3
|
+
export const RepoLogsInputSchema = z.object({
|
|
4
|
+
prNumber: z.number(),
|
|
5
|
+
grep: z.string().optional(),
|
|
6
|
+
});
|
|
7
|
+
export async function repoLogsHandler(args) {
|
|
8
|
+
const params = ["repo", "logs", args.prNumber.toString()];
|
|
9
|
+
if (args.grep) {
|
|
10
|
+
params.push("--grep", args.grep);
|
|
11
|
+
}
|
|
12
|
+
const output = await runTDCli(params);
|
|
13
|
+
return {
|
|
14
|
+
logs: output.logs || (output.data ? output.data.logs : undefined)
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { config } from "../config.js";
|
|
5
|
+
export const ReadAgentContextInputSchema = z.object({});
|
|
6
|
+
export async function readAgentContextHandler(_args) {
|
|
7
|
+
const contextPath = path.join(config.repoPath, ".agent-context.json");
|
|
8
|
+
try {
|
|
9
|
+
const content = await fs.readFile(contextPath, "utf-8");
|
|
10
|
+
const parsed = JSON.parse(content);
|
|
11
|
+
return {
|
|
12
|
+
status: "success",
|
|
13
|
+
...parsed
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
console.error("Failed to read agent context:", e.message);
|
|
18
|
+
throw new Error(`Failed to read agent context from ${contextPath}. Ensure it exists by running 'td-cli context-warm'.`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { runTDCli } from "../lib/td-cli.js";
|
|
3
|
+
export const ReadCiLogsInputSchema = z.object({
|
|
4
|
+
prNumber: z.number(),
|
|
5
|
+
all: z.boolean().optional(),
|
|
6
|
+
});
|
|
7
|
+
export async function readCiLogsHandler(args) {
|
|
8
|
+
const params = ["repo", "ci-logs", args.prNumber.toString()];
|
|
9
|
+
if (args.all) {
|
|
10
|
+
params.push("--all");
|
|
11
|
+
}
|
|
12
|
+
const output = await runTDCli(params);
|
|
13
|
+
return {
|
|
14
|
+
checks: output.checks,
|
|
15
|
+
failedChecks: output.failedChecks,
|
|
16
|
+
logs: output.logs
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { runCommand } from "../lib/shell.js";
|
|
3
|
+
export const RunLighthouseInputSchema = z.object({
|
|
4
|
+
route: z.string().optional().default("/"),
|
|
5
|
+
worktreePath: z.string().optional(),
|
|
6
|
+
});
|
|
7
|
+
export async function runLighthouseHandler(args) {
|
|
8
|
+
const results = await runCommand("pnpm", ["lhci", "autorun", "--json"], { cwd: args.worktreePath });
|
|
9
|
+
let summary = {};
|
|
10
|
+
const failures = [];
|
|
11
|
+
try {
|
|
12
|
+
// Attempt to parse summary from LHCI output if available
|
|
13
|
+
// For MVP, we'll provide a structured placeholder if actual JSON isn't present
|
|
14
|
+
if (results.stdout.includes("{")) {
|
|
15
|
+
const possibleJson = results.stdout.substring(results.stdout.indexOf("{"));
|
|
16
|
+
summary = JSON.parse(possibleJson);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
summary = {
|
|
20
|
+
performance: 0,
|
|
21
|
+
accessibility: 0,
|
|
22
|
+
bestPractices: 0,
|
|
23
|
+
seo: 0
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
failures.push("Failed to parse Lighthouse report");
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
success: results.exitCode === 0,
|
|
32
|
+
route: args.route,
|
|
33
|
+
summary,
|
|
34
|
+
failures,
|
|
35
|
+
command: results.command
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { runCommand } from "../lib/shell.js";
|
|
3
|
+
export const RunPlaywrightInputSchema = z.object({
|
|
4
|
+
grep: z.string().optional(),
|
|
5
|
+
worktreePath: z.string().optional(),
|
|
6
|
+
});
|
|
7
|
+
export async function runPlaywrightHandler(args) {
|
|
8
|
+
const tdArgs = ["repo", "run-playwright"];
|
|
9
|
+
if (args.grep) {
|
|
10
|
+
tdArgs.push("--grep", args.grep);
|
|
11
|
+
}
|
|
12
|
+
if (args.worktreePath) {
|
|
13
|
+
tdArgs.push("--worktree", args.worktreePath);
|
|
14
|
+
}
|
|
15
|
+
const result = await runCommand("td-cli", tdArgs);
|
|
16
|
+
if (result.exitCode !== 0) {
|
|
17
|
+
// Playwright might exit with non-zero if tests fail, but our td-cli should ideally handle it
|
|
18
|
+
// and return a JSON with success: false.
|
|
19
|
+
}
|
|
20
|
+
const output = JSON.parse(result.stdout);
|
|
21
|
+
if (output.status === "error") {
|
|
22
|
+
throw new Error(`Failed to run Playwright: ${output.message}`);
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
success: output.success,
|
|
26
|
+
command: output.command,
|
|
27
|
+
failedTests: output.failedTests
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { runCommand } from "../lib/shell.js";
|
|
3
|
+
export const RunTestsInputSchema = z.object({
|
|
4
|
+
commands: z.array(z.string()).optional(),
|
|
5
|
+
timeoutSeconds: z.number().optional().default(300),
|
|
6
|
+
worktreePath: z.string().optional(),
|
|
7
|
+
});
|
|
8
|
+
export async function runTestsHandler(args) {
|
|
9
|
+
const commands = args.commands || [
|
|
10
|
+
"pnpm install --frozen-lockfile",
|
|
11
|
+
"pnpm lint",
|
|
12
|
+
"pnpm test",
|
|
13
|
+
"pnpm build"
|
|
14
|
+
];
|
|
15
|
+
const results = [];
|
|
16
|
+
let success = true;
|
|
17
|
+
for (const fullCmd of commands) {
|
|
18
|
+
const [cmd, ...cmdArgs] = fullCmd.split(" ");
|
|
19
|
+
try {
|
|
20
|
+
const res = await runCommand(cmd, cmdArgs, {
|
|
21
|
+
cwd: args.worktreePath,
|
|
22
|
+
timeout: args.timeoutSeconds * 1000
|
|
23
|
+
});
|
|
24
|
+
results.push(res);
|
|
25
|
+
if (res.exitCode !== 0) {
|
|
26
|
+
success = false;
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
success = false;
|
|
32
|
+
results.push({
|
|
33
|
+
command: fullCmd,
|
|
34
|
+
stdout: "",
|
|
35
|
+
stderr: e instanceof Error ? e.message : String(e),
|
|
36
|
+
exitCode: 1,
|
|
37
|
+
durationMs: 0
|
|
38
|
+
});
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return { success, results };
|
|
43
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# pylint: disable=missing-docstring,unused-import,no-value-for-parameter
|
|
2
|
+
import sys
|
|
3
|
+
from typing import Any, Dict, List
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from dev_tools.services.dependency_graph import DependencyGraph
|
|
9
|
+
from dev_tools.services.vector_store import VectorStore
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_context(filepath: str, diff_text: str, graph: DependencyGraph, store: VectorStore, n_results: int = 3):
|
|
13
|
+
context: Dict[str, Any] = {"path": filepath, "dependencies": [], "dependents": [], "semantic": []}
|
|
14
|
+
|
|
15
|
+
if not isinstance(filepath, str) or not isinstance(diff_text, str) or not filepath.strip() or not diff_text.strip():
|
|
16
|
+
return context
|
|
17
|
+
|
|
18
|
+
context["dependencies"] = graph.get_dependencies(filepath)
|
|
19
|
+
context["dependents"] = graph.get_dependents(filepath)
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
# Use diff_text for semantic search
|
|
23
|
+
results = store.query(diff_text, n_results=n_results)
|
|
24
|
+
for res in results:
|
|
25
|
+
if res["metadata"].get("path") != filepath:
|
|
26
|
+
context["semantic"].append({"path": res["metadata"].get("path"), "document": res["document"]})
|
|
27
|
+
except Exception as e:
|
|
28
|
+
print(f"Error querying vector store: {e}", file=sys.stderr)
|
|
29
|
+
|
|
30
|
+
return context
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@click.command()
|
|
34
|
+
@click.argument("input_file", type=click.File("r"), default="-")
|
|
35
|
+
def main(input_file):
|
|
36
|
+
"""Retrieve dependency and semantic context for a set of changed files."""
|
|
37
|
+
try:
|
|
38
|
+
input_data = json.load(input_file)
|
|
39
|
+
except Exception as e:
|
|
40
|
+
click.echo(f"Error parsing input JSON: {e}", err=True)
|
|
41
|
+
sys.exit(1)
|
|
42
|
+
|
|
43
|
+
files_data: List[Dict[str, Any]] = input_data.get("files", [])
|
|
44
|
+
if not files_data:
|
|
45
|
+
print(json.dumps([]))
|
|
46
|
+
sys.exit(0)
|
|
47
|
+
|
|
48
|
+
# Initialize services once
|
|
49
|
+
graph = DependencyGraph()
|
|
50
|
+
store = VectorStore()
|
|
51
|
+
|
|
52
|
+
results: List[Dict[str, Any]] = []
|
|
53
|
+
for item in files_data:
|
|
54
|
+
filepath = item.get("path")
|
|
55
|
+
diff_text = item.get("diff")
|
|
56
|
+
if filepath and diff_text:
|
|
57
|
+
results.append(get_context(filepath, diff_text, graph, store))
|
|
58
|
+
|
|
59
|
+
click.echo(json.dumps(results))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if __name__ == "__main__":
|
|
63
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# pylint: disable=missing-docstring
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# pylint: disable=line-too-long,missing-docstring,no-else-return,too-few-public-methods,unused-argument
|
|
2
|
+
from typing import Any, Dict, Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class CommandHandler:
|
|
6
|
+
def __init__(self, orchestrator):
|
|
7
|
+
self.orchestrator = orchestrator
|
|
8
|
+
|
|
9
|
+
def handle(self, pr_number: int, command_text: str, comment_id: Optional[str] = None) -> Dict[str, Any]:
|
|
10
|
+
"""
|
|
11
|
+
Parses and dispatches slash commands.
|
|
12
|
+
"""
|
|
13
|
+
command = command_text.strip().lower()
|
|
14
|
+
|
|
15
|
+
if command.startswith("/ai-review"):
|
|
16
|
+
return self._handle_ai_review(pr_number)
|
|
17
|
+
elif command.startswith("/ai-fix"):
|
|
18
|
+
return self._handle_ai_fix(pr_number)
|
|
19
|
+
|
|
20
|
+
return {"status": "ignored", "message": f"Unknown command: {command}"}
|
|
21
|
+
|
|
22
|
+
def _handle_ai_review(self, pr_number: int) -> Dict[str, Any]:
|
|
23
|
+
review_data = self.orchestrator.review_pr(pr_number)
|
|
24
|
+
recommendation = review_data.get("recommendation", "COMMENT")
|
|
25
|
+
|
|
26
|
+
# Map recommendation to GH event
|
|
27
|
+
event = "COMMENT"
|
|
28
|
+
if recommendation == "Approved":
|
|
29
|
+
event = "APPROVE"
|
|
30
|
+
elif recommendation == "Not Approved":
|
|
31
|
+
event = "REQUEST_CHANGES"
|
|
32
|
+
|
|
33
|
+
comment_body = f"## 🤖 AI Code Review\n\n{review_data.get('reviewComment', 'No comment provided.')}\n\n**Recommendation:** {recommendation}"
|
|
34
|
+
|
|
35
|
+
self.orchestrator.github.create_review(pr_number, comment_body, [], event)
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
"status": "success",
|
|
39
|
+
"message": f"Submitted {event} review for PR #{pr_number}",
|
|
40
|
+
"review": review_data,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
def _handle_ai_fix(self, pr_number: int) -> Dict[str, Any]:
|
|
44
|
+
# First, ensure we have the conflict files
|
|
45
|
+
conflict_files = self.orchestrator.find_conflict_files()
|
|
46
|
+
if not conflict_files:
|
|
47
|
+
return {
|
|
48
|
+
"status": "error",
|
|
49
|
+
"message": "No merge conflicts detected locally. Ensure you have merged the base branch and markers are present.",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
results = [(f, self.orchestrator.resolve_conflict(f)) for f in conflict_files]
|
|
53
|
+
resolved = [f for f, success in results if success]
|
|
54
|
+
failed = [f for f, success in results if not success]
|
|
55
|
+
|
|
56
|
+
if failed:
|
|
57
|
+
msg = f"Failed to resolve conflicts in: {', '.join(failed)}"
|
|
58
|
+
return {
|
|
59
|
+
"status": "partial_success" if resolved else "error",
|
|
60
|
+
"message": msg,
|
|
61
|
+
"resolved": resolved,
|
|
62
|
+
"failed": failed,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
"status": "success",
|
|
67
|
+
"message": f"Successfully resolved conflicts in {len(resolved)} files.",
|
|
68
|
+
"resolved": resolved,
|
|
69
|
+
}
|
dev_tools/mcp_server.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP Server management for boomtick-cli.
|
|
3
|
+
"""
|
|
4
|
+
import subprocess
|
|
5
|
+
from dev_tools.utils import resolve_resource_path
|
|
6
|
+
|
|
7
|
+
class NodeNotFoundError(RuntimeError):
|
|
8
|
+
"""Raised when Node.js is not found on the PATH."""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def start_mcp_server():
|
|
12
|
+
"""
|
|
13
|
+
Spawns the TypeScript MCP server as a Node.js subprocess.
|
|
14
|
+
"""
|
|
15
|
+
try:
|
|
16
|
+
server_js = resolve_resource_path("dist/index.js")
|
|
17
|
+
except FileNotFoundError as e:
|
|
18
|
+
raise RuntimeError(
|
|
19
|
+
f"MCP server source file not found: {e}. "
|
|
20
|
+
"Ensure the package is correctly installed with bundled JS artifacts."
|
|
21
|
+
) from e
|
|
22
|
+
|
|
23
|
+
# Spawn Node subprocess to communicate via standard I/O pipes
|
|
24
|
+
try:
|
|
25
|
+
return subprocess.Popen(
|
|
26
|
+
["node", server_js],
|
|
27
|
+
stdin=subprocess.PIPE,
|
|
28
|
+
stdout=subprocess.PIPE,
|
|
29
|
+
stderr=subprocess.PIPE,
|
|
30
|
+
text=True
|
|
31
|
+
)
|
|
32
|
+
except FileNotFoundError as e:
|
|
33
|
+
raise NodeNotFoundError(
|
|
34
|
+
"Node.js is required to run the MCP server but was not found on the PATH. "
|
|
35
|
+
"Please install Node.js 24+ and try again."
|
|
36
|
+
) from e
|