@neuralnomads/codenomad-dev 0.13.3-dev-20260413-1ce58b9d → 0.14.0-dev-20260417-9bf4d351

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 (51) hide show
  1. package/dist/index.js +22 -0
  2. package/dist/plugins/voice-mode.js +2 -1
  3. package/dist/server/http-server.js +5 -45
  4. package/dist/server/routes/plugin.js +5 -1
  5. package/dist/server/routes/workspaces.js +118 -0
  6. package/dist/workspaces/git-mutations.js +98 -0
  7. package/dist/workspaces/git-status.js +323 -0
  8. package/dist/workspaces/git-worktrees.js +18 -1
  9. package/dist/workspaces/worktree-directory.js +74 -0
  10. package/package.json +1 -1
  11. package/public/assets/{ChangesTab-CFkk3tAp.js → ChangesTab-DEgBUjVz.js} +2 -2
  12. package/public/assets/{DiffToolbar-CQQLR1mp.js → DiffToolbar-C0C8bWki.js} +1 -1
  13. package/public/assets/{FilesTab-CXgucPtN.js → FilesTab-B9WaUTzy.js} +2 -2
  14. package/public/assets/GitChangesTab-C2Asw384.js +2 -0
  15. package/public/assets/{SplitFilePanel-CupweEA5.js → SplitFilePanel-VxErPoCs.js} +1 -1
  16. package/public/assets/StatusTab-DTCwmZhf.js +1 -0
  17. package/public/assets/{bundle-full-B-5u6Iv3.js → bundle-full-CZ8BwlPh.js} +1 -1
  18. package/public/assets/{diff-viewer-_ehhYA-Z.js → diff-viewer-Djp9avVa.js} +1 -1
  19. package/public/assets/index-Br2gh9Q_.js +1 -0
  20. package/public/assets/index-CCdacqcD.js +1 -0
  21. package/public/assets/index-CMpU59Nd.js +1 -0
  22. package/public/assets/index-CP2CRaiZ.js +2 -0
  23. package/public/assets/{index-CxQr-wOF.js → index-CS8igigR.js} +1 -1
  24. package/public/assets/index-Cf8hOS2M.js +1 -0
  25. package/public/assets/index-CiwaMGVS.js +1 -0
  26. package/public/assets/{index-BwCEJ4BE.css → index-D6aXme8N.css} +1 -1
  27. package/public/assets/index-Dwg_Dz3e.js +1 -0
  28. package/public/assets/{index-BqiNjV6R.js → index-gOb9MvU2.js} +1 -1
  29. package/public/assets/{loading-Dck2H3XH.js → loading-Cr_BWqKy.js} +1 -1
  30. package/public/assets/main-CBLtYtDo.js +48 -0
  31. package/public/assets/{markdown-CFV9kR80.js → markdown-c_rVsd2S.js} +3 -3
  32. package/public/assets/monaco-viewer-DrmF2KUz.js +25 -0
  33. package/public/assets/{todo-Ghwirj-b.js → todo-DXEegYgH.js} +1 -1
  34. package/public/assets/{tool-call-BC2Pv3uG.js → tool-call-CTg71hze.js} +3 -3
  35. package/public/assets/{unified-picker-Dw0kTXoT.js → unified-picker-BgW5kaQo.js} +1 -1
  36. package/public/assets/{wrap-text-D2MJk2ad.js → wrap-text-DKmMMyWB.js} +1 -1
  37. package/public/index.html +4 -4
  38. package/public/loading.html +4 -4
  39. package/public/sw.js +1 -1
  40. package/public/ui-version.json +1 -1
  41. package/public/assets/GitChangesTab-BidSb28_.js +0 -2
  42. package/public/assets/StatusTab-C2Pl_Vce.js +0 -1
  43. package/public/assets/index-BnfDkr8C.js +0 -1
  44. package/public/assets/index-BntAaLZs.js +0 -1
  45. package/public/assets/index-DXaitXVC.js +0 -1
  46. package/public/assets/index-DzRzoTK8.js +0 -2
  47. package/public/assets/index-ShHad48G.js +0 -1
  48. package/public/assets/index-_EaXFUIj.js +0 -1
  49. package/public/assets/index-mMh5TJvW.js +0 -1
  50. package/public/assets/main-BsIf5lOY.js +0 -50
  51. package/public/assets/monaco-viewer-CMM6n0T3.js +0 -25
