@longshot/cli 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,8 @@
1
+ Copyright (c) 2026 Rob Sherrill. All Rights Reserved.
2
+
3
+ This software is proprietary and confidential. No part of this software may be
4
+ reproduced, distributed, or transmitted in any form or by any means, without
5
+ the prior written permission of the copyright holder.
6
+
7
+ Unauthorized copying, modification, distribution, or use of this software,
8
+ via any medium, is strictly prohibited.
package/dist/agent.js ADDED
@@ -0,0 +1,214 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createInterface } from "node:readline";
3
+ export function spawnAgent(task, cwd, timeoutMs = 5 * 60 * 1000, mcpConfig) {
4
+ const args = [
5
+ "@anthropic-ai/claude-code",
6
+ "-p",
7
+ task,
8
+ "--output-format",
9
+ "stream-json",
10
+ "--verbose",
11
+ "--dangerously-skip-permissions",
12
+ ];
13
+ if (mcpConfig) {
14
+ args.push("--mcp-config", JSON.stringify(mcpConfig));
15
+ }
16
+ const proc = spawn("npx", args, {
17
+ cwd,
18
+ stdio: ["ignore", "pipe", "pipe"],
19
+ env: {
20
+ ...process.env,
21
+ CLAUDECODE: undefined, // Clear so it doesn't think it's nested
22
+ },
23
+ });
24
+ let killed = false;
25
+ const timer = setTimeout(() => {
26
+ killed = true;
27
+ proc.kill("SIGTERM");
28
+ }, timeoutMs);
29
+ const kill = () => {
30
+ killed = true;
31
+ clearTimeout(timer);
32
+ proc.kill("SIGTERM");
33
+ };
34
+ async function* events() {
35
+ const rl = createInterface({ input: proc.stdout });
36
+ // Collect stderr as it arrives
37
+ let stderr = "";
38
+ proc.stderr?.on("data", (chunk) => {
39
+ stderr += chunk.toString();
40
+ });
41
+ for await (const line of rl) {
42
+ if (!line.trim())
43
+ continue;
44
+ let parsed;
45
+ try {
46
+ parsed = JSON.parse(line);
47
+ }
48
+ catch {
49
+ continue;
50
+ }
51
+ // stream-json format: each line is a top-level event
52
+ // type: "system" (init), "assistant" (model output), "user" (tool results), "result" (final)
53
+ if (parsed.type === "assistant" && parsed.message?.content) {
54
+ for (const block of parsed.message.content) {
55
+ if (block.type === "text" && block.text) {
56
+ yield { type: "text", content: block.text };
57
+ }
58
+ else if (block.type === "tool_use") {
59
+ yield {
60
+ type: "tool_use",
61
+ name: block.name ?? "unknown",
62
+ id: block.id ?? "",
63
+ input: block.input,
64
+ };
65
+ }
66
+ }
67
+ }
68
+ else if (parsed.type === "user" && parsed.message?.content) {
69
+ // Tool results — extract result content (truncated if large)
70
+ for (const block of parsed.message.content) {
71
+ if (block.type === "tool_result") {
72
+ let content;
73
+ if (typeof block.content === "string") {
74
+ content = block.content.slice(0, 2000);
75
+ }
76
+ else if (Array.isArray(block.content)) {
77
+ const textParts = block.content
78
+ .filter((p) => p.type === "text")
79
+ .map((p) => p.text)
80
+ .join("\n");
81
+ content = textParts.slice(0, 2000);
82
+ }
83
+ yield {
84
+ type: "tool_result",
85
+ name: block.tool_use_id ?? "",
86
+ id: block.tool_use_id ?? "",
87
+ content,
88
+ };
89
+ }
90
+ }
91
+ }
92
+ }
93
+ // Wait for process to exit
94
+ const exitCode = await new Promise((resolve) => {
95
+ if (proc.exitCode !== null) {
96
+ resolve(proc.exitCode);
97
+ }
98
+ else {
99
+ proc.on("close", (code) => resolve(code ?? 1));
100
+ }
101
+ });
102
+ clearTimeout(timer);
103
+ if (killed) {
104
+ yield { type: "error", message: "Agent timed out" };
105
+ }
106
+ if (exitCode !== 0 && stderr) {
107
+ yield { type: "error", message: stderr.slice(0, 2000) };
108
+ }
109
+ yield { type: "done", exitCode };
110
+ }
111
+ return { events: events(), process: proc, kill };
112
+ }
113
+ export async function chatWithClaude(messages, cwd, opts) {
114
+ const timeoutMs = opts?.timeoutMs ?? 60 * 1000;
115
+ // Build conversation context for the prompt
116
+ const contextMessages = messages.slice(-20); // last 20 for context
117
+ const conversationContext = contextMessages
118
+ .map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${m.content}`)
119
+ .join("\n\n");
120
+ const prompt = opts?.systemPrompt
121
+ ? `${opts.systemPrompt}
122
+
123
+ Previous conversation:
124
+ ${conversationContext}
125
+
126
+ Respond to the latest user message. Be concise — the user is reading on a mobile phone.`
127
+ : `You are a helpful assistant for a software project. You can read files and search the codebase to answer questions accurately, but do not modify anything. You have access to Read, Glob, Grep, and LS tools for discovery.
128
+
129
+ Previous conversation:
130
+ ${conversationContext}
131
+
132
+ Respond to the latest user message. Be concise — the user is reading on a mobile phone.`;
133
+ const readOnlyTools = ["Read", "Glob", "Grep", "LS"];
134
+ const args = [
135
+ "@anthropic-ai/claude-code",
136
+ "-p",
137
+ prompt,
138
+ "--output-format",
139
+ "stream-json",
140
+ "--verbose",
141
+ "--allowedTools",
142
+ readOnlyTools.join(","),
143
+ ];
144
+ const proc = spawn("npx", args, {
145
+ cwd,
146
+ stdio: ["ignore", "pipe", "pipe"],
147
+ env: {
148
+ ...process.env,
149
+ CLAUDECODE: undefined,
150
+ },
151
+ });
152
+ let killed = false;
153
+ const timer = setTimeout(() => {
154
+ killed = true;
155
+ proc.kill("SIGTERM");
156
+ }, timeoutMs);
157
+ return new Promise((resolve, reject) => {
158
+ let fullText = "";
159
+ const rl = createInterface({ input: proc.stdout });
160
+ let stderr = "";
161
+ proc.stderr?.on("data", (chunk) => {
162
+ stderr += chunk.toString();
163
+ });
164
+ rl.on("line", (line) => {
165
+ if (!line.trim())
166
+ return;
167
+ try {
168
+ const parsed = JSON.parse(line);
169
+ if (parsed.type === "assistant" && parsed.message?.content) {
170
+ for (const block of parsed.message.content) {
171
+ if (block.type === "text" && block.text) {
172
+ fullText += block.text;
173
+ }
174
+ }
175
+ }
176
+ else if (parsed.type === "result" && parsed.result) {
177
+ // Final result text
178
+ if (!fullText && typeof parsed.result === "string") {
179
+ fullText = parsed.result;
180
+ }
181
+ }
182
+ }
183
+ catch (e) {
184
+ console.error("chatWithClaude: failed to parse JSON line:", e.message, "line:", line.slice(0, 200));
185
+ }
186
+ });
187
+ proc.on("close", (code) => {
188
+ clearTimeout(timer);
189
+ if (stderr) {
190
+ console.error("chatWithClaude stderr:", stderr.slice(0, 2000));
191
+ }
192
+ if (killed) {
193
+ resolve(fullText || "Sorry, the response timed out.");
194
+ }
195
+ else if (fullText) {
196
+ resolve(fullText);
197
+ }
198
+ else {
199
+ console.error(`chatWithClaude: no text output (exit code ${code})`);
200
+ const stderrSummary = stderr.trim().slice(-500);
201
+ if (stderrSummary) {
202
+ resolve(`Error from Claude CLI (exit code ${code}):\n${stderrSummary}`);
203
+ }
204
+ else {
205
+ resolve(`Sorry, I couldn't generate a response (exit code ${code}).`);
206
+ }
207
+ }
208
+ });
209
+ proc.on("error", (err) => {
210
+ clearTimeout(timer);
211
+ reject(err);
212
+ });
213
+ });
214
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from "node:util";
3
+ import { resolve, join, dirname } from "node:path";
4
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
5
+ import { fileURLToPath } from "node:url";
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ function getVersion() {
8
+ // Walk up from src/ or dist/ to find package.json
9
+ const candidates = [
10
+ join(__dirname, "..", "package.json"),
11
+ join(__dirname, "..", "..", "package.json"),
12
+ ];
13
+ for (const p of candidates) {
14
+ if (existsSync(p)) {
15
+ return JSON.parse(readFileSync(p, "utf-8")).version || "unknown";
16
+ }
17
+ }
18
+ return "unknown";
19
+ }
20
+ function printHelp() {
21
+ console.log(`longshot — Mobile-first Claude Code orchestrator
22
+
23
+ Usage: longshot [options]
24
+
25
+ Options:
26
+ -p, --port <number> Port for the web server (default: 3333)
27
+ -d, --dir <path> Project root directory (default: current directory)
28
+ -h, --help Show this help message
29
+ -v, --version Show version number
30
+
31
+ Examples:
32
+ longshot Start in current directory on port 3333
33
+ longshot -p 8080 Start on port 8080
34
+ longshot -d ~/my-project Start with a specific project directory
35
+
36
+ First run:
37
+ If the target directory isn't a longshot project yet, you'll be guided
38
+ through setup. Just run \`longshot\` and follow the prompts.`);
39
+ }
40
+ export function parseCliArgs(args) {
41
+ const { values } = parseArgs({
42
+ args,
43
+ options: {
44
+ port: { type: "string", short: "p" },
45
+ dir: { type: "string", short: "d" },
46
+ help: { type: "boolean", short: "h", default: false },
47
+ version: { type: "boolean", short: "v", default: false },
48
+ },
49
+ strict: true,
50
+ allowPositionals: false,
51
+ });
52
+ return {
53
+ port: values.port ? parseInt(values.port, 10) : 3333,
54
+ dir: values.dir ? resolve(values.dir) : process.cwd(),
55
+ help: values.help ?? false,
56
+ version: values.version ?? false,
57
+ };
58
+ }
59
+ class InquirerPrompter {
60
+ async selectMode() {
61
+ const inquirer = await import("inquirer");
62
+ try {
63
+ const { mode } = await inquirer.default.prompt([
64
+ {
65
+ type: "list",
66
+ name: "mode",
67
+ message: "How would you like to use longshot?",
68
+ choices: [
69
+ { name: "Single project — initialize this directory", value: "single" },
70
+ { name: "Multi-project — manage multiple subdirectories", value: "multi" },
71
+ ],
72
+ },
73
+ ]);
74
+ return mode;
75
+ }
76
+ catch {
77
+ return null;
78
+ }
79
+ }
80
+ async selectFolders(folders) {
81
+ const inquirer = await import("inquirer");
82
+ try {
83
+ const { selected } = await inquirer.default.prompt([
84
+ {
85
+ type: "checkbox",
86
+ name: "selected",
87
+ message: "Select project directories:",
88
+ choices: folders.map((f) => ({ name: f, value: f })),
89
+ },
90
+ ]);
91
+ return selected;
92
+ }
93
+ catch {
94
+ return [];
95
+ }
96
+ }
97
+ }
98
+ export async function runOnboarding(dir, prompter) {
99
+ // Check if it already has .longshot/ (single project)
100
+ if (existsSync(join(dir, ".longshot"))) {
101
+ return { action: "existing" };
102
+ }
103
+ // Check for multi-project mode (parent with projects.json)
104
+ if (existsSync(join(dir, ".longshot", "projects.json"))) {
105
+ return { action: "existing" };
106
+ }
107
+ // Non-interactive (no prompter injected and stdin is not a TTY) — auto-initialize as single project
108
+ if (!prompter && !process.stdin.isTTY) {
109
+ const { initSingleProject } = await import("./projects.js");
110
+ initSingleProject(dir);
111
+ return { action: "single" };
112
+ }
113
+ const prompt = prompter ?? new InquirerPrompter();
114
+ const mode = await prompt.selectMode();
115
+ if (mode === null) {
116
+ console.log('Run `longshot --help` for usage');
117
+ return { action: "cancelled" };
118
+ }
119
+ if (mode === "single") {
120
+ const { initSingleProject } = await import("./projects.js");
121
+ initSingleProject(dir);
122
+ console.log("Initialized longshot project in", dir);
123
+ return { action: "single" };
124
+ }
125
+ if (mode === "multi") {
126
+ // List subdirectories
127
+ const entries = readdirSync(dir, { withFileTypes: true });
128
+ const folders = entries
129
+ .filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules")
130
+ .map((e) => e.name);
131
+ if (folders.length === 0) {
132
+ console.log("No subdirectories found. Create some project folders first.");
133
+ return { action: "cancelled" };
134
+ }
135
+ const selected = await prompt.selectFolders(folders);
136
+ if (selected.length === 0) {
137
+ console.log('Run `longshot --help` for usage');
138
+ return { action: "cancelled" };
139
+ }
140
+ const { initMultiProject } = await import("./projects.js");
141
+ initMultiProject(dir, selected);
142
+ console.log(`Initialized ${selected.length} project(s):`, selected.join(", "));
143
+ return { action: "multi", selected };
144
+ }
145
+ return { action: "cancelled" };
146
+ }
147
+ async function main() {
148
+ const parsed = parseCliArgs(process.argv.slice(2));
149
+ if (parsed.help) {
150
+ printHelp();
151
+ process.exit(0);
152
+ }
153
+ if (parsed.version) {
154
+ console.log(getVersion());
155
+ process.exit(0);
156
+ }
157
+ // Run onboarding if needed
158
+ const result = await runOnboarding(parsed.dir);
159
+ if (result.action === "cancelled") {
160
+ process.exit(0);
161
+ }
162
+ // Set environment for the server
163
+ process.env.PORT = String(parsed.port);
164
+ process.env.PROJECT_ROOT = parsed.dir;
165
+ // Start the server
166
+ const { startServer } = await import("./index.js");
167
+ await startServer();
168
+ }
169
+ main().catch((err) => {
170
+ console.error("Fatal error:", err);
171
+ process.exit(1);
172
+ });
package/dist/git.js ADDED
@@ -0,0 +1,291 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { existsSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ const exec = promisify(execFile);
6
+ async function git(args, cwd) {
7
+ return exec("git", args, { cwd, maxBuffer: 10 * 1024 * 1024 });
8
+ }
9
+ export async function ensureRepo(cwd) {
10
+ if (!existsSync(join(cwd, ".git"))) {
11
+ await git(["init"], cwd);
12
+ // Initial commit so we have a HEAD to diff against
13
+ await git(["commit", "--allow-empty", "-m", "init"], cwd);
14
+ }
15
+ }
16
+ export async function snapshot(cwd) {
17
+ const { stdout } = await git(["rev-parse", "HEAD"], cwd);
18
+ return stdout.trim();
19
+ }
20
+ export async function diff(cwd, fromHash) {
21
+ // Stage everything so we capture new files too
22
+ await git(["add", "-A"], cwd);
23
+ const { stdout } = await git([
24
+ "diff",
25
+ "--cached",
26
+ fromHash,
27
+ "--",
28
+ ".",
29
+ ":!CLAUDE.md",
30
+ ":!.longshot/",
31
+ ], cwd);
32
+ // Unstage so we don't interfere with the working tree
33
+ await git(["reset", "HEAD"], cwd);
34
+ return stdout;
35
+ }
36
+ export async function commit(cwd, message) {
37
+ await git(["add", "-A"], cwd);
38
+ await git(["commit", "-m", message], cwd);
39
+ const { stdout } = await git(["rev-parse", "HEAD"], cwd);
40
+ return stdout.trim();
41
+ }
42
+ export async function reset(cwd, toHash) {
43
+ await git(["reset", "--hard", toHash], cwd);
44
+ await git(["clean", "-fd"], cwd);
45
+ }
46
+ export async function hasUncommittedChanges(cwd) {
47
+ const { stdout } = await git(["status", "--porcelain"], cwd);
48
+ return stdout.trim().length > 0;
49
+ }
50
+ export async function stashSave(cwd, message) {
51
+ const { stdout: statusOut } = await git(["status", "--porcelain"], cwd);
52
+ if (!statusOut.trim())
53
+ return null;
54
+ // Parse changed files, filtering out .longshot/ and CLAUDE.md
55
+ const files = [];
56
+ for (const line of statusOut.split("\n")) {
57
+ if (!line.trim())
58
+ continue;
59
+ // Porcelain format: XY filename (exactly 2 status chars + 1 space + filename)
60
+ const filePart = line.slice(3);
61
+ const file = filePart.includes(" -> ") ? filePart.split(" -> ")[1] : filePart;
62
+ if (!file.startsWith(".longshot/") && file !== "CLAUDE.md") {
63
+ files.push(file);
64
+ }
65
+ }
66
+ if (files.length === 0)
67
+ return null;
68
+ await git(["stash", "push", "--include-untracked", "-m", message, "--", ...files], cwd);
69
+ const { stdout: hashOut } = await git(["rev-parse", "stash@{0}"], cwd);
70
+ return hashOut.trim();
71
+ }
72
+ export async function stashApply(cwd, hash) {
73
+ await git(["stash", "apply", hash], cwd);
74
+ }
75
+ /** Apply stash allowing conflicts — writes conflict markers to disk instead of aborting */
76
+ export async function stashApplyAllowConflicts(cwd, hash) {
77
+ try {
78
+ await git(["stash", "apply", hash], cwd);
79
+ return { conflicts: [] };
80
+ }
81
+ catch (err) {
82
+ // Check if files were partially applied with conflict markers
83
+ const { stdout } = await git(["status", "--porcelain"], cwd);
84
+ const conflicts = [];
85
+ for (const line of stdout.split("\n")) {
86
+ if (!line.trim())
87
+ continue;
88
+ // "UU" = both modified (conflict), "AA" = both added, "DU"/"UD" = delete/modify conflicts
89
+ const status = line.slice(0, 2);
90
+ if (status === "UU" || status === "AA" || status === "DU" || status === "UD") {
91
+ conflicts.push(line.slice(3));
92
+ }
93
+ }
94
+ if (conflicts.length > 0) {
95
+ // Conflicts were written to disk — add conflicted files so they're tracked
96
+ await git(["add", "-A"], cwd);
97
+ return { conflicts };
98
+ }
99
+ // No conflicts detected — the error was something else
100
+ throw err;
101
+ }
102
+ }
103
+ export async function stashDrop(cwd, hash) {
104
+ const { stdout: listOut } = await git(["stash", "list", "--format=%H %gd"], cwd);
105
+ for (const line of listOut.trim().split("\n")) {
106
+ if (!line.trim())
107
+ continue;
108
+ const parts = line.split(" ");
109
+ const stashHash = parts[0];
110
+ const ref = parts[1];
111
+ if (stashHash === hash) {
112
+ await git(["stash", "drop", ref], cwd);
113
+ return;
114
+ }
115
+ }
116
+ // Not found — already dropped, idempotent
117
+ }
118
+ export async function getRemoteStatus(cwd) {
119
+ const { stdout: branchOut } = await git(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
120
+ const branch = branchOut.trim();
121
+ // Check if there's a remote tracking branch
122
+ let remote = null;
123
+ try {
124
+ const { stdout: remoteOut } = await git(["config", "--get", `branch.${branch}.remote`], cwd);
125
+ remote = remoteOut.trim() || null;
126
+ }
127
+ catch {
128
+ return { ahead: 0, behind: 0, remote: null, branch };
129
+ }
130
+ // Fetch latest from remote
131
+ try {
132
+ await git(["fetch", remote], cwd);
133
+ }
134
+ catch {
135
+ // Fetch failed (no network, etc.) — continue with local state
136
+ }
137
+ const upstream = `${remote}/${branch}`;
138
+ let ahead = 0;
139
+ let behind = 0;
140
+ try {
141
+ const { stdout: aheadOut } = await git(["rev-list", "--count", `${upstream}..HEAD`], cwd);
142
+ ahead = parseInt(aheadOut.trim(), 10) || 0;
143
+ const { stdout: behindOut } = await git(["rev-list", "--count", `HEAD..${upstream}`], cwd);
144
+ behind = parseInt(behindOut.trim(), 10) || 0;
145
+ }
146
+ catch {
147
+ // Upstream branch doesn't exist yet — treat as no remote
148
+ return { ahead: 0, behind: 0, remote: null, branch };
149
+ }
150
+ return { ahead, behind, remote, branch };
151
+ }
152
+ export async function getOutgoingCommits(cwd) {
153
+ try {
154
+ const { stdout } = await git(["log", "@{u}..HEAD", "--format=%h %s"], cwd);
155
+ if (!stdout.trim())
156
+ return [];
157
+ return stdout
158
+ .trim()
159
+ .split("\n")
160
+ .map((line) => {
161
+ const spaceIdx = line.indexOf(" ");
162
+ return {
163
+ hash: line.slice(0, spaceIdx),
164
+ subject: line.slice(spaceIdx + 1),
165
+ };
166
+ });
167
+ }
168
+ catch {
169
+ return [];
170
+ }
171
+ }
172
+ export async function push(cwd) {
173
+ try {
174
+ await git(["push"], cwd);
175
+ return { success: true };
176
+ }
177
+ catch (err) {
178
+ return { success: false, error: err.stderr || err.message };
179
+ }
180
+ }
181
+ export async function pull(cwd) {
182
+ try {
183
+ await git(["pull", "--ff-only"], cwd);
184
+ return { success: true };
185
+ }
186
+ catch (err) {
187
+ return { success: false, error: err.stderr || err.message };
188
+ }
189
+ }
190
+ export async function pullRebase(cwd) {
191
+ try {
192
+ await git(["pull", "--rebase"], cwd);
193
+ return { success: true };
194
+ }
195
+ catch (err) {
196
+ return { success: false, error: err.stderr || err.message };
197
+ }
198
+ }
199
+ // --- Branch operations ---
200
+ export async function getCurrentBranch(cwd) {
201
+ const { stdout } = await git(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
202
+ return stdout.trim();
203
+ }
204
+ export async function createBranch(cwd, name) {
205
+ await git(["checkout", "-b", name], cwd);
206
+ }
207
+ export async function switchBranch(cwd, name) {
208
+ await git(["checkout", name], cwd);
209
+ }
210
+ export async function listBranches(cwd) {
211
+ const { stdout } = await git(["branch", "--list"], cwd);
212
+ const branches = [];
213
+ let current = "";
214
+ for (const line of stdout.split("\n")) {
215
+ if (!line.trim())
216
+ continue;
217
+ const isCurrent = line.startsWith("* ");
218
+ const name = line.replace(/^\*?\s+/, "").trim();
219
+ branches.push(name);
220
+ if (isCurrent)
221
+ current = name;
222
+ }
223
+ return { local: branches, current };
224
+ }
225
+ export async function mergeBranch(cwd, source, target) {
226
+ // Check out target branch
227
+ await git(["checkout", target], cwd);
228
+ try {
229
+ await git(["merge", source, "--no-edit"], cwd);
230
+ const { stdout } = await git(["rev-parse", "HEAD"], cwd);
231
+ return { success: true, commitHash: stdout.trim() };
232
+ }
233
+ catch (err) {
234
+ // Check for merge conflicts
235
+ const { stdout: statusOut } = await git(["status", "--porcelain"], cwd);
236
+ const conflicts = [];
237
+ for (const line of statusOut.split("\n")) {
238
+ if (!line.trim())
239
+ continue;
240
+ const status = line.slice(0, 2);
241
+ if (status === "UU" || status === "AA" || status === "DU" || status === "UD") {
242
+ conflicts.push(line.slice(3));
243
+ }
244
+ }
245
+ if (conflicts.length > 0) {
246
+ // Abort the merge
247
+ await git(["merge", "--abort"], cwd);
248
+ return { success: false, conflicts };
249
+ }
250
+ return { success: false, error: err.stderr || err.message };
251
+ }
252
+ }
253
+ export async function deleteBranch(cwd, name) {
254
+ try {
255
+ await git(["branch", "-d", name], cwd);
256
+ return { success: true };
257
+ }
258
+ catch (err) {
259
+ return { success: false, error: err.stderr || err.message };
260
+ }
261
+ }
262
+ export async function pushBranch(cwd, name) {
263
+ try {
264
+ await git(["push", "-u", "origin", name], cwd);
265
+ return { success: true };
266
+ }
267
+ catch (err) {
268
+ return { success: false, error: err.stderr || err.message };
269
+ }
270
+ }
271
+ export async function commitFiles(cwd, message, files) {
272
+ try {
273
+ await git(["add", "--", ...files], cwd);
274
+ }
275
+ catch {
276
+ // Files may not exist — nothing to add
277
+ return null;
278
+ }
279
+ try {
280
+ await git(["commit", "-m", message], cwd);
281
+ }
282
+ catch (err) {
283
+ // "nothing to commit" is fine
284
+ if (err.stderr?.includes("nothing to commit") || err.stdout?.includes("nothing to commit") || err.message?.includes("nothing to commit")) {
285
+ return null;
286
+ }
287
+ throw err;
288
+ }
289
+ const { stdout } = await git(["rev-parse", "HEAD"], cwd);
290
+ return stdout.trim();
291
+ }