@arii/boomtick-mcp 0.4.2 → 0.5.0

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 (82) hide show
  1. package/.mcp/schemas/github.get_pr_commits.json +32 -0
  2. package/.mcp/schemas/github.get_pr_reviews.json +32 -0
  3. package/.mcp/schemas/github_get_pr_commits.json +32 -0
  4. package/.mcp/schemas/github_get_pr_reviews.json +32 -0
  5. package/.mcp/schemas/repo.get_workflow_runs.json +50 -0
  6. package/.mcp/schemas/repo.logs.json +14 -0
  7. package/.mcp/schemas/repo.read_ci_logs.json +14 -0
  8. package/.mcp/schemas/repo_get_workflow_runs.json +50 -0
  9. package/.mcp/schemas/repo_logs.json +14 -0
  10. package/.mcp/schemas/repo_read_ci_logs.json +14 -0
  11. package/CHANGELOG.md +7 -0
  12. package/README.md +0 -1
  13. package/actions/impact-analysis/action.yml +4 -16
  14. package/actions/run-project-gate/action.yml +2 -9
  15. package/actions/test-build/action.yml +1 -19
  16. package/dist/lib/git.js +44 -0
  17. package/dist/lib/git.test.js +82 -0
  18. package/dist/mcp/definitions.js +16 -39
  19. package/dist/mcp/server.js +0 -46
  20. package/dist/mcp/server.test.js +0 -33
  21. package/dist/tools/contract.js +135 -2
  22. package/dist/tools/github.get_merge_conflict_files.js +10 -18
  23. package/dist/tools/github.get_pr_commits.js +21 -0
  24. package/dist/tools/github.get_pr_reviews.js +21 -0
  25. package/dist/tools/github.get_pr_reviews.test.js +99 -0
  26. package/dist/tools/jules/create-session.js +1 -2
  27. package/dist/tools/jules/get-pr.js +1 -2
  28. package/dist/tools/jules/get-session.js +1 -2
  29. package/dist/tools/jules/shared.js +24 -0
  30. package/dist/tools/repo.get_changed_files.js +2 -13
  31. package/dist/tools/repo.get_workflow_runs.js +29 -0
  32. package/dist/tools/repo.logs.js +5 -1
  33. package/dist/tools/repo.read_agent_context.js +27 -1
  34. package/dist/tools/repo.read_agent_context.test.js +1 -1
  35. package/dist/tools/repo.read_ci_logs.js +5 -1
  36. package/dist/tools/repo.run_tests.js +1 -1
  37. package/package.json +3 -6
  38. package/src/agents/verifier-agent.prompt.md +0 -4
  39. package/src/lib/git.test.ts +110 -0
  40. package/src/lib/git.ts +75 -0
  41. package/src/mcp/definitions.ts +18 -42
  42. package/src/mcp/server.test.ts +0 -42
  43. package/src/mcp/server.ts +0 -45
  44. package/src/tools/contract.ts +147 -2
  45. package/src/tools/github.get_merge_conflict_files.ts +10 -20
  46. package/src/tools/github.get_pr_commits.ts +27 -0
  47. package/src/tools/github.get_pr_reviews.test.ts +110 -0
  48. package/src/tools/github.get_pr_reviews.ts +27 -0
  49. package/src/tools/jules/create-session.ts +1 -2
  50. package/src/tools/jules/get-pr.ts +1 -2
  51. package/src/tools/jules/get-session.ts +1 -2
  52. package/src/tools/jules/shared.ts +35 -0
  53. package/src/tools/repo.get_changed_files.ts +2 -16
  54. package/src/tools/repo.get_workflow_runs.ts +36 -0
  55. package/src/tools/repo.logs.ts +5 -1
  56. package/src/tools/repo.read_agent_context.test.ts +1 -1
  57. package/src/tools/repo.read_agent_context.ts +35 -1
  58. package/src/tools/repo.read_ci_logs.ts +5 -1
  59. package/src/tools/repo.run_tests.ts +1 -1
  60. package/.mcp/schemas/agent.search_ddgs.json +0 -31
  61. package/.mcp/schemas/agent_search_ddgs.json +0 -31
  62. package/.mcp/schemas/repo.get_route_map.json +0 -23
  63. package/.mcp/schemas/repo.run_lighthouse.json +0 -29
  64. package/.mcp/schemas/repo.run_playwright.json +0 -36
  65. package/.mcp/schemas/repo_get_route_map.json +0 -23
  66. package/.mcp/schemas/repo_run_lighthouse.json +0 -29
  67. package/.mcp/schemas/repo_run_playwright.json +0 -36
  68. package/dist/lib/td-cli.js +0 -54
  69. package/dist/tools/ddgs.search.js +0 -50
  70. package/dist/tools/ddgs.search.test.js +0 -67
  71. package/dist/tools/ddgs_search.py +0 -23
  72. package/dist/tools/repo.get_route_map.js +0 -37
  73. package/dist/tools/repo.run_lighthouse.js +0 -37
  74. package/dist/tools/repo.run_playwright.js +0 -34
  75. package/scripts/export-mcp-schema.ts +0 -7
  76. package/src/lib/td-cli.ts +0 -73
  77. package/src/tools/ddgs.search.test.ts +0 -92
  78. package/src/tools/ddgs.search.ts +0 -58
  79. package/src/tools/ddgs_search.py +0 -23
  80. package/src/tools/repo.get_route_map.ts +0 -41
  81. package/src/tools/repo.run_lighthouse.ts +0 -40
  82. package/src/tools/repo.run_playwright.ts +0 -42
@@ -1,31 +0,0 @@
1
- {
2
- "name": "agent_search_ddgs",
3
- "description": "Search the web using DuckDuckGo (via ddgs python library).",
4
- "parameters": {
5
- "properties": {
6
- "query": {
7
- "description": "The search query.",
8
- "title": "Query",
9
- "type": "string"
10
- },
11
- "maxResults": {
12
- "anyOf": [
13
- {
14
- "type": "integer"
15
- },
16
- {
17
- "type": "null"
18
- }
19
- ],
20
- "default": null,
21
- "description": "Maximum number of results to return.",
22
- "title": "Maxresults"
23
- }
24
- },
25
- "required": [
26
- "query"
27
- ],
28
- "title": "SearchDdgsInput",
29
- "type": "object"
30
- }
31
- }
@@ -1,23 +0,0 @@
1
- {
2
- "name": "repo.get_route_map",
3
- "description": "Get the mapping of routes to content files.",
4
- "parameters": {
5
- "properties": {
6
- "includeStatic": {
7
- "anyOf": [
8
- {
9
- "type": "boolean"
10
- },
11
- {
12
- "type": "null"
13
- }
14
- ],
15
- "default": null,
16
- "description": "Whether to include static assets in the route map.",
17
- "title": "Includestatic"
18
- }
19
- },
20
- "title": "GetRouteMapInput",
21
- "type": "object"
22
- }
23
- }
@@ -1,29 +0,0 @@
1
- {
2
- "name": "repo.run_lighthouse",
3
- "description": "Run Lighthouse CI audits.",
4
- "parameters": {
5
- "properties": {
6
- "route": {
7
- "default": "/",
8
- "description": "The route to audit (default: '/').",
9
- "title": "Route",
10
- "type": "string"
11
- },
12
- "worktreePath": {
13
- "anyOf": [
14
- {
15
- "type": "string"
16
- },
17
- {
18
- "type": "null"
19
- }
20
- ],
21
- "default": null,
22
- "description": "Optional path to the worktree to run the audit in.",
23
- "title": "Worktreepath"
24
- }
25
- },
26
- "title": "RunLighthouseInput",
27
- "type": "object"
28
- }
29
- }
@@ -1,36 +0,0 @@
1
- {
2
- "name": "repo.run_playwright",
3
- "description": "Run Playwright E2E tests.",
4
- "parameters": {
5
- "properties": {
6
- "grep": {
7
- "anyOf": [
8
- {
9
- "type": "string"
10
- },
11
- {
12
- "type": "null"
13
- }
14
- ],
15
- "default": null,
16
- "description": "Optional pattern to filter tests by name.",
17
- "title": "Grep"
18
- },
19
- "worktreePath": {
20
- "anyOf": [
21
- {
22
- "type": "string"
23
- },
24
- {
25
- "type": "null"
26
- }
27
- ],
28
- "default": null,
29
- "description": "Optional path to the worktree to run tests in.",
30
- "title": "Worktreepath"
31
- }
32
- },
33
- "title": "RunPlaywrightInput",
34
- "type": "object"
35
- }
36
- }
@@ -1,23 +0,0 @@
1
- {
2
- "name": "repo_get_route_map",
3
- "description": "Get the mapping of routes to content files.",
4
- "parameters": {
5
- "properties": {
6
- "includeStatic": {
7
- "anyOf": [
8
- {
9
- "type": "boolean"
10
- },
11
- {
12
- "type": "null"
13
- }
14
- ],
15
- "default": null,
16
- "description": "Whether to include static assets in the route map.",
17
- "title": "Includestatic"
18
- }
19
- },
20
- "title": "GetRouteMapInput",
21
- "type": "object"
22
- }
23
- }
@@ -1,29 +0,0 @@
1
- {
2
- "name": "repo_run_lighthouse",
3
- "description": "Run Lighthouse CI audits.",
4
- "parameters": {
5
- "properties": {
6
- "route": {
7
- "default": "/",
8
- "description": "The route to audit (default: '/').",
9
- "title": "Route",
10
- "type": "string"
11
- },
12
- "worktreePath": {
13
- "anyOf": [
14
- {
15
- "type": "string"
16
- },
17
- {
18
- "type": "null"
19
- }
20
- ],
21
- "default": null,
22
- "description": "Optional path to the worktree to run the audit in.",
23
- "title": "Worktreepath"
24
- }
25
- },
26
- "title": "RunLighthouseInput",
27
- "type": "object"
28
- }
29
- }
@@ -1,36 +0,0 @@
1
- {
2
- "name": "repo_run_playwright",
3
- "description": "Run Playwright E2E tests.",
4
- "parameters": {
5
- "properties": {
6
- "grep": {
7
- "anyOf": [
8
- {
9
- "type": "string"
10
- },
11
- {
12
- "type": "null"
13
- }
14
- ],
15
- "default": null,
16
- "description": "Optional pattern to filter tests by name.",
17
- "title": "Grep"
18
- },
19
- "worktreePath": {
20
- "anyOf": [
21
- {
22
- "type": "string"
23
- },
24
- {
25
- "type": "null"
26
- }
27
- ],
28
- "default": null,
29
- "description": "Optional path to the worktree to run tests in.",
30
- "title": "Worktreepath"
31
- }
32
- },
33
- "title": "RunPlaywrightInput",
34
- "type": "object"
35
- }
36
- }
@@ -1,54 +0,0 @@
1
- import { runCommand } from "../lib/shell.js";
2
- import { execSync } from "child_process";
3
- import * as fs from "fs";
4
- import * as path from "path";
5
- export const loadStandaloneAgentContext = (repoRoot = process.cwd()) => {
6
- const configPath = path.join(repoRoot, "project_config.json");
7
- let config = {};
8
- if (fs.existsSync(configPath)) {
9
- try {
10
- config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
11
- }
12
- catch (e) {
13
- console.warn(`[MCP Warning] Failed to parse ${configPath}:`, e);
14
- }
15
- }
16
- let gitCommit = "unknown";
17
- try {
18
- gitCommit = execSync("git rev-parse HEAD", { cwd: repoRoot, encoding: "utf-8" }).trim();
19
- }
20
- catch {
21
- // Fallback if git is unavailable (e.g. Docker container without git)
22
- }
23
- return {
24
- packageName: config.packageName ?? "boomtick",
25
- gitCommit,
26
- config,
27
- };
28
- };
29
- function tryParseJson(text) {
30
- try {
31
- return JSON.parse(text);
32
- }
33
- catch {
34
- return null;
35
- }
36
- }
37
- export async function runTDCli(args) {
38
- const result = await runCommand("td-cli", args);
39
- const stdout = result.stdout.trim();
40
- const output = stdout.startsWith("{") ? tryParseJson(stdout) : null;
41
- // Handle structured errors (status: "error") regardless of exit code
42
- if (output?.status === "error") {
43
- throw new Error(`td-cli returned error: ${output.message ?? "Unknown error"}`);
44
- }
45
- // Handle non-zero exit codes (shell failure)
46
- if (result.exitCode !== 0) {
47
- throw new Error(`td-cli command failed (${args.join(" ")}): ${result.stderr || stdout}`);
48
- }
49
- // Ensure we have a valid JSON response for successful exit
50
- if (!output) {
51
- throw new Error(`td-cli returned non-JSON output with exit code 0: ${stdout}`);
52
- }
53
- return output;
54
- }
@@ -1,50 +0,0 @@
1
- import { z } from "zod";
2
- import { runCommand } from "../lib/shell.js";
3
- export const DdgsSearchInputSchema = z.object({
4
- query: z.string(),
5
- maxResults: z.number().optional().default(5),
6
- });
7
- import path from "path";
8
- import { fileURLToPath } from "url";
9
- const __filename = fileURLToPath(import.meta.url);
10
- const __dirname = path.dirname(__filename);
11
- export async function ddgsSearchHandler(args) {
12
- const scriptPath = path.join(__dirname, "ddgs_search.py");
13
- // Sanitize query by removing control characters, newlines, and null bytes
14
- // eslint-disable-next-line no-control-regex
15
- const safeQuery = args.query.replace(/[\x00-\x1F\x7F-\x9F]/g, " ").trim();
16
- if (!safeQuery) {
17
- throw new Error("Query cannot be empty after sanitization.");
18
- }
19
- const result = await runCommand("python3", [scriptPath, safeQuery, (args.maxResults ?? 5).toString()]);
20
- // duckduckgo_search logs annoying deprecation warnings to stderr we want to ignore
21
- // if it really failed, exitCode will be non-zero and we'll have stdout/stderr
22
- if (result.exitCode !== 0) {
23
- let parsedError = result.stderr || result.stdout;
24
- const lines = parsedError.split("\n");
25
- for (const line of lines) {
26
- try {
27
- const json = JSON.parse(line);
28
- if (json.error) {
29
- parsedError = json.error;
30
- break;
31
- }
32
- }
33
- catch {
34
- // continue
35
- }
36
- }
37
- throw new Error(`Failed to search ddgs: ${parsedError}`);
38
- }
39
- try {
40
- const data = JSON.parse(result.stdout);
41
- if (!Array.isArray(data)) {
42
- throw new Error("Expected an array of search results");
43
- }
44
- return { results: data };
45
- }
46
- catch (e) {
47
- const message = e instanceof Error ? e.message : String(e);
48
- throw new Error(`Failed to parse ddgs search results. Error: ${message}. Output was: ${result.stdout}`);
49
- }
50
- }
@@ -1,67 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import { ddgsSearchHandler } from "./ddgs.search.js";
3
- import { runCommand } from "../lib/shell.js";
4
- vi.mock("../lib/shell.js");
5
- describe("ddgsSearchHandler", () => {
6
- beforeEach(() => {
7
- vi.resetAllMocks();
8
- });
9
- it("should parse arguments, sanitize query, and return search results", async () => {
10
- const mockOutput = JSON.stringify([
11
- { title: "Result 1", href: "https://example.com/1", body: "Description 1" },
12
- { title: "Result 2", href: "https://example.com/2", body: "Description 2" },
13
- ]);
14
- vi.mocked(runCommand).mockResolvedValue({
15
- stdout: mockOutput,
16
- stderr: "",
17
- exitCode: 0,
18
- durationMs: 100,
19
- command: "mock python3",
20
- });
21
- const result = await ddgsSearchHandler({ query: "test\nquery\x00", maxResults: 2 });
22
- expect(runCommand).toHaveBeenCalledTimes(1);
23
- expect(runCommand).toHaveBeenCalledWith("python3", [expect.stringContaining("ddgs_search.py"), "test query", "2"]);
24
- expect(result.results).toHaveLength(2);
25
- expect(result.results[0].title).toBe("Result 1");
26
- });
27
- it("should handle raw error when python script exits with non-zero code and non-JSON output", async () => {
28
- vi.mocked(runCommand).mockResolvedValue({
29
- stdout: "",
30
- stderr: "Python script failed",
31
- exitCode: 1,
32
- durationMs: 100,
33
- command: "mock python3",
34
- });
35
- await expect(ddgsSearchHandler({ query: "test query", maxResults: 2 })).rejects.toThrow("Failed to search ddgs: Python script failed");
36
- });
37
- it("should handle parsed JSON error when python script exits with non-zero code", async () => {
38
- vi.mocked(runCommand).mockResolvedValue({
39
- stdout: "",
40
- stderr: JSON.stringify({ error: "Structured JSON error" }),
41
- exitCode: 1,
42
- durationMs: 100,
43
- command: "mock python3",
44
- });
45
- await expect(ddgsSearchHandler({ query: "test query", maxResults: 2 })).rejects.toThrow("Failed to search ddgs: Structured JSON error");
46
- });
47
- it("should handle error when python output is not an array", async () => {
48
- vi.mocked(runCommand).mockResolvedValue({
49
- stdout: JSON.stringify({ error: "Something went wrong" }),
50
- stderr: "",
51
- exitCode: 0,
52
- durationMs: 100,
53
- command: "mock python3",
54
- });
55
- await expect(ddgsSearchHandler({ query: "test query", maxResults: 2 })).rejects.toThrow("Expected an array of search results");
56
- });
57
- it("should handle error when python output cannot be parsed", async () => {
58
- vi.mocked(runCommand).mockResolvedValue({
59
- stdout: "invalid json",
60
- stderr: "",
61
- exitCode: 0,
62
- durationMs: 100,
63
- command: "mock python3",
64
- });
65
- await expect(ddgsSearchHandler({ query: "test query", maxResults: 2 })).rejects.toThrow("Failed to parse ddgs search results");
66
- });
67
- });
@@ -1,23 +0,0 @@
1
- # pylint: disable=line-too-long
2
- """DuckDuckGo Search tool."""
3
- import json
4
- import sys
5
-
6
- try:
7
- from duckduckgo_search import DDGS
8
- except ImportError:
9
- print(json.dumps({"error": "duckduckgo_search package not found. Please install it using 'pip install duckduckgo-search' (preferably in a virtual environment)."}), file=sys.stderr)
10
- sys.exit(1)
11
-
12
- if len(sys.argv) != 3:
13
- print(json.dumps({"error": f"Invalid argument count. Expected 2 (query, max_results), got {len(sys.argv) - 1}."}), file=sys.stderr)
14
- sys.exit(1)
15
-
16
- try:
17
- query = sys.argv[1]
18
- max_results = int(sys.argv[2])
19
- results = DDGS().text(query, max_results=max_results)
20
- print(json.dumps(list(results)))
21
- except Exception as e:
22
- print(json.dumps({"error": str(e)}), file=sys.stderr)
23
- sys.exit(1)
@@ -1,37 +0,0 @@
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
- }
@@ -1,37 +0,0 @@
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
- }
@@ -1,34 +0,0 @@
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
- timeoutSeconds: z.number().optional().default(180),
7
- });
8
- export async function runPlaywrightHandler(args) {
9
- const parsed = RunPlaywrightInputSchema.parse(args || {});
10
- const tdArgs = ["repo", "run-playwright"];
11
- if (parsed.grep) {
12
- tdArgs.push("--grep", parsed.grep);
13
- }
14
- if (parsed.worktreePath) {
15
- tdArgs.push("--worktree", parsed.worktreePath);
16
- }
17
- const result = await runCommand("td-cli", tdArgs, {
18
- cwd: parsed.worktreePath,
19
- timeout: parsed.timeoutSeconds * 1000,
20
- });
21
- if (result.exitCode !== 0) {
22
- // Playwright might exit with non-zero if tests fail, but our td-cli should ideally handle it
23
- // and return a JSON with success: false.
24
- }
25
- const output = JSON.parse(result.stdout);
26
- if (output.status === "error") {
27
- throw new Error(`Failed to run Playwright: ${output.message}`);
28
- }
29
- return {
30
- success: output.success,
31
- command: output.command,
32
- failedTests: output.failedTests
33
- };
34
- }
@@ -1,7 +0,0 @@
1
- import { MCP_TOOLS, MCP_PROMPTS, MCP_RESOURCES } from "../src/mcp/definitions.js";
2
-
3
- console.log(JSON.stringify({
4
- tools: MCP_TOOLS,
5
- prompts: MCP_PROMPTS,
6
- resources: MCP_RESOURCES
7
- }, null, 2));
package/src/lib/td-cli.ts DELETED
@@ -1,73 +0,0 @@
1
- import { runCommand } from "../lib/shell.js";
2
- import { execSync } from "child_process";
3
- import * as fs from "fs";
4
- import * as path from "path";
5
-
6
- export interface TDCliResponse {
7
- status: string;
8
- message?: string;
9
- [key: string]: any;
10
- }
11
-
12
- export interface AgentContext {
13
- packageName: string;
14
- gitCommit: string;
15
- config: Record<string, unknown>;
16
- }
17
-
18
- export const loadStandaloneAgentContext = (repoRoot: string = process.cwd()): AgentContext => {
19
- const configPath = path.join(repoRoot, "project_config.json");
20
- let config: Record<string, unknown> = {};
21
-
22
- if (fs.existsSync(configPath)) {
23
- try {
24
- config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
25
- } catch (e) {
26
- console.warn(`[MCP Warning] Failed to parse ${configPath}:`, e);
27
- }
28
- }
29
-
30
- let gitCommit = "unknown";
31
- try {
32
- gitCommit = execSync("git rev-parse HEAD", { cwd: repoRoot, encoding: "utf-8" }).trim();
33
- } catch {
34
- // Fallback if git is unavailable (e.g. Docker container without git)
35
- }
36
-
37
- return {
38
- packageName: (config.packageName as string) ?? "boomtick",
39
- gitCommit,
40
- config,
41
- };
42
- };
43
-
44
- function tryParseJson(text: string): TDCliResponse | null {
45
- try {
46
- return JSON.parse(text) as TDCliResponse;
47
- } catch {
48
- return null;
49
- }
50
- }
51
-
52
- export async function runTDCli(args: string[]): Promise<TDCliResponse> {
53
- const result = await runCommand("td-cli", args);
54
- const stdout = result.stdout.trim();
55
- const output = stdout.startsWith("{") ? tryParseJson(stdout) : null;
56
-
57
- // Handle structured errors (status: "error") regardless of exit code
58
- if (output?.status === "error") {
59
- throw new Error(`td-cli returned error: ${output.message ?? "Unknown error"}`);
60
- }
61
-
62
- // Handle non-zero exit codes (shell failure)
63
- if (result.exitCode !== 0) {
64
- throw new Error(`td-cli command failed (${args.join(" ")}): ${result.stderr || stdout}`);
65
- }
66
-
67
- // Ensure we have a valid JSON response for successful exit
68
- if (!output) {
69
- throw new Error(`td-cli returned non-JSON output with exit code 0: ${stdout}`);
70
- }
71
-
72
- return output;
73
- }