@vibe80/vibe80 0.2.1 → 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 +93 -4
- package/bin/vibe80.js +1728 -16
- package/client/dist/assets/{DiffPanel-Luk1GrAd.js → DiffPanel-BUJhQj_Q.js} +1 -1
- package/client/dist/assets/{ExplorerPanel-CGVmxql1.js → ExplorerPanel-DugEeaO2.js} +1 -1
- package/client/dist/assets/{LogsPanel-D8Z8Zam8.js → LogsPanel-BQrGxMu_.js} +1 -1
- package/client/dist/assets/{SettingsPanel-H2q2eoDf.js → SettingsPanel-Ci2BdIYO.js} +1 -1
- package/client/dist/assets/{TerminalPanel-kEz-cjes.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 +204 -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 +282 -90
- 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-knjescVH.js +0 -711
|
@@ -11,18 +11,18 @@ import storage from "../storage/index.js";
|
|
|
11
11
|
import { generateId } from "../helpers.js";
|
|
12
12
|
import { logDebug } from "../middleware/debug.js";
|
|
13
13
|
|
|
14
|
-
const deploymentMode = process.env.
|
|
14
|
+
const deploymentMode = process.env.VIBE80_DEPLOYMENT_MODE;
|
|
15
15
|
const isMonoUser = deploymentMode === "mono_user";
|
|
16
|
-
const workspaceHomeBase = process.env.
|
|
17
|
-
const workspaceRootBase = process.env.
|
|
16
|
+
const workspaceHomeBase = process.env.VIBE80_WORKSPACE_HOME_BASE || "/home";
|
|
17
|
+
const workspaceRootBase = process.env.VIBE80_WORKSPACE_ROOT_DIRECTORY || "/workspaces";
|
|
18
18
|
const workspaceRootName = "vibe80_workspace";
|
|
19
19
|
const monoUserWorkspaceDir =
|
|
20
|
-
process.env.
|
|
20
|
+
process.env.VIBE80_MONO_USER_WORKSPACE_DIR || path.join(os.homedir(), workspaceRootName);
|
|
21
21
|
const workspaceSessionsDirName = "sessions";
|
|
22
22
|
const rootHelperPath = process.env.VIBE80_ROOT_HELPER || "/usr/local/bin/vibe80-root";
|
|
23
23
|
const sudoPath = process.env.VIBE80_SUDO_PATH || "sudo";
|
|
24
|
-
const workspaceUidMin = Number.parseInt(process.env.
|
|
25
|
-
const workspaceUidMax = Number.parseInt(process.env.
|
|
24
|
+
const workspaceUidMin = Number.parseInt(process.env.VIBE80_WORKSPACE_UID_MIN, 10) || 200000;
|
|
25
|
+
const workspaceUidMax = Number.parseInt(process.env.VIBE80_WORKSPACE_UID_MAX, 10) || 999999999;
|
|
26
26
|
const workspaceUserExistsCache = new Map();
|
|
27
27
|
|
|
28
28
|
export const workspaceIdPattern = isMonoUser ? /^default$/ : /^w[0-9a-f]{24}$/;
|
|
@@ -36,6 +36,36 @@ const runRootCommand = (args, options = {}) => {
|
|
|
36
36
|
return runCommand(sudoPath, ["-n", rootHelperPath, ...args], options);
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
+
const runWorkspaceStatOutput = async (
|
|
40
|
+
workspaceId,
|
|
41
|
+
targetPath,
|
|
42
|
+
{ gnuFormat, bsdFormat, options = {} }
|
|
43
|
+
) => {
|
|
44
|
+
try {
|
|
45
|
+
return await runAsCommandOutput(
|
|
46
|
+
workspaceId,
|
|
47
|
+
"/usr/bin/stat",
|
|
48
|
+
["-c", gnuFormat, targetPath],
|
|
49
|
+
options
|
|
50
|
+
);
|
|
51
|
+
} catch {
|
|
52
|
+
return runAsCommandOutput(
|
|
53
|
+
workspaceId,
|
|
54
|
+
"/usr/bin/stat",
|
|
55
|
+
["-f", bsdFormat, targetPath],
|
|
56
|
+
options
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const runHostStatOutput = async (targetPath, { gnuFormat, bsdFormat }) => {
|
|
62
|
+
try {
|
|
63
|
+
return await runCommandOutput("/usr/bin/stat", ["-c", gnuFormat, targetPath]);
|
|
64
|
+
} catch {
|
|
65
|
+
return runCommandOutput("/usr/bin/stat", ["-f", bsdFormat, targetPath]);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
39
69
|
export const getWorkspacePaths = (workspaceId) => {
|
|
40
70
|
const home = isMonoUser ? os.homedir() : path.join(workspaceHomeBase, workspaceId);
|
|
41
71
|
const root = isMonoUser
|
|
@@ -161,34 +191,50 @@ export const listWorkspaceEntries = async (workspaceId, dirPath) => {
|
|
|
161
191
|
};
|
|
162
192
|
|
|
163
193
|
export const getWorkspaceStat = async (workspaceId, targetPath, options = {}) => {
|
|
164
|
-
const output = await
|
|
165
|
-
"
|
|
166
|
-
"%
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const [
|
|
170
|
-
const modeValue = Number.parseInt(
|
|
194
|
+
const output = await runWorkspaceStatOutput(workspaceId, targetPath, {
|
|
195
|
+
gnuFormat: "%f\t%s\t%a",
|
|
196
|
+
bsdFormat: "%HT\t%z\t%Lp",
|
|
197
|
+
options,
|
|
198
|
+
});
|
|
199
|
+
const [typeOrMode, sizeRaw, modeRaw] = output.trim().split("\t");
|
|
200
|
+
const modeValue = Number.parseInt(typeOrMode, 16);
|
|
171
201
|
const typeBits = Number.isFinite(modeValue) ? modeValue & 0o170000 : null;
|
|
172
202
|
let type = "";
|
|
173
|
-
if (typeBits
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
203
|
+
if (Number.isFinite(typeBits)) {
|
|
204
|
+
if (typeBits === 0o100000) {
|
|
205
|
+
type = "regular";
|
|
206
|
+
} else if (typeBits === 0o040000) {
|
|
207
|
+
type = "directory";
|
|
208
|
+
} else if (typeBits === 0o120000) {
|
|
209
|
+
type = "symlink";
|
|
210
|
+
} else {
|
|
211
|
+
type = "other";
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
const normalizedType = String(typeOrMode || "").toLowerCase();
|
|
215
|
+
if (normalizedType.includes("regular")) {
|
|
216
|
+
type = "regular";
|
|
217
|
+
} else if (normalizedType.includes("directory")) {
|
|
218
|
+
type = "directory";
|
|
219
|
+
} else if (normalizedType.includes("symbolic")) {
|
|
220
|
+
type = "symlink";
|
|
221
|
+
} else if (normalizedType) {
|
|
222
|
+
type = "other";
|
|
223
|
+
}
|
|
181
224
|
}
|
|
182
225
|
return {
|
|
183
226
|
type,
|
|
184
227
|
size: Number.parseInt(sizeRaw, 10),
|
|
185
|
-
mode: modeRaw,
|
|
228
|
+
mode: String(modeRaw || ""),
|
|
186
229
|
};
|
|
187
230
|
};
|
|
188
231
|
|
|
189
232
|
export const workspacePathExists = async (workspaceId, targetPath) => {
|
|
190
233
|
try {
|
|
191
|
-
await
|
|
234
|
+
await runWorkspaceStatOutput(workspaceId, targetPath, {
|
|
235
|
+
gnuFormat: "%F",
|
|
236
|
+
bsdFormat: "%HT",
|
|
237
|
+
});
|
|
192
238
|
return true;
|
|
193
239
|
} catch {
|
|
194
240
|
return false;
|
|
@@ -578,11 +624,10 @@ const recoverWorkspaceIds = async (workspaceId) => {
|
|
|
578
624
|
let gid = Number.isFinite(workspaceRecord?.gid) ? Number(workspaceRecord.gid) : null;
|
|
579
625
|
if (!Number.isFinite(uid) || !Number.isFinite(gid)) {
|
|
580
626
|
try {
|
|
581
|
-
const output = await
|
|
582
|
-
"
|
|
583
|
-
"%u\t%g",
|
|
584
|
-
|
|
585
|
-
]);
|
|
627
|
+
const output = await runWorkspaceStatOutput(workspaceId, homeDir, {
|
|
628
|
+
gnuFormat: "%u\t%g",
|
|
629
|
+
bsdFormat: "%u\t%g",
|
|
630
|
+
});
|
|
586
631
|
const [uidRaw, gidRaw] = output.trim().split("\t");
|
|
587
632
|
if (!Number.isFinite(uid)) {
|
|
588
633
|
uid = Number(uidRaw);
|
|
@@ -592,11 +637,10 @@ const recoverWorkspaceIds = async (workspaceId) => {
|
|
|
592
637
|
}
|
|
593
638
|
} catch {
|
|
594
639
|
try {
|
|
595
|
-
const output = await
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
]);
|
|
640
|
+
const output = await runHostStatOutput(homeDir, {
|
|
641
|
+
gnuFormat: "%u\t%g",
|
|
642
|
+
bsdFormat: "%u\t%g",
|
|
643
|
+
});
|
|
600
644
|
const [uidRaw, gidRaw] = output.trim().split("\t");
|
|
601
645
|
if (!Number.isFinite(uid)) {
|
|
602
646
|
uid = Number(uidRaw);
|
|
@@ -843,12 +887,12 @@ const applyMonoEnabledProviders = (providers = {}, enabledConfig) => {
|
|
|
843
887
|
};
|
|
844
888
|
|
|
845
889
|
const buildMonoUserProviderOverridesFromEnv = () => {
|
|
846
|
-
const codexApiKey = readEnvValue("
|
|
847
|
-
const codexAuthJsonB64Raw = process.env.
|
|
890
|
+
const codexApiKey = readEnvValue("VIBE80_CODEX_API_KEY");
|
|
891
|
+
const codexAuthJsonB64Raw = process.env.VIBE80_CODEX_AUTH_JSON_B64;
|
|
848
892
|
const hasCodexAuthJsonB64 = typeof codexAuthJsonB64Raw === "string";
|
|
849
893
|
const codexAuthJsonB64 = hasCodexAuthJsonB64 ? codexAuthJsonB64Raw.trim() : "";
|
|
850
|
-
const claudeApiKey = readEnvValue("
|
|
851
|
-
const claudeSetupTokenRaw = process.env.
|
|
894
|
+
const claudeApiKey = readEnvValue("VIBE80_CLAUDE_API_KEY");
|
|
895
|
+
const claudeSetupTokenRaw = process.env.VIBE80_CLAUDE_SETUP_TOKEN;
|
|
852
896
|
const hasClaudeSetupToken = typeof claudeSetupTokenRaw === "string";
|
|
853
897
|
const claudeSetupToken = hasClaudeSetupToken ? claudeSetupTokenRaw.trim() : "";
|
|
854
898
|
|
|
@@ -856,13 +900,13 @@ const buildMonoUserProviderOverridesFromEnv = () => {
|
|
|
856
900
|
|
|
857
901
|
if (codexApiKey && hasCodexAuthJsonB64) {
|
|
858
902
|
console.warn(
|
|
859
|
-
"[warn] Both
|
|
903
|
+
"[warn] Both VIBE80_CODEX_API_KEY and VIBE80_CODEX_AUTH_JSON_B64 are set; using VIBE80_CODEX_AUTH_JSON_B64."
|
|
860
904
|
);
|
|
861
905
|
}
|
|
862
906
|
if (hasCodexAuthJsonB64) {
|
|
863
907
|
if (!codexAuthJsonB64) {
|
|
864
908
|
console.warn(
|
|
865
|
-
"[warn] Invalid
|
|
909
|
+
"[warn] Invalid VIBE80_CODEX_AUTH_JSON_B64 detected; ignoring codex preprovisioning."
|
|
866
910
|
);
|
|
867
911
|
} else {
|
|
868
912
|
try {
|
|
@@ -873,7 +917,7 @@ const buildMonoUserProviderOverridesFromEnv = () => {
|
|
|
873
917
|
};
|
|
874
918
|
} catch {
|
|
875
919
|
console.warn(
|
|
876
|
-
"[warn] Invalid
|
|
920
|
+
"[warn] Invalid VIBE80_CODEX_AUTH_JSON_B64 detected; ignoring codex preprovisioning."
|
|
877
921
|
);
|
|
878
922
|
}
|
|
879
923
|
}
|
|
@@ -885,13 +929,13 @@ const buildMonoUserProviderOverridesFromEnv = () => {
|
|
|
885
929
|
|
|
886
930
|
if (claudeApiKey && hasClaudeSetupToken) {
|
|
887
931
|
console.warn(
|
|
888
|
-
"[warn] Both
|
|
932
|
+
"[warn] Both VIBE80_CLAUDE_API_KEY and VIBE80_CLAUDE_SETUP_TOKEN are set; using VIBE80_CLAUDE_SETUP_TOKEN."
|
|
889
933
|
);
|
|
890
934
|
}
|
|
891
935
|
if (hasClaudeSetupToken) {
|
|
892
936
|
if (!claudeSetupToken) {
|
|
893
937
|
console.warn(
|
|
894
|
-
"[warn] Invalid
|
|
938
|
+
"[warn] Invalid VIBE80_CLAUDE_SETUP_TOKEN detected; ignoring claude preprovisioning."
|
|
895
939
|
);
|
|
896
940
|
} else {
|
|
897
941
|
overrides.claude = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createRedisStorage } from "./redis.js";
|
|
2
2
|
import { createSqliteStorage } from "./sqlite.js";
|
|
3
3
|
|
|
4
|
-
const backend = process.env.
|
|
4
|
+
const backend = process.env.VIBE80_STORAGE_BACKEND || "sqlite";
|
|
5
5
|
|
|
6
6
|
let storage = null;
|
|
7
7
|
|
|
@@ -10,7 +10,7 @@ if (backend === "redis") {
|
|
|
10
10
|
} else if (backend === "sqlite") {
|
|
11
11
|
storage = createSqliteStorage();
|
|
12
12
|
} else {
|
|
13
|
-
throw new Error(`Unsupported
|
|
13
|
+
throw new Error(`Unsupported VIBE80_STORAGE_BACKEND: ${backend}.`);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export const storageBackend = backend;
|
|
@@ -24,11 +24,11 @@ const sanitizeSessionData = (data) => {
|
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
export const createRedisStorage = () => {
|
|
27
|
-
const url = process.env.
|
|
27
|
+
const url = process.env.VIBE80_REDIS_URL;
|
|
28
28
|
if (!url) {
|
|
29
|
-
throw new Error("
|
|
29
|
+
throw new Error("VIBE80_REDIS_URL is required when VIBE80_STORAGE_BACKEND=redis.");
|
|
30
30
|
}
|
|
31
|
-
const prefix = process.env.
|
|
31
|
+
const prefix = process.env.VIBE80_REDIS_KEY_PREFIX || DEFAULT_PREFIX;
|
|
32
32
|
const client = createClient({ url });
|
|
33
33
|
|
|
34
34
|
const sessionKey = (sessionId) => buildKey(prefix, "session", sessionId);
|
|
@@ -36,13 +36,14 @@ export const createRedisStorage = () => {
|
|
|
36
36
|
buildKey(prefix, "session", sessionId, "worktrees");
|
|
37
37
|
const workspaceSessionsKey = (workspaceId) =>
|
|
38
38
|
buildKey(prefix, "workspace", workspaceId, "sessions");
|
|
39
|
-
const worktreeKey = (worktreeId) =>
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
39
|
+
const worktreeKey = (sessionId, worktreeId) =>
|
|
40
|
+
buildKey(prefix, "worktree", sessionId, worktreeId);
|
|
41
|
+
const worktreeMessagesKey = (sessionId, worktreeId) =>
|
|
42
|
+
buildKey(prefix, "worktree", sessionId, worktreeId, "messages");
|
|
43
|
+
const worktreeMessageIndexKey = (sessionId, worktreeId) =>
|
|
44
|
+
buildKey(prefix, "worktree", sessionId, worktreeId, "messageIndex");
|
|
45
|
+
const worktreeMessageSeqKey = (sessionId, worktreeId) =>
|
|
46
|
+
buildKey(prefix, "worktree", sessionId, worktreeId, "messageSeq");
|
|
46
47
|
const workspaceUserIdsKey = (workspaceId) =>
|
|
47
48
|
buildKey(prefix, "workspaceUserIds", workspaceId);
|
|
48
49
|
const workspaceKey = (workspaceId) => buildKey(prefix, "workspace", workspaceId);
|
|
@@ -52,11 +53,12 @@ export const createRedisStorage = () => {
|
|
|
52
53
|
const refreshTokenKey = (tokenHash) => buildKey(prefix, "refreshToken", tokenHash);
|
|
53
54
|
const globalSessionsKey = () => buildKey(prefix, "sessions");
|
|
54
55
|
|
|
55
|
-
const sessionTtlMs =
|
|
56
|
+
const sessionTtlMs =
|
|
57
|
+
(Number.parseInt(process.env.VIBE80_SESSION_MAX_TTL_SECONDS, 10) || 0) * 1000;
|
|
56
58
|
const workspaceUidMin =
|
|
57
|
-
Number.parseInt(process.env.
|
|
59
|
+
Number.parseInt(process.env.VIBE80_WORKSPACE_UID_MIN, 10) || 200000;
|
|
58
60
|
const workspaceUidMax =
|
|
59
|
-
Number.parseInt(process.env.
|
|
61
|
+
Number.parseInt(process.env.VIBE80_WORKSPACE_UID_MAX, 10) || 999999999;
|
|
60
62
|
|
|
61
63
|
const ensureConnected = async () => {
|
|
62
64
|
if (client.isOpen) {
|
|
@@ -104,10 +106,10 @@ export const createRedisStorage = () => {
|
|
|
104
106
|
const worktreeIds = await client.sMembers(sessionWorktreesKey(sessionId));
|
|
105
107
|
if (worktreeIds.length) {
|
|
106
108
|
const keys = worktreeIds.flatMap((id) => [
|
|
107
|
-
worktreeKey(id),
|
|
108
|
-
worktreeMessagesKey(id),
|
|
109
|
-
worktreeMessageIndexKey(id),
|
|
110
|
-
worktreeMessageSeqKey(id),
|
|
109
|
+
worktreeKey(sessionId, id),
|
|
110
|
+
worktreeMessagesKey(sessionId, id),
|
|
111
|
+
worktreeMessageIndexKey(sessionId, id),
|
|
112
|
+
worktreeMessageSeqKey(sessionId, id),
|
|
111
113
|
]);
|
|
112
114
|
await client.del(keys);
|
|
113
115
|
}
|
|
@@ -143,24 +145,24 @@ export const createRedisStorage = () => {
|
|
|
143
145
|
|
|
144
146
|
const saveWorktree = async (sessionId, worktreeId, data) => {
|
|
145
147
|
await ensureConnected();
|
|
146
|
-
await setWithTtl(worktreeKey(worktreeId), toJson(data), sessionTtlMs);
|
|
148
|
+
await setWithTtl(worktreeKey(sessionId, worktreeId), toJson(data), sessionTtlMs);
|
|
147
149
|
await client.sAdd(sessionWorktreesKey(sessionId), worktreeId);
|
|
148
150
|
await touchTtl(sessionWorktreesKey(sessionId), sessionTtlMs);
|
|
149
151
|
};
|
|
150
152
|
|
|
151
|
-
const getWorktree = async (worktreeId) => {
|
|
153
|
+
const getWorktree = async (sessionId, worktreeId) => {
|
|
152
154
|
await ensureConnected();
|
|
153
|
-
const raw = await client.get(worktreeKey(worktreeId));
|
|
155
|
+
const raw = await client.get(worktreeKey(sessionId, worktreeId));
|
|
154
156
|
return fromJson(raw);
|
|
155
157
|
};
|
|
156
158
|
|
|
157
159
|
const deleteWorktree = async (sessionId, worktreeId) => {
|
|
158
160
|
await ensureConnected();
|
|
159
161
|
await client.del(
|
|
160
|
-
worktreeKey(worktreeId),
|
|
161
|
-
worktreeMessagesKey(worktreeId),
|
|
162
|
-
worktreeMessageIndexKey(worktreeId),
|
|
163
|
-
worktreeMessageSeqKey(worktreeId)
|
|
162
|
+
worktreeKey(sessionId, worktreeId),
|
|
163
|
+
worktreeMessagesKey(sessionId, worktreeId),
|
|
164
|
+
worktreeMessageIndexKey(sessionId, worktreeId),
|
|
165
|
+
worktreeMessageSeqKey(sessionId, worktreeId)
|
|
164
166
|
);
|
|
165
167
|
await client.sRem(sessionWorktreesKey(sessionId), worktreeId);
|
|
166
168
|
};
|
|
@@ -171,7 +173,7 @@ export const createRedisStorage = () => {
|
|
|
171
173
|
if (!ids.length) {
|
|
172
174
|
return [];
|
|
173
175
|
}
|
|
174
|
-
const keys = ids.map((id) => worktreeKey(id));
|
|
176
|
+
const keys = ids.map((id) => worktreeKey(sessionId, id));
|
|
175
177
|
const raw = await client.mGet(keys);
|
|
176
178
|
return raw.map(fromJson).filter(Boolean);
|
|
177
179
|
};
|
|
@@ -182,12 +184,12 @@ export const createRedisStorage = () => {
|
|
|
182
184
|
if (!messageId) {
|
|
183
185
|
throw new Error("Message id is required.");
|
|
184
186
|
}
|
|
185
|
-
const seq = await client.incr(worktreeMessageSeqKey(worktreeId));
|
|
186
|
-
await client.hSet(worktreeMessageIndexKey(worktreeId), messageId, seq);
|
|
187
|
-
await client.rPush(worktreeMessagesKey(worktreeId), toJson(message));
|
|
188
|
-
await touchTtl(worktreeMessagesKey(worktreeId), sessionTtlMs);
|
|
189
|
-
await touchTtl(worktreeMessageIndexKey(worktreeId), sessionTtlMs);
|
|
190
|
-
await touchTtl(worktreeMessageSeqKey(worktreeId), sessionTtlMs);
|
|
187
|
+
const seq = await client.incr(worktreeMessageSeqKey(sessionId, worktreeId));
|
|
188
|
+
await client.hSet(worktreeMessageIndexKey(sessionId, worktreeId), messageId, seq);
|
|
189
|
+
await client.rPush(worktreeMessagesKey(sessionId, worktreeId), toJson(message));
|
|
190
|
+
await touchTtl(worktreeMessagesKey(sessionId, worktreeId), sessionTtlMs);
|
|
191
|
+
await touchTtl(worktreeMessageIndexKey(sessionId, worktreeId), sessionTtlMs);
|
|
192
|
+
await touchTtl(worktreeMessageSeqKey(sessionId, worktreeId), sessionTtlMs);
|
|
191
193
|
};
|
|
192
194
|
|
|
193
195
|
const getWorktreeMessages = async (
|
|
@@ -196,7 +198,7 @@ export const createRedisStorage = () => {
|
|
|
196
198
|
{ limit = null, beforeMessageId = null } = {}
|
|
197
199
|
) => {
|
|
198
200
|
await ensureConnected();
|
|
199
|
-
const listKey = worktreeMessagesKey(worktreeId);
|
|
201
|
+
const listKey = worktreeMessagesKey(sessionId, worktreeId);
|
|
200
202
|
const listLength = await client.lLen(listKey);
|
|
201
203
|
if (!listLength) {
|
|
202
204
|
return [];
|
|
@@ -207,7 +209,7 @@ export const createRedisStorage = () => {
|
|
|
207
209
|
|
|
208
210
|
if (beforeMessageId) {
|
|
209
211
|
const seqValue = await client.hGet(
|
|
210
|
-
worktreeMessageIndexKey(worktreeId),
|
|
212
|
+
worktreeMessageIndexKey(sessionId, worktreeId),
|
|
211
213
|
beforeMessageId
|
|
212
214
|
);
|
|
213
215
|
const seq = Number.parseInt(seqValue, 10);
|
|
@@ -233,9 +235,9 @@ export const createRedisStorage = () => {
|
|
|
233
235
|
const clearWorktreeMessages = async (sessionId, worktreeId) => {
|
|
234
236
|
await ensureConnected();
|
|
235
237
|
await client.del(
|
|
236
|
-
worktreeMessagesKey(worktreeId),
|
|
237
|
-
worktreeMessageIndexKey(worktreeId),
|
|
238
|
-
worktreeMessageSeqKey(worktreeId)
|
|
238
|
+
worktreeMessagesKey(sessionId, worktreeId),
|
|
239
|
+
worktreeMessageIndexKey(sessionId, worktreeId),
|
|
240
|
+
worktreeMessageSeqKey(sessionId, worktreeId)
|
|
239
241
|
);
|
|
240
242
|
};
|
|
241
243
|
|
|
@@ -72,12 +72,12 @@ const all = (db, sql, params = []) =>
|
|
|
72
72
|
|
|
73
73
|
export const createSqliteStorage = () => {
|
|
74
74
|
const homeDir = process.env.HOME || os.homedir();
|
|
75
|
-
const isMonoUser = process.env.
|
|
75
|
+
const isMonoUser = process.env.VIBE80_DEPLOYMENT_MODE === "mono_user";
|
|
76
76
|
const defaultDataDirectory = isMonoUser
|
|
77
77
|
? path.join(homeDir, ".vibe80")
|
|
78
78
|
: "/var/lib/vibe80";
|
|
79
79
|
const dataDirectory = process.env.VIBE80_DATA_DIRECTORY || defaultDataDirectory;
|
|
80
|
-
const dbPath = process.env.
|
|
80
|
+
const dbPath = process.env.VIBE80_SQLITE_PATH || path.join(dataDirectory, "base.sqlite");
|
|
81
81
|
const resolvedPath = path.resolve(dbPath);
|
|
82
82
|
const dir = path.dirname(resolvedPath);
|
|
83
83
|
fs.mkdirSync(dir, { recursive: true, mode: 0o750 });
|
|
@@ -108,9 +108,10 @@ export const createSqliteStorage = () => {
|
|
|
108
108
|
await run(
|
|
109
109
|
db,
|
|
110
110
|
`CREATE TABLE IF NOT EXISTS worktrees (
|
|
111
|
-
worktreeId TEXT
|
|
111
|
+
worktreeId TEXT NOT NULL,
|
|
112
112
|
sessionId TEXT NOT NULL,
|
|
113
113
|
data TEXT NOT NULL,
|
|
114
|
+
PRIMARY KEY (sessionId, worktreeId),
|
|
114
115
|
FOREIGN KEY(sessionId) REFERENCES sessions(sessionId) ON DELETE CASCADE
|
|
115
116
|
);`
|
|
116
117
|
);
|
|
@@ -127,7 +128,7 @@ export const createSqliteStorage = () => {
|
|
|
127
128
|
worktreeId TEXT NOT NULL,
|
|
128
129
|
createdAt INTEGER NOT NULL,
|
|
129
130
|
data TEXT NOT NULL,
|
|
130
|
-
PRIMARY KEY (worktreeId, messageId),
|
|
131
|
+
PRIMARY KEY (sessionId, worktreeId, messageId),
|
|
131
132
|
FOREIGN KEY(sessionId) REFERENCES sessions(sessionId) ON DELETE CASCADE
|
|
132
133
|
);`
|
|
133
134
|
);
|
|
@@ -194,7 +195,7 @@ export const createSqliteStorage = () => {
|
|
|
194
195
|
const row = await get(db, "SELECT lastUid FROM workspace_uid_seq WHERE id = 1");
|
|
195
196
|
if (!row) {
|
|
196
197
|
const workspaceUidMin =
|
|
197
|
-
Number.parseInt(process.env.
|
|
198
|
+
Number.parseInt(process.env.VIBE80_WORKSPACE_UID_MIN, 10) || 200000;
|
|
198
199
|
await run(
|
|
199
200
|
db,
|
|
200
201
|
"INSERT INTO workspace_uid_seq (id, lastUid) VALUES (1, ?)",
|
|
@@ -275,19 +276,18 @@ export const createSqliteStorage = () => {
|
|
|
275
276
|
db,
|
|
276
277
|
`INSERT INTO worktrees (worktreeId, sessionId, data)
|
|
277
278
|
VALUES (?, ?, ?)
|
|
278
|
-
ON CONFLICT(worktreeId) DO UPDATE SET
|
|
279
|
-
sessionId=excluded.sessionId,
|
|
279
|
+
ON CONFLICT(sessionId, worktreeId) DO UPDATE SET
|
|
280
280
|
data=excluded.data;`,
|
|
281
281
|
[worktreeId, sessionId, toJson(data)]
|
|
282
282
|
);
|
|
283
283
|
};
|
|
284
284
|
|
|
285
|
-
const getWorktree = async (worktreeId) => {
|
|
285
|
+
const getWorktree = async (sessionId, worktreeId) => {
|
|
286
286
|
await ensureConnected();
|
|
287
287
|
const row = await get(
|
|
288
288
|
db,
|
|
289
|
-
"SELECT data FROM worktrees WHERE worktreeId = ?",
|
|
290
|
-
[worktreeId]
|
|
289
|
+
"SELECT data FROM worktrees WHERE sessionId = ? AND worktreeId = ?",
|
|
290
|
+
[sessionId, worktreeId]
|
|
291
291
|
);
|
|
292
292
|
return fromJson(row?.data);
|
|
293
293
|
};
|
|
@@ -328,7 +328,7 @@ export const createSqliteStorage = () => {
|
|
|
328
328
|
db,
|
|
329
329
|
`INSERT INTO worktree_messages (messageId, sessionId, worktreeId, createdAt, data)
|
|
330
330
|
VALUES (?, ?, ?, ?, ?)
|
|
331
|
-
ON CONFLICT(worktreeId, messageId) DO NOTHING;`,
|
|
331
|
+
ON CONFLICT(sessionId, worktreeId, messageId) DO NOTHING;`,
|
|
332
332
|
[messageId, sessionId, worktreeId, createdAt, toJson(message)]
|
|
333
333
|
);
|
|
334
334
|
};
|
|
@@ -559,9 +559,9 @@ export const createSqliteStorage = () => {
|
|
|
559
559
|
const getNextWorkspaceUid = async () => {
|
|
560
560
|
await ensureConnected();
|
|
561
561
|
const workspaceUidMin =
|
|
562
|
-
Number.parseInt(process.env.
|
|
562
|
+
Number.parseInt(process.env.VIBE80_WORKSPACE_UID_MIN, 10) || 200000;
|
|
563
563
|
const workspaceUidMax =
|
|
564
|
-
Number.parseInt(process.env.
|
|
564
|
+
Number.parseInt(process.env.VIBE80_WORKSPACE_UID_MAX, 10) || 999999999;
|
|
565
565
|
await run(db, "BEGIN IMMEDIATE");
|
|
566
566
|
try {
|
|
567
567
|
const row = await get(db, "SELECT lastUid FROM workspace_uid_seq WHERE id = 1");
|