@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.
- package/README.md +132 -16
- package/bin/vibe80.js +1728 -16
- package/client/dist/assets/{DiffPanel-BKLnyIAZ.js → DiffPanel-BUJhQj_Q.js} +1 -1
- package/client/dist/assets/{ExplorerPanel-D3IbBsXz.js → ExplorerPanel-DugEeaO2.js} +1 -1
- package/client/dist/assets/{LogsPanel-BwJAFHRP.js → LogsPanel-BQrGxMu_.js} +1 -1
- package/client/dist/assets/{SettingsPanel-BfkchMnR.js → SettingsPanel-Ci2BdIYO.js} +1 -1
- package/client/dist/assets/{TerminalPanel-BQfMEm-u.js → TerminalPanel-C-T3t-6T.js} +1 -1
- package/client/dist/assets/index-cFi4LM0j.js +711 -0
- package/client/dist/assets/index-qNyFxUjK.css +32 -0
- package/client/dist/icon_square-512x512.png +0 -0
- package/client/dist/icon_square.svg +58 -0
- package/client/dist/index.html +3 -2
- package/client/dist/sw.js +1 -1
- package/client/index.html +1 -0
- package/client/public/icon_square-512x512.png +0 -0
- package/client/public/icon_square.svg +58 -0
- package/client/src/App.jsx +205 -2
- package/client/src/assets/vibe80_dark.png +0 -0
- package/client/src/assets/vibe80_light.png +0 -0
- package/client/src/components/Chat/ChatMessages.jsx +1 -1
- package/client/src/components/SessionGate/SessionGate.jsx +295 -91
- package/client/src/components/WorktreeTabs.css +11 -0
- package/client/src/components/WorktreeTabs.jsx +77 -47
- package/client/src/hooks/useChatSocket.js +8 -7
- package/client/src/hooks/useRepoBranchesModels.js +12 -6
- package/client/src/hooks/useWorktreeCloseConfirm.js +19 -7
- package/client/src/hooks/useWorktrees.js +3 -1
- package/client/src/index.css +26 -3
- package/client/src/locales/en.json +12 -1
- package/client/src/locales/fr.json +12 -1
- package/docs/api/openapi.json +1 -1
- package/package.json +2 -1
- package/server/scripts/rotate-workspace-secret.js +1 -1
- package/server/src/claudeClient.js +3 -3
- package/server/src/codexClient.js +3 -3
- package/server/src/config.js +6 -6
- package/server/src/index.js +14 -12
- package/server/src/middleware/auth.js +7 -7
- package/server/src/middleware/debug.js +36 -4
- package/server/src/providerLogger.js +2 -2
- package/server/src/routes/sessions.js +133 -21
- package/server/src/routes/workspaces.js +1 -1
- package/server/src/runAs.js +14 -14
- package/server/src/services/auth.js +3 -3
- package/server/src/services/session.js +182 -14
- package/server/src/services/workspace.js +86 -42
- package/server/src/storage/index.js +2 -2
- package/server/src/storage/redis.js +38 -36
- package/server/src/storage/sqlite.js +13 -13
- package/server/src/worktreeManager.js +87 -19
- package/server/tests/integration/routes/workspaces-routes.test.js +8 -8
- package/server/tests/setup/env.js +5 -5
- package/server/tests/unit/services/auth.test.js +3 -3
- package/client/dist/assets/index-BDQQz6SJ.css +0 -32
- 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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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.
|
|
92
|
+
const deploymentMode = process.env.VIBE80_DEPLOYMENT_MODE;
|
|
93
93
|
|
|
94
94
|
router.post("/workspaces", async (req, res) => {
|
|
95
95
|
if (deploymentMode === "mono_user") {
|
package/server/src/runAs.js
CHANGED
|
@@ -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
|
|
9
|
-
const IS_MONO_USER =
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
process.env.
|
|
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.
|
|
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
|
-
?
|
|
157
|
-
: path.join(
|
|
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=${
|
|
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=${
|
|
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=${
|
|
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=${
|
|
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=${
|
|
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=${
|
|
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.
|
|
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.
|
|
9
|
+
Number(process.env.VIBE80_HANDOFF_TOKEN_TTL_MS) || 120 * 1000;
|
|
10
10
|
const monoAuthTokenTtlMs =
|
|
11
|
-
Number(process.env.
|
|
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
|
-
|
|
22
|
-
|
|
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.
|
|
79
|
+
Number(process.env.VIBE80_SESSION_GC_INTERVAL_MS) || 5 * 60 * 1000;
|
|
65
80
|
const sessionIdleTtlMs =
|
|
66
|
-
|
|
81
|
+
parseSessionTtlSeconds(process.env.VIBE80_SESSION_IDLE_TTL_SECONDS) * 1000;
|
|
67
82
|
const sessionMaxTtlMs =
|
|
68
|
-
|
|
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
|
-
|
|
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
|
|
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 = [
|
|
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 (
|
|
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",
|
|
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",
|
|
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
|
// ---------------------------------------------------------------------------
|