@vibe80/vibe80 0.1.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 +201 -0
- package/README.md +52 -0
- package/bin/vibe80.js +176 -0
- package/client/dist/assets/DiffPanel-C_IGzKI5.js +1 -0
- package/client/dist/assets/ExplorerPanel-BtlyAT00.js +11 -0
- package/client/dist/assets/LogsPanel-BW79JWzR.js +1 -0
- package/client/dist/assets/SettingsPanel-b9B7ygP_.js +1 -0
- package/client/dist/assets/TerminalPanel-C3fc1HbK.js +1 -0
- package/client/dist/assets/browser-e3WgtMs-.js +8 -0
- package/client/dist/assets/index-CgqGyssr.css +32 -0
- package/client/dist/assets/index-DnwKjoj7.js +706 -0
- package/client/dist/assets/vibe80_dark-D7OVPKcU.svg +51 -0
- package/client/dist/assets/vibe80_light-BJK37ybI.svg +50 -0
- package/client/dist/favicon.ico +0 -0
- package/client/dist/favicon.png +0 -0
- package/client/dist/favicon.svg +35 -0
- package/client/dist/index.html +14 -0
- package/client/index.html +16 -0
- package/client/package.json +34 -0
- package/client/public/favicon.ico +0 -0
- package/client/public/favicon.png +0 -0
- package/client/public/favicon.svg +35 -0
- package/client/public/pwa-192x192.png +0 -0
- package/client/public/pwa-512x512.png +0 -0
- package/client/src/App.jsx +3131 -0
- package/client/src/assets/logo_small.png +0 -0
- package/client/src/assets/vibe80_dark.svg +51 -0
- package/client/src/assets/vibe80_light.svg +50 -0
- package/client/src/components/Chat/ChatComposer.jsx +228 -0
- package/client/src/components/Chat/ChatMessages.jsx +811 -0
- package/client/src/components/Chat/ChatToolbar.jsx +109 -0
- package/client/src/components/Chat/useChatComposer.js +462 -0
- package/client/src/components/Diff/DiffPanel.jsx +129 -0
- package/client/src/components/Explorer/ExplorerPanel.jsx +449 -0
- package/client/src/components/Logs/LogsPanel.jsx +80 -0
- package/client/src/components/SessionGate/SessionGate.jsx +874 -0
- package/client/src/components/Settings/SettingsPanel.jsx +212 -0
- package/client/src/components/Terminal/TerminalPanel.jsx +39 -0
- package/client/src/components/Topbar/Topbar.jsx +101 -0
- package/client/src/components/WorktreeTabs.css +419 -0
- package/client/src/components/WorktreeTabs.jsx +604 -0
- package/client/src/hooks/useAttachments.jsx +125 -0
- package/client/src/hooks/useBacklog.js +254 -0
- package/client/src/hooks/useChatClear.js +90 -0
- package/client/src/hooks/useChatCollapse.js +42 -0
- package/client/src/hooks/useChatCommands.js +294 -0
- package/client/src/hooks/useChatExport.js +144 -0
- package/client/src/hooks/useChatMessagesState.js +69 -0
- package/client/src/hooks/useChatSend.js +158 -0
- package/client/src/hooks/useChatSocket.js +1239 -0
- package/client/src/hooks/useDiffNavigation.js +19 -0
- package/client/src/hooks/useExplorerActions.js +1184 -0
- package/client/src/hooks/useGitIdentity.js +114 -0
- package/client/src/hooks/useLayoutMode.js +31 -0
- package/client/src/hooks/useLocalPreferences.js +131 -0
- package/client/src/hooks/useMessageSync.js +30 -0
- package/client/src/hooks/useNotifications.js +132 -0
- package/client/src/hooks/usePaneNavigation.js +67 -0
- package/client/src/hooks/usePanelState.js +13 -0
- package/client/src/hooks/useProviderSelection.js +70 -0
- package/client/src/hooks/useRepoBranchesModels.js +218 -0
- package/client/src/hooks/useRepoStatus.js +350 -0
- package/client/src/hooks/useRpcLogActions.js +19 -0
- package/client/src/hooks/useRpcLogView.js +58 -0
- package/client/src/hooks/useSessionHandoff.js +97 -0
- package/client/src/hooks/useSessionLifecycle.js +287 -0
- package/client/src/hooks/useSessionReset.js +63 -0
- package/client/src/hooks/useSessionResync.js +77 -0
- package/client/src/hooks/useTerminalSession.js +328 -0
- package/client/src/hooks/useToolbarExport.js +27 -0
- package/client/src/hooks/useTurnInterrupt.js +43 -0
- package/client/src/hooks/useVibe80Forms.js +128 -0
- package/client/src/hooks/useWorkspaceAuth.js +932 -0
- package/client/src/hooks/useWorktreeCloseConfirm.js +46 -0
- package/client/src/hooks/useWorktrees.js +396 -0
- package/client/src/i18n.jsx +87 -0
- package/client/src/index.css +5147 -0
- package/client/src/locales/en.json +37 -0
- package/client/src/locales/fr.json +321 -0
- package/client/src/main.jsx +16 -0
- package/client/vite.config.js +62 -0
- package/docs/api/asyncapi.json +1511 -0
- package/docs/api/openapi.json +3242 -0
- package/git_hooks/prepare-commit-msg +35 -0
- package/package.json +36 -0
- package/server/package.json +29 -0
- package/server/scripts/rotate-workspace-secret.js +101 -0
- package/server/src/claudeClient.js +454 -0
- package/server/src/clientEvents.js +594 -0
- package/server/src/clientFactory.js +164 -0
- package/server/src/codexClient.js +468 -0
- package/server/src/config.js +27 -0
- package/server/src/helpers.js +138 -0
- package/server/src/index.js +1641 -0
- package/server/src/middleware/auth.js +93 -0
- package/server/src/middleware/debug.js +89 -0
- package/server/src/middleware/errorTypes.js +60 -0
- package/server/src/providerLogger.js +60 -0
- package/server/src/routes/files.js +114 -0
- package/server/src/routes/git.js +183 -0
- package/server/src/routes/health.js +13 -0
- package/server/src/routes/sessions.js +407 -0
- package/server/src/routes/workspaces.js +296 -0
- package/server/src/routes/worktrees.js +993 -0
- package/server/src/runAs.js +458 -0
- package/server/src/runtimeStore.js +32 -0
- package/server/src/services/auth.js +157 -0
- package/server/src/services/claudeThreadDirectory.js +33 -0
- package/server/src/services/session.js +918 -0
- package/server/src/services/workspace.js +858 -0
- package/server/src/storage/index.js +17 -0
- package/server/src/storage/redis.js +412 -0
- package/server/src/storage/sqlite.js +649 -0
- package/server/src/worktreeManager.js +717 -0
- package/server/tests/README.md +13 -0
- package/server/tests/factories/workspaceFactory.js +13 -0
- package/server/tests/fixtures/workspaceCredentials.json +4 -0
- package/server/tests/integration/routes/workspaces-routes.test.js +626 -0
- package/server/tests/setup/env.js +9 -0
- package/server/tests/unit/helpers.test.js +95 -0
- package/server/tests/unit/services/auth.test.js +181 -0
- package/server/tests/unit/services/workspace.test.js +115 -0
- package/server/vitest.config.js +23 -0
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import crypto from "crypto";
|
|
3
|
+
import { runAsCommand, runAsCommandOutput } from "./runAs.js";
|
|
4
|
+
import storage from "./storage/index.js";
|
|
5
|
+
import { getSessionRuntime } from "./runtimeStore.js";
|
|
6
|
+
import { createWorktreeClient } from "./clientFactory.js";
|
|
7
|
+
import { createMessageId, toIsoDateTime } from "./helpers.js";
|
|
8
|
+
import { copyClaudeThreadDirectory } from "./services/claudeThreadDirectory.js";
|
|
9
|
+
|
|
10
|
+
const MAIN_WORKTREE_SENTINEL = "main";
|
|
11
|
+
const MAIN_WORKTREE_PREFIX = "main-";
|
|
12
|
+
|
|
13
|
+
export const getMainWorktreeStorageId = (sessionId) =>
|
|
14
|
+
`${MAIN_WORKTREE_PREFIX}${sessionId}`;
|
|
15
|
+
|
|
16
|
+
const resolveWorktreeStorageId = (session, worktreeId) => {
|
|
17
|
+
if (!worktreeId) return worktreeId;
|
|
18
|
+
if (worktreeId === MAIN_WORKTREE_SENTINEL) {
|
|
19
|
+
return getMainWorktreeStorageId(session.sessionId);
|
|
20
|
+
}
|
|
21
|
+
return worktreeId;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const isMainWorktreeStorageId = (session, worktreeId) =>
|
|
25
|
+
worktreeId === getMainWorktreeStorageId(session.sessionId);
|
|
26
|
+
|
|
27
|
+
// Palette de couleurs pour distinguer les worktrees
|
|
28
|
+
const WORKTREE_COLORS = [
|
|
29
|
+
"#3b82f6", // blue
|
|
30
|
+
"#10b981", // emerald
|
|
31
|
+
"#f59e0b", // amber
|
|
32
|
+
"#ef4444", // red
|
|
33
|
+
"#8b5cf6", // violet
|
|
34
|
+
"#ec4899", // pink
|
|
35
|
+
"#06b6d4", // cyan
|
|
36
|
+
"#84cc16", // lime
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
let colorIndex = 0;
|
|
40
|
+
const getNextColor = () => {
|
|
41
|
+
const color = WORKTREE_COLORS[colorIndex % WORKTREE_COLORS.length];
|
|
42
|
+
colorIndex += 1;
|
|
43
|
+
return color;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const buildSessionEnv = (session, options = {}) => {
|
|
47
|
+
const env = { ...(options.env || {}) };
|
|
48
|
+
if (session?.dir) {
|
|
49
|
+
env.TMPDIR = path.join(session.dir, "tmp");
|
|
50
|
+
}
|
|
51
|
+
return env;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const runSessionCommand = (session, command, args, options = {}) =>
|
|
55
|
+
runAsCommand(session.workspaceId, command, args, {
|
|
56
|
+
...options,
|
|
57
|
+
env: buildSessionEnv(session, options),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const runSessionCommandOutput = (session, command, args, options = {}) =>
|
|
61
|
+
runAsCommandOutput(session.workspaceId, command, args, {
|
|
62
|
+
...options,
|
|
63
|
+
env: buildSessionEnv(session, options),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const resolveSessionGitDir = (session) =>
|
|
67
|
+
session?.gitDir || path.join(session.dir, "git");
|
|
68
|
+
|
|
69
|
+
const ensureSessionGitDir = async (session) => {
|
|
70
|
+
const gitDir = resolveSessionGitDir(session);
|
|
71
|
+
await runAsCommand(session.workspaceId, "/bin/mkdir", ["-p", gitDir]);
|
|
72
|
+
await runAsCommand(session.workspaceId, "/bin/chmod", ["2750", gitDir]);
|
|
73
|
+
return gitDir;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const applyVibe80WorktreeMetadata = async (session, worktreePath, worktreeId) => {
|
|
77
|
+
await runSessionCommand(
|
|
78
|
+
session,
|
|
79
|
+
"git",
|
|
80
|
+
["-C", worktreePath, "config", "--worktree", "vibe80.workspaceId", session.workspaceId]
|
|
81
|
+
);
|
|
82
|
+
await runSessionCommand(
|
|
83
|
+
session,
|
|
84
|
+
"git",
|
|
85
|
+
["-C", worktreePath, "config", "--worktree", "vibe80.sessionId", session.sessionId]
|
|
86
|
+
);
|
|
87
|
+
await runSessionCommand(
|
|
88
|
+
session,
|
|
89
|
+
"git",
|
|
90
|
+
["-C", worktreePath, "config", "--worktree", "vibe80.worktreeId", worktreeId]
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const resolveStartingRef = (startingBranch, remote = "origin") => {
|
|
95
|
+
if (!startingBranch || typeof startingBranch !== "string") {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const trimmed = startingBranch.trim();
|
|
99
|
+
if (!trimmed) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
if (trimmed.startsWith("refs/")) {
|
|
103
|
+
return trimmed;
|
|
104
|
+
}
|
|
105
|
+
if (trimmed.startsWith(`${remote}/`)) {
|
|
106
|
+
return trimmed;
|
|
107
|
+
}
|
|
108
|
+
return `${remote}/${trimmed}`;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const normalizeBranchName = (value, remote = "origin") => {
|
|
112
|
+
if (!value || typeof value !== "string") {
|
|
113
|
+
return "";
|
|
114
|
+
}
|
|
115
|
+
let name = value.trim();
|
|
116
|
+
if (!name) {
|
|
117
|
+
return "";
|
|
118
|
+
}
|
|
119
|
+
if (name.startsWith("refs/heads/")) {
|
|
120
|
+
name = name.slice("refs/heads/".length);
|
|
121
|
+
} else if (name.startsWith(`refs/remotes/${remote}/`)) {
|
|
122
|
+
name = name.slice(`refs/remotes/${remote}/`.length);
|
|
123
|
+
} else if (name.startsWith(`${remote}/`)) {
|
|
124
|
+
name = name.slice(`${remote}/`.length);
|
|
125
|
+
}
|
|
126
|
+
return name.trim();
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const resolveCurrentBranchName = async (session) => {
|
|
130
|
+
const output = await runSessionCommandOutput(
|
|
131
|
+
session,
|
|
132
|
+
"git",
|
|
133
|
+
["rev-parse", "--abbrev-ref", "HEAD"],
|
|
134
|
+
{ cwd: session.repoDir }
|
|
135
|
+
);
|
|
136
|
+
return output.trim();
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const serializeWorktree = (worktree) => {
|
|
140
|
+
if (!worktree) return null;
|
|
141
|
+
const { client, ...persisted } = worktree;
|
|
142
|
+
return persisted;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const resolveSessionDenyGitCredentialsAccess = (session) =>
|
|
146
|
+
typeof session?.defaultDenyGitCredentialsAccess === "boolean"
|
|
147
|
+
? session.defaultDenyGitCredentialsAccess
|
|
148
|
+
: true;
|
|
149
|
+
|
|
150
|
+
const touchSession = async (session) => {
|
|
151
|
+
const updated = { ...session, lastActivityAt: Date.now() };
|
|
152
|
+
await storage.saveSession(session.sessionId, updated);
|
|
153
|
+
return updated;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const loadWorktree = async (worktreeId) => {
|
|
157
|
+
if (!worktreeId) return null;
|
|
158
|
+
return storage.getWorktree(worktreeId);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const ensureMainWorktree = async (session) => {
|
|
162
|
+
const worktreeId = getMainWorktreeStorageId(session.sessionId);
|
|
163
|
+
const existing = await loadWorktree(worktreeId);
|
|
164
|
+
if (existing) return existing;
|
|
165
|
+
const branchName = await resolveCurrentBranchName(session);
|
|
166
|
+
const worktree = {
|
|
167
|
+
id: worktreeId,
|
|
168
|
+
sessionId: session.sessionId,
|
|
169
|
+
name: branchName || "main",
|
|
170
|
+
branchName: branchName || "main",
|
|
171
|
+
threadId: session.threadId || null,
|
|
172
|
+
path: session.repoDir,
|
|
173
|
+
provider: session.activeProvider || "codex",
|
|
174
|
+
model: null,
|
|
175
|
+
reasoningEffort: null,
|
|
176
|
+
internetAccess: Boolean(session.defaultInternetAccess),
|
|
177
|
+
denyGitCredentialsAccess: resolveSessionDenyGitCredentialsAccess(session),
|
|
178
|
+
startingBranch: branchName || null,
|
|
179
|
+
workspaceId: session.workspaceId,
|
|
180
|
+
status: "ready",
|
|
181
|
+
parentWorktreeId: null,
|
|
182
|
+
createdAt: new Date().toISOString(),
|
|
183
|
+
lastActivityAt: new Date().toISOString(),
|
|
184
|
+
color: getNextColor(),
|
|
185
|
+
};
|
|
186
|
+
await storage.saveWorktree(session.sessionId, worktreeId, serializeWorktree(worktree));
|
|
187
|
+
return worktree;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Génère un nom de worktree à partir du premier message ou un nom par défaut
|
|
192
|
+
*/
|
|
193
|
+
const generateWorktreeName = (text, index) => {
|
|
194
|
+
if (text) {
|
|
195
|
+
const cleaned = text
|
|
196
|
+
.slice(0, 30)
|
|
197
|
+
.toLowerCase()
|
|
198
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
199
|
+
.replace(/^-+|-+$/g, "");
|
|
200
|
+
if (cleaned.length > 2) {
|
|
201
|
+
return cleaned;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return `branch-${index}`;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Crée un nouveau worktree pour une session
|
|
209
|
+
*/
|
|
210
|
+
export async function createWorktree(session, options) {
|
|
211
|
+
const {
|
|
212
|
+
context,
|
|
213
|
+
provider,
|
|
214
|
+
sourceWorktree,
|
|
215
|
+
name,
|
|
216
|
+
parentWorktreeId,
|
|
217
|
+
startingBranch,
|
|
218
|
+
model,
|
|
219
|
+
reasoningEffort,
|
|
220
|
+
internetAccess,
|
|
221
|
+
denyGitCredentialsAccess,
|
|
222
|
+
} = options;
|
|
223
|
+
const creationContext = context === "fork" ? "fork" : "new";
|
|
224
|
+
let resolvedProvider = provider;
|
|
225
|
+
let resolvedModel = model || null;
|
|
226
|
+
let resolvedReasoningEffort = reasoningEffort || null;
|
|
227
|
+
let resolvedSourceWorktreeId = null;
|
|
228
|
+
let forkSourceThreadId = null;
|
|
229
|
+
let sourceWorktreeRecord = null;
|
|
230
|
+
|
|
231
|
+
if (creationContext === "fork") {
|
|
232
|
+
sourceWorktreeRecord = await getWorktree(session, sourceWorktree);
|
|
233
|
+
if (!sourceWorktreeRecord) {
|
|
234
|
+
throw new Error("Source worktree not found.");
|
|
235
|
+
}
|
|
236
|
+
resolvedProvider = sourceWorktreeRecord.provider;
|
|
237
|
+
resolvedModel = null;
|
|
238
|
+
resolvedReasoningEffort = null;
|
|
239
|
+
resolvedSourceWorktreeId = sourceWorktree === "main" ? "main" : sourceWorktreeRecord.id;
|
|
240
|
+
forkSourceThreadId =
|
|
241
|
+
sourceWorktreeRecord.threadId ||
|
|
242
|
+
(resolvedSourceWorktreeId === "main" ? session.threadId || null : null);
|
|
243
|
+
if (!forkSourceThreadId) {
|
|
244
|
+
throw new Error("Source worktree has no threadId to fork from.");
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (resolvedProvider !== "codex" && resolvedProvider !== "claude") {
|
|
249
|
+
throw new Error("Invalid provider for worktree creation.");
|
|
250
|
+
}
|
|
251
|
+
const resolvedDenyGitCredentialsAccess =
|
|
252
|
+
typeof denyGitCredentialsAccess === "boolean"
|
|
253
|
+
? denyGitCredentialsAccess
|
|
254
|
+
: typeof sourceWorktreeRecord?.denyGitCredentialsAccess === "boolean"
|
|
255
|
+
? sourceWorktreeRecord.denyGitCredentialsAccess
|
|
256
|
+
: resolveSessionDenyGitCredentialsAccess(session);
|
|
257
|
+
const resolvedInternetAccess =
|
|
258
|
+
typeof internetAccess === "boolean"
|
|
259
|
+
? internetAccess
|
|
260
|
+
: typeof sourceWorktreeRecord?.internetAccess === "boolean"
|
|
261
|
+
? sourceWorktreeRecord.internetAccess
|
|
262
|
+
: true;
|
|
263
|
+
const shareGitCredentials = !resolvedDenyGitCredentialsAccess;
|
|
264
|
+
|
|
265
|
+
const worktreesDir = path.join(session.dir, "worktrees");
|
|
266
|
+
await runAsCommand(session.workspaceId, "/bin/mkdir", ["-p", worktreesDir]);
|
|
267
|
+
await runAsCommand(session.workspaceId, "/bin/chmod", ["2750", worktreesDir]);
|
|
268
|
+
|
|
269
|
+
const worktreeId = crypto.randomBytes(8).toString("hex");
|
|
270
|
+
const existingWorktrees = await listStoredWorktrees(session);
|
|
271
|
+
const worktreeIndex = existingWorktrees.length + 1;
|
|
272
|
+
const requestedBranchName = normalizeBranchName(name);
|
|
273
|
+
const worktreePath = path.join(worktreesDir, worktreeId);
|
|
274
|
+
|
|
275
|
+
let startCommit = "HEAD";
|
|
276
|
+
let sourceBranchName = "";
|
|
277
|
+
if (parentWorktreeId) {
|
|
278
|
+
const parent = await loadWorktree(parentWorktreeId);
|
|
279
|
+
if (parent) {
|
|
280
|
+
startCommit = await runSessionCommandOutput(
|
|
281
|
+
session,
|
|
282
|
+
"git",
|
|
283
|
+
["rev-parse", "HEAD"],
|
|
284
|
+
{ cwd: parent.path }
|
|
285
|
+
);
|
|
286
|
+
startCommit = startCommit.trim();
|
|
287
|
+
sourceBranchName = parent.branchName || "";
|
|
288
|
+
}
|
|
289
|
+
} else if (startingBranch) {
|
|
290
|
+
startCommit = resolveStartingRef(startingBranch) || startingBranch;
|
|
291
|
+
sourceBranchName = normalizeBranchName(startingBranch);
|
|
292
|
+
} else {
|
|
293
|
+
sourceBranchName = await resolveCurrentBranchName(session);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const baseName =
|
|
297
|
+
requestedBranchName ||
|
|
298
|
+
sourceBranchName ||
|
|
299
|
+
generateWorktreeName(null, worktreeIndex);
|
|
300
|
+
const defaultDisplayName = `wt-${worktreeId.slice(0, 6)}-${baseName}`;
|
|
301
|
+
const branchName = requestedBranchName
|
|
302
|
+
? requestedBranchName
|
|
303
|
+
: `wt-${worktreeId.slice(0, 6)}-${baseName}`;
|
|
304
|
+
|
|
305
|
+
const checkRemoteBranchExists = async (branch) => {
|
|
306
|
+
const remoteRef = resolveStartingRef(branch);
|
|
307
|
+
if (!remoteRef) return false;
|
|
308
|
+
const remoteVerifyRef = remoteRef.startsWith("refs/")
|
|
309
|
+
? remoteRef
|
|
310
|
+
: `refs/remotes/${remoteRef}`;
|
|
311
|
+
try {
|
|
312
|
+
await runSessionCommand(session, "git", ["show-ref", "--verify", remoteVerifyRef], {
|
|
313
|
+
cwd: session.repoDir,
|
|
314
|
+
});
|
|
315
|
+
return true;
|
|
316
|
+
} catch {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const remoteBranchExists = requestedBranchName
|
|
322
|
+
? await checkRemoteBranchExists(requestedBranchName)
|
|
323
|
+
: false;
|
|
324
|
+
|
|
325
|
+
if (requestedBranchName) {
|
|
326
|
+
if (remoteBranchExists) {
|
|
327
|
+
await runSessionCommand(
|
|
328
|
+
session,
|
|
329
|
+
"git",
|
|
330
|
+
["branch", branchName, resolveStartingRef(requestedBranchName)],
|
|
331
|
+
{ cwd: session.repoDir }
|
|
332
|
+
);
|
|
333
|
+
} else {
|
|
334
|
+
if (!parentWorktreeId && !startingBranch) {
|
|
335
|
+
throw new Error("Branche source requise pour creer une nouvelle branche.");
|
|
336
|
+
}
|
|
337
|
+
await runSessionCommand(session, "git", ["branch", branchName, startCommit], {
|
|
338
|
+
cwd: session.repoDir,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
} else {
|
|
342
|
+
await runSessionCommand(session, "git", ["branch", branchName, startCommit], {
|
|
343
|
+
cwd: session.repoDir,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
await runSessionCommand(
|
|
348
|
+
session,
|
|
349
|
+
"git",
|
|
350
|
+
["config", `branch.${branchName}.remote`, "origin"],
|
|
351
|
+
{ cwd: session.repoDir }
|
|
352
|
+
);
|
|
353
|
+
await runSessionCommand(
|
|
354
|
+
session,
|
|
355
|
+
"git",
|
|
356
|
+
["config", `branch.${branchName}.merge`, `refs/heads/${branchName}`],
|
|
357
|
+
{ cwd: session.repoDir }
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
await runSessionCommand(
|
|
361
|
+
session,
|
|
362
|
+
"git",
|
|
363
|
+
["worktree", "add", worktreePath, branchName],
|
|
364
|
+
{ cwd: session.repoDir }
|
|
365
|
+
);
|
|
366
|
+
await runAsCommand(session.workspaceId, "/bin/chmod", ["2750", worktreePath]);
|
|
367
|
+
await applyVibe80WorktreeMetadata(session, worktreePath, worktreeId);
|
|
368
|
+
if (creationContext === "fork" && resolvedProvider === "claude" && sourceWorktreeRecord?.path) {
|
|
369
|
+
await copyClaudeThreadDirectory(session.workspaceId, sourceWorktreeRecord.path, worktreePath);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (shareGitCredentials) {
|
|
373
|
+
await ensureSessionGitDir(session);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const worktree = {
|
|
377
|
+
id: worktreeId,
|
|
378
|
+
sessionId: session.sessionId,
|
|
379
|
+
name: name && String(name).trim() ? baseName : defaultDisplayName,
|
|
380
|
+
branchName,
|
|
381
|
+
threadId: null,
|
|
382
|
+
path: worktreePath,
|
|
383
|
+
provider: resolvedProvider,
|
|
384
|
+
model: resolvedModel,
|
|
385
|
+
reasoningEffort: resolvedReasoningEffort,
|
|
386
|
+
context: creationContext,
|
|
387
|
+
sourceWorktreeId: resolvedSourceWorktreeId,
|
|
388
|
+
forkSourceThreadId: creationContext === "fork" ? forkSourceThreadId : null,
|
|
389
|
+
internetAccess: Boolean(resolvedInternetAccess),
|
|
390
|
+
denyGitCredentialsAccess: resolvedDenyGitCredentialsAccess,
|
|
391
|
+
startingBranch: startingBranch || null,
|
|
392
|
+
workspaceId: session.workspaceId,
|
|
393
|
+
status: "creating",
|
|
394
|
+
parentWorktreeId: parentWorktreeId || null,
|
|
395
|
+
createdAt: new Date().toISOString(),
|
|
396
|
+
lastActivityAt: new Date().toISOString(),
|
|
397
|
+
color: getNextColor(),
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
await storage.saveWorktree(session.sessionId, worktreeId, serializeWorktree(worktree));
|
|
401
|
+
await touchSession(session);
|
|
402
|
+
|
|
403
|
+
try {
|
|
404
|
+
const client = createWorktreeClient(
|
|
405
|
+
worktree,
|
|
406
|
+
session.attachmentsDir,
|
|
407
|
+
session.repoDir,
|
|
408
|
+
worktree.internetAccess,
|
|
409
|
+
worktree.threadId,
|
|
410
|
+
resolveSessionGitDir(session),
|
|
411
|
+
{
|
|
412
|
+
threadStartMode: creationContext === "fork" ? "fork" : "new",
|
|
413
|
+
sourceThreadId: creationContext === "fork" ? forkSourceThreadId : null,
|
|
414
|
+
}
|
|
415
|
+
);
|
|
416
|
+
const runtime = getSessionRuntime(session.sessionId);
|
|
417
|
+
if (runtime) {
|
|
418
|
+
runtime.worktreeClients.set(worktreeId, client);
|
|
419
|
+
}
|
|
420
|
+
worktree.client = client;
|
|
421
|
+
worktree.status = "ready";
|
|
422
|
+
} catch (error) {
|
|
423
|
+
worktree.status = "error";
|
|
424
|
+
await storage.saveWorktree(session.sessionId, worktreeId, serializeWorktree(worktree));
|
|
425
|
+
throw error;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return worktree;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Supprime un worktree
|
|
433
|
+
*/
|
|
434
|
+
export async function removeWorktree(session, worktreeId, deleteBranch = true) {
|
|
435
|
+
const resolvedId = resolveWorktreeStorageId(session, worktreeId);
|
|
436
|
+
if (isMainWorktreeStorageId(session, resolvedId)) {
|
|
437
|
+
throw new Error("Main worktree cannot be removed");
|
|
438
|
+
}
|
|
439
|
+
const worktree = await loadWorktree(resolvedId);
|
|
440
|
+
if (!worktree) {
|
|
441
|
+
throw new Error("Worktree not found");
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const runtime = getSessionRuntime(session.sessionId);
|
|
445
|
+
const client = runtime?.worktreeClients?.get(resolvedId);
|
|
446
|
+
if (client) {
|
|
447
|
+
try {
|
|
448
|
+
if (typeof client.stop === "function") {
|
|
449
|
+
await client.stop();
|
|
450
|
+
}
|
|
451
|
+
} catch (error) {
|
|
452
|
+
console.error("Error stopping worktree client:", error);
|
|
453
|
+
}
|
|
454
|
+
runtime.worktreeClients.delete(resolvedId);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
await runSessionCommand(
|
|
458
|
+
session,
|
|
459
|
+
"git",
|
|
460
|
+
["worktree", "remove", "--force", worktree.path],
|
|
461
|
+
{ cwd: session.repoDir }
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
if (deleteBranch) {
|
|
465
|
+
try {
|
|
466
|
+
await runSessionCommand(session, "git", ["branch", "-D", worktree.branchName], {
|
|
467
|
+
cwd: session.repoDir,
|
|
468
|
+
});
|
|
469
|
+
} catch {
|
|
470
|
+
console.warn("Could not delete branch:", worktree.branchName);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
await storage.deleteWorktree(session.sessionId, resolvedId);
|
|
475
|
+
await touchSession(session);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export async function getWorktreeDiff(session, worktreeId) {
|
|
479
|
+
const resolvedId = resolveWorktreeStorageId(session, worktreeId);
|
|
480
|
+
const worktree = await loadWorktree(resolvedId);
|
|
481
|
+
if (!worktree) {
|
|
482
|
+
throw new Error("Worktree not found");
|
|
483
|
+
}
|
|
484
|
+
const [status, diff] = await Promise.all([
|
|
485
|
+
runSessionCommandOutput(session, "git", ["status", "--porcelain"], {
|
|
486
|
+
cwd: worktree.path,
|
|
487
|
+
}),
|
|
488
|
+
runSessionCommandOutput(session, "git", ["diff"], { cwd: worktree.path }),
|
|
489
|
+
]);
|
|
490
|
+
return { status, diff };
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export async function getWorktreeCommits(session, worktreeId, limit = 20) {
|
|
494
|
+
const resolvedId = resolveWorktreeStorageId(session, worktreeId);
|
|
495
|
+
const worktree = await loadWorktree(resolvedId);
|
|
496
|
+
if (!worktree) {
|
|
497
|
+
throw new Error("Worktree not found");
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const output = await runSessionCommandOutput(
|
|
501
|
+
session,
|
|
502
|
+
"git",
|
|
503
|
+
["log", `--max-count=${limit}`, "--format=%H|%s|%cI"],
|
|
504
|
+
{ cwd: worktree.path }
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
return output
|
|
508
|
+
.trim()
|
|
509
|
+
.split("\n")
|
|
510
|
+
.filter(Boolean)
|
|
511
|
+
.map((line) => {
|
|
512
|
+
const [sha, message, date] = line.split("|");
|
|
513
|
+
return { sha, message, date: toIsoDateTime(date) };
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export async function mergeWorktree(session, sourceWorktreeId, targetWorktreeId) {
|
|
518
|
+
const source = await loadWorktree(resolveWorktreeStorageId(session, sourceWorktreeId));
|
|
519
|
+
if (!source) {
|
|
520
|
+
throw new Error("Source worktree not found");
|
|
521
|
+
}
|
|
522
|
+
const target = await loadWorktree(resolveWorktreeStorageId(session, targetWorktreeId));
|
|
523
|
+
if (!target) {
|
|
524
|
+
throw new Error("Target worktree not found");
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
try {
|
|
528
|
+
await runSessionCommand(session, "git", ["merge", source.branchName, "--no-edit"], {
|
|
529
|
+
cwd: target.path,
|
|
530
|
+
});
|
|
531
|
+
return { success: true };
|
|
532
|
+
} catch (error) {
|
|
533
|
+
const status = await runSessionCommandOutput(
|
|
534
|
+
session,
|
|
535
|
+
"git",
|
|
536
|
+
["status", "--porcelain"],
|
|
537
|
+
{ cwd: target.path }
|
|
538
|
+
);
|
|
539
|
+
const conflicts = status
|
|
540
|
+
.split("\n")
|
|
541
|
+
.filter((line) => line.startsWith("UU") || line.startsWith("AA"))
|
|
542
|
+
.map((line) => line.slice(3).trim());
|
|
543
|
+
if (conflicts.length > 0) {
|
|
544
|
+
return { success: false, conflicts };
|
|
545
|
+
}
|
|
546
|
+
throw error;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export async function abortMerge(session, worktreeId) {
|
|
551
|
+
const worktree = await loadWorktree(resolveWorktreeStorageId(session, worktreeId));
|
|
552
|
+
if (!worktree) {
|
|
553
|
+
throw new Error("Worktree not found");
|
|
554
|
+
}
|
|
555
|
+
await runSessionCommand(session, "git", ["merge", "--abort"], { cwd: worktree.path });
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export async function cherryPickCommit(session, commitSha, targetWorktreeId) {
|
|
559
|
+
const target = await loadWorktree(resolveWorktreeStorageId(session, targetWorktreeId));
|
|
560
|
+
if (!target) {
|
|
561
|
+
throw new Error("Target worktree not found");
|
|
562
|
+
}
|
|
563
|
+
try {
|
|
564
|
+
await runSessionCommand(session, "git", ["cherry-pick", commitSha], {
|
|
565
|
+
cwd: target.path,
|
|
566
|
+
});
|
|
567
|
+
return { success: true };
|
|
568
|
+
} catch (error) {
|
|
569
|
+
const status = await runSessionCommandOutput(
|
|
570
|
+
session,
|
|
571
|
+
"git",
|
|
572
|
+
["status", "--porcelain"],
|
|
573
|
+
{ cwd: target.path }
|
|
574
|
+
);
|
|
575
|
+
const conflicts = status
|
|
576
|
+
.split("\n")
|
|
577
|
+
.filter((line) => line.startsWith("UU") || line.startsWith("AA"))
|
|
578
|
+
.map((line) => line.slice(3).trim());
|
|
579
|
+
if (conflicts.length > 0) {
|
|
580
|
+
return { success: false, conflicts };
|
|
581
|
+
}
|
|
582
|
+
throw error;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
export async function listStoredWorktrees(session) {
|
|
587
|
+
const worktrees = await storage.listWorktrees(session.sessionId);
|
|
588
|
+
return worktrees.filter((wt) => !isMainWorktreeStorageId(session, wt?.id));
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
export async function listWorktrees(session) {
|
|
592
|
+
const worktrees = await listStoredWorktrees(session);
|
|
593
|
+
return worktrees.map((wt) => ({
|
|
594
|
+
id: wt.id,
|
|
595
|
+
name: wt.name,
|
|
596
|
+
branchName: wt.branchName,
|
|
597
|
+
provider: wt.provider,
|
|
598
|
+
model: wt.model || null,
|
|
599
|
+
reasoningEffort: wt.reasoningEffort || null,
|
|
600
|
+
context: wt.context || "new",
|
|
601
|
+
sourceWorktreeId: wt.sourceWorktreeId || null,
|
|
602
|
+
status: wt.status,
|
|
603
|
+
parentWorktreeId: wt.parentWorktreeId,
|
|
604
|
+
internetAccess: Boolean(wt.internetAccess),
|
|
605
|
+
denyGitCredentialsAccess:
|
|
606
|
+
typeof wt.denyGitCredentialsAccess === "boolean"
|
|
607
|
+
? wt.denyGitCredentialsAccess
|
|
608
|
+
: true,
|
|
609
|
+
createdAt: toIsoDateTime(wt.createdAt),
|
|
610
|
+
lastActivityAt: toIsoDateTime(wt.lastActivityAt),
|
|
611
|
+
color: wt.color,
|
|
612
|
+
}));
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
export async function getWorktree(session, worktreeId) {
|
|
616
|
+
const resolvedId = resolveWorktreeStorageId(session, worktreeId);
|
|
617
|
+
if (isMainWorktreeStorageId(session, resolvedId)) {
|
|
618
|
+
const mainWorktree = await ensureMainWorktree(session);
|
|
619
|
+
return mainWorktree;
|
|
620
|
+
}
|
|
621
|
+
const worktree = await loadWorktree(resolvedId);
|
|
622
|
+
if (!worktree) return null;
|
|
623
|
+
const runtime = getSessionRuntime(session.sessionId);
|
|
624
|
+
if (runtime?.worktreeClients?.has(resolvedId)) {
|
|
625
|
+
worktree.client = runtime.worktreeClients.get(resolvedId);
|
|
626
|
+
}
|
|
627
|
+
return worktree;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
export async function updateWorktreeStatus(session, worktreeId, status) {
|
|
631
|
+
const resolvedId = resolveWorktreeStorageId(session, worktreeId);
|
|
632
|
+
let worktree = await loadWorktree(resolvedId);
|
|
633
|
+
if (!worktree && isMainWorktreeStorageId(session, resolvedId)) {
|
|
634
|
+
worktree = await ensureMainWorktree(session);
|
|
635
|
+
}
|
|
636
|
+
if (!worktree) return;
|
|
637
|
+
const updated = {
|
|
638
|
+
...worktree,
|
|
639
|
+
status,
|
|
640
|
+
lastActivityAt: new Date().toISOString(),
|
|
641
|
+
};
|
|
642
|
+
await storage.saveWorktree(session.sessionId, resolvedId, serializeWorktree(updated));
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
export async function updateWorktreeThreadId(session, worktreeId, threadId) {
|
|
646
|
+
const resolvedId = resolveWorktreeStorageId(session, worktreeId);
|
|
647
|
+
const worktree = await loadWorktree(resolvedId);
|
|
648
|
+
if (!worktree || !threadId) return;
|
|
649
|
+
const updated = {
|
|
650
|
+
...worktree,
|
|
651
|
+
threadId,
|
|
652
|
+
lastActivityAt: new Date().toISOString(),
|
|
653
|
+
};
|
|
654
|
+
await storage.saveWorktree(session.sessionId, resolvedId, serializeWorktree(updated));
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
export async function updateWorktreeModel(
|
|
658
|
+
session,
|
|
659
|
+
worktreeId,
|
|
660
|
+
model = null,
|
|
661
|
+
reasoningEffort = null
|
|
662
|
+
) {
|
|
663
|
+
const resolvedId = resolveWorktreeStorageId(session, worktreeId);
|
|
664
|
+
let worktree = await loadWorktree(resolvedId);
|
|
665
|
+
if (!worktree && isMainWorktreeStorageId(session, resolvedId)) {
|
|
666
|
+
worktree = await ensureMainWorktree(session);
|
|
667
|
+
}
|
|
668
|
+
if (!worktree) return;
|
|
669
|
+
const updated = {
|
|
670
|
+
...worktree,
|
|
671
|
+
model: model || null,
|
|
672
|
+
reasoningEffort: reasoningEffort || null,
|
|
673
|
+
lastActivityAt: new Date().toISOString(),
|
|
674
|
+
};
|
|
675
|
+
await storage.saveWorktree(session.sessionId, resolvedId, serializeWorktree(updated));
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
export async function appendWorktreeMessage(session, worktreeId, message) {
|
|
679
|
+
const resolvedId = resolveWorktreeStorageId(session, worktreeId);
|
|
680
|
+
let worktree = await loadWorktree(resolvedId);
|
|
681
|
+
if (!worktree && isMainWorktreeStorageId(session, resolvedId)) {
|
|
682
|
+
worktree = await ensureMainWorktree(session);
|
|
683
|
+
}
|
|
684
|
+
if (!worktree) return;
|
|
685
|
+
const messageId = message?.id || createMessageId();
|
|
686
|
+
const payload = message?.id ? message : { ...message, id: messageId };
|
|
687
|
+
await storage.appendWorktreeMessage(session.sessionId, resolvedId, payload);
|
|
688
|
+
const updated = {
|
|
689
|
+
...worktree,
|
|
690
|
+
lastActivityAt: new Date().toISOString(),
|
|
691
|
+
};
|
|
692
|
+
await storage.saveWorktree(session.sessionId, resolvedId, serializeWorktree(updated));
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
export async function clearWorktreeMessages(session, worktreeId) {
|
|
696
|
+
const resolvedId = resolveWorktreeStorageId(session, worktreeId);
|
|
697
|
+
let worktree = await loadWorktree(resolvedId);
|
|
698
|
+
if (!worktree && isMainWorktreeStorageId(session, resolvedId)) {
|
|
699
|
+
worktree = await ensureMainWorktree(session);
|
|
700
|
+
}
|
|
701
|
+
if (!worktree) return;
|
|
702
|
+
await storage.clearWorktreeMessages(session.sessionId, resolvedId);
|
|
703
|
+
const updated = {
|
|
704
|
+
...worktree,
|
|
705
|
+
lastActivityAt: new Date().toISOString(),
|
|
706
|
+
};
|
|
707
|
+
await storage.saveWorktree(session.sessionId, resolvedId, serializeWorktree(updated));
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
export async function renameWorktree(session, worktreeId, newName) {
|
|
711
|
+
const resolvedId = resolveWorktreeStorageId(session, worktreeId);
|
|
712
|
+
if (isMainWorktreeStorageId(session, resolvedId)) return;
|
|
713
|
+
const worktree = await loadWorktree(resolvedId);
|
|
714
|
+
if (!worktree || !newName) return;
|
|
715
|
+
const updated = { ...worktree, name: newName };
|
|
716
|
+
await storage.saveWorktree(session.sessionId, resolvedId, serializeWorktree(updated));
|
|
717
|
+
}
|