@vibe80/vibe80 0.2.0 → 0.2.2

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 (55) hide show
  1. package/README.md +132 -16
  2. package/bin/vibe80.js +1728 -16
  3. package/client/dist/assets/{DiffPanel-BKLnyIAZ.js → DiffPanel-BUJhQj_Q.js} +1 -1
  4. package/client/dist/assets/{ExplorerPanel-D3IbBsXz.js → ExplorerPanel-DugEeaO2.js} +1 -1
  5. package/client/dist/assets/{LogsPanel-BwJAFHRP.js → LogsPanel-BQrGxMu_.js} +1 -1
  6. package/client/dist/assets/{SettingsPanel-BfkchMnR.js → SettingsPanel-Ci2BdIYO.js} +1 -1
  7. package/client/dist/assets/{TerminalPanel-BQfMEm-u.js → TerminalPanel-C-T3t-6T.js} +1 -1
  8. package/client/dist/assets/index-cFi4LM0j.js +711 -0
  9. package/client/dist/assets/index-qNyFxUjK.css +32 -0
  10. package/client/dist/icon_square-512x512.png +0 -0
  11. package/client/dist/icon_square.svg +58 -0
  12. package/client/dist/index.html +3 -2
  13. package/client/dist/sw.js +1 -1
  14. package/client/index.html +1 -0
  15. package/client/public/icon_square-512x512.png +0 -0
  16. package/client/public/icon_square.svg +58 -0
  17. package/client/src/App.jsx +205 -2
  18. package/client/src/assets/vibe80_dark.png +0 -0
  19. package/client/src/assets/vibe80_light.png +0 -0
  20. package/client/src/components/Chat/ChatMessages.jsx +1 -1
  21. package/client/src/components/SessionGate/SessionGate.jsx +295 -91
  22. package/client/src/components/WorktreeTabs.css +11 -0
  23. package/client/src/components/WorktreeTabs.jsx +77 -47
  24. package/client/src/hooks/useChatSocket.js +8 -7
  25. package/client/src/hooks/useRepoBranchesModels.js +12 -6
  26. package/client/src/hooks/useWorktreeCloseConfirm.js +19 -7
  27. package/client/src/hooks/useWorktrees.js +3 -1
  28. package/client/src/index.css +26 -3
  29. package/client/src/locales/en.json +12 -1
  30. package/client/src/locales/fr.json +12 -1
  31. package/docs/api/openapi.json +1 -1
  32. package/package.json +2 -1
  33. package/server/scripts/rotate-workspace-secret.js +1 -1
  34. package/server/src/claudeClient.js +3 -3
  35. package/server/src/codexClient.js +3 -3
  36. package/server/src/config.js +6 -6
  37. package/server/src/index.js +14 -12
  38. package/server/src/middleware/auth.js +7 -7
  39. package/server/src/middleware/debug.js +36 -4
  40. package/server/src/providerLogger.js +2 -2
  41. package/server/src/routes/sessions.js +133 -21
  42. package/server/src/routes/workspaces.js +1 -1
  43. package/server/src/runAs.js +14 -14
  44. package/server/src/services/auth.js +3 -3
  45. package/server/src/services/session.js +182 -14
  46. package/server/src/services/workspace.js +86 -42
  47. package/server/src/storage/index.js +2 -2
  48. package/server/src/storage/redis.js +38 -36
  49. package/server/src/storage/sqlite.js +13 -13
  50. package/server/src/worktreeManager.js +87 -19
  51. package/server/tests/integration/routes/workspaces-routes.test.js +8 -8
  52. package/server/tests/setup/env.js +5 -5
  53. package/server/tests/unit/services/auth.test.js +3 -3
  54. package/client/dist/assets/index-BDQQz6SJ.css +0 -32
  55. package/client/dist/assets/index-D1UJw1oP.js +0 -711