package/dist/index.js CHANGED
@@ -24,6 +24,9 @@ import { resolveNetworkAddresses, resolveRemoteAddresses } from "./server/networ
24
24
  import { startDevReleaseMonitor } from "./releases/dev-release-monitor";
25
25
  import { SpeechService } from "./speech/service";
26
26
  import { SideCarManager } from "./sidecars/manager";
27
+ import { ClientConnectionManager } from "./clients/connection-manager";
28
+ import { PluginChannelManager } from "./plugins/channel";
29
+ import { VoiceModeManager } from "./plugins/voice-mode";
27
30
  const require = createRequire(import.meta.url);
28
31
  const packageJson = require("../package.json");
29
32
  const __filename = fileURLToPath(import.meta.url);
@@ -263,6 +266,13 @@ async function main() {
263
266
  throw new InvalidArgumentError("UI dev proxy is only supported with --https=false --http=true");
264
267
  }
265
268
  const remoteAccessEnabled = options.host === "0.0.0.0" || !isLoopbackHost(options.host);
269
+ const clientConnectionManager = new ClientConnectionManager(logger.child({ component: "client-connections" }));
270
+ const pluginChannel = new PluginChannelManager(logger.child({ component: "plugin-channel" }));
271
+ const voiceModeManager = new VoiceModeManager({
272
+ connections: clientConnectionManager,
273
+ channel: pluginChannel,
274
+ logger: logger.child({ component: "voice-mode" }),
275
+ });
266
276
  const httpsPortExplicit = programHasArg(process.argv.slice(2), "--https-port") || Boolean(process.env.CLI_HTTPS_PORT);
267
277
  const httpPortExplicit = programHasArg(process.argv.slice(2), "--http-port") || Boolean(process.env.CLI_HTTP_PORT);
268
278
  const httpsBindPort = httpsPortExplicit ? options.httpsPort : 0;
@@ -289,6 +299,9 @@ async function main() {
289
299
  speechService,
290
300
  sidecarManager,
291
301
  authManager,
302
+ clientConnectionManager,
303
+ pluginChannel,
304
+ voiceModeManager,
292
305
  uiStaticDir: uiResolution.uiStaticDir ?? DEFAULT_UI_STATIC_DIR,
293
306
  uiDevServerUrl: uiResolution.uiDevServerUrl,
294
307
  logger,
@@ -310,6 +323,9 @@ async function main() {
310
323
  speechService,
311
324
  sidecarManager,
312
325
  authManager,
326
+ clientConnectionManager,
327
+ pluginChannel,
328
+ voiceModeManager,
313
329
  uiStaticDir: uiResolution.uiStaticDir ?? DEFAULT_UI_STATIC_DIR,
314
330
  uiDevServerUrl: undefined,
315
331
  logger,
@@ -405,6 +421,12 @@ async function main() {
405
421
  catch (error) {
406
422
  logger.error({ err: error }, "SideCar manager shutdown failed");
407
423
  }
424
+ try {
425
+ clientConnectionManager.shutdown();
426
+ }
427
+ catch (error) {
428
+ logger.warn({ err: error }, "Client connection manager shutdown failed");
429
+ }
408
430
  try {
409
431
  await workspaceManager.shutdown();
410
432
  logger.info("Workspace manager shutdown complete");
@@ -12,7 +12,7 @@ export class VoiceModeManager {
12
12
  setEnabled(instanceId, connection, enabled) {
13
13
  if (enabled && !this.options.connections.isConnected(connection)) {
14
14
  this.options.logger.debug({ instanceId, clientId: connection.clientId, connectionId: connection.connectionId }, "Ignoring voice mode enable for disconnected client connection");
15
- return;
15
+ return false;
16
16
  }
17
17
  const key = getConnectionKey(connection);
18
18
  const current = this.enabledConnectionsByInstance.get(instanceId) ?? new Set();
@@ -30,6 +30,7 @@ export class VoiceModeManager {
30
30
  }
31
31
  this.options.logger.debug({ instanceId, clientId: connection.clientId, connectionId: connection.connectionId, enabled }, "Voice mode updated for client connection");
32
32
  this.publishIfChanged(instanceId);
33
+ return true;
33
34
  }
34
35
  syncInstance(instanceId) {
35
36
  this.options.channel.send(instanceId, buildVoiceModeEvent(this.isEnabled(instanceId)));
@@ -7,7 +7,8 @@ import { connect as connectTcp } from "net";
7
7
  import path from "path";
8
8
  import { connect as connectTls } from "tls";
9
9
  import { fetch } from "undici";
10
- import { isValidWorktreeSlug, listWorktrees, resolveRepoRoot } from "../workspaces/git-worktrees";
10
+ import { isValidWorktreeSlug } from "../workspaces/git-worktrees";
11
+ import { resolveWorktreeDirectory } from "../workspaces/worktree-directory";
11
12
  import { registerWorkspaceRoutes } from "./routes/workspaces";
12
13
  import { registerSettingsRoutes } from "./routes/settings";
13
14
  import { registerFilesystemRoutes } from "./routes/filesystem";
@@ -23,9 +24,6 @@ import { registerSideCarRoutes } from "./routes/sidecars";
23
24
  import { BackgroundProcessManager } from "../background-processes/manager";
24
25
  import { registerAuthRoutes } from "./routes/auth";
25
26
  import { sendUnauthorized, wantsHtml } from "../auth/http-auth";
26
- import { ClientConnectionManager } from "../clients/connection-manager";
27
- import { PluginChannelManager } from "../plugins/channel";
28
- import { VoiceModeManager } from "../plugins/voice-mode";
29
27
  export function createHttpServer(deps) {
30
28
  // Fastify's type-level RawServer inference gets noisy when toggling HTTP vs HTTPS.
31
29
  // We keep the runtime behavior correct and cast the instance to a generic FastifyInstance.
@@ -132,13 +130,6 @@ export function createHttpServer(deps) {
132
130
  eventBus: deps.eventBus,
133
131
  logger: deps.logger.child({ component: "background-processes" }),
134
132
  });
135
- const clientConnectionManager = new ClientConnectionManager(deps.logger.child({ component: "client-connections" }));
136
- const pluginChannel = new PluginChannelManager(deps.logger.child({ component: "plugin-channel" }));
137
- const voiceModeManager = new VoiceModeManager({
138
- connections: clientConnectionManager,
139
- channel: pluginChannel,
140
- logger: deps.logger.child({ component: "voice-mode" }),
141
- });
142
133
  registerAuthRoutes(app, { authManager: deps.authManager });
143
134
  app.addHook("preHandler", (request, reply, done) => {
144
135
  const rawUrl = request.raw.url ?? request.url;
@@ -203,7 +194,7 @@ export function createHttpServer(deps) {
203
194
  eventBus: deps.eventBus,
204
195
  registerClient: registerSseClient,
205
196
  logger: sseLogger,
206
- connectionManager: clientConnectionManager,
197
+ connectionManager: deps.clientConnectionManager,
207
198
  });
208
199
  registerWorktreeRoutes(app, { workspaceManager: deps.workspaceManager });
209
200
  registerStorageRoutes(app, {
@@ -224,8 +215,8 @@ export function createHttpServer(deps) {
224
215
  workspaceManager: deps.workspaceManager,
225
216
  eventBus: deps.eventBus,
226
217
  logger: proxyLogger,
227
- channel: pluginChannel,
228
- voiceModeManager,
218
+ channel: deps.pluginChannel,
219
+ voiceModeManager: deps.voiceModeManager,
229
220
  });
230
221
  registerBackgroundProcessRoutes(app, { backgroundProcessManager });
231
222
  registerInstanceProxyRoutes(app, { workspaceManager: deps.workspaceManager, logger: proxyLogger });
@@ -284,7 +275,6 @@ export function createHttpServer(deps) {
284
275
  },
285
276
  stop: () => {
286
277
  closeSseClients();
287
- clientConnectionManager.shutdown();
288
278
  return app.close();
289
279
  },
290
280
  };
@@ -613,36 +603,6 @@ function normalizeInstanceSuffix(pathSuffix) {
613
603
  const trimmed = pathSuffix.replace(/^\/+/, "");
614
604
  return trimmed.length === 0 ? "/" : `/${trimmed}`;
615
605
  }
616
- const WORKTREE_CACHE_TTL_MS = 2000;
617
- const worktreeCache = new Map();
618
- async function getCachedWorktrees(params) {
619
- const cached = worktreeCache.get(params.workspaceId);
620
- const now = Date.now();
621
- if (cached && cached.expiresAt > now) {
622
- return cached;
623
- }
624
- const { repoRoot } = await resolveRepoRoot(params.workspacePath, params.logger);
625
- const worktrees = await listWorktrees({ repoRoot, workspaceFolder: params.workspacePath, logger: params.logger });
626
- const entry = {
627
- expiresAt: now + WORKTREE_CACHE_TTL_MS,
628
- repoRoot,
629
- worktrees: worktrees.map((wt) => ({ slug: wt.slug, directory: wt.directory })),
630
- };
631
- worktreeCache.set(params.workspaceId, entry);
632
- return entry;
633
- }
634
- async function resolveWorktreeDirectory(params) {
635
- const { worktreeSlug } = params;
636
- const cached = await getCachedWorktrees({ workspaceId: params.workspaceId, workspacePath: params.workspacePath, logger: params.logger });
637
- const match = cached.worktrees.find((wt) => wt.slug === worktreeSlug);
638
- if (match) {
639
- return match.directory;
640
- }
641
- // If the slug is new (e.g., created moments ago), refresh once.
642
- worktreeCache.delete(params.workspaceId);
643
- const refreshed = await getCachedWorktrees({ workspaceId: params.workspaceId, workspacePath: params.workspacePath, logger: params.logger });
644
- return refreshed.worktrees.find((wt) => wt.slug === worktreeSlug)?.directory ?? null;
645
- }
646
606
  function setupStaticUi(app, uiDir, authManager) {
647
607
  if (!uiDir) {
648
608
  app.log.warn("UI static directory not provided; API endpoints only");
@@ -41,7 +41,11 @@ export function registerPluginRoutes(app, deps) {
41
41
  return;
42
42
  }
43
43
  const payload = VoiceModeStateSchema.parse(request.body ?? {});
44
- deps.voiceModeManager.setEnabled(request.params.id, { clientId: payload.clientId, connectionId: payload.connectionId }, payload.enabled);
44
+ const applied = deps.voiceModeManager.setEnabled(request.params.id, { clientId: payload.clientId, connectionId: payload.connectionId }, payload.enabled);
45
+ if (payload.enabled && !applied) {
46
+ reply.code(409).send({ error: "Client connection not active for voice mode enable" });
47
+ return;
48
+ }
45
49
  return { enabled: payload.enabled };
46
50
  });
47
51
  const handleWildcard = async (request, reply) => {
@@ -1,4 +1,8 @@
1
1
  import { z } from "zod";
2
+ import { getWorktreeGitDiff, getWorktreeGitStatus } from "../../workspaces/git-status";
3
+ import { commitWorktreeChanges, isGitMutationError, stageWorktreePaths, unstageWorktreePaths } from "../../workspaces/git-mutations";
4
+ import { isGitAvailable, resolveRepoRoot } from "../../workspaces/git-worktrees";
5
+ import { resolveWorktreeDirectory } from "../../workspaces/worktree-directory";
2
6
  const WorkspaceCreateSchema = z.object({
3
7
  path: z.string(),
4
8
  name: z.string().optional(),
@@ -12,6 +16,17 @@ const WorkspaceFileContentQuerySchema = z.object({
12
16
  const WorkspaceFileContentBodySchema = z.object({
13
17
  contents: z.string(),
14
18
  });
19
+ const WorktreeGitDiffQuerySchema = z.object({
20
+ path: z.string().trim().min(1, "Path is required"),
21
+ originalPath: z.string().trim().optional(),
22
+ scope: z.enum(["staged", "unstaged"]),
23
+ });
24
+ const WorktreeGitPathsBodySchema = z.object({
25
+ paths: z.array(z.string().trim().min(1, "Path is required")).min(1, "At least one path is required"),
26
+ });
27
+ const WorktreeGitCommitBodySchema = z.object({
28
+ message: z.string().trim().min(1, "Commit message is required"),
29
+ });
15
30
  const WorkspaceFileSearchQuerySchema = z.object({
16
31
  q: z.string().trim().min(1, "Query is required"),
17
32
  limit: z.coerce.number().int().positive().max(200).optional(),
@@ -92,8 +107,111 @@ export function registerWorkspaceRoutes(app, deps) {
92
107
  return handleWorkspaceError(error, reply);
93
108
  }
94
109
  });
110
+ app.get("/api/workspaces/:id/worktrees/:slug/git-status", async (request, reply) => {
111
+ try {
112
+ const directory = await resolveGitWorktreeDirectory(deps.workspaceManager, request.params.id, request.params.slug, request.log, reply);
113
+ if (!directory)
114
+ return;
115
+ return await getWorktreeGitStatus({ workspaceFolder: directory, logger: request.log });
116
+ }
117
+ catch (error) {
118
+ return handleWorkspaceError(error, reply);
119
+ }
120
+ });
121
+ app.get("/api/workspaces/:id/worktrees/:slug/git-diff", async (request, reply) => {
122
+ try {
123
+ const query = WorktreeGitDiffQuerySchema.parse(request.query ?? {});
124
+ const directory = await resolveGitWorktreeDirectory(deps.workspaceManager, request.params.id, request.params.slug, request.log, reply);
125
+ if (!directory)
126
+ return;
127
+ return await getWorktreeGitDiff({
128
+ workspaceFolder: directory,
129
+ path: query.path,
130
+ originalPath: query.originalPath,
131
+ scope: query.scope,
132
+ });
133
+ }
134
+ catch (error) {
135
+ return handleWorkspaceError(error, reply);
136
+ }
137
+ });
138
+ app.post("/api/workspaces/:id/worktrees/:slug/git-stage", async (request, reply) => {
139
+ try {
140
+ const body = WorktreeGitPathsBodySchema.parse(request.body ?? {});
141
+ const directory = await resolveGitWorktreeDirectory(deps.workspaceManager, request.params.id, request.params.slug, request.log, reply);
142
+ if (!directory)
143
+ return;
144
+ await stageWorktreePaths({ workspaceFolder: directory, paths: body.paths });
145
+ return { ok: true };
146
+ }
147
+ catch (error) {
148
+ return handleWorkspaceError(error, reply);
149
+ }
150
+ });
151
+ app.post("/api/workspaces/:id/worktrees/:slug/git-unstage", async (request, reply) => {
152
+ try {
153
+ const body = WorktreeGitPathsBodySchema.parse(request.body ?? {});
154
+ const directory = await resolveGitWorktreeDirectory(deps.workspaceManager, request.params.id, request.params.slug, request.log, reply);
155
+ if (!directory)
156
+ return;
157
+ await unstageWorktreePaths({ workspaceFolder: directory, paths: body.paths });
158
+ return { ok: true };
159
+ }
160
+ catch (error) {
161
+ return handleWorkspaceError(error, reply);
162
+ }
163
+ });
164
+ app.post("/api/workspaces/:id/worktrees/:slug/git-commit", async (request, reply) => {
165
+ try {
166
+ const body = WorktreeGitCommitBodySchema.parse(request.body ?? {});
167
+ const directory = await resolveGitWorktreeDirectory(deps.workspaceManager, request.params.id, request.params.slug, request.log, reply);
168
+ if (!directory)
169
+ return;
170
+ const result = await commitWorktreeChanges({ workspaceFolder: directory, message: body.message });
171
+ return { ok: true, ...result };
172
+ }
173
+ catch (error) {
174
+ return handleWorkspaceError(error, reply);
175
+ }
176
+ });
177
+ }
178
+ async function resolveGitWorktreeDirectory(workspaceManager, workspaceId, worktreeSlug, logger, reply) {
179
+ const workspace = workspaceManager.get(workspaceId);
180
+ if (!workspace) {
181
+ reply.code(404);
182
+ reply.send({ error: "Workspace not found" });
183
+ return null;
184
+ }
185
+ const gitAvailable = await isGitAvailable(workspace.path);
186
+ if (!gitAvailable) {
187
+ reply.code(503);
188
+ reply.send({ error: "Git is not installed or not available in PATH" });
189
+ return null;
190
+ }
191
+ const { isGitRepo } = await resolveRepoRoot(workspace.path, logger);
192
+ if (!isGitRepo) {
193
+ reply.code(400);
194
+ reply.send({ error: "Workspace is not a Git repository" });
195
+ return null;
196
+ }
197
+ const directory = await resolveWorktreeDirectory({
198
+ workspaceId: workspace.id,
199
+ workspacePath: workspace.path,
200
+ worktreeSlug,
201
+ logger,
202
+ });
203
+ if (!directory) {
204
+ reply.code(404);
205
+ reply.send({ error: "Worktree not found" });
206
+ return null;
207
+ }
208
+ return directory;
95
209
  }
96
210
  function handleWorkspaceError(error, reply) {
211
+ if (isGitMutationError(error)) {
212
+ reply.code(error.statusCode);
213
+ return { error: error.message };
214
+ }
97
215
  if (error instanceof Error && error.message === "Workspace not found") {
98
216
  reply.code(404);
99
217
  return { error: "Workspace not found" };
@@ -0,0 +1,98 @@
1
+ import { spawn } from "child_process";
2
+ import path from "path";
3
+ class GitMutationError extends Error {
4
+ constructor(message, statusCode = 400) {
5
+ super(message);
6
+ this.name = "GitMutationError";
7
+ this.statusCode = statusCode;
8
+ }
9
+ }
10
+ function runGit(args, cwd) {
11
+ return new Promise((resolve) => {
12
+ const child = spawn("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
13
+ let stdout = "";
14
+ let stderr = "";
15
+ child.stdout?.on("data", (chunk) => {
16
+ stdout += chunk.toString();
17
+ });
18
+ child.stderr?.on("data", (chunk) => {
19
+ stderr += chunk.toString();
20
+ });
21
+ child.once("error", (error) => {
22
+ resolve({ ok: false, error, stdout, stderr });
23
+ });
24
+ child.once("close", (code) => {
25
+ if (code === 0) {
26
+ resolve({ ok: true, stdout });
27
+ }
28
+ else {
29
+ const error = new Error(stderr.trim() || `git ${args.join(" ")} failed with code ${code}`);
30
+ resolve({ ok: false, error, stdout, stderr });
31
+ }
32
+ });
33
+ });
34
+ }
35
+ export function normalizeGitWorktreeRelativePath(input) {
36
+ const normalized = input.trim().replace(/\\+/g, "/").replace(/^\.\//, "");
37
+ if (!normalized) {
38
+ throw new GitMutationError("Path is required", 400);
39
+ }
40
+ if (path.posix.isAbsolute(normalized) || path.win32.isAbsolute(normalized)) {
41
+ throw new GitMutationError(`Absolute paths are not allowed: ${input}`, 400);
42
+ }
43
+ if (normalized === "." || normalized === "..") {
44
+ throw new GitMutationError(`Invalid path: ${input}`, 400);
45
+ }
46
+ if (normalized.startsWith("../") || normalized.includes("/../") || normalized.endsWith("/..")) {
47
+ throw new GitMutationError(`Path traversal is not allowed: ${input}`, 400);
48
+ }
49
+ return normalized;
50
+ }
51
+ function normalizeGitMutationPaths(paths) {
52
+ const deduped = new Set();
53
+ for (const rawPath of paths) {
54
+ deduped.add(normalizeGitWorktreeRelativePath(rawPath));
55
+ }
56
+ const normalized = Array.from(deduped);
57
+ if (normalized.length === 0) {
58
+ throw new GitMutationError("At least one path is required", 400);
59
+ }
60
+ return normalized;
61
+ }
62
+ async function ensureGitCommandSucceeded(resultPromise, fallbackMessage) {
63
+ const result = await resultPromise;
64
+ if (!result.ok) {
65
+ const message = result.stderr?.trim() || result.error.message || fallbackMessage;
66
+ throw new GitMutationError(message, 409);
67
+ }
68
+ return result.stdout;
69
+ }
70
+ export function isGitMutationError(error) {
71
+ return error instanceof GitMutationError;
72
+ }
73
+ export async function stageWorktreePaths(params) {
74
+ const paths = normalizeGitMutationPaths(params.paths);
75
+ await ensureGitCommandSucceeded(runGit(["add", "--", ...paths], params.workspaceFolder), "Failed to stage files");
76
+ }
77
+ export async function unstageWorktreePaths(params) {
78
+ const paths = normalizeGitMutationPaths(params.paths);
79
+ const headResult = await runGit(["rev-parse", "--verify", "HEAD"], params.workspaceFolder);
80
+ if (headResult.ok) {
81
+ await ensureGitCommandSucceeded(runGit(["restore", "--staged", "--", ...paths], params.workspaceFolder), "Failed to unstage files");
82
+ return;
83
+ }
84
+ await ensureGitCommandSucceeded(runGit(["rm", "--cached", "--quiet", "--", ...paths], params.workspaceFolder), "Failed to unstage files");
85
+ }
86
+ export async function commitWorktreeChanges(params) {
87
+ const message = params.message.trim();
88
+ if (!message) {
89
+ throw new GitMutationError("Commit message is required", 400);
90
+ }
91
+ await ensureGitCommandSucceeded(runGit(["commit", "-m", message], params.workspaceFolder), "Failed to create commit");
92
+ const shaResult = await runGit(["rev-parse", "HEAD"], params.workspaceFolder);
93
+ if (!shaResult.ok) {
94
+ return {};
95
+ }
96
+ const commitSha = shaResult.stdout.trim();
97
+ return commitSha ? { commitSha } : {};
98
+ }