crosspad-mcp-server 5.1.0 → 6.0.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.
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Serial log capture from a connected CrossPad device.
3
+ *
4
+ * Uses Python/pyserial for direct serial reading — no TTY required.
5
+ * This avoids idf.py monitor's TTY dependency while still using
6
+ * the IDF Python venv (which has pyserial installed).
7
+ *
8
+ * Multi-device aware: requires port when multiple devices connected.
9
+ */
10
+ import { OnLine } from "../utils/exec.js";
11
+ export interface MonitorResult {
12
+ success: boolean;
13
+ port: string;
14
+ duration_seconds: number;
15
+ lines: string[];
16
+ line_count: number;
17
+ truncated: boolean;
18
+ error?: string;
19
+ }
20
+ /**
21
+ * Capture serial logs from a connected CrossPad device.
22
+ *
23
+ * @param port - Serial port (auto-detect if undefined)
24
+ * @param timeoutSeconds - How long to capture (default 10)
25
+ * @param maxLines - Maximum lines to return (default 500)
26
+ * @param filter - Optional string filter — only return lines containing this
27
+ * @param onLine - Streaming callback for real-time output
28
+ */
29
+ export declare function crosspadIdfMonitor(port: string | undefined, timeoutSeconds: number | undefined, maxLines: number | undefined, filter: string | undefined, onLine?: OnLine): Promise<MonitorResult>;
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Serial log capture from a connected CrossPad device.
3
+ *
4
+ * Uses Python/pyserial for direct serial reading — no TTY required.
5
+ * This avoids idf.py monitor's TTY dependency while still using
6
+ * the IDF Python venv (which has pyserial installed).
7
+ *
8
+ * Multi-device aware: requires port when multiple devices connected.
9
+ */
10
+ import { IS_WINDOWS } from "../config.js";
11
+ import { getIdfEnv } from "../utils/exec.js";
12
+ import { findCrosspadPort } from "../utils/device.js";
13
+ import { spawn } from "child_process";
14
+ /**
15
+ * Build a Python script that reads from serial port for a given duration.
16
+ * Uses pyserial (available in IDF venv). No TTY dependency.
17
+ */
18
+ function buildSerialReaderScript(port, timeoutSeconds) {
19
+ // Escape for embedding in shell command
20
+ const escapedPort = port.replace(/'/g, "\\'");
21
+ return [
22
+ "import serial, sys, time",
23
+ `ser = serial.Serial('${escapedPort}', 115200, timeout=1)`,
24
+ `end = time.time() + ${timeoutSeconds}`,
25
+ "try:",
26
+ " while time.time() < end:",
27
+ " line = ser.readline()",
28
+ " if line:",
29
+ " text = line.decode('utf-8', errors='replace').rstrip()",
30
+ " if text:",
31
+ " print(text, flush=True)",
32
+ "except KeyboardInterrupt:",
33
+ " pass",
34
+ "finally:",
35
+ " ser.close()",
36
+ ].join("\n");
37
+ }
38
+ /**
39
+ * Capture serial logs from a connected CrossPad device.
40
+ *
41
+ * @param port - Serial port (auto-detect if undefined)
42
+ * @param timeoutSeconds - How long to capture (default 10)
43
+ * @param maxLines - Maximum lines to return (default 500)
44
+ * @param filter - Optional string filter — only return lines containing this
45
+ * @param onLine - Streaming callback for real-time output
46
+ */
47
+ export async function crosspadIdfMonitor(port, timeoutSeconds = 10, maxLines = 500, filter, onLine) {
48
+ const startTime = Date.now();
49
+ // Resolve port
50
+ const resolved = findCrosspadPort(port);
51
+ if (resolved.error) {
52
+ return {
53
+ success: false,
54
+ port: "",
55
+ duration_seconds: 0,
56
+ lines: [],
57
+ line_count: 0,
58
+ truncated: false,
59
+ error: resolved.error,
60
+ };
61
+ }
62
+ const targetPort = resolved.port;
63
+ onLine?.("stdout", `[idf-monitor] Capturing from ${targetPort} for ${timeoutSeconds}s...`);
64
+ // Get IDF environment (for Python with pyserial)
65
+ let env;
66
+ try {
67
+ env = getIdfEnv();
68
+ }
69
+ catch (err) {
70
+ return {
71
+ success: false,
72
+ port: targetPort,
73
+ duration_seconds: 0,
74
+ lines: [],
75
+ line_count: 0,
76
+ truncated: false,
77
+ error: `Failed to initialize IDF environment: ${err.message}`,
78
+ };
79
+ }
80
+ const script = buildSerialReaderScript(targetPort, timeoutSeconds);
81
+ const shell = IS_WINDOWS ? "cmd.exe" : "/bin/bash";
82
+ return new Promise((resolve) => {
83
+ const lines = [];
84
+ let totalLines = 0;
85
+ let truncated = false;
86
+ let stdoutBuf = "";
87
+ // Run Python script with IDF env (has pyserial on PATH)
88
+ const child = spawn("python3", ["-c", script], {
89
+ env,
90
+ shell: false,
91
+ stdio: ["pipe", "pipe", "pipe"],
92
+ windowsHide: true,
93
+ });
94
+ // Safety kill: slightly after the Python timeout
95
+ const timer = setTimeout(() => {
96
+ onLine?.("stdout", `[idf-monitor] Timeout reached (${timeoutSeconds}s), stopping...`);
97
+ child.kill("SIGTERM");
98
+ setTimeout(() => {
99
+ if (!child.killed)
100
+ child.kill("SIGKILL");
101
+ }, 2000);
102
+ }, (timeoutSeconds + 3) * 1000);
103
+ function processLine(line) {
104
+ // Strip ANSI escape codes
105
+ const clean = line.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/\r/g, "");
106
+ if (!clean.trim())
107
+ return;
108
+ totalLines++;
109
+ // Apply filter if specified
110
+ if (filter && !clean.toLowerCase().includes(filter.toLowerCase())) {
111
+ return;
112
+ }
113
+ if (lines.length < maxLines) {
114
+ lines.push(clean);
115
+ onLine?.("stdout", clean);
116
+ }
117
+ else {
118
+ truncated = true;
119
+ }
120
+ }
121
+ child.stdout?.on("data", (chunk) => {
122
+ stdoutBuf += chunk.toString();
123
+ const parts = stdoutBuf.split("\n");
124
+ for (let i = 0; i < parts.length - 1; i++) {
125
+ processLine(parts[i]);
126
+ }
127
+ stdoutBuf = parts[parts.length - 1];
128
+ });
129
+ child.stderr?.on("data", (chunk) => {
130
+ const text = chunk.toString();
131
+ for (const sline of text.split("\n")) {
132
+ if (sline.trim()) {
133
+ onLine?.("stderr", sline.replace(/\r/g, ""));
134
+ }
135
+ }
136
+ });
137
+ child.on("close", (code) => {
138
+ clearTimeout(timer);
139
+ if (stdoutBuf.trim()) {
140
+ processLine(stdoutBuf);
141
+ }
142
+ const duration = (Date.now() - startTime) / 1000;
143
+ onLine?.("stdout", `[idf-monitor] Captured ${lines.length} lines in ${duration.toFixed(1)}s`);
144
+ resolve({
145
+ success: code === 0 || lines.length > 0,
146
+ port: targetPort,
147
+ duration_seconds: duration,
148
+ lines,
149
+ line_count: totalLines,
150
+ truncated,
151
+ });
152
+ });
153
+ child.on("error", (err) => {
154
+ clearTimeout(timer);
155
+ const duration = (Date.now() - startTime) / 1000;
156
+ resolve({
157
+ success: false,
158
+ port: targetPort,
159
+ duration_seconds: duration,
160
+ lines,
161
+ line_count: totalLines,
162
+ truncated,
163
+ error: err.message,
164
+ });
165
+ });
166
+ });
167
+ }
168
+ //# sourceMappingURL=idf-monitor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idf-monitor.js","sourceRoot":"","sources":["../../src/tools/idf-monitor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Send MIDI events to the CrossPad simulator via TCP remote control.
3
+ *
4
+ * Supports: note_on, note_off, cc (control change), program_change.
5
+ * Uses the same remote control protocol as other sim commands.
6
+ */
7
+ export type MidiEventType = "note_on" | "note_off" | "cc" | "program_change";
8
+ export interface MidiSendParams {
9
+ type: MidiEventType;
10
+ channel: number;
11
+ note?: number;
12
+ velocity?: number;
13
+ cc_num?: number;
14
+ value?: number;
15
+ program?: number;
16
+ }
17
+ export interface MidiSendResult {
18
+ success: boolean;
19
+ type: MidiEventType;
20
+ channel: number;
21
+ details: Record<string, number>;
22
+ error?: string;
23
+ }
24
+ /**
25
+ * Send a MIDI event to the running simulator.
26
+ */
27
+ export declare function crosspadMidiSend(params: MidiSendParams): Promise<MidiSendResult>;
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Send MIDI events to the CrossPad simulator via TCP remote control.
3
+ *
4
+ * Supports: note_on, note_off, cc (control change), program_change.
5
+ * Uses the same remote control protocol as other sim commands.
6
+ */
7
+ import { sendRemoteCommand, isSimulatorRunning } from "../utils/remote-client.js";
8
+ /**
9
+ * Send a MIDI event to the running simulator.
10
+ */
11
+ export async function crosspadMidiSend(params) {
12
+ const running = await isSimulatorRunning();
13
+ if (!running) {
14
+ return {
15
+ success: false,
16
+ type: params.type,
17
+ channel: params.channel,
18
+ details: {},
19
+ error: "Simulator is not running. Use crosspad_build action=pc_run to start it.",
20
+ };
21
+ }
22
+ // Validate channel
23
+ if (params.channel < 0 || params.channel > 15) {
24
+ return {
25
+ success: false,
26
+ type: params.type,
27
+ channel: params.channel,
28
+ details: {},
29
+ error: "MIDI channel must be 0-15",
30
+ };
31
+ }
32
+ let cmd;
33
+ const details = { channel: params.channel };
34
+ switch (params.type) {
35
+ case "note_on": {
36
+ const note = params.note ?? 60;
37
+ const velocity = params.velocity ?? 127;
38
+ if (note < 0 || note > 127) {
39
+ return { success: false, type: params.type, channel: params.channel, details, error: "Note must be 0-127" };
40
+ }
41
+ if (velocity < 0 || velocity > 127) {
42
+ return { success: false, type: params.type, channel: params.channel, details, error: "Velocity must be 0-127" };
43
+ }
44
+ cmd = { cmd: "midi", type: "note_on", channel: params.channel, note, velocity };
45
+ details.note = note;
46
+ details.velocity = velocity;
47
+ break;
48
+ }
49
+ case "note_off": {
50
+ const note = params.note ?? 60;
51
+ const velocity = params.velocity ?? 0;
52
+ if (note < 0 || note > 127) {
53
+ return { success: false, type: params.type, channel: params.channel, details, error: "Note must be 0-127" };
54
+ }
55
+ cmd = { cmd: "midi", type: "note_off", channel: params.channel, note, velocity };
56
+ details.note = note;
57
+ details.velocity = velocity;
58
+ break;
59
+ }
60
+ case "cc": {
61
+ const ccNum = params.cc_num ?? 0;
62
+ const value = params.value ?? 0;
63
+ if (ccNum < 0 || ccNum > 127) {
64
+ return { success: false, type: params.type, channel: params.channel, details, error: "CC number must be 0-127" };
65
+ }
66
+ if (value < 0 || value > 127) {
67
+ return { success: false, type: params.type, channel: params.channel, details, error: "Value must be 0-127" };
68
+ }
69
+ cmd = { cmd: "midi", type: "cc", channel: params.channel, cc: ccNum, value };
70
+ details.cc = ccNum;
71
+ details.value = value;
72
+ break;
73
+ }
74
+ case "program_change": {
75
+ const program = params.program ?? 0;
76
+ if (program < 0 || program > 127) {
77
+ return { success: false, type: params.type, channel: params.channel, details, error: "Program must be 0-127" };
78
+ }
79
+ cmd = { cmd: "midi", type: "program_change", channel: params.channel, program };
80
+ details.program = program;
81
+ break;
82
+ }
83
+ default:
84
+ return {
85
+ success: false,
86
+ type: params.type,
87
+ channel: params.channel,
88
+ details: {},
89
+ error: `Unknown MIDI event type: ${params.type}`,
90
+ };
91
+ }
92
+ try {
93
+ const resp = await sendRemoteCommand(cmd);
94
+ return {
95
+ success: resp.ok === true,
96
+ type: params.type,
97
+ channel: params.channel,
98
+ details,
99
+ error: resp.ok ? undefined : resp.error ?? "Simulator rejected MIDI command",
100
+ };
101
+ }
102
+ catch (err) {
103
+ return {
104
+ success: false,
105
+ type: params.type,
106
+ channel: params.channel,
107
+ details,
108
+ error: err.message,
109
+ };
110
+ }
111
+ }
112
+ //# sourceMappingURL=midi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"midi.js","sourceRoot":"","sources":["../../src/tools/midi.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Mutable repo operations: submodule update and commit.
3
+ *
4
+ * These are intentionally separate from repos.ts (read-only status)
5
+ * to make the mutation surface explicit.
6
+ */
7
+ export interface SubmoduleUpdateResult {
8
+ success: boolean;
9
+ submodule: string;
10
+ repo: string;
11
+ old_sha: string | null;
12
+ new_sha: string | null;
13
+ commits_pulled: number;
14
+ changed_files: string[];
15
+ staged: boolean;
16
+ error?: string;
17
+ }
18
+ export interface CommitResult {
19
+ success: boolean;
20
+ repo: string;
21
+ commit_hash: string | null;
22
+ message: string;
23
+ files_committed: string[];
24
+ error?: string;
25
+ }
26
+ /**
27
+ * Update a submodule in a parent repo to the latest commit on its tracking branch.
28
+ *
29
+ * Workflow:
30
+ * 1. cd into submodule
31
+ * 2. git fetch origin
32
+ * 3. git checkout origin/<branch> (default: main)
33
+ * 4. cd back to parent, git add <submodule>
34
+ * 5. Report old→new SHA, commits pulled, files changed
35
+ */
36
+ export declare function crosspadSubmoduleUpdate(submodule: string, repo: string, branch?: string): SubmoduleUpdateResult;
37
+ /**
38
+ * Commit changes in a specific repo.
39
+ *
40
+ * Safety:
41
+ * - Refuses if working tree has merge conflicts
42
+ * - If files specified, stages only those files
43
+ * - If no files specified, commits whatever is currently staged
44
+ * - Never pushes to remote
45
+ */
46
+ export declare function crosspadCommit(repo: string, message: string, files?: string[]): CommitResult;
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Mutable repo operations: submodule update and commit.
3
+ *
4
+ * These are intentionally separate from repos.ts (read-only status)
5
+ * to make the mutation surface explicit.
6
+ */
7
+ import fs from "fs";
8
+ import path from "path";
9
+ import { getRepos } from "../config.js";
10
+ import { runCommand } from "../utils/exec.js";
11
+ import { getHead } from "../utils/git.js";
12
+ const REPO_ALIASES = {
13
+ idf: "platform-idf",
14
+ pc: "crosspad-pc",
15
+ arduino: "ESP32-S3",
16
+ core: "crosspad-core",
17
+ gui: "crosspad-gui",
18
+ };
19
+ function resolveRepo(repo) {
20
+ const repos = getRepos();
21
+ const canonical = REPO_ALIASES[repo] ?? repo;
22
+ if (repos[canonical]) {
23
+ return { name: canonical, root: repos[canonical] };
24
+ }
25
+ // Try partial match
26
+ for (const [name, repoPath] of Object.entries(repos)) {
27
+ if (name.toLowerCase().includes(repo.toLowerCase())) {
28
+ return { name, root: repoPath };
29
+ }
30
+ }
31
+ return null;
32
+ }
33
+ function getAvailableRepoNames() {
34
+ return Object.keys(getRepos());
35
+ }
36
+ // ═══════════════════════════════════════════════════════════════════════
37
+ // SUBMODULE PATHS — where each submodule lives inside parent repos
38
+ // ═══════════════════════════════════════════════════════════════════════
39
+ const SUBMODULE_PATHS = {
40
+ "platform-idf": {
41
+ "crosspad-core": "components/crosspad-core",
42
+ "crosspad-gui": "components/crosspad-gui",
43
+ "crosspad-instructions": "components/crosspad-instructions",
44
+ "crosspad-sampler": "components/crosspad-sampler",
45
+ },
46
+ "crosspad-pc": {
47
+ "crosspad-core": "crosspad-core",
48
+ "crosspad-gui": "crosspad-gui",
49
+ },
50
+ "ESP32-S3": {
51
+ "crosspad-core": "lib/crosspad-core",
52
+ "crosspad-gui": "lib/crosspad-gui",
53
+ },
54
+ };
55
+ function getSubmodulePath(repoName, submodule) {
56
+ return SUBMODULE_PATHS[repoName]?.[submodule] ?? null;
57
+ }
58
+ // ═══════════════════════════════════════════════════════════════════════
59
+ // SUBMODULE UPDATE
60
+ // ═══════════════════════════════════════════════════════════════════════
61
+ /**
62
+ * Update a submodule in a parent repo to the latest commit on its tracking branch.
63
+ *
64
+ * Workflow:
65
+ * 1. cd into submodule
66
+ * 2. git fetch origin
67
+ * 3. git checkout origin/<branch> (default: main)
68
+ * 4. cd back to parent, git add <submodule>
69
+ * 5. Report old→new SHA, commits pulled, files changed
70
+ */
71
+ export function crosspadSubmoduleUpdate(submodule, repo, branch = "main") {
72
+ const resolvedRepo = resolveRepo(repo);
73
+ if (!resolvedRepo) {
74
+ return {
75
+ success: false,
76
+ submodule,
77
+ repo,
78
+ old_sha: null,
79
+ new_sha: null,
80
+ commits_pulled: 0,
81
+ changed_files: [],
82
+ staged: false,
83
+ error: `Unknown repo "${repo}". Available: ${getAvailableRepoNames().join(", ")}`,
84
+ };
85
+ }
86
+ const subPath = getSubmodulePath(resolvedRepo.name, submodule);
87
+ if (!subPath) {
88
+ const knownSubs = Object.keys(SUBMODULE_PATHS[resolvedRepo.name] ?? {});
89
+ return {
90
+ success: false,
91
+ submodule,
92
+ repo: resolvedRepo.name,
93
+ old_sha: null,
94
+ new_sha: null,
95
+ commits_pulled: 0,
96
+ changed_files: [],
97
+ staged: false,
98
+ error: `Submodule "${submodule}" not found in ${resolvedRepo.name}. Known: ${knownSubs.join(", ")}`,
99
+ };
100
+ }
101
+ const fullSubPath = path.join(resolvedRepo.root, subPath);
102
+ if (!fs.existsSync(fullSubPath)) {
103
+ return {
104
+ success: false,
105
+ submodule,
106
+ repo: resolvedRepo.name,
107
+ old_sha: null,
108
+ new_sha: null,
109
+ commits_pulled: 0,
110
+ changed_files: [],
111
+ staged: false,
112
+ error: `Submodule directory not found: ${fullSubPath}`,
113
+ };
114
+ }
115
+ // Get current SHA
116
+ const oldSha = getHead(fullSubPath);
117
+ // Fetch latest
118
+ const fetchResult = runCommand("git fetch origin", fullSubPath, 30_000);
119
+ if (!fetchResult.success) {
120
+ return {
121
+ success: false,
122
+ submodule,
123
+ repo: resolvedRepo.name,
124
+ old_sha: oldSha,
125
+ new_sha: null,
126
+ commits_pulled: 0,
127
+ changed_files: [],
128
+ staged: false,
129
+ error: `git fetch failed: ${fetchResult.stderr}`,
130
+ };
131
+ }
132
+ // Checkout target
133
+ const checkoutResult = runCommand(`git checkout origin/${branch}`, fullSubPath, 15_000);
134
+ if (!checkoutResult.success) {
135
+ return {
136
+ success: false,
137
+ submodule,
138
+ repo: resolvedRepo.name,
139
+ old_sha: oldSha,
140
+ new_sha: null,
141
+ commits_pulled: 0,
142
+ changed_files: [],
143
+ staged: false,
144
+ error: `git checkout origin/${branch} failed: ${checkoutResult.stderr}`,
145
+ };
146
+ }
147
+ // Get new SHA
148
+ const newSha = getHead(fullSubPath);
149
+ // Count commits between old and new
150
+ let commitsPulled = 0;
151
+ let changedFiles = [];
152
+ if (oldSha && newSha && oldSha !== newSha) {
153
+ const countResult = runCommand(`git rev-list --count ${oldSha}..${newSha}`, fullSubPath);
154
+ if (countResult.success) {
155
+ commitsPulled = parseInt(countResult.stdout.trim(), 10) || 0;
156
+ }
157
+ const diffResult = runCommand(`git diff --name-only ${oldSha}..${newSha}`, fullSubPath);
158
+ if (diffResult.success) {
159
+ changedFiles = diffResult.stdout
160
+ .trim()
161
+ .split("\n")
162
+ .filter((l) => l.length > 0);
163
+ }
164
+ }
165
+ // Stage the submodule update in parent repo
166
+ const addResult = runCommand(`git add ${subPath}`, resolvedRepo.root, 10_000);
167
+ return {
168
+ success: true,
169
+ submodule,
170
+ repo: resolvedRepo.name,
171
+ old_sha: oldSha,
172
+ new_sha: newSha,
173
+ commits_pulled: commitsPulled,
174
+ changed_files: changedFiles,
175
+ staged: addResult.success,
176
+ };
177
+ }
178
+ // ═══════════════════════════════════════════════════════════════════════
179
+ // COMMIT
180
+ // ═══════════════════════════════════════════════════════════════════════
181
+ /**
182
+ * Commit changes in a specific repo.
183
+ *
184
+ * Safety:
185
+ * - Refuses if working tree has merge conflicts
186
+ * - If files specified, stages only those files
187
+ * - If no files specified, commits whatever is currently staged
188
+ * - Never pushes to remote
189
+ */
190
+ export function crosspadCommit(repo, message, files) {
191
+ const resolvedRepo = resolveRepo(repo);
192
+ if (!resolvedRepo) {
193
+ return {
194
+ success: false,
195
+ repo,
196
+ commit_hash: null,
197
+ message,
198
+ files_committed: [],
199
+ error: `Unknown repo "${repo}". Available: ${getAvailableRepoNames().join(", ")}`,
200
+ };
201
+ }
202
+ // Check for merge conflicts
203
+ const statusResult = runCommand("git status --porcelain", resolvedRepo.root);
204
+ if (statusResult.success) {
205
+ const conflicted = statusResult.stdout
206
+ .split("\n")
207
+ .filter((l) => l.startsWith("UU") || l.startsWith("AA") || l.startsWith("DD"));
208
+ if (conflicted.length > 0) {
209
+ return {
210
+ success: false,
211
+ repo: resolvedRepo.name,
212
+ commit_hash: null,
213
+ message,
214
+ files_committed: [],
215
+ error: `Merge conflicts detected:\n${conflicted.join("\n")}\nResolve conflicts before committing.`,
216
+ };
217
+ }
218
+ }
219
+ // Stage specific files if provided
220
+ if (files && files.length > 0) {
221
+ const fileList = files.map((f) => `"${f}"`).join(" ");
222
+ const addResult = runCommand(`git add ${fileList}`, resolvedRepo.root, 10_000);
223
+ if (!addResult.success) {
224
+ return {
225
+ success: false,
226
+ repo: resolvedRepo.name,
227
+ commit_hash: null,
228
+ message,
229
+ files_committed: [],
230
+ error: `git add failed: ${addResult.stderr}`,
231
+ };
232
+ }
233
+ }
234
+ // Check something is staged
235
+ const diffResult = runCommand("git diff --cached --name-only", resolvedRepo.root);
236
+ const stagedFiles = diffResult.success
237
+ ? diffResult.stdout.trim().split("\n").filter((l) => l.length > 0)
238
+ : [];
239
+ if (stagedFiles.length === 0) {
240
+ return {
241
+ success: false,
242
+ repo: resolvedRepo.name,
243
+ commit_hash: null,
244
+ message,
245
+ files_committed: [],
246
+ error: "Nothing staged to commit. Stage files first or specify files parameter.",
247
+ };
248
+ }
249
+ // Commit — escape message for shell safety
250
+ const escapedMessage = message.replace(/'/g, "'\\''");
251
+ const commitResult = runCommand(`git commit -m '${escapedMessage}'`, resolvedRepo.root, 30_000);
252
+ if (!commitResult.success) {
253
+ return {
254
+ success: false,
255
+ repo: resolvedRepo.name,
256
+ commit_hash: null,
257
+ message,
258
+ files_committed: stagedFiles,
259
+ error: `git commit failed: ${commitResult.stderr || commitResult.stdout}`,
260
+ };
261
+ }
262
+ // Get the new commit hash
263
+ const hashResult = runCommand("git rev-parse HEAD", resolvedRepo.root);
264
+ const commitHash = hashResult.success ? hashResult.stdout.trim() : null;
265
+ return {
266
+ success: true,
267
+ repo: resolvedRepo.name,
268
+ commit_hash: commitHash,
269
+ message,
270
+ files_committed: stagedFiles,
271
+ };
272
+ }
273
+ //# sourceMappingURL=repo-actions.js.map