@@ -21,6 +21,7 @@ import {
21
21
  getSession,
22
22
  touchSession,
23
23
  createSession,
24
+ updateSessionAuth,
24
25
  cleanupSession,
25
26
  getRepoDiff,
26
27
  getCurrentBranch,
@@ -46,6 +47,59 @@ export default function sessionRoutes(deps) {
46
47
  } = deps;
47
48
 
48
49
  const router = Router();
50
+ const buildSessionResponse = async (session) => {
51
+ const repoDiff = await getRepoDiff(session);
52
+ const activeProvider = session.activeProvider || "codex";
53
+ const enabledProviders = await resolveEnabledProviders(session.workspaceId);
54
+ return {
55
+ sessionId: session.sessionId,
56
+ workspaceId: session.workspaceId,
57
+ path: session.dir,
58
+ repoUrl: session.repoUrl,
59
+ name: session.name || "",
60
+ defaultProvider: activeProvider,
61
+ providers: enabledProviders,
62
+ defaultInternetAccess:
63
+ typeof session.defaultInternetAccess === "boolean"
64
+ ? session.defaultInternetAccess
65
+ : true,
66
+ defaultDenyGitCredentialsAccess: resolveDefaultDenyGitCredentialsAccess(session),
67
+ repoDiff,
68
+ rpcLogsEnabled: debugApiWsLog,
69
+ rpcLogs: debugApiWsLog ? session.rpcLogs || [] : [],
70
+ terminalEnabled,
71
+ };
72
+ };
73
+
74
+ const restartMainProvider = async (session) => {
75
+ const provider = session.activeProvider || "codex";
76
+ const client = await getOrCreateClient(session, provider);
77
+ if (!client) {
78
+ return;
79
+ }
80
+ if (typeof session.defaultInternetAccess === "boolean") {
81
+ client.internetAccess = session.defaultInternetAccess;
82
+ }
83
+ client.denyGitCredentialsAccess = resolveDefaultDenyGitCredentialsAccess(session);
84
+ client.gitDir = session.gitDir || client.gitDir || null;
85
+ const status = typeof client.getStatus === "function" ? client.getStatus() : "";
86
+ if (status === "idle" && typeof client.restart === "function") {
87
+ await client.restart();
88
+ return;
89
+ }
90
+ if (
91
+ (status === "stopped" || !status) &&
92
+ !client.ready &&
93
+ !client.proc &&
94
+ typeof client.start === "function"
95
+ ) {
96
+ await client.start();
97
+ return;
98
+ }
99
+ if (typeof client.requestRestart === "function") {
100
+ client.requestRestart();
101
+ }
102
+ };
49
103
  const resolveEnabledProviders = async (workspaceId) => {
50
104
  try {
51
105
  const workspaceConfig = await readWorkspaceConfig(workspaceId);
@@ -144,27 +198,85 @@ export default function sessionRoutes(deps) {
144
198
  return;
145
199
  }
146
200
  await touchSession(session);
147
- const repoDiff = await getRepoDiff(session);
148
- const activeProvider = session.activeProvider || "codex";
149
- const enabledProviders = await resolveEnabledProviders(session.workspaceId);
150
- res.json({
151
- sessionId: req.params.sessionId,
152
- workspaceId: session.workspaceId,
153
- path: session.dir,
154
- repoUrl: session.repoUrl,
155
- name: session.name || "",
156
- defaultProvider: activeProvider,
157
- providers: enabledProviders,
158
- defaultInternetAccess:
159
- typeof session.defaultInternetAccess === "boolean"
160
- ? session.defaultInternetAccess
161
- : true,
162
- defaultDenyGitCredentialsAccess: resolveDefaultDenyGitCredentialsAccess(session),
163
- repoDiff,
164
- rpcLogsEnabled: debugApiWsLog,
165
- rpcLogs: debugApiWsLog ? session.rpcLogs || [] : [],
166
- terminalEnabled,
167
- });
201
+ res.json(await buildSessionResponse(session));
202
+ });
203
+
204
+ router.patch("/sessions/:sessionId", async (req, res) => {
205
+ const session = await getSession(req.params.sessionId, req.workspaceId);
206
+ if (!session) {
207
+ res.status(404).json({ error: "Session not found." });
208
+ return;
209
+ }
210
+ await touchSession(session);
211
+
212
+ const hasName = Object.prototype.hasOwnProperty.call(req.body || {}, "name");
213
+ const hasAuth = Object.prototype.hasOwnProperty.call(req.body || {}, "auth");
214
+ const hasInternet = Object.prototype.hasOwnProperty.call(req.body || {}, "defaultInternetAccess");
215
+ const hasDeny = Object.prototype.hasOwnProperty.call(
216
+ req.body || {},
217
+ "defaultDenyGitCredentialsAccess"
218
+ );
219
+ if (!hasName && !hasAuth && !hasInternet && !hasDeny) {
220
+ res.status(400).json({ error: "No updatable fields provided." });
221
+ return;
222
+ }
223
+
224
+ let updated = { ...session };
225
+ const changes = {};
226
+
227
+ try {
228
+ if (hasName) {
229
+ const name = typeof req.body?.name === "string" ? req.body.name.trim() : "";
230
+ if (!name) {
231
+ res.status(400).json({ error: "name is required." });
232
+ return;
233
+ }
234
+ updated.name = name;
235
+ changes.name = name;
236
+ }
237
+
238
+ if (hasInternet) {
239
+ if (typeof req.body?.defaultInternetAccess !== "boolean") {
240
+ res.status(400).json({ error: "defaultInternetAccess must be a boolean." });
241
+ return;
242
+ }
243
+ updated.defaultInternetAccess = req.body.defaultInternetAccess;
244
+ changes.defaultInternetAccess = req.body.defaultInternetAccess;
245
+ }
246
+
247
+ if (hasDeny) {
248
+ if (typeof req.body?.defaultDenyGitCredentialsAccess !== "boolean") {
249
+ res.status(400).json({ error: "defaultDenyGitCredentialsAccess must be a boolean." });
250
+ return;
251
+ }
252
+ updated.defaultDenyGitCredentialsAccess = req.body.defaultDenyGitCredentialsAccess;
253
+ changes.defaultDenyGitCredentialsAccess = req.body.defaultDenyGitCredentialsAccess;
254
+ }
255
+
256
+ if (hasAuth) {
257
+ const auth = req.body?.auth;
258
+ if (!auth || typeof auth !== "object" || Array.isArray(auth)) {
259
+ res.status(400).json({ error: "auth object is required." });
260
+ return;
261
+ }
262
+ updated = await updateSessionAuth(updated, auth);
263
+ changes.authUpdated = true;
264
+ }
265
+
266
+ updated.lastActivityAt = Date.now();
267
+ await storage.saveSession(session.sessionId, updated);
268
+ await restartMainProvider(updated);
269
+
270
+ const runtimePayload = await buildSessionResponse(updated);
271
+ res.json(runtimePayload);
272
+ broadcastToSession(session.sessionId, {
273
+ type: "session_updated",
274
+ sessionId: session.sessionId,
275
+ changes,
276
+ });
277
+ } catch (error) {
278
+ res.status(400).json({ error: error?.message || "Failed to update session." });
279
+ }
168
280
  });
169
281
 
170
282
  router.delete("/sessions/:sessionId", async (req, res) => {
@@ -89,7 +89,7 @@ const restartCodexClientsForWorkspace = async (workspaceId) => {
89
89
 
90
90
  export default function workspaceRoutes() {
91
91
  const router = Router();
92
- const deploymentMode = process.env.DEPLOYMENT_MODE;
92
+ const deploymentMode = process.env.VIBE80_DEPLOYMENT_MODE;
93
93
 
94
94
  router.post("/workspaces", async (req, res) => {
95
95
  if (deploymentMode === "mono_user") {
@@ -5,11 +5,11 @@ import { GIT_HOOKS_DIR } from "./config.js";
5
5
 
6
6
  const RUN_AS_HELPER = process.env.VIBE80_RUN_AS_HELPER || "/usr/local/bin/vibe80-run-as";
7
7
  const SUDO_PATH = process.env.VIBE80_SUDO_PATH || "sudo";
8
- const DEPLOYMENT_MODE = process.env.DEPLOYMENT_MODE;
9
- const IS_MONO_USER = DEPLOYMENT_MODE === "mono_user";
10
- const WORKSPACE_ROOT_DIRECTORY = process.env.WORKSPACE_ROOT_DIRECTORY || "/workspaces";
11
- const MONO_USER_WORKSPACE_DIR =
12
- process.env.MONO_USER_WORKSPACE_DIR || path.join(os.homedir(), "vibe80_workspace");
8
+ const VIBE80_DEPLOYMENT_MODE = process.env.VIBE80_DEPLOYMENT_MODE;
9
+ const IS_MONO_USER = VIBE80_DEPLOYMENT_MODE === "mono_user";
10
+ const VIBE80_WORKSPACE_ROOT_DIRECTORY = process.env.VIBE80_WORKSPACE_ROOT_DIRECTORY || "/workspaces";
11
+ const VIBE80_MONO_USER_WORKSPACE_DIR =
12
+ process.env.VIBE80_MONO_USER_WORKSPACE_DIR || path.join(os.homedir(), "vibe80_workspace");
13
13
  const ALLOWED_ENV_KEYS = new Set([
14
14
  "GIT_SSH_COMMAND",
15
15
  "GIT_CONFIG_GLOBAL",
@@ -147,14 +147,14 @@ const buildRunEnv = (options = {}) => {
147
147
  };
148
148
 
149
149
  export const getWorkspaceHome = (workspaceId) => {
150
- const homeBase = process.env.WORKSPACE_HOME_BASE || "/home";
150
+ const homeBase = process.env.VIBE80_WORKSPACE_HOME_BASE || "/home";
151
151
  return IS_MONO_USER ? os.homedir() : path.join(homeBase, workspaceId);
152
152
  };
153
153
 
154
154
  export const getWorkspaceRoot = (workspaceId) =>
155
155
  (IS_MONO_USER
156
- ? MONO_USER_WORKSPACE_DIR
157
- : path.join(WORKSPACE_ROOT_DIRECTORY, workspaceId));
156
+ ? VIBE80_MONO_USER_WORKSPACE_DIR
157
+ : path.join(VIBE80_WORKSPACE_ROOT_DIRECTORY, workspaceId));
158
158
 
159
159
  const validateCwd = (workspaceId, cwd) => {
160
160
  const resolved = path.resolve(cwd);
@@ -311,7 +311,7 @@ export const runAsCommand = (workspaceId, command, args, options = {}) =>
311
311
  ).catch((error) => {
312
312
  const details = [
313
313
  "run-as failed",
314
- `mode=${DEPLOYMENT_MODE || "unknown"}`,
314
+ `mode=${VIBE80_DEPLOYMENT_MODE || "unknown"}`,
315
315
  `sudo=${SUDO_PATH}`,
316
316
  `helper=${RUN_AS_HELPER}`,
317
317
  `workspace=${workspaceId}`,
@@ -329,7 +329,7 @@ export const runAsCommand = (workspaceId, command, args, options = {}) =>
329
329
  const envPairs = collectEnvPairs(options.env || {});
330
330
  const details = [
331
331
  "run-as failed",
332
- `mode=${DEPLOYMENT_MODE || "unknown"}`,
332
+ `mode=${VIBE80_DEPLOYMENT_MODE || "unknown"}`,
333
333
  IS_MONO_USER ? null : `sudo=${SUDO_PATH}`,
334
334
  IS_MONO_USER ? null : `helper=${RUN_AS_HELPER}`,
335
335
  `workspace=${workspaceId}`,
@@ -372,7 +372,7 @@ export const runAsCommandOutput = (workspaceId, command, args, options = {}) =>
372
372
  ).catch((error) => {
373
373
  const details = [
374
374
  "run-as output failed",
375
- `mode=${DEPLOYMENT_MODE || "unknown"}`,
375
+ `mode=${VIBE80_DEPLOYMENT_MODE || "unknown"}`,
376
376
  `sudo=${SUDO_PATH}`,
377
377
  `helper=${RUN_AS_HELPER}`,
378
378
  `workspace=${workspaceId}`,
@@ -390,7 +390,7 @@ export const runAsCommandOutput = (workspaceId, command, args, options = {}) =>
390
390
  const envPairs = collectEnvPairs(options.env || {});
391
391
  const details = [
392
392
  "run-as output failed",
393
- `mode=${DEPLOYMENT_MODE || "unknown"}`,
393
+ `mode=${VIBE80_DEPLOYMENT_MODE || "unknown"}`,
394
394
  IS_MONO_USER ? null : `sudo=${SUDO_PATH}`,
395
395
  IS_MONO_USER ? null : `helper=${RUN_AS_HELPER}`,
396
396
  `workspace=${workspaceId}`,
@@ -433,7 +433,7 @@ export const runAsCommandOutputWithStatus = (workspaceId, command, args, options
433
433
  ).catch((error) => {
434
434
  const details = [
435
435
  "run-as output failed",
436
- `mode=${DEPLOYMENT_MODE || "unknown"}`,
436
+ `mode=${VIBE80_DEPLOYMENT_MODE || "unknown"}`,
437
437
  `sudo=${SUDO_PATH}`,
438
438
  `helper=${RUN_AS_HELPER}`,
439
439
  `workspace=${workspaceId}`,
@@ -451,7 +451,7 @@ export const runAsCommandOutputWithStatus = (workspaceId, command, args, options
451
451
  const envPairs = collectEnvPairs(options.env || {});
452
452
  const details = [
453
453
  "run-as output failed",
454
- `mode=${DEPLOYMENT_MODE || "unknown"}`,
454
+ `mode=${VIBE80_DEPLOYMENT_MODE || "unknown"}`,
455
455
  IS_MONO_USER ? null : `sudo=${SUDO_PATH}`,
456
456
  IS_MONO_USER ? null : `helper=${RUN_AS_HELPER}`,
457
457
  `workspace=${workspaceId}`,
@@ -3,12 +3,12 @@ import { createWorkspaceToken, accessTokenTtlSeconds } from "../middleware/auth.
3
3
  import { generateId, hashRefreshToken, generateRefreshToken } from "../helpers.js";
4
4
 
5
5
  const refreshTokenTtlSeconds =
6
- Number(process.env.REFRESH_TOKEN_TTL_SECONDS) || 30 * 24 * 60 * 60;
6
+ Number(process.env.VIBE80_REFRESH_TOKEN_TTL_SECONDS) || 30 * 24 * 60 * 60;
7
7
  const refreshTokenTtlMs = refreshTokenTtlSeconds * 1000;
8
8
  const handoffTokenTtlMs =
9
- Number(process.env.HANDOFF_TOKEN_TTL_MS) || 120 * 1000;
9
+ Number(process.env.VIBE80_HANDOFF_TOKEN_TTL_MS) || 120 * 1000;
10
10
  const monoAuthTokenTtlMs =
11
- Number(process.env.MONO_AUTH_TOKEN_TTL_MS) || 5 * 60 * 1000;
11
+ Number(process.env.VIBE80_MONO_AUTH_TOKEN_TTL_MS) || 5 * 60 * 1000;
12
12
 
13
13
  export const handoffTokens = new Map();
14
14
  const monoAuthTokens = new Map();
@@ -18,8 +18,8 @@ import {
18
18
  } from "../helpers.js";
19
19
  import { debugApiWsLog } from "../middleware/debug.js";
20
20
  import {
21
- DEFAULT_GIT_AUTHOR_NAME,
22
- DEFAULT_GIT_AUTHOR_EMAIL,
21
+ VIBE80_DEFAULT_GIT_AUTHOR_NAME,
22
+ VIBE80_DEFAULT_GIT_AUTHOR_EMAIL,
23
23
  GIT_HOOKS_DIR,
24
24
  } from "../config.js";
25
25
  import {
@@ -60,12 +60,27 @@ import {
60
60
  const __filename = fileURLToPath(import.meta.url);
61
61
  const __dirname = path.dirname(__filename);
62
62
 
63
+ const parseSessionTtlSeconds = (value) => {
64
+ if (value == null) {
65
+ return 0;
66
+ }
67
+ const trimmed = String(value).trim();
68
+ if (!trimmed) {
69
+ return 0;
70
+ }
71
+ const parsed = Number(trimmed);
72
+ if (!Number.isFinite(parsed) || parsed <= 0) {
73
+ return 0;
74
+ }
75
+ return parsed;
76
+ };
77
+
63
78
  const sessionGcIntervalMs =
64
- Number(process.env.SESSION_GC_INTERVAL_MS) || 5 * 60 * 1000;
79
+ Number(process.env.VIBE80_SESSION_GC_INTERVAL_MS) || 5 * 60 * 1000;
65
80
  const sessionIdleTtlMs =
66
- Number(process.env.SESSION_IDLE_TTL_MS) || 24 * 60 * 60 * 1000;
81
+ parseSessionTtlSeconds(process.env.VIBE80_SESSION_IDLE_TTL_SECONDS) * 1000;
67
82
  const sessionMaxTtlMs =
68
- Number(process.env.SESSION_MAX_TTL_MS) || 7 * 24 * 60 * 60 * 1000;
83
+ parseSessionTtlSeconds(process.env.VIBE80_SESSION_MAX_TTL_SECONDS) * 1000;
69
84
  export const sessionIdPattern = /^s[0-9a-f]{24}$/;
70
85
 
71
86
  const TREE_IGNORED_NAMES = new Set([
@@ -94,6 +109,16 @@ export { modelCache, modelCacheTtlMs };
94
109
 
95
110
  export { sessionGcIntervalMs };
96
111
 
112
+ const parseCloneDepth = (value) => {
113
+ const parsed = Number.parseInt(String(value ?? "").trim(), 10);
114
+ if (!Number.isFinite(parsed) || parsed <= 0) {
115
+ return 50;
116
+ }
117
+ return parsed;
118
+ };
119
+
120
+ const cloneDepth = parseCloneDepth(process.env.VIBE80_CLONE_DEPTH);
121
+
97
122
  // ---------------------------------------------------------------------------
98
123
  // Session env / command helpers
99
124
  // ---------------------------------------------------------------------------
@@ -269,6 +294,20 @@ const normalizeRemoteBranches = (output, remote) =>
269
294
  ref.startsWith(`${remote}/`) ? ref.slice(remote.length + 1) : ref
270
295
  );
271
296
 
297
+ const parseLsRemoteBranches = (output) =>
298
+ output
299
+ .split(/\r?\n/)
300
+ .map((line) => line.trim())
301
+ .filter(Boolean)
302
+ .map((line) => {
303
+ const [, ref] = line.split(/\s+/);
304
+ if (!ref || !ref.startsWith("refs/heads/")) {
305
+ return "";
306
+ }
307
+ return ref.slice("refs/heads/".length).trim();
308
+ })
309
+ .filter(Boolean);
310
+
272
311
  export const getCurrentBranch = async (session) => {
273
312
  const output = await runSessionCommandOutput(
274
313
  session,
@@ -292,10 +331,7 @@ export const getLastCommit = async (session, cwd) => {
292
331
  };
293
332
 
294
333
  export const getBranchInfo = async (session, remote = "origin") => {
295
- await runSessionCommand(session, "git", ["fetch", "--prune"], {
296
- cwd: session.repoDir,
297
- });
298
- const [current, branchesOutput] = await Promise.all([
334
+ const [current, localBranchesOutput, remoteBranchesOutput] = await Promise.all([
299
335
  getCurrentBranch(session),
300
336
  runSessionCommandOutput(
301
337
  session,
@@ -303,11 +339,20 @@ export const getBranchInfo = async (session, remote = "origin") => {
303
339
  ["for-each-ref", "--format=%(refname:short)", `refs/remotes/${remote}`],
304
340
  { cwd: session.repoDir }
305
341
  ),
342
+ runSessionCommandOutput(
343
+ session,
344
+ "git",
345
+ ["ls-remote", "--heads", "--refs", remote],
346
+ { cwd: session.repoDir }
347
+ ).catch(() => ""),
306
348
  ]);
349
+ const localBranches = normalizeRemoteBranches(localBranchesOutput, remote);
350
+ const remoteBranches = parseLsRemoteBranches(remoteBranchesOutput);
351
+ const branches = Array.from(new Set([...localBranches, ...remoteBranches])).sort();
307
352
  return {
308
353
  current,
309
354
  remote,
310
- branches: normalizeRemoteBranches(branchesOutput, remote).sort(),
355
+ branches,
311
356
  };
312
357
  };
313
358
 
@@ -450,7 +495,20 @@ export const createSession = async (
450
495
  );
451
496
  await runAsCommand(workspaceId, "/bin/rm", ["-f", credInputPath]);
452
497
  }
453
- const cloneArgs = ["clone", repoUrl, repoDir];
498
+ const cloneArgs = [
499
+ "-c",
500
+ "http.version=HTTP/2",
501
+ "-c",
502
+ "fetch.parallel=10",
503
+ "clone",
504
+ "--depth",
505
+ String(cloneDepth),
506
+ "--filter=blob:none",
507
+ "--single-branch",
508
+ "--no-tags",
509
+ repoUrl,
510
+ repoDir,
511
+ ];
454
512
  const cloneEnv = { ...env };
455
513
  const cloneCmd = [];
456
514
  if (auth?.type === "http" && auth.username && auth.password) {
@@ -482,17 +540,17 @@ export const createSession = async (
482
540
  { cwd: repoDir }
483
541
  );
484
542
  }
485
- if (DEFAULT_GIT_AUTHOR_NAME && DEFAULT_GIT_AUTHOR_EMAIL) {
543
+ if (VIBE80_DEFAULT_GIT_AUTHOR_NAME && VIBE80_DEFAULT_GIT_AUTHOR_EMAIL) {
486
544
  await runAsCommand(
487
545
  workspaceId,
488
546
  "git",
489
- ["-C", repoDir, "config", "user.name", DEFAULT_GIT_AUTHOR_NAME],
547
+ ["-C", repoDir, "config", "user.name", VIBE80_DEFAULT_GIT_AUTHOR_NAME],
490
548
  { env }
491
549
  );
492
550
  await runAsCommand(
493
551
  workspaceId,
494
552
  "git",
495
- ["-C", repoDir, "config", "user.email", DEFAULT_GIT_AUTHOR_EMAIL],
553
+ ["-C", repoDir, "config", "user.email", VIBE80_DEFAULT_GIT_AUTHOR_EMAIL],
496
554
  { env }
497
555
  );
498
556
  }
@@ -599,6 +657,116 @@ export const createSession = async (
599
657
  }
600
658
  };
601
659
 
660
+ export const updateSessionAuth = async (session, auth) => {
661
+ if (!session) {
662
+ throw new Error("Invalid session.");
663
+ }
664
+ const authType = typeof auth?.type === "string" ? auth.type.trim() : "";
665
+ if (authType !== "none" && authType !== "ssh" && authType !== "http") {
666
+ throw new Error("Invalid auth type.");
667
+ }
668
+
669
+ const gitCredsDir = session.gitDir || path.join(session.dir, "git");
670
+ const sshPaths = getWorkspaceSshPaths(getWorkspacePaths(session.workspaceId).homeDir);
671
+ const credentialFile = path.join(gitCredsDir, "git-credentials");
672
+ const credentialInputFile = path.join(gitCredsDir, "git-credential-input");
673
+ const next = {
674
+ ...session,
675
+ gitDir: gitCredsDir,
676
+ sshKeyPath: null,
677
+ lastActivityAt: Date.now(),
678
+ };
679
+
680
+ await runAsCommand(session.workspaceId, "/bin/mkdir", ["-p", gitCredsDir]);
681
+ await runAsCommand(session.workspaceId, "/bin/chmod", ["2750", gitCredsDir]);
682
+
683
+ if (session.sshKeyPath) {
684
+ await runAsCommand(session.workspaceId, "/bin/rm", ["-f", session.sshKeyPath]).catch(() => {});
685
+ }
686
+ await runAsCommand(session.workspaceId, "/bin/rm", ["-f", credentialFile]).catch(() => {});
687
+ await runAsCommand(session.workspaceId, "/bin/rm", ["-f", credentialInputFile]).catch(() => {});
688
+ await runAsCommand(
689
+ session.workspaceId,
690
+ "git",
691
+ ["-C", session.repoDir, "config", "--unset-all", "core.sshCommand"]
692
+ ).catch(() => {});
693
+ await runAsCommand(
694
+ session.workspaceId,
695
+ "git",
696
+ ["-C", session.repoDir, "config", "--unset-all", "credential.helper"]
697
+ ).catch(() => {});
698
+
699
+ if (authType === "none") {
700
+ return next;
701
+ }
702
+
703
+ if (authType === "ssh") {
704
+ const privateKey = typeof auth?.privateKey === "string" ? auth.privateKey.trim() : "";
705
+ if (!privateKey) {
706
+ throw new Error("SSH private key is required.");
707
+ }
708
+ await ensureWorkspaceDir(session.workspaceId, sshPaths.sshDir, 0o700);
709
+ const keyPath = path.join(gitCredsDir, `ssh-key-${session.sessionId}`);
710
+ await writeWorkspaceFile(session.workspaceId, keyPath, `${privateKey}\n`, 0o600);
711
+ await ensureKnownHost(session.workspaceId, session.repoUrl, sshPaths);
712
+ await runAsCommand(
713
+ session.workspaceId,
714
+ "git",
715
+ [
716
+ "-C",
717
+ session.repoDir,
718
+ "config",
719
+ "core.sshCommand",
720
+ `ssh -i ${keyPath} -o IdentitiesOnly=yes`,
721
+ ]
722
+ );
723
+ return {
724
+ ...next,
725
+ sshKeyPath: keyPath,
726
+ };
727
+ }
728
+
729
+ const username = typeof auth?.username === "string" ? auth.username.trim() : "";
730
+ const password = typeof auth?.password === "string" ? auth.password : "";
731
+ if (!username || !password) {
732
+ throw new Error("HTTP username and password are required.");
733
+ }
734
+ const authInfo = resolveHttpAuthInfo(session.repoUrl);
735
+ if (!authInfo) {
736
+ throw new Error("Invalid HTTP repository URL for credential auth.");
737
+ }
738
+ const credentialPayload = [
739
+ `protocol=${authInfo.protocol}`,
740
+ `host=${authInfo.host}`,
741
+ `username=${username}`,
742
+ `password=${password}`,
743
+ "",
744
+ "",
745
+ ].join("\n");
746
+ await writeWorkspaceFile(session.workspaceId, credentialFile, "", 0o600);
747
+ await writeWorkspaceFile(session.workspaceId, credentialInputFile, credentialPayload, 0o600);
748
+ await runAsCommand(
749
+ session.workspaceId,
750
+ "git",
751
+ ["-c", `credential.helper=store --file ${credentialFile}`, "credential", "approve"],
752
+ { input: credentialPayload }
753
+ );
754
+ await runAsCommand(session.workspaceId, "/bin/rm", ["-f", credentialInputFile]).catch(() => {});
755
+ await runAsCommand(
756
+ session.workspaceId,
757
+ "git",
758
+ [
759
+ "-C",
760
+ session.repoDir,
761
+ "config",
762
+ "--add",
763
+ "credential.helper",
764
+ `store --file ${credentialFile}`,
765
+ ]
766
+ );
767
+ return next;
768
+ };
769
+
602
770
  // ---------------------------------------------------------------------------
603
771
  // Message helpers
604
772
  // ---------------------------------------------------------------------------