@cyanheads/git-mcp-server 2.1.4 → 2.1.6

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.
@@ -1,4 +1,4 @@
1
- import { exec } from "child_process";
1
+ import { execFile } from "child_process";
2
2
  import { promisify } from "util";
3
3
  import { z } from "zod";
4
4
  // Import utils from barrel (logger from ../utils/internal/logger.js)
@@ -7,7 +7,7 @@ import { logger } from "../../../utils/index.js";
7
7
  import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Keep direct import for types-global
8
8
  // Import utils from barrel (sanitization from ../utils/security/sanitization.js)
9
9
  import { sanitization } from "../../../utils/index.js";
10
- const execAsync = promisify(exec);
10
+ const execFileAsync = promisify(execFile);
11
11
  // Define the base input schema without refinement
12
12
  const GitDiffInputBaseSchema = z.object({
13
13
  path: z
@@ -98,30 +98,32 @@ export async function diffGitChanges(input, context) {
98
98
  let untrackedFilesCount = 0;
99
99
  try {
100
100
  // Construct the standard git diff command
101
- let standardDiffCommand = `git -C "${targetPath}" diff`;
101
+ const standardDiffArgs = ["-C", targetPath, "diff"];
102
102
  if (input.staged) {
103
- standardDiffCommand += " --staged"; // Or --cached
103
+ standardDiffArgs.push("--staged"); // Or --cached
104
104
  }
105
105
  else {
106
106
  // Add commit references if not doing staged diff
107
107
  if (safeCommit1) {
108
- standardDiffCommand += ` ${safeCommit1}`;
108
+ standardDiffArgs.push(safeCommit1);
109
109
  }
110
110
  if (safeCommit2) {
111
- standardDiffCommand += ` ${safeCommit2}`;
111
+ standardDiffArgs.push(safeCommit2);
112
112
  }
113
113
  }
114
114
  // Add file path limiter if provided for standard diff
115
115
  // Note: `input.file` will not apply to the untracked files part unless we explicitly filter them.
116
116
  // For simplicity, `includeUntracked` will show all untracked files if `input.file` is also set.
117
117
  if (safeFile) {
118
- standardDiffCommand += ` -- "${safeFile}"`; // Use '--' to separate paths from revisions
118
+ standardDiffArgs.push("--", safeFile); // Use '--' to separate paths from revisions
119
119
  }
120
- logger.debug(`Executing standard diff command: ${standardDiffCommand}`, {
120
+ logger.debug(`Executing standard diff command: git ${standardDiffArgs.join(" ")}`, {
121
121
  ...context,
122
122
  operation,
123
123
  });
124
- const { stdout: standardStdout, stderr: standardStderr } = await execAsync(standardDiffCommand, { maxBuffer: 1024 * 1024 * 20 });
124
+ const { stdout: standardStdout, stderr: standardStderr } = await execFileAsync("git", standardDiffArgs, {
125
+ maxBuffer: 1024 * 1024 * 20,
126
+ });
125
127
  if (standardStderr) {
126
128
  logger.warning(`Git diff (standard) stderr: ${standardStderr}`, {
127
129
  ...context,
@@ -132,9 +134,15 @@ export async function diffGitChanges(input, context) {
132
134
  // Handle untracked files if requested
133
135
  if (input.includeUntracked) {
134
136
  logger.debug("Including untracked files.", { ...context, operation });
135
- const listUntrackedCommand = `git -C "${targetPath}" ls-files --others --exclude-standard`;
137
+ const listUntrackedArgs = [
138
+ "-C",
139
+ targetPath,
140
+ "ls-files",
141
+ "--others",
142
+ "--exclude-standard",
143
+ ];
136
144
  try {
137
- const { stdout: untrackedFilesStdOut } = await execAsync(listUntrackedCommand);
145
+ const { stdout: untrackedFilesStdOut } = await execFileAsync("git", listUntrackedArgs);
138
146
  const untrackedFiles = untrackedFilesStdOut
139
147
  .trim()
140
148
  .split("\n")
@@ -152,10 +160,17 @@ export async function diffGitChanges(input, context) {
152
160
  // Skip if file path becomes empty after sanitization (unlikely but safe)
153
161
  if (!safeUntrackedFile)
154
162
  continue;
155
- const untrackedDiffCommand = `git -C "${targetPath}" diff --no-index /dev/null "${safeUntrackedFile}"`;
156
- logger.debug(`Executing diff for untracked file: ${untrackedDiffCommand}`, { ...context, operation, file: safeUntrackedFile });
163
+ const untrackedDiffArgs = [
164
+ "-C",
165
+ targetPath,
166
+ "diff",
167
+ "--no-index",
168
+ "/dev/null",
169
+ safeUntrackedFile,
170
+ ];
171
+ logger.debug(`Executing diff for untracked file: git ${untrackedDiffArgs.join(" ")}`, { ...context, operation, file: safeUntrackedFile });
157
172
  try {
158
- const { stdout: untrackedFileDiffOut } = await execAsync(untrackedDiffCommand);
173
+ const { stdout: untrackedFileDiffOut } = await execFileAsync("git", untrackedDiffArgs);
159
174
  individualUntrackedDiffs += untrackedFileDiffOut;
160
175
  untrackedFilesCount++;
161
176
  }
@@ -1,4 +1,4 @@
1
- import { exec } from "child_process";
1
+ import { execFile } from "child_process";
2
2
  import { promisify } from "util";
3
3
  import { z } from "zod";
4
4
  // Import utils from barrel (logger from ../utils/internal/logger.js)
@@ -7,7 +7,7 @@ import { logger } from "../../../utils/index.js";
7
7
  import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Keep direct import for types-global
8
8
  // Import utils from barrel (sanitization from ../utils/security/sanitization.js)
9
9
  import { sanitization } from "../../../utils/index.js";
10
- const execAsync = promisify(exec);
10
+ const execFileAsync = promisify(execFile);
11
11
  // Define the input schema for the git_fetch tool using Zod
12
12
  export const GitFetchInputSchema = z.object({
13
13
  path: z
@@ -76,27 +76,28 @@ export async function fetchGitRemote(input, context) {
76
76
  throw error;
77
77
  throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
78
78
  }
79
- // Basic sanitization for remote name
80
- const safeRemote = input.remote?.replace(/[^a-zA-Z0-9_.\-/]/g, "");
81
79
  try {
82
80
  // Construct the git fetch command
83
- let command = `git -C "${targetPath}" fetch`;
81
+ const args = ["-C", targetPath, "fetch"];
84
82
  if (input.prune) {
85
- command += " --prune";
83
+ args.push("--prune");
86
84
  }
87
85
  if (input.tags) {
88
- command += " --tags";
86
+ args.push("--tags");
89
87
  }
90
88
  if (input.all) {
91
- command += " --all";
89
+ args.push("--all");
92
90
  }
93
- else if (safeRemote) {
94
- command += ` ${safeRemote}`; // Fetch specific remote if 'all' is not used
91
+ else if (input.remote) {
92
+ args.push(input.remote); // Fetch specific remote if 'all' is not used
95
93
  }
96
94
  // If neither 'all' nor 'remote' is specified, git fetch defaults to 'origin' or configured upstream.
97
- logger.debug(`Executing command: ${command}`, { ...context, operation });
95
+ logger.debug(`Executing command: git ${args.join(" ")}`, {
96
+ ...context,
97
+ operation,
98
+ });
98
99
  // Execute command. Fetch output is primarily on stderr.
99
- const { stdout, stderr } = await execAsync(command);
100
+ const { stdout, stderr } = await execFileAsync("git", args);
100
101
  logger.debug(`Git fetch stdout: ${stdout}`, { ...context, operation }); // stdout is usually empty
101
102
  if (stderr) {
102
103
  logger.debug(`Git fetch stderr: ${stderr}`, { ...context, operation }); // stderr contains fetch details
@@ -1,4 +1,4 @@
1
- import { exec } from "child_process";
1
+ import { execFile } from "child_process";
2
2
  import fs from "fs/promises";
3
3
  import path from "path";
4
4
  import { promisify } from "util";
@@ -9,7 +9,7 @@ import { logger } from "../../../utils/index.js";
9
9
  import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Keep direct import for types-global
10
10
  // Import utils from barrel (sanitization from ../utils/security/sanitization.js)
11
11
  import { sanitization } from "../../../utils/index.js";
12
- const execAsync = promisify(exec);
12
+ const execFileAsync = promisify(execFile);
13
13
  // Define the input schema for the git_init tool using Zod
14
14
  export const GitInitInputSchema = z.object({
15
15
  path: z
@@ -83,20 +83,23 @@ export async function gitInitLogic(input, context) {
83
83
  }
84
84
  try {
85
85
  // Construct the git init command
86
- let command = `git init`;
86
+ const args = ["init"];
87
87
  if (input.quiet) {
88
- command += " --quiet";
88
+ args.push("--quiet");
89
89
  }
90
90
  if (input.bare) {
91
- command += " --bare";
91
+ args.push("--bare");
92
92
  }
93
93
  // Determine the initial branch name, defaulting to 'main' if not provided
94
94
  const branchNameToUse = input.initialBranch || "main";
95
- command += ` -b "${branchNameToUse.replace(/"/g, '\\"')}"`;
95
+ args.push("-b", branchNameToUse);
96
96
  // Add the target directory path at the end
97
- command += ` "${targetPath}"`;
98
- logger.debug(`Executing command: ${command}`, { ...context, operation });
99
- const { stdout, stderr } = await execAsync(command);
97
+ args.push(targetPath);
98
+ logger.debug(`Executing command: git ${args.join(" ")}`, {
99
+ ...context,
100
+ operation,
101
+ });
102
+ const { stdout, stderr } = await execFileAsync("git", args);
100
103
  if (stderr && !input.quiet) {
101
104
  // Log stderr as warning but proceed, as init might still succeed (e.g., reinitializing)
102
105
  logger.warning(`Git init command produced stderr`, {
@@ -1,4 +1,4 @@
1
- import { exec } from "child_process";
1
+ import { execFile } from "child_process";
2
2
  import { promisify } from "util";
3
3
  import { z } from "zod";
4
4
  // Import utils from barrel (logger from ../utils/internal/logger.js)
@@ -7,7 +7,7 @@ import { logger } from "../../../utils/index.js";
7
7
  import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Keep direct import for types-global
8
8
  // Import utils from barrel (sanitization from ../utils/security/sanitization.js)
9
9
  import { sanitization } from "../../../utils/index.js";
10
- const execAsync = promisify(exec);
10
+ const execFileAsync = promisify(execFile);
11
11
  // Define the structure for a single commit entry
12
12
  export const CommitEntrySchema = z.object({
13
13
  hash: z.string().describe("Full commit hash"),
@@ -107,53 +107,40 @@ export async function logGitHistory(input, context) {
107
107
  throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
108
108
  }
109
109
  try {
110
- let command;
110
+ const args = ["-C", targetPath, "log"];
111
111
  let isRawOutput = false; // Flag to indicate if we should parse or return raw
112
112
  if (input.showSignature) {
113
113
  isRawOutput = true;
114
- command = `git -C "${targetPath}" log --show-signature`;
114
+ args.push("--show-signature");
115
115
  logger.info("Show signature requested, returning raw output.", {
116
116
  ...context,
117
117
  operation,
118
118
  });
119
- // Append other filters if provided
120
- if (input.maxCount)
121
- command += ` -n ${input.maxCount}`;
122
- if (input.author)
123
- command += ` --author="${input.author.replace(/[`"$&;*()|<>]/g, "")}"`;
124
- if (input.since)
125
- command += ` --since="${input.since.replace(/[`"$&;*()|<>]/g, "")}"`;
126
- if (input.until)
127
- command += ` --until="${input.until.replace(/[`"$&;*()|<>]/g, "")}"`;
128
- if (input.branchOrFile)
129
- command += ` ${input.branchOrFile.replace(/[`"$&;*()|<>]/g, "")}`;
130
119
  }
131
120
  else {
132
- // Construct the git log command with the fixed format for parsing
133
- command = `git -C "${targetPath}" log ${GIT_LOG_FORMAT}`;
134
- if (input.maxCount)
135
- command += ` -n ${input.maxCount}`;
121
+ args.push(GIT_LOG_FORMAT);
122
+ }
123
+ if (input.maxCount) {
124
+ args.push(`-n${input.maxCount}`);
136
125
  }
137
126
  if (input.author) {
138
- // Basic sanitization for author string
139
- const safeAuthor = input.author.replace(/[`"$&;*()|<>]/g, "");
140
- command += ` --author="${safeAuthor}"`;
127
+ args.push(`--author=${input.author}`);
141
128
  }
142
129
  if (input.since) {
143
- const safeSince = input.since.replace(/[`"$&;*()|<>]/g, "");
144
- command += ` --since="${safeSince}"`;
130
+ args.push(`--since=${input.since}`);
145
131
  }
146
132
  if (input.until) {
147
- const safeUntil = input.until.replace(/[`"$&;*()|<>]/g, "");
148
- command += ` --until="${safeUntil}"`;
133
+ args.push(`--until=${input.until}`);
149
134
  }
150
135
  if (input.branchOrFile) {
151
- const safeBranchOrFile = input.branchOrFile.replace(/[`"$&;*()|<>]/g, "");
152
- command += ` ${safeBranchOrFile}`; // Add branch or file path at the end
136
+ args.push(input.branchOrFile);
153
137
  }
154
- logger.debug(`Executing command: ${command}`, { ...context, operation });
138
+ logger.debug(`Executing command: git ${args.join(" ")}`, {
139
+ ...context,
140
+ operation,
141
+ });
155
142
  // Increase maxBuffer if logs can be large
156
- const { stdout, stderr } = await execAsync(command, {
143
+ const { stdout, stderr } = await execFileAsync("git", args, {
157
144
  maxBuffer: 1024 * 1024 * 10,
158
145
  }); // 10MB buffer
159
146
  if (stderr) {
@@ -1,4 +1,4 @@
1
- import { exec } from "child_process";
1
+ import { execFile } from "child_process";
2
2
  import { promisify } from "util";
3
3
  import { z } from "zod";
4
4
  // Import utils from barrel (logger from ../utils/internal/logger.js)
@@ -8,7 +8,7 @@ import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Ke
8
8
  // Import utils from barrel (sanitization from ../utils/security/sanitization.js)
9
9
  import path from "path"; // Import path module
10
10
  import { sanitization } from "../../../utils/index.js";
11
- const execAsync = promisify(exec);
11
+ const execFileAsync = promisify(execFile);
12
12
  // Define the input schema for the git_merge tool
13
13
  export const GitMergeInputSchema = z.object({
14
14
  path: z
@@ -107,29 +107,34 @@ export async function gitMergeLogic(input, context) {
107
107
  throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
108
108
  }
109
109
  // --- Construct the git merge command ---
110
- let command = `git -C "${targetPath}" merge`;
110
+ const args = ["-C", targetPath, "merge"];
111
111
  if (input.abort) {
112
- command += " --abort";
112
+ args.push("--abort");
113
113
  }
114
114
  else {
115
115
  // Standard merge options
116
- if (input.noFf)
117
- command += " --no-ff";
118
- if (input.squash)
119
- command += " --squash";
116
+ if (input.noFf) {
117
+ args.push("--no-ff");
118
+ }
119
+ if (input.squash) {
120
+ args.push("--squash");
121
+ }
120
122
  if (input.commitMessage && !input.squash) {
121
123
  // Commit message only relevant if not squashing (squash requires separate commit)
122
- command += ` -m "${input.commitMessage.replace(/"/g, '\\"')}"`;
124
+ args.push("-m", input.commitMessage);
123
125
  }
124
126
  else if (input.squash && input.commitMessage) {
125
127
  logger.warning("Commit message provided with --squash, but it will be ignored. Squash requires a separate commit.", { ...context, operation });
126
128
  }
127
- command += ` "${input.branch.replace(/"/g, '\\"')}"`; // Add branch to merge
129
+ args.push(input.branch); // Add branch to merge
128
130
  }
129
- logger.debug(`Executing command: ${command}`, { ...context, operation });
131
+ logger.debug(`Executing command: git ${args.join(" ")}`, {
132
+ ...context,
133
+ operation,
134
+ });
130
135
  // --- Execute and Parse ---
131
136
  try {
132
- const { stdout, stderr } = await execAsync(command);
137
+ const { stdout, stderr } = await execFileAsync("git", args);
133
138
  logger.debug(`Command stdout: ${stdout}`, { ...context, operation });
134
139
  if (stderr)
135
140
  logger.debug(`Command stderr: ${stderr}`, { ...context, operation }); // Log stderr even on success
@@ -1,4 +1,4 @@
1
- import { exec } from "child_process";
1
+ import { execFile } from "child_process";
2
2
  import { promisify } from "util";
3
3
  import { z } from "zod";
4
4
  // Import utils from barrel (logger from ../utils/internal/logger.js)
@@ -7,7 +7,7 @@ import { logger } from "../../../utils/index.js";
7
7
  import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Keep direct import for types-global
8
8
  // Import utils from barrel (sanitization from ../utils/security/sanitization.js)
9
9
  import { sanitization } from "../../../utils/index.js";
10
- const execAsync = promisify(exec);
10
+ const execFileAsync = promisify(execFile);
11
11
  // Define the input schema for the git_pull tool using Zod
12
12
  export const GitPullInputSchema = z.object({
13
13
  path: z
@@ -91,30 +91,29 @@ export async function pullGitChanges(input, context) {
91
91
  }
92
92
  try {
93
93
  // Construct the git pull command
94
- let command = `git -C "${targetPath}" pull`;
94
+ const args = ["-C", targetPath, "pull"];
95
95
  if (input.rebase) {
96
- command += " --rebase";
96
+ args.push("--rebase");
97
97
  }
98
98
  if (input.ffOnly) {
99
- command += " --ff-only";
99
+ args.push("--ff-only");
100
100
  }
101
101
  if (input.remote) {
102
- // Sanitize remote and branch names - basic alphanumeric + common chars
103
- const safeRemote = input.remote.replace(/[^a-zA-Z0-9_.\-/]/g, "");
104
- command += ` ${safeRemote}`;
102
+ args.push(input.remote);
105
103
  if (input.branch) {
106
- const safeBranch = input.branch.replace(/[^a-zA-Z0-9_.\-/]/g, "");
107
- command += ` ${safeBranch}`;
104
+ args.push(input.branch);
108
105
  }
109
106
  }
110
107
  else if (input.branch) {
111
108
  // If only branch is specified, assume 'origin' or tracked remote
112
- const safeBranch = input.branch.replace(/[^a-zA-Z0-9_.\-/]/g, "");
113
- command += ` origin ${safeBranch}`; // Defaulting to origin if remote not specified but branch is
109
+ args.push("origin", input.branch); // Defaulting to origin if remote not specified but branch is
114
110
  logger.warning(`Remote not specified, defaulting to 'origin' for branch pull`, { ...context, operation });
115
111
  }
116
- logger.debug(`Executing command: ${command}`, { ...context, operation });
117
- const { stdout, stderr } = await execAsync(command);
112
+ logger.debug(`Executing command: git ${args.join(" ")}`, {
113
+ ...context,
114
+ operation,
115
+ });
116
+ const { stdout, stderr } = await execFileAsync("git", args);
118
117
  logger.debug(`Git pull stdout: ${stdout}`, { ...context, operation });
119
118
  if (stderr) {
120
119
  logger.debug(`Git pull stderr: ${stderr}`, { ...context, operation });
@@ -1,4 +1,4 @@
1
- import { exec } from "child_process";
1
+ import { execFile } from "child_process";
2
2
  import { promisify } from "util";
3
3
  import { z } from "zod";
4
4
  // Import utils from barrel (logger from ../utils/internal/logger.js)
@@ -7,7 +7,7 @@ import { logger } from "../../../utils/index.js";
7
7
  import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Keep direct import for types-global
8
8
  // Import utils from barrel (sanitization from ../utils/security/sanitization.js)
9
9
  import { sanitization } from "../../../utils/index.js";
10
- const execAsync = promisify(exec);
10
+ const execFileAsync = promisify(execFile);
11
11
  // Define the input schema for the git_push tool using Zod
12
12
  export const GitPushInputSchema = z.object({
13
13
  path: z
@@ -111,45 +111,43 @@ export async function pushGitChanges(input, context) {
111
111
  }
112
112
  try {
113
113
  // Construct the git push command
114
- let command = `git -C "${targetPath}" push`;
114
+ const args = ["-C", targetPath, "push"];
115
115
  if (input.force) {
116
- command += " --force";
116
+ args.push("--force");
117
117
  }
118
118
  else if (input.forceWithLease) {
119
- command += " --force-with-lease";
119
+ args.push("--force-with-lease");
120
120
  }
121
121
  if (input.setUpstream) {
122
- command += " --set-upstream";
122
+ args.push("--set-upstream");
123
123
  }
124
124
  if (input.tags) {
125
- command += " --tags";
125
+ args.push("--tags");
126
126
  }
127
127
  if (input.delete) {
128
- command += " --delete";
128
+ args.push("--delete");
129
129
  }
130
130
  // Add remote and branch specification
131
- const remote = input.remote
132
- ? input.remote.replace(/[^a-zA-Z0-9_.\-/]/g, "")
133
- : "origin"; // Default to origin
134
- command += ` ${remote}`;
131
+ const remote = input.remote || "origin"; // Default to origin
132
+ args.push(remote);
135
133
  if (input.branch) {
136
- const localBranch = input.branch.replace(/[^a-zA-Z0-9_.\-/]/g, "");
137
- command += ` ${localBranch}`;
138
134
  if (input.remoteBranch && !input.delete) {
139
- // remoteBranch only makes sense if not deleting
140
- const remoteBranch = input.remoteBranch.replace(/[^a-zA-Z0-9_.\-/]/g, "");
141
- command += `:${remoteBranch}`;
135
+ args.push(`${input.branch}:${input.remoteBranch}`);
136
+ }
137
+ else {
138
+ args.push(input.branch);
142
139
  }
143
140
  }
144
141
  else if (!input.tags && !input.delete) {
145
142
  // If no branch, tags, or delete specified, push the current branch by default
146
- // Git might handle this automatically, but being explicit can be clearer
147
- // command += ' HEAD'; // Or let git figure out the default push behavior
148
143
  logger.debug("No specific branch, tags, or delete specified. Relying on default git push behavior for current branch.", { ...context, operation });
149
144
  }
150
- logger.debug(`Executing command: ${command}`, { ...context, operation });
145
+ logger.debug(`Executing command: git ${args.join(" ")}`, {
146
+ ...context,
147
+ operation,
148
+ });
151
149
  // Execute command. Note: Git push often uses stderr for progress and success messages.
152
- const { stdout, stderr } = await execAsync(command);
150
+ const { stdout, stderr } = await execFileAsync("git", args);
153
151
  logger.debug(`Git push stdout: ${stdout}`, { ...context, operation });
154
152
  if (stderr) {
155
153
  logger.debug(`Git push stderr: ${stderr}`, { ...context, operation });
@@ -1,4 +1,4 @@
1
- import { exec } from "child_process";
1
+ import { execFile } from "child_process";
2
2
  import { promisify } from "util";
3
3
  import { z } from "zod";
4
4
  // Import utils from barrel (logger from ../utils/internal/logger.js)
@@ -7,7 +7,7 @@ import { logger } from "../../../utils/index.js";
7
7
  import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Keep direct import for types-global
8
8
  // Import utils from barrel (sanitization from ../utils/security/sanitization.js)
9
9
  import { sanitization } from "../../../utils/index.js";
10
- const execAsync = promisify(exec);
10
+ const execFileAsync = promisify(execFile);
11
11
  // Define the BASE input schema for the git_rebase tool using Zod
12
12
  export const GitRebaseBaseSchema = z.object({
13
13
  path: z
@@ -117,39 +117,48 @@ export async function gitRebaseLogic(input, context) {
117
117
  throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
118
118
  }
119
119
  try {
120
- let command = `git -C "${targetPath}" rebase`;
120
+ const args = ["-C", targetPath, "rebase"];
121
121
  switch (input.mode) {
122
122
  case "start":
123
- if (input.interactive)
124
- command += " -i";
125
- if (input.strategy)
126
- command += ` --strategy=${input.strategy}`;
127
- if (input.strategyOption)
128
- command += ` -X${input.strategyOption}`; // Note: -X for strategy options
129
- if (input.onto)
130
- command += ` --onto "${input.onto.replace(/"/g, '\\"')}"`;
123
+ if (input.interactive) {
124
+ args.push("-i");
125
+ }
126
+ if (input.strategy) {
127
+ args.push(`--strategy=${input.strategy}`);
128
+ }
129
+ if (input.strategyOption) {
130
+ args.push(`-X${input.strategyOption}`);
131
+ } // Note: -X for strategy options
132
+ if (input.onto) {
133
+ args.push("--onto", input.onto);
134
+ }
131
135
  // Upstream is required by refine unless interactive
132
- if (input.upstream)
133
- command += ` "${input.upstream.replace(/"/g, '\\"')}"`;
134
- if (input.branch)
135
- command += ` "${input.branch.replace(/"/g, '\\"')}"`;
136
+ if (input.upstream) {
137
+ args.push(input.upstream);
138
+ }
139
+ if (input.branch) {
140
+ args.push(input.branch);
141
+ }
136
142
  break;
137
143
  case "continue":
138
- command += " --continue";
144
+ args.push("--continue");
139
145
  break;
140
146
  case "abort":
141
- command += " --abort";
147
+ args.push("--abort");
142
148
  break;
143
149
  case "skip":
144
- command += " --skip";
150
+ args.push("--skip");
145
151
  break;
146
152
  default:
147
153
  // Should not happen due to Zod validation
148
154
  throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid mode: ${input.mode}`, { context, operation });
149
155
  }
150
- logger.debug(`Executing command: ${command}`, { ...context, operation });
156
+ logger.debug(`Executing command: git ${args.join(" ")}`, {
157
+ ...context,
158
+ operation,
159
+ });
151
160
  try {
152
- const { stdout, stderr } = await execAsync(command);
161
+ const { stdout, stderr } = await execFileAsync("git", args);
153
162
  const output = stdout + stderr;
154
163
  const message = `Rebase ${input.mode} executed successfully. Output: ${output.trim()}`;
155
164
  logger.info(message, { ...context, operation, path: targetPath });
@@ -1,9 +1,9 @@
1
- import { exec } from "child_process";
1
+ import { execFile } from "child_process";
2
2
  import { promisify } from "util";
3
3
  import { z } from "zod";
4
4
  import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Direct import for types-global
5
5
  import { logger, sanitization } from "../../../utils/index.js"; // Logger (./utils/internal/logger.js) & RequestContext (./utils/internal/requestContext.js) & sanitization (./utils/security/sanitization.js)
6
- const execAsync = promisify(exec);
6
+ const execFileAsync = promisify(execFile);
7
7
  // Define the input schema for the git_remote tool using Zod
8
8
  export const GitRemoteInputSchema = z.object({
9
9
  path: z
@@ -79,16 +79,16 @@ export async function gitRemoteLogic(input, context) {
79
79
  throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
80
80
  }
81
81
  try {
82
- let command;
82
+ let args;
83
83
  let result;
84
84
  switch (input.mode) {
85
85
  case "list":
86
- command = `git -C "${targetPath}" remote -v`;
87
- logger.debug(`Executing command: ${command}`, {
86
+ args = ["-C", targetPath, "remote", "-v"];
87
+ logger.debug(`Executing command: git ${args.join(" ")}`, {
88
88
  ...context,
89
89
  operation,
90
90
  });
91
- const { stdout: listStdout } = await execAsync(command);
91
+ const { stdout: listStdout } = await execFileAsync("git", args);
92
92
  const remotes = [];
93
93
  const lines = listStdout.trim().split("\n");
94
94
  const remoteMap = new Map();
@@ -127,12 +127,12 @@ export async function gitRemoteLogic(input, context) {
127
127
  if (!/^[a-zA-Z0-9_.-]+$/.test(input.name)) {
128
128
  throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid remote name: ${input.name}`, { context, operation });
129
129
  }
130
- command = `git -C "${targetPath}" remote add "${input.name}" "${input.url}"`;
131
- logger.debug(`Executing command: ${command}`, {
130
+ args = ["-C", targetPath, "remote", "add", input.name, input.url];
131
+ logger.debug(`Executing command: git ${args.join(" ")}`, {
132
132
  ...context,
133
133
  operation,
134
134
  });
135
- await execAsync(command);
135
+ await execFileAsync("git", args);
136
136
  result = {
137
137
  success: true,
138
138
  mode: "add",
@@ -146,12 +146,12 @@ export async function gitRemoteLogic(input, context) {
146
146
  if (!/^[a-zA-Z0-9_.-]+$/.test(input.name)) {
147
147
  throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid remote name: ${input.name}`, { context, operation });
148
148
  }
149
- command = `git -C "${targetPath}" remote remove "${input.name}"`;
150
- logger.debug(`Executing command: ${command}`, {
149
+ args = ["-C", targetPath, "remote", "remove", input.name];
150
+ logger.debug(`Executing command: git ${args.join(" ")}`, {
151
151
  ...context,
152
152
  operation,
153
153
  });
154
- await execAsync(command);
154
+ await execFileAsync("git", args);
155
155
  result = {
156
156
  success: true,
157
157
  mode: "remove",
@@ -165,12 +165,12 @@ export async function gitRemoteLogic(input, context) {
165
165
  if (!/^[a-zA-Z0-9_.-]+$/.test(input.name)) {
166
166
  throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid remote name: ${input.name}`, { context, operation });
167
167
  }
168
- command = `git -C "${targetPath}" remote show "${input.name}"`;
169
- logger.debug(`Executing command: ${command}`, {
168
+ args = ["-C", targetPath, "remote", "show", input.name];
169
+ logger.debug(`Executing command: git ${args.join(" ")}`, {
170
170
  ...context,
171
171
  operation,
172
172
  });
173
- const { stdout: showStdout } = await execAsync(command);
173
+ const { stdout: showStdout } = await execFileAsync("git", args);
174
174
  result = { success: true, mode: "show", details: showStdout.trim() };
175
175
  break;
176
176
  default: