@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
package/bin/vibe80.js
CHANGED
|
@@ -2,26 +2,370 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
const { spawn } = require("child_process");
|
|
5
|
+
const { Command } = require("commander");
|
|
5
6
|
const fs = require("fs");
|
|
6
7
|
const path = require("path");
|
|
7
8
|
const os = require("os");
|
|
8
9
|
|
|
9
10
|
const rootDir = path.resolve(__dirname, "..");
|
|
10
11
|
const homeDir = process.env.HOME || os.homedir();
|
|
12
|
+
const defaultEnv = {
|
|
13
|
+
VIBE80_DEPLOYMENT_MODE: "mono_user",
|
|
14
|
+
VIBE80_DATA_DIRECTORY: path.join(homeDir, ".vibe80"),
|
|
15
|
+
VIBE80_STORAGE_BACKEND: "sqlite",
|
|
16
|
+
};
|
|
11
17
|
const monoAuthUrlFile = path.join(
|
|
12
18
|
os.tmpdir(),
|
|
13
19
|
`vibe80-mono-auth-${process.pid}-${Date.now()}.url`
|
|
14
20
|
);
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
const defaultBaseUrl = process.env.VIBE80_BASE_URL || "http://localhost:5179";
|
|
22
|
+
|
|
23
|
+
const resolveCliStatePath = () => {
|
|
24
|
+
const dataDir = process.env.VIBE80_DATA_DIRECTORY || defaultEnv.VIBE80_DATA_DIRECTORY;
|
|
25
|
+
return path.join(dataDir, "cli", "state.json");
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const loadCliState = () => {
|
|
29
|
+
const statePath = resolveCliStatePath();
|
|
30
|
+
if (!fs.existsSync(statePath)) {
|
|
31
|
+
return {
|
|
32
|
+
version: 1,
|
|
33
|
+
currentWorkspaceId: null,
|
|
34
|
+
workspaces: {},
|
|
35
|
+
currentSessionByWorkspace: {},
|
|
36
|
+
sessionsByWorkspace: {},
|
|
37
|
+
currentWorktreeBySession: {},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(fs.readFileSync(statePath, "utf8"));
|
|
42
|
+
if (!parsed || typeof parsed !== "object") {
|
|
43
|
+
return {
|
|
44
|
+
version: 1,
|
|
45
|
+
currentWorkspaceId: null,
|
|
46
|
+
workspaces: {},
|
|
47
|
+
currentSessionByWorkspace: {},
|
|
48
|
+
sessionsByWorkspace: {},
|
|
49
|
+
currentWorktreeBySession: {},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
version: 1,
|
|
54
|
+
currentWorkspaceId:
|
|
55
|
+
typeof parsed.currentWorkspaceId === "string" && parsed.currentWorkspaceId
|
|
56
|
+
? parsed.currentWorkspaceId
|
|
57
|
+
: null,
|
|
58
|
+
workspaces:
|
|
59
|
+
parsed.workspaces && typeof parsed.workspaces === "object" ? parsed.workspaces : {},
|
|
60
|
+
currentSessionByWorkspace:
|
|
61
|
+
parsed.currentSessionByWorkspace && typeof parsed.currentSessionByWorkspace === "object"
|
|
62
|
+
? parsed.currentSessionByWorkspace
|
|
63
|
+
: {},
|
|
64
|
+
sessionsByWorkspace:
|
|
65
|
+
parsed.sessionsByWorkspace && typeof parsed.sessionsByWorkspace === "object"
|
|
66
|
+
? parsed.sessionsByWorkspace
|
|
67
|
+
: {},
|
|
68
|
+
currentWorktreeBySession:
|
|
69
|
+
parsed.currentWorktreeBySession && typeof parsed.currentWorktreeBySession === "object"
|
|
70
|
+
? parsed.currentWorktreeBySession
|
|
71
|
+
: {},
|
|
72
|
+
};
|
|
73
|
+
} catch {
|
|
74
|
+
return {
|
|
75
|
+
version: 1,
|
|
76
|
+
currentWorkspaceId: null,
|
|
77
|
+
workspaces: {},
|
|
78
|
+
currentSessionByWorkspace: {},
|
|
79
|
+
sessionsByWorkspace: {},
|
|
80
|
+
currentWorktreeBySession: {},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const saveCliState = (state) => {
|
|
86
|
+
const statePath = resolveCliStatePath();
|
|
87
|
+
fs.mkdirSync(path.dirname(statePath), { recursive: true, mode: 0o700 });
|
|
88
|
+
fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const normalizeBaseUrl = (baseUrl) => String(baseUrl || defaultBaseUrl).replace(/\/+$/, "");
|
|
92
|
+
|
|
93
|
+
const toIsoStringOrNull = (value) => {
|
|
94
|
+
if (!value) return null;
|
|
95
|
+
const date = new Date(value);
|
|
96
|
+
if (Number.isNaN(date.getTime())) return null;
|
|
97
|
+
return date.toISOString();
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const maskToken = (value) => {
|
|
101
|
+
if (!value || typeof value !== "string") return "";
|
|
102
|
+
if (value.length <= 12) return `${value.slice(0, 3)}...${value.slice(-3)}`;
|
|
103
|
+
return `${value.slice(0, 6)}...${value.slice(-4)}`;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const ensureWorkspaceEntry = (state, workspaceId) => {
|
|
107
|
+
const id = String(workspaceId || "").trim();
|
|
108
|
+
if (!id) {
|
|
109
|
+
throw new Error("workspaceId is required.");
|
|
110
|
+
}
|
|
111
|
+
if (!state.workspaces[id] || typeof state.workspaces[id] !== "object") {
|
|
112
|
+
state.workspaces[id] = { workspaceId: id, baseUrl: normalizeBaseUrl(defaultBaseUrl) };
|
|
113
|
+
}
|
|
114
|
+
if (!state.workspaces[id].workspaceId) {
|
|
115
|
+
state.workspaces[id].workspaceId = id;
|
|
116
|
+
}
|
|
117
|
+
if (!state.workspaces[id].baseUrl) {
|
|
118
|
+
state.workspaces[id].baseUrl = normalizeBaseUrl(defaultBaseUrl);
|
|
119
|
+
}
|
|
120
|
+
return state.workspaces[id];
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const ensureSessionWorkspaceMap = (state, workspaceId) => {
|
|
124
|
+
const id = String(workspaceId || "").trim();
|
|
125
|
+
if (!id) {
|
|
126
|
+
throw new Error("workspaceId is required.");
|
|
127
|
+
}
|
|
128
|
+
if (!state.sessionsByWorkspace || typeof state.sessionsByWorkspace !== "object") {
|
|
129
|
+
state.sessionsByWorkspace = {};
|
|
130
|
+
}
|
|
131
|
+
if (
|
|
132
|
+
!state.sessionsByWorkspace[id]
|
|
133
|
+
|| typeof state.sessionsByWorkspace[id] !== "object"
|
|
134
|
+
|| Array.isArray(state.sessionsByWorkspace[id])
|
|
135
|
+
) {
|
|
136
|
+
state.sessionsByWorkspace[id] = {};
|
|
137
|
+
}
|
|
138
|
+
if (!state.currentSessionByWorkspace || typeof state.currentSessionByWorkspace !== "object") {
|
|
139
|
+
state.currentSessionByWorkspace = {};
|
|
140
|
+
}
|
|
141
|
+
return state.sessionsByWorkspace[id];
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const setCurrentSessionForWorkspace = (state, workspaceId, sessionId) => {
|
|
145
|
+
ensureSessionWorkspaceMap(state, workspaceId);
|
|
146
|
+
if (!sessionId) {
|
|
147
|
+
delete state.currentSessionByWorkspace[workspaceId];
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
state.currentSessionByWorkspace[workspaceId] = sessionId;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const getCurrentSessionForWorkspace = (state, workspaceId) =>
|
|
154
|
+
state.currentSessionByWorkspace?.[workspaceId] || null;
|
|
155
|
+
|
|
156
|
+
const upsertKnownSession = (state, workspaceId, session) => {
|
|
157
|
+
const map = ensureSessionWorkspaceMap(state, workspaceId);
|
|
158
|
+
const sessionId = String(session?.sessionId || "").trim();
|
|
159
|
+
if (!sessionId) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
map[sessionId] = {
|
|
163
|
+
sessionId,
|
|
164
|
+
name: session.name || "",
|
|
165
|
+
repoUrl: session.repoUrl || "",
|
|
166
|
+
createdAt: session.createdAt || null,
|
|
167
|
+
lastActivityAt: session.lastActivityAt || null,
|
|
168
|
+
defaultProvider: session.defaultProvider || session.activeProvider || null,
|
|
169
|
+
providers: Array.isArray(session.providers) ? session.providers : [],
|
|
170
|
+
};
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const getSessionKey = (workspaceId, sessionId) => `${workspaceId}/${sessionId}`;
|
|
174
|
+
|
|
175
|
+
const setCurrentWorktreeForSession = (state, workspaceId, sessionId, worktreeId) => {
|
|
176
|
+
if (!state.currentWorktreeBySession || typeof state.currentWorktreeBySession !== "object") {
|
|
177
|
+
state.currentWorktreeBySession = {};
|
|
178
|
+
}
|
|
179
|
+
const key = getSessionKey(workspaceId, sessionId);
|
|
180
|
+
if (!worktreeId) {
|
|
181
|
+
delete state.currentWorktreeBySession[key];
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
state.currentWorktreeBySession[key] = worktreeId;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const getCurrentWorktreeForSession = (state, workspaceId, sessionId) =>
|
|
188
|
+
state.currentWorktreeBySession?.[getSessionKey(workspaceId, sessionId)] || null;
|
|
189
|
+
|
|
190
|
+
const parseListOption = (value, previous = []) => {
|
|
191
|
+
const parts = String(value || "")
|
|
192
|
+
.split(",")
|
|
193
|
+
.map((item) => item.trim())
|
|
194
|
+
.filter(Boolean);
|
|
195
|
+
return [...previous, ...parts];
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const parseRepeatOption = (value, previous = []) => {
|
|
199
|
+
const trimmed = String(value || "").trim();
|
|
200
|
+
if (!trimmed) {
|
|
201
|
+
return previous;
|
|
202
|
+
}
|
|
203
|
+
return [...previous, trimmed];
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const parseProviderName = (value) => {
|
|
207
|
+
const provider = String(value || "").trim().toLowerCase();
|
|
208
|
+
if (provider !== "codex" && provider !== "claude") {
|
|
209
|
+
throw new Error(`Unknown provider "${value}". Use codex or claude.`);
|
|
210
|
+
}
|
|
211
|
+
return provider;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const buildProvidersPatch = (options) => {
|
|
215
|
+
const patch = {};
|
|
216
|
+
for (const providerName of options.enable || []) {
|
|
217
|
+
const provider = parseProviderName(providerName);
|
|
218
|
+
patch[provider] = { ...(patch[provider] || {}), enabled: true };
|
|
219
|
+
}
|
|
220
|
+
for (const providerName of options.disable || []) {
|
|
221
|
+
const provider = parseProviderName(providerName);
|
|
222
|
+
patch[provider] = { ...(patch[provider] || {}), enabled: false };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (options.codexAuthType || options.codexAuthValue) {
|
|
226
|
+
patch.codex = {
|
|
227
|
+
...(patch.codex || {}),
|
|
228
|
+
auth: {
|
|
229
|
+
type: options.codexAuthType || "api_key",
|
|
230
|
+
value: options.codexAuthValue || "",
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
if (options.claudeAuthType || options.claudeAuthValue) {
|
|
235
|
+
patch.claude = {
|
|
236
|
+
...(patch.claude || {}),
|
|
237
|
+
auth: {
|
|
238
|
+
type: options.claudeAuthType || "api_key",
|
|
239
|
+
value: options.claudeAuthValue || "",
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
return patch;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const apiRequest = async ({ baseUrl, pathname, method = "GET", body, workspaceToken }) => {
|
|
247
|
+
const url = `${normalizeBaseUrl(baseUrl)}${pathname}`;
|
|
248
|
+
const headers = {};
|
|
249
|
+
if (workspaceToken) {
|
|
250
|
+
headers.authorization = `Bearer ${workspaceToken}`;
|
|
251
|
+
}
|
|
252
|
+
if (body != null) {
|
|
253
|
+
headers["content-type"] = "application/json";
|
|
254
|
+
}
|
|
255
|
+
const response = await fetch(url, {
|
|
256
|
+
method,
|
|
257
|
+
headers,
|
|
258
|
+
body: body != null ? JSON.stringify(body) : undefined,
|
|
259
|
+
});
|
|
260
|
+
const raw = await response.text();
|
|
261
|
+
let payload = null;
|
|
262
|
+
if (raw) {
|
|
263
|
+
try {
|
|
264
|
+
payload = JSON.parse(raw);
|
|
265
|
+
} catch {
|
|
266
|
+
payload = { raw };
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (!response.ok) {
|
|
270
|
+
const message =
|
|
271
|
+
payload?.error || payload?.message || payload?.code || `Request failed (${response.status}).`;
|
|
272
|
+
const error = new Error(message);
|
|
273
|
+
error.status = response.status;
|
|
274
|
+
error.payload = payload;
|
|
275
|
+
error.url = url;
|
|
276
|
+
throw error;
|
|
277
|
+
}
|
|
278
|
+
return payload || {};
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const isAccessTokenFresh = (entry, skewMs = 30 * 1000) => {
|
|
282
|
+
const token = typeof entry?.workspaceToken === "string" ? entry.workspaceToken : "";
|
|
283
|
+
if (!token) {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
const expiresAt = Date.parse(entry?.expiresAt || "");
|
|
287
|
+
if (!Number.isFinite(expiresAt)) {
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
return Date.now() + skewMs < expiresAt;
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const refreshWorkspaceAccessToken = async ({
|
|
294
|
+
state,
|
|
295
|
+
workspaceId,
|
|
296
|
+
entry,
|
|
297
|
+
baseUrl,
|
|
298
|
+
}) => {
|
|
299
|
+
if (!entry?.refreshToken) {
|
|
300
|
+
throw new Error(`No refresh token saved for workspace "${workspaceId}". Run workspace login first.`);
|
|
301
|
+
}
|
|
302
|
+
const payload = await apiRequest({
|
|
303
|
+
baseUrl,
|
|
304
|
+
pathname: "/api/v1/workspaces/refresh",
|
|
305
|
+
method: "POST",
|
|
306
|
+
body: { refreshToken: entry.refreshToken },
|
|
307
|
+
});
|
|
308
|
+
upsertWorkspaceFromTokens(state, payload.workspaceId || workspaceId, baseUrl, payload, null);
|
|
309
|
+
const updatedId = payload.workspaceId || workspaceId;
|
|
310
|
+
const updatedEntry = ensureWorkspaceEntry(state, updatedId);
|
|
311
|
+
saveCliState(state);
|
|
312
|
+
return { workspaceId: updatedId, entry: updatedEntry };
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const ensureWorkspaceAccessToken = async ({
|
|
316
|
+
state,
|
|
317
|
+
workspaceId,
|
|
318
|
+
entry,
|
|
319
|
+
baseUrl,
|
|
320
|
+
}) => {
|
|
321
|
+
if (isAccessTokenFresh(entry)) {
|
|
322
|
+
return { workspaceId, entry };
|
|
323
|
+
}
|
|
324
|
+
if (!entry?.refreshToken) {
|
|
325
|
+
if (entry?.workspaceToken) {
|
|
326
|
+
return { workspaceId, entry };
|
|
327
|
+
}
|
|
328
|
+
throw new Error(`No workspace token/refresh token for "${workspaceId}". Run workspace login first.`);
|
|
329
|
+
}
|
|
330
|
+
return refreshWorkspaceAccessToken({ state, workspaceId, entry, baseUrl });
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const authedApiRequest = async ({
|
|
334
|
+
state,
|
|
335
|
+
workspaceId,
|
|
336
|
+
entry,
|
|
337
|
+
baseUrl,
|
|
338
|
+
retryOnUnauthorized = true,
|
|
339
|
+
...request
|
|
340
|
+
}) => {
|
|
341
|
+
const ensured = await ensureWorkspaceAccessToken({ state, workspaceId, entry, baseUrl });
|
|
342
|
+
let activeWorkspaceId = ensured.workspaceId;
|
|
343
|
+
let activeEntry = ensured.entry;
|
|
344
|
+
try {
|
|
345
|
+
return await apiRequest({
|
|
346
|
+
baseUrl,
|
|
347
|
+
workspaceToken: activeEntry.workspaceToken,
|
|
348
|
+
...request,
|
|
349
|
+
});
|
|
350
|
+
} catch (error) {
|
|
351
|
+
if (!retryOnUnauthorized || error?.status !== 401) {
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
354
|
+
const refreshed = await refreshWorkspaceAccessToken({
|
|
355
|
+
state,
|
|
356
|
+
workspaceId: activeWorkspaceId,
|
|
357
|
+
entry: activeEntry,
|
|
358
|
+
baseUrl,
|
|
359
|
+
});
|
|
360
|
+
activeWorkspaceId = refreshed.workspaceId;
|
|
361
|
+
activeEntry = refreshed.entry;
|
|
362
|
+
return apiRequest({
|
|
363
|
+
baseUrl,
|
|
364
|
+
workspaceToken: activeEntry.workspaceToken,
|
|
365
|
+
...request,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
19
368
|
};
|
|
20
|
-
const deploymentMode = process.env.DEPLOYMENT_MODE || defaultEnv.DEPLOYMENT_MODE;
|
|
21
|
-
const serverPort = process.env.PORT || "5179";
|
|
22
|
-
const cliArgs = process.argv.slice(2);
|
|
23
|
-
const enableCodexFromCli = cliArgs.includes("--codex");
|
|
24
|
-
const enableClaudeFromCli = cliArgs.includes("--claude");
|
|
25
369
|
|
|
26
370
|
const spawnProcess = (cmd, args, label, extraEnv = {}) => {
|
|
27
371
|
const child = spawn(cmd, args, {
|
|
@@ -42,7 +386,6 @@ const spawnProcess = (cmd, args, label, extraEnv = {}) => {
|
|
|
42
386
|
};
|
|
43
387
|
|
|
44
388
|
let server = null;
|
|
45
|
-
|
|
46
389
|
let shuttingDown = false;
|
|
47
390
|
|
|
48
391
|
const unlinkMonoAuthUrlFile = () => {
|
|
@@ -59,7 +402,7 @@ const tryOpenUrl = (url) =>
|
|
|
59
402
|
resolve(false);
|
|
60
403
|
return;
|
|
61
404
|
}
|
|
62
|
-
const
|
|
405
|
+
const openCommand = process.platform === "darwin"
|
|
63
406
|
? "open"
|
|
64
407
|
: process.platform === "win32"
|
|
65
408
|
? "cmd"
|
|
@@ -69,7 +412,7 @@ const tryOpenUrl = (url) =>
|
|
|
69
412
|
: process.platform === "win32"
|
|
70
413
|
? ["/c", "start", "", url]
|
|
71
414
|
: [url];
|
|
72
|
-
const opener = spawn(
|
|
415
|
+
const opener = spawn(openCommand, args, {
|
|
73
416
|
stdio: "ignore",
|
|
74
417
|
detached: true,
|
|
75
418
|
});
|
|
@@ -103,7 +446,8 @@ const waitForMonoAuthUrl = (timeoutMs = 15000) =>
|
|
|
103
446
|
poll();
|
|
104
447
|
});
|
|
105
448
|
|
|
106
|
-
const maybeOpenMonoAuthUrl = async () => {
|
|
449
|
+
const maybeOpenMonoAuthUrl = async (serverPort) => {
|
|
450
|
+
const deploymentMode = process.env.VIBE80_DEPLOYMENT_MODE || defaultEnv.VIBE80_DEPLOYMENT_MODE;
|
|
107
451
|
if (deploymentMode !== "mono_user") {
|
|
108
452
|
return;
|
|
109
453
|
}
|
|
@@ -128,7 +472,12 @@ const shutdown = (code = 0) => {
|
|
|
128
472
|
process.exit(code);
|
|
129
473
|
};
|
|
130
474
|
|
|
131
|
-
const startServer = () => {
|
|
475
|
+
const startServer = (options = {}) => {
|
|
476
|
+
const enableCodexFromCli = Boolean(options.codex);
|
|
477
|
+
const enableClaudeFromCli = Boolean(options.claude);
|
|
478
|
+
const shouldOpenBrowser = options.open !== false;
|
|
479
|
+
const serverPort = options.port || process.env.VIBE80_PORT || "5179";
|
|
480
|
+
|
|
132
481
|
unlinkMonoAuthUrlFile();
|
|
133
482
|
const monoProviderEnv = {};
|
|
134
483
|
if (enableCodexFromCli) {
|
|
@@ -137,6 +486,14 @@ const startServer = () => {
|
|
|
137
486
|
if (enableClaudeFromCli) {
|
|
138
487
|
monoProviderEnv.VIBE80_MONO_ENABLE_CLAUDE = "true";
|
|
139
488
|
}
|
|
489
|
+
if (options.dataDir) {
|
|
490
|
+
monoProviderEnv.VIBE80_DATA_DIRECTORY = path.resolve(options.dataDir);
|
|
491
|
+
}
|
|
492
|
+
if (options.storageBackend) {
|
|
493
|
+
monoProviderEnv.VIBE80_STORAGE_BACKEND = options.storageBackend;
|
|
494
|
+
}
|
|
495
|
+
monoProviderEnv.VIBE80_PORT = String(serverPort);
|
|
496
|
+
|
|
140
497
|
server = spawnProcess(
|
|
141
498
|
process.execPath,
|
|
142
499
|
["server/src/index.js"],
|
|
@@ -146,7 +503,9 @@ const startServer = () => {
|
|
|
146
503
|
...monoProviderEnv,
|
|
147
504
|
}
|
|
148
505
|
);
|
|
149
|
-
|
|
506
|
+
if (shouldOpenBrowser) {
|
|
507
|
+
void maybeOpenMonoAuthUrl(serverPort);
|
|
508
|
+
}
|
|
150
509
|
|
|
151
510
|
server.on("exit", (code, signal) => {
|
|
152
511
|
if (shuttingDown) return;
|
|
@@ -162,7 +521,1360 @@ const startServer = () => {
|
|
|
162
521
|
});
|
|
163
522
|
};
|
|
164
523
|
|
|
165
|
-
|
|
524
|
+
const resolveWorkspaceForCommand = (state, workspaceIdArg) => {
|
|
525
|
+
const workspaceId = workspaceIdArg || state.currentWorkspaceId;
|
|
526
|
+
if (!workspaceId) {
|
|
527
|
+
throw new Error("No workspace selected. Use `vibe80 workspace use <workspaceId>`.");
|
|
528
|
+
}
|
|
529
|
+
const entry = state.workspaces[workspaceId];
|
|
530
|
+
if (!entry) {
|
|
531
|
+
throw new Error(`Unknown workspace "${workspaceId}".`);
|
|
532
|
+
}
|
|
533
|
+
return { workspaceId, entry };
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
const upsertWorkspaceFromTokens = (state, workspaceId, baseUrl, payload, workspaceSecret) => {
|
|
537
|
+
const entry = ensureWorkspaceEntry(state, workspaceId);
|
|
538
|
+
entry.baseUrl = normalizeBaseUrl(baseUrl || entry.baseUrl || defaultBaseUrl);
|
|
539
|
+
if (workspaceSecret) {
|
|
540
|
+
entry.workspaceSecret = workspaceSecret;
|
|
541
|
+
}
|
|
542
|
+
if (payload.workspaceToken) {
|
|
543
|
+
entry.workspaceToken = payload.workspaceToken;
|
|
544
|
+
}
|
|
545
|
+
if (payload.refreshToken) {
|
|
546
|
+
entry.refreshToken = payload.refreshToken;
|
|
547
|
+
}
|
|
548
|
+
if (payload.expiresIn) {
|
|
549
|
+
entry.expiresAt = new Date(Date.now() + Number(payload.expiresIn) * 1000).toISOString();
|
|
550
|
+
}
|
|
551
|
+
if (payload.refreshExpiresIn) {
|
|
552
|
+
entry.refreshExpiresAt = new Date(
|
|
553
|
+
Date.now() + Number(payload.refreshExpiresIn) * 1000
|
|
554
|
+
).toISOString();
|
|
555
|
+
}
|
|
556
|
+
entry.lastLoginAt = new Date().toISOString();
|
|
557
|
+
state.currentWorkspaceId = workspaceId;
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
const program = new Command();
|
|
561
|
+
|
|
562
|
+
program
|
|
563
|
+
.name("vibe80")
|
|
564
|
+
.description("Vibe80 CLI")
|
|
565
|
+
.showHelpAfterError()
|
|
566
|
+
.showSuggestionAfterError(true);
|
|
567
|
+
|
|
568
|
+
program
|
|
569
|
+
.command("run")
|
|
570
|
+
.description("Run the Vibe80 server (mono_user by default)")
|
|
571
|
+
.option("--codex", "Enable Codex provider in mono_user mode")
|
|
572
|
+
.option("--claude", "Enable Claude provider in mono_user mode")
|
|
573
|
+
.option("--port <port>", "Server port (default: 5179)")
|
|
574
|
+
.option("--data-dir <path>", "Override VIBE80_DATA_DIRECTORY")
|
|
575
|
+
.option("--storage-backend <backend>", "Override VIBE80_STORAGE_BACKEND (default: sqlite)")
|
|
576
|
+
.option("--no-open", "Do not auto-open authentication URL in a browser")
|
|
577
|
+
.action((options) => {
|
|
578
|
+
if (!options.codex && !options.claude) {
|
|
579
|
+
throw new Error("`vibe80 run` requires at least one provider flag: --codex or --claude.");
|
|
580
|
+
}
|
|
581
|
+
startServer(options);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
const workspaceCommand = program
|
|
585
|
+
.command("workspace")
|
|
586
|
+
.alias("ws")
|
|
587
|
+
.description("Manage workspace context and authentication");
|
|
588
|
+
|
|
589
|
+
workspaceCommand
|
|
590
|
+
.command("ls")
|
|
591
|
+
.description("List known local workspaces")
|
|
592
|
+
.option("--json", "Output JSON")
|
|
593
|
+
.action((options) => {
|
|
594
|
+
const state = loadCliState();
|
|
595
|
+
const rows = Object.values(state.workspaces).map((entry) => ({
|
|
596
|
+
workspaceId: entry.workspaceId,
|
|
597
|
+
current: state.currentWorkspaceId === entry.workspaceId,
|
|
598
|
+
baseUrl: entry.baseUrl || normalizeBaseUrl(defaultBaseUrl),
|
|
599
|
+
hasToken: Boolean(entry.workspaceToken),
|
|
600
|
+
hasRefreshToken: Boolean(entry.refreshToken),
|
|
601
|
+
lastLoginAt: toIsoStringOrNull(entry.lastLoginAt),
|
|
602
|
+
}));
|
|
603
|
+
rows.sort((a, b) => a.workspaceId.localeCompare(b.workspaceId));
|
|
604
|
+
if (options.json) {
|
|
605
|
+
console.log(JSON.stringify({ currentWorkspaceId: state.currentWorkspaceId, workspaces: rows }, null, 2));
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
if (!rows.length) {
|
|
609
|
+
console.log("No workspace saved locally.");
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
for (const row of rows) {
|
|
613
|
+
const currentLabel = row.current ? "*" : " ";
|
|
614
|
+
const tokenLabel = row.hasToken ? "token" : "no-token";
|
|
615
|
+
console.log(
|
|
616
|
+
`${currentLabel} ${row.workspaceId} (${tokenLabel}) ${row.baseUrl}${row.lastLoginAt ? ` lastLogin=${row.lastLoginAt}` : ""}`
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
workspaceCommand
|
|
622
|
+
.command("current")
|
|
623
|
+
.description("Show current workspace")
|
|
624
|
+
.option("--json", "Output JSON")
|
|
625
|
+
.action((options) => {
|
|
626
|
+
const state = loadCliState();
|
|
627
|
+
const workspaceId = state.currentWorkspaceId;
|
|
628
|
+
if (!workspaceId) {
|
|
629
|
+
console.log("No current workspace selected.");
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
const entry = state.workspaces[workspaceId] || { workspaceId };
|
|
633
|
+
const payload = {
|
|
634
|
+
workspaceId,
|
|
635
|
+
baseUrl: entry.baseUrl || normalizeBaseUrl(defaultBaseUrl),
|
|
636
|
+
hasToken: Boolean(entry.workspaceToken),
|
|
637
|
+
hasRefreshToken: Boolean(entry.refreshToken),
|
|
638
|
+
lastLoginAt: toIsoStringOrNull(entry.lastLoginAt),
|
|
639
|
+
};
|
|
640
|
+
if (options.json) {
|
|
641
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
console.log(payload.workspaceId);
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
workspaceCommand
|
|
648
|
+
.command("use <workspaceId>")
|
|
649
|
+
.description("Set current workspace")
|
|
650
|
+
.option("--base-url <url>", "Default API base URL for this workspace")
|
|
651
|
+
.action((workspaceId, options) => {
|
|
652
|
+
const state = loadCliState();
|
|
653
|
+
const entry = ensureWorkspaceEntry(state, workspaceId);
|
|
654
|
+
if (options.baseUrl) {
|
|
655
|
+
entry.baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
656
|
+
}
|
|
657
|
+
state.currentWorkspaceId = workspaceId;
|
|
658
|
+
saveCliState(state);
|
|
659
|
+
console.log(`Current workspace: ${workspaceId}`);
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
workspaceCommand
|
|
663
|
+
.command("show [workspaceId]")
|
|
664
|
+
.description("Show workspace details (local + remote when possible)")
|
|
665
|
+
.option("--base-url <url>", "Override API base URL for remote call")
|
|
666
|
+
.option("--json", "Output JSON")
|
|
667
|
+
.action(async (workspaceIdArg, options) => {
|
|
668
|
+
const state = loadCliState();
|
|
669
|
+
const { workspaceId, entry } = resolveWorkspaceForCommand(state, workspaceIdArg);
|
|
670
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl || entry.baseUrl || defaultBaseUrl);
|
|
671
|
+
const localPayload = {
|
|
672
|
+
workspaceId,
|
|
673
|
+
baseUrl,
|
|
674
|
+
workspaceSecretSaved: Boolean(entry.workspaceSecret),
|
|
675
|
+
workspaceToken: entry.workspaceToken ? maskToken(entry.workspaceToken) : null,
|
|
676
|
+
refreshToken: entry.refreshToken ? maskToken(entry.refreshToken) : null,
|
|
677
|
+
expiresAt: toIsoStringOrNull(entry.expiresAt),
|
|
678
|
+
refreshExpiresAt: toIsoStringOrNull(entry.refreshExpiresAt),
|
|
679
|
+
lastLoginAt: toIsoStringOrNull(entry.lastLoginAt),
|
|
680
|
+
};
|
|
681
|
+
let remotePayload = null;
|
|
682
|
+
let remoteError = null;
|
|
683
|
+
if (entry.workspaceToken || entry.refreshToken) {
|
|
684
|
+
try {
|
|
685
|
+
remotePayload = await authedApiRequest({
|
|
686
|
+
state,
|
|
687
|
+
workspaceId,
|
|
688
|
+
entry,
|
|
689
|
+
baseUrl,
|
|
690
|
+
pathname: `/api/v1/workspaces/${workspaceId}`,
|
|
691
|
+
});
|
|
692
|
+
} catch (error) {
|
|
693
|
+
remoteError = error.message || String(error);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
const payload = { local: localPayload, remote: remotePayload, remoteError };
|
|
697
|
+
if (options.json) {
|
|
698
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
console.log(`Workspace: ${workspaceId}`);
|
|
702
|
+
console.log(`Base URL: ${baseUrl}`);
|
|
703
|
+
if (remotePayload) {
|
|
704
|
+
console.log("Remote providers:");
|
|
705
|
+
console.log(JSON.stringify(remotePayload.providers || {}, null, 2));
|
|
706
|
+
} else if (remoteError) {
|
|
707
|
+
console.log(`Remote check failed: ${remoteError}`);
|
|
708
|
+
} else {
|
|
709
|
+
console.log("Remote check skipped (no workspace token saved).");
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
workspaceCommand
|
|
714
|
+
.command("login")
|
|
715
|
+
.description("Login workspace and persist tokens locally")
|
|
716
|
+
.option("--workspace-id <id>", "Workspace ID")
|
|
717
|
+
.option("--workspace-secret <secret>", "Workspace secret (multi_user)")
|
|
718
|
+
.option("--mono-auth-token <token>", "One-shot mono auth token (mono_user)")
|
|
719
|
+
.option("--base-url <url>", "API base URL (default: http://localhost:5179)")
|
|
720
|
+
.option("--json", "Output JSON")
|
|
721
|
+
.action(async (options) => {
|
|
722
|
+
const state = loadCliState();
|
|
723
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl || defaultBaseUrl);
|
|
724
|
+
const payload = options.monoAuthToken
|
|
725
|
+
? {
|
|
726
|
+
grantType: "mono_auth_token",
|
|
727
|
+
monoAuthToken: String(options.monoAuthToken),
|
|
728
|
+
}
|
|
729
|
+
: {
|
|
730
|
+
workspaceId: options.workspaceId,
|
|
731
|
+
workspaceSecret: options.workspaceSecret,
|
|
732
|
+
};
|
|
733
|
+
const response = await apiRequest({
|
|
734
|
+
baseUrl,
|
|
735
|
+
pathname: "/api/v1/workspaces/login",
|
|
736
|
+
method: "POST",
|
|
737
|
+
body: payload,
|
|
738
|
+
});
|
|
739
|
+
const workspaceId =
|
|
740
|
+
response.workspaceId || options.workspaceId || state.currentWorkspaceId || "default";
|
|
741
|
+
upsertWorkspaceFromTokens(
|
|
742
|
+
state,
|
|
743
|
+
workspaceId,
|
|
744
|
+
baseUrl,
|
|
745
|
+
response,
|
|
746
|
+
options.workspaceSecret || null
|
|
747
|
+
);
|
|
748
|
+
saveCliState(state);
|
|
749
|
+
if (options.json) {
|
|
750
|
+
console.log(
|
|
751
|
+
JSON.stringify(
|
|
752
|
+
{
|
|
753
|
+
workspaceId,
|
|
754
|
+
expiresAt: state.workspaces[workspaceId]?.expiresAt || null,
|
|
755
|
+
refreshExpiresAt: state.workspaces[workspaceId]?.refreshExpiresAt || null,
|
|
756
|
+
},
|
|
757
|
+
null,
|
|
758
|
+
2
|
|
759
|
+
)
|
|
760
|
+
);
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
console.log(`Workspace login success: ${workspaceId}`);
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
workspaceCommand
|
|
767
|
+
.command("refresh")
|
|
768
|
+
.description("Refresh workspace token with saved refresh token")
|
|
769
|
+
.option("--workspace-id <id>", "Workspace ID (default: current)")
|
|
770
|
+
.option("--base-url <url>", "API base URL")
|
|
771
|
+
.option("--json", "Output JSON")
|
|
772
|
+
.action(async (options) => {
|
|
773
|
+
const state = loadCliState();
|
|
774
|
+
const { workspaceId, entry } = resolveWorkspaceForCommand(state, options.workspaceId);
|
|
775
|
+
if (!entry.refreshToken) {
|
|
776
|
+
throw new Error(`No refresh token saved for workspace "${workspaceId}".`);
|
|
777
|
+
}
|
|
778
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl || entry.baseUrl || defaultBaseUrl);
|
|
779
|
+
const response = await apiRequest({
|
|
780
|
+
baseUrl,
|
|
781
|
+
pathname: "/api/v1/workspaces/refresh",
|
|
782
|
+
method: "POST",
|
|
783
|
+
body: { refreshToken: entry.refreshToken },
|
|
784
|
+
});
|
|
785
|
+
upsertWorkspaceFromTokens(state, response.workspaceId || workspaceId, baseUrl, response, null);
|
|
786
|
+
saveCliState(state);
|
|
787
|
+
if (options.json) {
|
|
788
|
+
console.log(JSON.stringify(response, null, 2));
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
console.log(`Workspace token refreshed: ${response.workspaceId || workspaceId}`);
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
workspaceCommand
|
|
795
|
+
.command("logout")
|
|
796
|
+
.description("Delete saved tokens for a workspace")
|
|
797
|
+
.option("--workspace-id <id>", "Workspace ID (default: current)")
|
|
798
|
+
.action((options) => {
|
|
799
|
+
const state = loadCliState();
|
|
800
|
+
const { workspaceId, entry } = resolveWorkspaceForCommand(state, options.workspaceId);
|
|
801
|
+
delete entry.workspaceToken;
|
|
802
|
+
delete entry.refreshToken;
|
|
803
|
+
delete entry.expiresAt;
|
|
804
|
+
delete entry.refreshExpiresAt;
|
|
805
|
+
saveCliState(state);
|
|
806
|
+
console.log(`Logged out: ${workspaceId}`);
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
workspaceCommand
|
|
810
|
+
.command("create")
|
|
811
|
+
.description("Create a workspace")
|
|
812
|
+
.option("--base-url <url>", "API base URL")
|
|
813
|
+
.option("--enable <provider>", "Enable provider (repeatable, supports comma-separated)", parseListOption, [])
|
|
814
|
+
.option("--codex-auth-type <type>", "Codex auth type (api_key|auth_json_b64)")
|
|
815
|
+
.option("--codex-auth-value <value>", "Codex auth value")
|
|
816
|
+
.option("--claude-auth-type <type>", "Claude auth type (api_key|setup_token)")
|
|
817
|
+
.option("--claude-auth-value <value>", "Claude auth value")
|
|
818
|
+
.option("--json", "Output JSON")
|
|
819
|
+
.action(async (options) => {
|
|
820
|
+
const patch = buildProvidersPatch(options);
|
|
821
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl || defaultBaseUrl);
|
|
822
|
+
const response = await apiRequest({
|
|
823
|
+
baseUrl,
|
|
824
|
+
pathname: "/api/v1/workspaces",
|
|
825
|
+
method: "POST",
|
|
826
|
+
body: { providers: patch },
|
|
827
|
+
});
|
|
828
|
+
const state = loadCliState();
|
|
829
|
+
const entry = ensureWorkspaceEntry(state, response.workspaceId);
|
|
830
|
+
entry.baseUrl = baseUrl;
|
|
831
|
+
entry.workspaceSecret = response.workspaceSecret || null;
|
|
832
|
+
state.currentWorkspaceId = response.workspaceId;
|
|
833
|
+
saveCliState(state);
|
|
834
|
+
if (options.json) {
|
|
835
|
+
console.log(JSON.stringify(response, null, 2));
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
console.log(`Workspace created: ${response.workspaceId}`);
|
|
839
|
+
if (response.workspaceSecret) {
|
|
840
|
+
console.log(`Workspace secret: ${response.workspaceSecret}`);
|
|
841
|
+
}
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
workspaceCommand
|
|
845
|
+
.command("update <workspaceId>")
|
|
846
|
+
.description("Update workspace providers/auth config")
|
|
847
|
+
.option("--base-url <url>", "API base URL")
|
|
848
|
+
.option("--enable <provider>", "Enable provider (repeatable, supports comma-separated)", parseListOption, [])
|
|
849
|
+
.option("--disable <provider>", "Disable provider (repeatable, supports comma-separated)", parseListOption, [])
|
|
850
|
+
.option("--codex-auth-type <type>", "Codex auth type (api_key|auth_json_b64)")
|
|
851
|
+
.option("--codex-auth-value <value>", "Codex auth value")
|
|
852
|
+
.option("--claude-auth-type <type>", "Claude auth type (api_key|setup_token)")
|
|
853
|
+
.option("--claude-auth-value <value>", "Claude auth value")
|
|
854
|
+
.option("--json", "Output JSON")
|
|
855
|
+
.action(async (workspaceId, options) => {
|
|
856
|
+
const state = loadCliState();
|
|
857
|
+
const entry = ensureWorkspaceEntry(state, workspaceId);
|
|
858
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl || entry.baseUrl || defaultBaseUrl);
|
|
859
|
+
const patch = buildProvidersPatch(options);
|
|
860
|
+
const response = await authedApiRequest({
|
|
861
|
+
state,
|
|
862
|
+
workspaceId,
|
|
863
|
+
entry,
|
|
864
|
+
baseUrl,
|
|
865
|
+
pathname: `/api/v1/workspaces/${workspaceId}`,
|
|
866
|
+
method: "PATCH",
|
|
867
|
+
body: { providers: patch },
|
|
868
|
+
});
|
|
869
|
+
entry.baseUrl = baseUrl;
|
|
870
|
+
saveCliState(state);
|
|
871
|
+
if (options.json) {
|
|
872
|
+
console.log(JSON.stringify(response, null, 2));
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
console.log(`Workspace updated: ${workspaceId}`);
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
workspaceCommand
|
|
879
|
+
.command("rm <workspaceId>")
|
|
880
|
+
.description("Delete workspace (server policy may refuse)")
|
|
881
|
+
.option("--base-url <url>", "API base URL")
|
|
882
|
+
.option("--yes", "Confirm deletion")
|
|
883
|
+
.action(async (workspaceId, options) => {
|
|
884
|
+
if (!options.yes) {
|
|
885
|
+
throw new Error("Refusing to delete without --yes.");
|
|
886
|
+
}
|
|
887
|
+
const state = loadCliState();
|
|
888
|
+
const entry = ensureWorkspaceEntry(state, workspaceId);
|
|
889
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl || entry.baseUrl || defaultBaseUrl);
|
|
890
|
+
await authedApiRequest({
|
|
891
|
+
state,
|
|
892
|
+
workspaceId,
|
|
893
|
+
entry,
|
|
894
|
+
baseUrl,
|
|
895
|
+
pathname: `/api/v1/workspaces/${workspaceId}`,
|
|
896
|
+
method: "DELETE",
|
|
897
|
+
});
|
|
898
|
+
delete state.workspaces[workspaceId];
|
|
899
|
+
if (state.currentWorkspaceId === workspaceId) {
|
|
900
|
+
state.currentWorkspaceId = null;
|
|
901
|
+
}
|
|
902
|
+
saveCliState(state);
|
|
903
|
+
console.log(`Workspace deleted: ${workspaceId}`);
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
const resolveWorkspaceAuthContext = async (state, options = {}) => {
|
|
907
|
+
const { workspaceId, entry } = resolveWorkspaceForCommand(state, options.workspaceId);
|
|
908
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl || entry.baseUrl || defaultBaseUrl);
|
|
909
|
+
const ensured = await ensureWorkspaceAccessToken({
|
|
910
|
+
state,
|
|
911
|
+
workspaceId,
|
|
912
|
+
entry,
|
|
913
|
+
baseUrl,
|
|
914
|
+
});
|
|
915
|
+
return {
|
|
916
|
+
workspaceId: ensured.workspaceId,
|
|
917
|
+
entry: ensured.entry,
|
|
918
|
+
baseUrl,
|
|
919
|
+
};
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
const resolveSessionForCommand = (state, workspaceId, sessionIdArg) => {
|
|
923
|
+
const sessionId = sessionIdArg || getCurrentSessionForWorkspace(state, workspaceId);
|
|
924
|
+
if (!sessionId) {
|
|
925
|
+
throw new Error("No session selected. Use `vibe80 session use <sessionId>`.");
|
|
926
|
+
}
|
|
927
|
+
return sessionId;
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
const sessionCommand = program
|
|
931
|
+
.command("session")
|
|
932
|
+
.alias("s")
|
|
933
|
+
.description("Manage sessions for the current workspace");
|
|
934
|
+
|
|
935
|
+
sessionCommand
|
|
936
|
+
.command("ls")
|
|
937
|
+
.description("List sessions from API for the selected workspace")
|
|
938
|
+
.option("--workspace-id <id>", "Workspace ID (default: current)")
|
|
939
|
+
.option("--base-url <url>", "API base URL")
|
|
940
|
+
.option("--json", "Output JSON")
|
|
941
|
+
.action(async (options) => {
|
|
942
|
+
const state = loadCliState();
|
|
943
|
+
const { workspaceId, baseUrl, entry } = await resolveWorkspaceAuthContext(state, options);
|
|
944
|
+
const response = await authedApiRequest({
|
|
945
|
+
state,
|
|
946
|
+
workspaceId,
|
|
947
|
+
entry,
|
|
948
|
+
baseUrl,
|
|
949
|
+
pathname: "/api/v1/sessions",
|
|
950
|
+
});
|
|
951
|
+
const sessions = Array.isArray(response.sessions) ? response.sessions : [];
|
|
952
|
+
for (const session of sessions) {
|
|
953
|
+
upsertKnownSession(state, workspaceId, session);
|
|
954
|
+
}
|
|
955
|
+
saveCliState(state);
|
|
956
|
+
const currentSessionId = getCurrentSessionForWorkspace(state, workspaceId);
|
|
957
|
+
if (options.json) {
|
|
958
|
+
console.log(JSON.stringify({ workspaceId, currentSessionId, sessions }, null, 2));
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
if (!sessions.length) {
|
|
962
|
+
console.log("No session found.");
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
for (const session of sessions) {
|
|
966
|
+
const marker = currentSessionId === session.sessionId ? "*" : " ";
|
|
967
|
+
const name = session.name ? ` name="${session.name}"` : "";
|
|
968
|
+
const repo = session.repoUrl ? ` repo=${session.repoUrl}` : "";
|
|
969
|
+
console.log(`${marker} ${session.sessionId}${name}${repo}`);
|
|
970
|
+
}
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
sessionCommand
|
|
974
|
+
.command("current")
|
|
975
|
+
.description("Show current session in selected workspace")
|
|
976
|
+
.option("--workspace-id <id>", "Workspace ID (default: current)")
|
|
977
|
+
.option("--json", "Output JSON")
|
|
978
|
+
.action((options) => {
|
|
979
|
+
const state = loadCliState();
|
|
980
|
+
const { workspaceId } = resolveWorkspaceForCommand(state, options.workspaceId);
|
|
981
|
+
const currentSessionId = getCurrentSessionForWorkspace(state, workspaceId);
|
|
982
|
+
if (options.json) {
|
|
983
|
+
console.log(JSON.stringify({ workspaceId, sessionId: currentSessionId }, null, 2));
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
if (!currentSessionId) {
|
|
987
|
+
console.log("No current session selected.");
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
console.log(currentSessionId);
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
sessionCommand
|
|
994
|
+
.command("use <sessionId>")
|
|
995
|
+
.description("Set current session for current workspace")
|
|
996
|
+
.option("--workspace-id <id>", "Workspace ID (default: current)")
|
|
997
|
+
.action((sessionId, options) => {
|
|
998
|
+
const state = loadCliState();
|
|
999
|
+
const { workspaceId } = resolveWorkspaceForCommand(state, options.workspaceId);
|
|
1000
|
+
ensureSessionWorkspaceMap(state, workspaceId);
|
|
1001
|
+
setCurrentSessionForWorkspace(state, workspaceId, sessionId);
|
|
1002
|
+
saveCliState(state);
|
|
1003
|
+
console.log(`Current session (${workspaceId}): ${sessionId}`);
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
sessionCommand
|
|
1007
|
+
.command("show [sessionId]")
|
|
1008
|
+
.description("Show session details")
|
|
1009
|
+
.option("--workspace-id <id>", "Workspace ID (default: current)")
|
|
1010
|
+
.option("--base-url <url>", "API base URL")
|
|
1011
|
+
.option("--json", "Output JSON")
|
|
1012
|
+
.action(async (sessionIdArg, options) => {
|
|
1013
|
+
const state = loadCliState();
|
|
1014
|
+
const { workspaceId, baseUrl, entry } = await resolveWorkspaceAuthContext(state, options);
|
|
1015
|
+
const sessionId = resolveSessionForCommand(state, workspaceId, sessionIdArg);
|
|
1016
|
+
const response = await authedApiRequest({
|
|
1017
|
+
state,
|
|
1018
|
+
workspaceId,
|
|
1019
|
+
entry,
|
|
1020
|
+
baseUrl,
|
|
1021
|
+
pathname: `/api/v1/sessions/${sessionId}`,
|
|
1022
|
+
});
|
|
1023
|
+
upsertKnownSession(state, workspaceId, response);
|
|
1024
|
+
saveCliState(state);
|
|
1025
|
+
if (options.json) {
|
|
1026
|
+
console.log(JSON.stringify(response, null, 2));
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
console.log(`Session: ${response.sessionId}`);
|
|
1030
|
+
console.log(`Name: ${response.name || "-"}`);
|
|
1031
|
+
console.log(`Repo: ${response.repoUrl || "-"}`);
|
|
1032
|
+
console.log(`Provider: ${response.defaultProvider || "-"}`);
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
sessionCommand
|
|
1036
|
+
.command("create")
|
|
1037
|
+
.description("Create a new session")
|
|
1038
|
+
.option("--workspace-id <id>", "Workspace ID (default: current)")
|
|
1039
|
+
.option("--base-url <url>", "API base URL")
|
|
1040
|
+
.requiredOption("--repo-url <url>", "Repository URL")
|
|
1041
|
+
.option("--name <name>", "Session display name")
|
|
1042
|
+
.option("--default-internet-access <bool>", "true|false")
|
|
1043
|
+
.option("--default-deny-git-credentials-access <bool>", "true|false")
|
|
1044
|
+
.option("--json", "Output JSON")
|
|
1045
|
+
.action(async (options) => {
|
|
1046
|
+
const state = loadCliState();
|
|
1047
|
+
const { workspaceId, baseUrl, entry } = await resolveWorkspaceAuthContext(state, options);
|
|
1048
|
+
const body = {
|
|
1049
|
+
repoUrl: options.repoUrl,
|
|
1050
|
+
name: options.name,
|
|
1051
|
+
};
|
|
1052
|
+
if (typeof options.defaultInternetAccess === "string") {
|
|
1053
|
+
body.defaultInternetAccess = options.defaultInternetAccess === "true";
|
|
1054
|
+
}
|
|
1055
|
+
if (typeof options.defaultDenyGitCredentialsAccess === "string") {
|
|
1056
|
+
body.defaultDenyGitCredentialsAccess =
|
|
1057
|
+
options.defaultDenyGitCredentialsAccess === "true";
|
|
1058
|
+
}
|
|
1059
|
+
const response = await authedApiRequest({
|
|
1060
|
+
state,
|
|
1061
|
+
workspaceId,
|
|
1062
|
+
entry,
|
|
1063
|
+
baseUrl,
|
|
1064
|
+
pathname: "/api/v1/sessions",
|
|
1065
|
+
method: "POST",
|
|
1066
|
+
body,
|
|
1067
|
+
});
|
|
1068
|
+
upsertKnownSession(state, workspaceId, response);
|
|
1069
|
+
setCurrentSessionForWorkspace(state, workspaceId, response.sessionId);
|
|
1070
|
+
saveCliState(state);
|
|
1071
|
+
if (options.json) {
|
|
1072
|
+
console.log(JSON.stringify(response, null, 2));
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
console.log(`Session created: ${response.sessionId}`);
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
sessionCommand
|
|
1079
|
+
.command("rm [sessionId]")
|
|
1080
|
+
.description("Delete a session")
|
|
1081
|
+
.option("--workspace-id <id>", "Workspace ID (default: current)")
|
|
1082
|
+
.option("--base-url <url>", "API base URL")
|
|
1083
|
+
.option("--yes", "Confirm deletion")
|
|
1084
|
+
.action(async (sessionIdArg, options) => {
|
|
1085
|
+
if (!options.yes) {
|
|
1086
|
+
throw new Error("Refusing to delete without --yes.");
|
|
1087
|
+
}
|
|
1088
|
+
const state = loadCliState();
|
|
1089
|
+
const { workspaceId, baseUrl, entry } = await resolveWorkspaceAuthContext(state, options);
|
|
1090
|
+
const sessionId = resolveSessionForCommand(state, workspaceId, sessionIdArg);
|
|
1091
|
+
const response = await authedApiRequest({
|
|
1092
|
+
state,
|
|
1093
|
+
workspaceId,
|
|
1094
|
+
entry,
|
|
1095
|
+
baseUrl,
|
|
1096
|
+
pathname: `/api/v1/sessions/${sessionId}`,
|
|
1097
|
+
method: "DELETE",
|
|
1098
|
+
});
|
|
1099
|
+
const map = ensureSessionWorkspaceMap(state, workspaceId);
|
|
1100
|
+
delete map[sessionId];
|
|
1101
|
+
setCurrentWorktreeForSession(state, workspaceId, sessionId, null);
|
|
1102
|
+
if (getCurrentSessionForWorkspace(state, workspaceId) === sessionId) {
|
|
1103
|
+
setCurrentSessionForWorkspace(state, workspaceId, null);
|
|
1104
|
+
}
|
|
1105
|
+
saveCliState(state);
|
|
1106
|
+
console.log(`Session deleted: ${response.sessionId || sessionId}`);
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
sessionCommand
|
|
1110
|
+
.command("health [sessionId]")
|
|
1111
|
+
.description("Get session health")
|
|
1112
|
+
.option("--workspace-id <id>", "Workspace ID (default: current)")
|
|
1113
|
+
.option("--base-url <url>", "API base URL")
|
|
1114
|
+
.option("--json", "Output JSON")
|
|
1115
|
+
.action(async (sessionIdArg, options) => {
|
|
1116
|
+
const state = loadCliState();
|
|
1117
|
+
const { workspaceId, baseUrl, entry } = await resolveWorkspaceAuthContext(state, options);
|
|
1118
|
+
const sessionId = resolveSessionForCommand(state, workspaceId, sessionIdArg);
|
|
1119
|
+
const response = await authedApiRequest({
|
|
1120
|
+
state,
|
|
1121
|
+
workspaceId,
|
|
1122
|
+
entry,
|
|
1123
|
+
baseUrl,
|
|
1124
|
+
pathname: `/api/v1/sessions/${sessionId}/health`,
|
|
1125
|
+
});
|
|
1126
|
+
if (options.json) {
|
|
1127
|
+
console.log(JSON.stringify(response, null, 2));
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
console.log(
|
|
1131
|
+
`${sessionId}: ok=${Boolean(response.ok)} ready=${Boolean(response.ready)} provider=${response.provider || "-"}`
|
|
1132
|
+
);
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
const handoffCommand = sessionCommand
|
|
1136
|
+
.command("handoff")
|
|
1137
|
+
.description("Create or consume session handoff tokens");
|
|
1138
|
+
|
|
1139
|
+
handoffCommand
|
|
1140
|
+
.command("create [sessionId]")
|
|
1141
|
+
.description("Create handoff token for a session")
|
|
1142
|
+
.option("--workspace-id <id>", "Workspace ID (default: current)")
|
|
1143
|
+
.option("--base-url <url>", "API base URL")
|
|
1144
|
+
.option("--json", "Output JSON")
|
|
1145
|
+
.action(async (sessionIdArg, options) => {
|
|
1146
|
+
const state = loadCliState();
|
|
1147
|
+
const { workspaceId, baseUrl, entry } = await resolveWorkspaceAuthContext(state, options);
|
|
1148
|
+
const sessionId = resolveSessionForCommand(state, workspaceId, sessionIdArg);
|
|
1149
|
+
const response = await authedApiRequest({
|
|
1150
|
+
state,
|
|
1151
|
+
workspaceId,
|
|
1152
|
+
entry,
|
|
1153
|
+
baseUrl,
|
|
1154
|
+
pathname: "/api/v1/sessions/handoff",
|
|
1155
|
+
method: "POST",
|
|
1156
|
+
body: { sessionId },
|
|
1157
|
+
});
|
|
1158
|
+
if (options.json) {
|
|
1159
|
+
console.log(JSON.stringify(response, null, 2));
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
console.log(`handoffToken=${response.handoffToken}`);
|
|
1163
|
+
if (response.expiresAt) {
|
|
1164
|
+
console.log(`expiresAt=${response.expiresAt}`);
|
|
1165
|
+
}
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
handoffCommand
|
|
1169
|
+
.command("consume")
|
|
1170
|
+
.description("Consume handoff token and save returned workspace/session context")
|
|
1171
|
+
.requiredOption("--token <handoffToken>", "Handoff token")
|
|
1172
|
+
.option("--base-url <url>", "API base URL")
|
|
1173
|
+
.option("--json", "Output JSON")
|
|
1174
|
+
.action(async (options) => {
|
|
1175
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl || defaultBaseUrl);
|
|
1176
|
+
const response = await apiRequest({
|
|
1177
|
+
baseUrl,
|
|
1178
|
+
pathname: "/api/v1/sessions/handoff/consume",
|
|
1179
|
+
method: "POST",
|
|
1180
|
+
body: { handoffToken: options.token },
|
|
1181
|
+
});
|
|
1182
|
+
const state = loadCliState();
|
|
1183
|
+
upsertWorkspaceFromTokens(state, response.workspaceId, baseUrl, response, null);
|
|
1184
|
+
if (response.sessionId) {
|
|
1185
|
+
upsertKnownSession(state, response.workspaceId, { sessionId: response.sessionId });
|
|
1186
|
+
setCurrentSessionForWorkspace(state, response.workspaceId, response.sessionId);
|
|
1187
|
+
}
|
|
1188
|
+
saveCliState(state);
|
|
1189
|
+
if (options.json) {
|
|
1190
|
+
console.log(JSON.stringify(response, null, 2));
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
console.log(
|
|
1194
|
+
`Handoff consumed: workspace=${response.workspaceId}${response.sessionId ? ` session=${response.sessionId}` : ""}`
|
|
1195
|
+
);
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
const resolveSessionAuthContext = async (state, options = {}, sessionIdArg = null) => {
|
|
1199
|
+
const { workspaceId, entry, baseUrl } = await resolveWorkspaceAuthContext(state, options);
|
|
1200
|
+
const sessionId = resolveSessionForCommand(state, workspaceId, sessionIdArg);
|
|
1201
|
+
return { workspaceId, entry, baseUrl, sessionId };
|
|
1202
|
+
};
|
|
1203
|
+
|
|
1204
|
+
const resolveWorktreeForCommand = (state, workspaceId, sessionId, worktreeIdArg) => {
|
|
1205
|
+
const worktreeId = worktreeIdArg || getCurrentWorktreeForSession(state, workspaceId, sessionId);
|
|
1206
|
+
if (!worktreeId) {
|
|
1207
|
+
throw new Error("No worktree selected. Use `vibe80 worktree use <worktreeId>`.");
|
|
1208
|
+
}
|
|
1209
|
+
return worktreeId;
|
|
1210
|
+
};
|
|
1211
|
+
|
|
1212
|
+
const uploadAttachmentFiles = async ({
|
|
1213
|
+
state,
|
|
1214
|
+
workspaceId,
|
|
1215
|
+
entry,
|
|
1216
|
+
baseUrl,
|
|
1217
|
+
sessionId,
|
|
1218
|
+
files,
|
|
1219
|
+
}) => {
|
|
1220
|
+
if (!Array.isArray(files) || !files.length) {
|
|
1221
|
+
return [];
|
|
1222
|
+
}
|
|
1223
|
+
const doUpload = async (workspaceToken) => {
|
|
1224
|
+
const formData = new FormData();
|
|
1225
|
+
for (const filePath of files) {
|
|
1226
|
+
const absPath = path.resolve(filePath);
|
|
1227
|
+
const filename = path.basename(absPath);
|
|
1228
|
+
const buffer = fs.readFileSync(absPath);
|
|
1229
|
+
const blob = new Blob([buffer]);
|
|
1230
|
+
formData.append("files", blob, filename);
|
|
1231
|
+
}
|
|
1232
|
+
const response = await fetch(
|
|
1233
|
+
`${normalizeBaseUrl(baseUrl)}/api/v1/sessions/${encodeURIComponent(sessionId)}/attachments/upload`,
|
|
1234
|
+
{
|
|
1235
|
+
method: "POST",
|
|
1236
|
+
headers: {
|
|
1237
|
+
authorization: `Bearer ${workspaceToken}`,
|
|
1238
|
+
},
|
|
1239
|
+
body: formData,
|
|
1240
|
+
}
|
|
1241
|
+
);
|
|
1242
|
+
const raw = await response.text();
|
|
1243
|
+
let payload = {};
|
|
1244
|
+
if (raw) {
|
|
1245
|
+
try {
|
|
1246
|
+
payload = JSON.parse(raw);
|
|
1247
|
+
} catch {
|
|
1248
|
+
payload = { raw };
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
if (!response.ok) {
|
|
1252
|
+
const message =
|
|
1253
|
+
payload?.error || payload?.message || `Attachment upload failed (${response.status}).`;
|
|
1254
|
+
const error = new Error(message);
|
|
1255
|
+
error.status = response.status;
|
|
1256
|
+
error.payload = payload;
|
|
1257
|
+
throw error;
|
|
1258
|
+
}
|
|
1259
|
+
return payload;
|
|
1260
|
+
};
|
|
1261
|
+
|
|
1262
|
+
const ensured = await ensureWorkspaceAccessToken({
|
|
1263
|
+
state,
|
|
1264
|
+
workspaceId,
|
|
1265
|
+
entry,
|
|
1266
|
+
baseUrl,
|
|
1267
|
+
});
|
|
1268
|
+
let activeWorkspaceId = ensured.workspaceId;
|
|
1269
|
+
let activeEntry = ensured.entry;
|
|
1270
|
+
try {
|
|
1271
|
+
const payload = await doUpload(activeEntry.workspaceToken);
|
|
1272
|
+
return Array.isArray(payload?.files) ? payload.files : [];
|
|
1273
|
+
} catch (error) {
|
|
1274
|
+
if (error?.status !== 401) {
|
|
1275
|
+
throw error;
|
|
1276
|
+
}
|
|
1277
|
+
const refreshed = await refreshWorkspaceAccessToken({
|
|
1278
|
+
state,
|
|
1279
|
+
workspaceId: activeWorkspaceId,
|
|
1280
|
+
entry: activeEntry,
|
|
1281
|
+
baseUrl,
|
|
1282
|
+
});
|
|
1283
|
+
activeWorkspaceId = refreshed.workspaceId;
|
|
1284
|
+
activeEntry = refreshed.entry;
|
|
1285
|
+
const payload = await doUpload(activeEntry.workspaceToken);
|
|
1286
|
+
return Array.isArray(payload?.files) ? payload.files : [];
|
|
1287
|
+
}
|
|
1288
|
+
};
|
|
1289
|
+
|
|
1290
|
+
const worktreeCommand = program
|
|
1291
|
+
.command("worktree")
|
|
1292
|
+
.alias("wt")
|
|
1293
|
+
.description("Manage worktrees for the current session");
|
|
1294
|
+
|
|
1295
|
+
worktreeCommand
|
|
1296
|
+
.command("ls")
|
|
1297
|
+
.description("List worktrees for a session")
|
|
1298
|
+
.option("--workspace-id <id>", "Workspace ID (default: current)")
|
|
1299
|
+
.option("--session-id <id>", "Session ID (default: current for workspace)")
|
|
1300
|
+
.option("--base-url <url>", "API base URL")
|
|
1301
|
+
.option("--json", "Output JSON")
|
|
1302
|
+
.action(async (options) => {
|
|
1303
|
+
const state = loadCliState();
|
|
1304
|
+
const { workspaceId, baseUrl, entry, sessionId } = await resolveSessionAuthContext(
|
|
1305
|
+
state,
|
|
1306
|
+
options,
|
|
1307
|
+
options.sessionId
|
|
1308
|
+
);
|
|
1309
|
+
const response = await authedApiRequest({
|
|
1310
|
+
state,
|
|
1311
|
+
workspaceId,
|
|
1312
|
+
entry,
|
|
1313
|
+
baseUrl,
|
|
1314
|
+
pathname: `/api/v1/sessions/${sessionId}/worktrees`,
|
|
1315
|
+
});
|
|
1316
|
+
const worktrees = Array.isArray(response.worktrees) ? response.worktrees : [];
|
|
1317
|
+
const currentWorktreeId = getCurrentWorktreeForSession(state, workspaceId, sessionId);
|
|
1318
|
+
if (options.json) {
|
|
1319
|
+
console.log(JSON.stringify({ workspaceId, sessionId, currentWorktreeId, worktrees }, null, 2));
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
if (!worktrees.length) {
|
|
1323
|
+
console.log("No worktree found.");
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
for (const wt of worktrees) {
|
|
1327
|
+
const marker = currentWorktreeId === wt.id ? "*" : " ";
|
|
1328
|
+
console.log(
|
|
1329
|
+
`${marker} ${wt.id} name="${wt.name || "-"}" branch=${wt.branchName || "-"} provider=${wt.provider || "-"} status=${wt.status || "-"}`
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
});
|
|
1333
|
+
|
|
1334
|
+
worktreeCommand
|
|
1335
|
+
.command("current")
|
|
1336
|
+
.description("Show current worktree in selected session")
|
|
1337
|
+
.option("--workspace-id <id>", "Workspace ID (default: current)")
|
|
1338
|
+
.option("--session-id <id>", "Session ID (default: current for workspace)")
|
|
1339
|
+
.option("--json", "Output JSON")
|
|
1340
|
+
.action((options) => {
|
|
1341
|
+
const state = loadCliState();
|
|
1342
|
+
const { workspaceId } = resolveWorkspaceForCommand(state, options.workspaceId);
|
|
1343
|
+
const sessionId = resolveSessionForCommand(state, workspaceId, options.sessionId);
|
|
1344
|
+
const worktreeId = getCurrentWorktreeForSession(state, workspaceId, sessionId);
|
|
1345
|
+
if (options.json) {
|
|
1346
|
+
console.log(JSON.stringify({ workspaceId, sessionId, worktreeId }, null, 2));
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
if (!worktreeId) {
|
|
1350
|
+
console.log("No current worktree selected.");
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
console.log(worktreeId);
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
worktreeCommand
|
|
1357
|
+
.command("use <worktreeId>")
|
|
1358
|
+
.description("Set current worktree for selected session")
|
|
1359
|
+
.option("--workspace-id <id>", "Workspace ID (default: current)")
|
|
1360
|
+
.option("--session-id <id>", "Session ID (default: current for workspace)")
|
|
1361
|
+
.action((worktreeId, options) => {
|
|
1362
|
+
const state = loadCliState();
|
|
1363
|
+
const { workspaceId } = resolveWorkspaceForCommand(state, options.workspaceId);
|
|
1364
|
+
const sessionId = resolveSessionForCommand(state, workspaceId, options.sessionId);
|
|
1365
|
+
setCurrentWorktreeForSession(state, workspaceId, sessionId, worktreeId);
|
|
1366
|
+
saveCliState(state);
|
|
1367
|
+
console.log(`Current worktree (${workspaceId}/${sessionId}): ${worktreeId}`);
|
|
1368
|
+
});
|
|
1369
|
+
|
|
1370
|
+
worktreeCommand
|
|
1371
|
+
.command("show [worktreeId]")
|
|
1372
|
+
.description("Show worktree details")
|
|
1373
|
+
.option("--workspace-id <id>", "Workspace ID (default: current)")
|
|
1374
|
+
.option("--session-id <id>", "Session ID (default: current for workspace)")
|
|
1375
|
+
.option("--base-url <url>", "API base URL")
|
|
1376
|
+
.option("--json", "Output JSON")
|
|
1377
|
+
.action(async (worktreeIdArg, options) => {
|
|
1378
|
+
const state = loadCliState();
|
|
1379
|
+
const { workspaceId, baseUrl, entry, sessionId } = await resolveSessionAuthContext(
|
|
1380
|
+
state,
|
|
1381
|
+
options,
|
|
1382
|
+
options.sessionId
|
|
1383
|
+
);
|
|
1384
|
+
const worktreeId = resolveWorktreeForCommand(state, workspaceId, sessionId, worktreeIdArg);
|
|
1385
|
+
const response = await authedApiRequest({
|
|
1386
|
+
state,
|
|
1387
|
+
workspaceId,
|
|
1388
|
+
entry,
|
|
1389
|
+
baseUrl,
|
|
1390
|
+
pathname: `/api/v1/sessions/${sessionId}/worktrees/${worktreeId}`,
|
|
1391
|
+
});
|
|
1392
|
+
if (options.json) {
|
|
1393
|
+
console.log(JSON.stringify(response, null, 2));
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
console.log(`Worktree: ${response.id}`);
|
|
1397
|
+
console.log(`Name: ${response.name || "-"}`);
|
|
1398
|
+
console.log(`Branch: ${response.branchName || "-"}`);
|
|
1399
|
+
console.log(`Provider: ${response.provider || "-"}`);
|
|
1400
|
+
console.log(`Status: ${response.status || "-"}`);
|
|
1401
|
+
});
|
|
1402
|
+
|
|
1403
|
+
worktreeCommand
|
|
1404
|
+
.command("create")
|
|
1405
|
+
.description("Create a new worktree (context=new)")
|
|
1406
|
+
.option("--workspace-id <id>", "Workspace ID (default: current)")
|
|
1407
|
+
.option("--session-id <id>", "Session ID (default: current for workspace)")
|
|
1408
|
+
.option("--base-url <url>", "API base URL")
|
|
1409
|
+
.requiredOption("--provider <provider>", "codex|claude")
|
|
1410
|
+
.option("--name <name>", "Worktree display name")
|
|
1411
|
+
.option("--json", "Output JSON")
|
|
1412
|
+
.action(async (options) => {
|
|
1413
|
+
const provider = parseProviderName(options.provider);
|
|
1414
|
+
const state = loadCliState();
|
|
1415
|
+
const { workspaceId, baseUrl, entry, sessionId } = await resolveSessionAuthContext(
|
|
1416
|
+
state,
|
|
1417
|
+
options,
|
|
1418
|
+
options.sessionId
|
|
1419
|
+
);
|
|
1420
|
+
const response = await authedApiRequest({
|
|
1421
|
+
state,
|
|
1422
|
+
workspaceId,
|
|
1423
|
+
entry,
|
|
1424
|
+
baseUrl,
|
|
1425
|
+
pathname: `/api/v1/sessions/${sessionId}/worktrees`,
|
|
1426
|
+
method: "POST",
|
|
1427
|
+
body: {
|
|
1428
|
+
context: "new",
|
|
1429
|
+
provider,
|
|
1430
|
+
name: options.name || null,
|
|
1431
|
+
},
|
|
1432
|
+
});
|
|
1433
|
+
setCurrentWorktreeForSession(state, workspaceId, sessionId, response.worktreeId);
|
|
1434
|
+
saveCliState(state);
|
|
1435
|
+
if (options.json) {
|
|
1436
|
+
console.log(JSON.stringify(response, null, 2));
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
console.log(`Worktree created: ${response.worktreeId}`);
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1442
|
+
worktreeCommand
|
|
1443
|
+
.command("fork")
|
|
1444
|
+
.description("Fork a worktree (context=fork)")
|
|
1445
|
+
.option("--workspace-id <id>", "Workspace ID (default: current)")
|
|
1446
|
+
.option("--session-id <id>", "Session ID (default: current for workspace)")
|
|
1447
|
+
.option("--base-url <url>", "API base URL")
|
|
1448
|
+
.requiredOption("--from <worktreeId>", "Source worktree id")
|
|
1449
|
+
.option("--name <name>", "Worktree display name")
|
|
1450
|
+
.option("--json", "Output JSON")
|
|
1451
|
+
.action(async (options) => {
|
|
1452
|
+
const state = loadCliState();
|
|
1453
|
+
const { workspaceId, baseUrl, entry, sessionId } = await resolveSessionAuthContext(
|
|
1454
|
+
state,
|
|
1455
|
+
options,
|
|
1456
|
+
options.sessionId
|
|
1457
|
+
);
|
|
1458
|
+
const response = await authedApiRequest({
|
|
1459
|
+
state,
|
|
1460
|
+
workspaceId,
|
|
1461
|
+
entry,
|
|
1462
|
+
baseUrl,
|
|
1463
|
+
pathname: `/api/v1/sessions/${sessionId}/worktrees`,
|
|
1464
|
+
method: "POST",
|
|
1465
|
+
body: {
|
|
1466
|
+
context: "fork",
|
|
1467
|
+
sourceWorktree: options.from,
|
|
1468
|
+
name: options.name || null,
|
|
1469
|
+
},
|
|
1470
|
+
});
|
|
1471
|
+
setCurrentWorktreeForSession(state, workspaceId, sessionId, response.worktreeId);
|
|
1472
|
+
saveCliState(state);
|
|
1473
|
+
if (options.json) {
|
|
1474
|
+
console.log(JSON.stringify(response, null, 2));
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
console.log(`Worktree forked: ${response.worktreeId} (from ${options.from})`);
|
|
1478
|
+
});
|
|
1479
|
+
|
|
1480
|
+
worktreeCommand
|
|
1481
|
+
.command("rm [worktreeId]")
|
|
1482
|
+
.description("Delete a worktree")
|
|
1483
|
+
.option("--workspace-id <id>", "Workspace ID (default: current)")
|
|
1484
|
+
.option("--session-id <id>", "Session ID (default: current for workspace)")
|
|
1485
|
+
.option("--base-url <url>", "API base URL")
|
|
1486
|
+
.option("--yes", "Confirm deletion")
|
|
1487
|
+
.action(async (worktreeIdArg, options) => {
|
|
1488
|
+
if (!options.yes) {
|
|
1489
|
+
throw new Error("Refusing to delete without --yes.");
|
|
1490
|
+
}
|
|
1491
|
+
const state = loadCliState();
|
|
1492
|
+
const { workspaceId, baseUrl, entry, sessionId } = await resolveSessionAuthContext(
|
|
1493
|
+
state,
|
|
1494
|
+
options,
|
|
1495
|
+
options.sessionId
|
|
1496
|
+
);
|
|
1497
|
+
const worktreeId = resolveWorktreeForCommand(state, workspaceId, sessionId, worktreeIdArg);
|
|
1498
|
+
await authedApiRequest({
|
|
1499
|
+
state,
|
|
1500
|
+
workspaceId,
|
|
1501
|
+
entry,
|
|
1502
|
+
baseUrl,
|
|
1503
|
+
pathname: `/api/v1/sessions/${sessionId}/worktrees/${worktreeId}`,
|
|
1504
|
+
method: "DELETE",
|
|
1505
|
+
});
|
|
1506
|
+
if (getCurrentWorktreeForSession(state, workspaceId, sessionId) === worktreeId) {
|
|
1507
|
+
setCurrentWorktreeForSession(state, workspaceId, sessionId, null);
|
|
1508
|
+
}
|
|
1509
|
+
saveCliState(state);
|
|
1510
|
+
console.log(`Worktree deleted: ${worktreeId}`);
|
|
1511
|
+
});
|
|
1512
|
+
|
|
1513
|
+
worktreeCommand
|
|
1514
|
+
.command("rename [worktreeId]")
|
|
1515
|
+
.description("Rename a worktree")
|
|
1516
|
+
.option("--workspace-id <id>", "Workspace ID (default: current)")
|
|
1517
|
+
.option("--session-id <id>", "Session ID (default: current for workspace)")
|
|
1518
|
+
.option("--base-url <url>", "API base URL")
|
|
1519
|
+
.requiredOption("--name <name>", "New worktree name")
|
|
1520
|
+
.option("--json", "Output JSON")
|
|
1521
|
+
.action(async (worktreeIdArg, options) => {
|
|
1522
|
+
const state = loadCliState();
|
|
1523
|
+
const { workspaceId, baseUrl, entry, sessionId } = await resolveSessionAuthContext(
|
|
1524
|
+
state,
|
|
1525
|
+
options,
|
|
1526
|
+
options.sessionId
|
|
1527
|
+
);
|
|
1528
|
+
const worktreeId = resolveWorktreeForCommand(state, workspaceId, sessionId, worktreeIdArg);
|
|
1529
|
+
const response = await authedApiRequest({
|
|
1530
|
+
state,
|
|
1531
|
+
workspaceId,
|
|
1532
|
+
entry,
|
|
1533
|
+
baseUrl,
|
|
1534
|
+
pathname: `/api/v1/sessions/${sessionId}/worktrees/${worktreeId}`,
|
|
1535
|
+
method: "PATCH",
|
|
1536
|
+
body: { name: options.name },
|
|
1537
|
+
});
|
|
1538
|
+
if (options.json) {
|
|
1539
|
+
console.log(JSON.stringify(response, null, 2));
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
console.log(`Worktree renamed: ${worktreeId} -> ${options.name}`);
|
|
1543
|
+
});
|
|
1544
|
+
|
|
1545
|
+
worktreeCommand
|
|
1546
|
+
.command("wakeup [worktreeId]")
|
|
1547
|
+
.description("Wake provider for a worktree")
|
|
1548
|
+
.option("--workspace-id <id>", "Workspace ID (default: current)")
|
|
1549
|
+
.option("--session-id <id>", "Session ID (default: current for workspace)")
|
|
1550
|
+
.option("--base-url <url>", "API base URL")
|
|
1551
|
+
.option("--timeout-ms <ms>", "Wakeup timeout in milliseconds")
|
|
1552
|
+
.option("--json", "Output JSON")
|
|
1553
|
+
.action(async (worktreeIdArg, options) => {
|
|
1554
|
+
const state = loadCliState();
|
|
1555
|
+
const { workspaceId, baseUrl, entry, sessionId } = await resolveSessionAuthContext(
|
|
1556
|
+
state,
|
|
1557
|
+
options,
|
|
1558
|
+
options.sessionId
|
|
1559
|
+
);
|
|
1560
|
+
const worktreeId = resolveWorktreeForCommand(state, workspaceId, sessionId, worktreeIdArg);
|
|
1561
|
+
const body = {};
|
|
1562
|
+
if (options.timeoutMs != null) {
|
|
1563
|
+
body.timeoutMs = Number.parseInt(options.timeoutMs, 10);
|
|
1564
|
+
}
|
|
1565
|
+
const response = await authedApiRequest({
|
|
1566
|
+
state,
|
|
1567
|
+
workspaceId,
|
|
1568
|
+
entry,
|
|
1569
|
+
baseUrl,
|
|
1570
|
+
pathname: `/api/v1/sessions/${sessionId}/worktrees/${worktreeId}/wakeup`,
|
|
1571
|
+
method: "POST",
|
|
1572
|
+
body,
|
|
1573
|
+
});
|
|
1574
|
+
if (options.json) {
|
|
1575
|
+
console.log(JSON.stringify(response, null, 2));
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
console.log(
|
|
1579
|
+
`${response.worktreeId || worktreeId}: status=${response.status || "ready"} provider=${response.provider || "-"}`
|
|
1580
|
+
);
|
|
1581
|
+
});
|
|
1582
|
+
|
|
1583
|
+
worktreeCommand
|
|
1584
|
+
.command("status [worktreeId]")
|
|
1585
|
+
.description("Show git status entries for a worktree")
|
|
1586
|
+
.option("--workspace-id <id>", "Workspace ID (default: current)")
|
|
1587
|
+
.option("--session-id <id>", "Session ID (default: current for workspace)")
|
|
1588
|
+
.option("--base-url <url>", "API base URL")
|
|
1589
|
+
.option("--json", "Output JSON")
|
|
1590
|
+
.action(async (worktreeIdArg, options) => {
|
|
1591
|
+
const state = loadCliState();
|
|
1592
|
+
const { workspaceId, baseUrl, entry, sessionId } = await resolveSessionAuthContext(
|
|
1593
|
+
state,
|
|
1594
|
+
options,
|
|
1595
|
+
options.sessionId
|
|
1596
|
+
);
|
|
1597
|
+
const worktreeId = resolveWorktreeForCommand(state, workspaceId, sessionId, worktreeIdArg);
|
|
1598
|
+
const response = await authedApiRequest({
|
|
1599
|
+
state,
|
|
1600
|
+
workspaceId,
|
|
1601
|
+
entry,
|
|
1602
|
+
baseUrl,
|
|
1603
|
+
pathname: `/api/v1/sessions/${sessionId}/worktrees/${worktreeId}/status`,
|
|
1604
|
+
});
|
|
1605
|
+
const entries = Array.isArray(response.entries) ? response.entries : [];
|
|
1606
|
+
if (options.json) {
|
|
1607
|
+
console.log(JSON.stringify({ worktreeId, entries }, null, 2));
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
if (!entries.length) {
|
|
1611
|
+
console.log("Clean worktree.");
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1614
|
+
for (const item of entries) {
|
|
1615
|
+
console.log(`${item.type || "modified"}\t${item.path || ""}`);
|
|
1616
|
+
}
|
|
1617
|
+
});
|
|
1618
|
+
|
|
1619
|
+
worktreeCommand
|
|
1620
|
+
.command("diff [worktreeId]")
|
|
1621
|
+
.description("Show worktree diff")
|
|
1622
|
+
.option("--workspace-id <id>", "Workspace ID (default: current)")
|
|
1623
|
+
.option("--session-id <id>", "Session ID (default: current for workspace)")
|
|
1624
|
+
.option("--base-url <url>", "API base URL")
|
|
1625
|
+
.option("--json", "Output JSON")
|
|
1626
|
+
.action(async (worktreeIdArg, options) => {
|
|
1627
|
+
const state = loadCliState();
|
|
1628
|
+
const { workspaceId, baseUrl, entry, sessionId } = await resolveSessionAuthContext(
|
|
1629
|
+
state,
|
|
1630
|
+
options,
|
|
1631
|
+
options.sessionId
|
|
1632
|
+
);
|
|
1633
|
+
const worktreeId = resolveWorktreeForCommand(state, workspaceId, sessionId, worktreeIdArg);
|
|
1634
|
+
const response = await authedApiRequest({
|
|
1635
|
+
state,
|
|
1636
|
+
workspaceId,
|
|
1637
|
+
entry,
|
|
1638
|
+
baseUrl,
|
|
1639
|
+
pathname: `/api/v1/sessions/${sessionId}/worktrees/${worktreeId}/diff`,
|
|
1640
|
+
});
|
|
1641
|
+
if (options.json) {
|
|
1642
|
+
console.log(JSON.stringify(response, null, 2));
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
if (typeof response.diff === "string") {
|
|
1646
|
+
console.log(response.diff);
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
console.log(JSON.stringify(response, null, 2));
|
|
1650
|
+
});
|
|
1651
|
+
|
|
1652
|
+
worktreeCommand
|
|
1653
|
+
.command("commits [worktreeId]")
|
|
1654
|
+
.description("List recent commits for a worktree")
|
|
1655
|
+
.option("--workspace-id <id>", "Workspace ID (default: current)")
|
|
1656
|
+
.option("--session-id <id>", "Session ID (default: current for workspace)")
|
|
1657
|
+
.option("--base-url <url>", "API base URL")
|
|
1658
|
+
.option("--limit <n>", "Number of commits (default: 20)")
|
|
1659
|
+
.option("--json", "Output JSON")
|
|
1660
|
+
.action(async (worktreeIdArg, options) => {
|
|
1661
|
+
const state = loadCliState();
|
|
1662
|
+
const { workspaceId, baseUrl, entry, sessionId } = await resolveSessionAuthContext(
|
|
1663
|
+
state,
|
|
1664
|
+
options,
|
|
1665
|
+
options.sessionId
|
|
1666
|
+
);
|
|
1667
|
+
const worktreeId = resolveWorktreeForCommand(state, workspaceId, sessionId, worktreeIdArg);
|
|
1668
|
+
const qs = options.limit ? `?limit=${encodeURIComponent(String(options.limit))}` : "";
|
|
1669
|
+
const response = await authedApiRequest({
|
|
1670
|
+
state,
|
|
1671
|
+
workspaceId,
|
|
1672
|
+
entry,
|
|
1673
|
+
baseUrl,
|
|
1674
|
+
pathname: `/api/v1/sessions/${sessionId}/worktrees/${worktreeId}/commits${qs}`,
|
|
1675
|
+
});
|
|
1676
|
+
const commits = Array.isArray(response.commits) ? response.commits : [];
|
|
1677
|
+
if (options.json) {
|
|
1678
|
+
console.log(JSON.stringify({ worktreeId, commits }, null, 2));
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
if (!commits.length) {
|
|
1682
|
+
console.log("No commit found.");
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
for (const commit of commits) {
|
|
1686
|
+
console.log(`${commit.sha || "-"} ${commit.date || ""} ${commit.message || ""}`);
|
|
1687
|
+
}
|
|
1688
|
+
});
|
|
1689
|
+
|
|
1690
|
+
const messageCommand = program
|
|
1691
|
+
.command("message")
|
|
1692
|
+
.alias("msg")
|
|
1693
|
+
.description("Send and inspect worktree messages");
|
|
1694
|
+
|
|
1695
|
+
messageCommand
|
|
1696
|
+
.command("send")
|
|
1697
|
+
.description("Send a user message to a worktree (supports attachments)")
|
|
1698
|
+
.requiredOption("--text <text>", "Message text")
|
|
1699
|
+
.option("--file <path>", "Attachment path (repeatable)", parseRepeatOption, [])
|
|
1700
|
+
.option("--workspace-id <id>", "Workspace ID (default: current)")
|
|
1701
|
+
.option("--session-id <id>", "Session ID (default: current for workspace)")
|
|
1702
|
+
.option("--worktree-id <id>", "Worktree ID (default: current for session)")
|
|
1703
|
+
.option("--base-url <url>", "API base URL")
|
|
1704
|
+
.option("--json", "Output JSON")
|
|
1705
|
+
.action(async (options) => {
|
|
1706
|
+
const state = loadCliState();
|
|
1707
|
+
const { workspaceId, baseUrl, entry, sessionId } = await resolveSessionAuthContext(
|
|
1708
|
+
state,
|
|
1709
|
+
options,
|
|
1710
|
+
options.sessionId
|
|
1711
|
+
);
|
|
1712
|
+
const worktreeId = resolveWorktreeForCommand(
|
|
1713
|
+
state,
|
|
1714
|
+
workspaceId,
|
|
1715
|
+
sessionId,
|
|
1716
|
+
options.worktreeId
|
|
1717
|
+
);
|
|
1718
|
+
const uploaded = await uploadAttachmentFiles({
|
|
1719
|
+
state,
|
|
1720
|
+
workspaceId,
|
|
1721
|
+
entry,
|
|
1722
|
+
baseUrl,
|
|
1723
|
+
sessionId,
|
|
1724
|
+
files: options.file || [],
|
|
1725
|
+
});
|
|
1726
|
+
const response = await authedApiRequest({
|
|
1727
|
+
state,
|
|
1728
|
+
workspaceId,
|
|
1729
|
+
entry,
|
|
1730
|
+
baseUrl,
|
|
1731
|
+
pathname: `/api/v1/sessions/${sessionId}/worktrees/${worktreeId}/messages`,
|
|
1732
|
+
method: "POST",
|
|
1733
|
+
body: {
|
|
1734
|
+
role: "user",
|
|
1735
|
+
text: options.text,
|
|
1736
|
+
attachments: uploaded,
|
|
1737
|
+
},
|
|
1738
|
+
});
|
|
1739
|
+
if (options.json) {
|
|
1740
|
+
console.log(JSON.stringify({ ...response, attachments: uploaded }, null, 2));
|
|
1741
|
+
return;
|
|
1742
|
+
}
|
|
1743
|
+
console.log(
|
|
1744
|
+
`Message sent: worktree=${worktreeId} messageId=${response.messageId || "-"} turnId=${response.turnId || "-"}`
|
|
1745
|
+
);
|
|
1746
|
+
if (uploaded.length) {
|
|
1747
|
+
console.log(`Attachments uploaded: ${uploaded.map((item) => item.name || item.path).join(", ")}`);
|
|
1748
|
+
}
|
|
1749
|
+
});
|
|
1750
|
+
|
|
1751
|
+
messageCommand
|
|
1752
|
+
.command("ls")
|
|
1753
|
+
.description("List messages for a worktree")
|
|
1754
|
+
.option("--workspace-id <id>", "Workspace ID (default: current)")
|
|
1755
|
+
.option("--session-id <id>", "Session ID (default: current for workspace)")
|
|
1756
|
+
.option("--worktree-id <id>", "Worktree ID (default: current for session)")
|
|
1757
|
+
.option("--base-url <url>", "API base URL")
|
|
1758
|
+
.option("--limit <n>", "Number of messages (default: 50)")
|
|
1759
|
+
.option("--before-message-id <id>", "Pagination cursor")
|
|
1760
|
+
.option("--json", "Output JSON")
|
|
1761
|
+
.action(async (options) => {
|
|
1762
|
+
const state = loadCliState();
|
|
1763
|
+
const { workspaceId, baseUrl, entry, sessionId } = await resolveSessionAuthContext(
|
|
1764
|
+
state,
|
|
1765
|
+
options,
|
|
1766
|
+
options.sessionId
|
|
1767
|
+
);
|
|
1768
|
+
const worktreeId = resolveWorktreeForCommand(
|
|
1769
|
+
state,
|
|
1770
|
+
workspaceId,
|
|
1771
|
+
sessionId,
|
|
1772
|
+
options.worktreeId
|
|
1773
|
+
);
|
|
1774
|
+
const qs = new URLSearchParams();
|
|
1775
|
+
if (options.limit) qs.set("limit", String(options.limit));
|
|
1776
|
+
if (options.beforeMessageId) qs.set("beforeMessageId", String(options.beforeMessageId));
|
|
1777
|
+
const response = await authedApiRequest({
|
|
1778
|
+
state,
|
|
1779
|
+
workspaceId,
|
|
1780
|
+
entry,
|
|
1781
|
+
baseUrl,
|
|
1782
|
+
pathname: `/api/v1/sessions/${sessionId}/worktrees/${worktreeId}/messages${qs.size ? `?${qs.toString()}` : ""}`,
|
|
1783
|
+
});
|
|
1784
|
+
const messages = Array.isArray(response.messages) ? response.messages : [];
|
|
1785
|
+
if (options.json) {
|
|
1786
|
+
console.log(JSON.stringify({ ...response, worktreeId, messages }, null, 2));
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
if (!messages.length) {
|
|
1790
|
+
console.log("No message found.");
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
for (const msg of messages) {
|
|
1794
|
+
const role = msg.role || "unknown";
|
|
1795
|
+
const text = String(msg.text || "").replace(/\s+/g, " ").trim();
|
|
1796
|
+
const attachments = Array.isArray(msg.attachments) ? msg.attachments : [];
|
|
1797
|
+
const suffix = attachments.length
|
|
1798
|
+
? ` [attachments: ${attachments.map((a) => a?.name || a?.path).filter(Boolean).join(", ")}]`
|
|
1799
|
+
: "";
|
|
1800
|
+
console.log(`${msg.id || "-"} ${role}: ${text}${suffix}`);
|
|
1801
|
+
}
|
|
1802
|
+
});
|
|
1803
|
+
|
|
1804
|
+
messageCommand
|
|
1805
|
+
.command("tail")
|
|
1806
|
+
.description("Poll and display new messages for a worktree")
|
|
1807
|
+
.option("--workspace-id <id>", "Workspace ID (default: current)")
|
|
1808
|
+
.option("--session-id <id>", "Session ID (default: current for workspace)")
|
|
1809
|
+
.option("--worktree-id <id>", "Worktree ID (default: current for session)")
|
|
1810
|
+
.option("--base-url <url>", "API base URL")
|
|
1811
|
+
.option("--limit <n>", "Initial number of messages (default: 50)")
|
|
1812
|
+
.option("--interval-ms <ms>", "Polling interval in milliseconds (default: 2000)")
|
|
1813
|
+
.action(async (options) => {
|
|
1814
|
+
const state = loadCliState();
|
|
1815
|
+
const { workspaceId, baseUrl, entry, sessionId } = await resolveSessionAuthContext(
|
|
1816
|
+
state,
|
|
1817
|
+
options,
|
|
1818
|
+
options.sessionId
|
|
1819
|
+
);
|
|
1820
|
+
const worktreeId = resolveWorktreeForCommand(
|
|
1821
|
+
state,
|
|
1822
|
+
workspaceId,
|
|
1823
|
+
sessionId,
|
|
1824
|
+
options.worktreeId
|
|
1825
|
+
);
|
|
1826
|
+
const intervalMs = Number.parseInt(options.intervalMs, 10) || 2000;
|
|
1827
|
+
const initialLimit = Number.parseInt(options.limit, 10) || 50;
|
|
1828
|
+
const seen = new Set();
|
|
1829
|
+
console.log(`Tailing messages for ${workspaceId}/${sessionId}/${worktreeId} (Ctrl+C to stop)...`);
|
|
1830
|
+
while (true) {
|
|
1831
|
+
const qs = new URLSearchParams();
|
|
1832
|
+
qs.set("limit", String(initialLimit));
|
|
1833
|
+
const response = await authedApiRequest({
|
|
1834
|
+
state,
|
|
1835
|
+
workspaceId,
|
|
1836
|
+
entry,
|
|
1837
|
+
baseUrl,
|
|
1838
|
+
pathname: `/api/v1/sessions/${sessionId}/worktrees/${worktreeId}/messages?${qs.toString()}`,
|
|
1839
|
+
});
|
|
1840
|
+
const messages = Array.isArray(response.messages) ? response.messages : [];
|
|
1841
|
+
for (const msg of messages) {
|
|
1842
|
+
const id = msg.id || `${msg.role}-${msg.createdAt || ""}-${msg.text || ""}`;
|
|
1843
|
+
if (seen.has(id)) {
|
|
1844
|
+
continue;
|
|
1845
|
+
}
|
|
1846
|
+
seen.add(id);
|
|
1847
|
+
const role = msg.role || "unknown";
|
|
1848
|
+
const text = String(msg.text || "").replace(/\s+/g, " ").trim();
|
|
1849
|
+
const attachments = Array.isArray(msg.attachments) ? msg.attachments : [];
|
|
1850
|
+
const suffix = attachments.length
|
|
1851
|
+
? ` [attachments: ${attachments.map((a) => a?.name || a?.path).filter(Boolean).join(", ")}]`
|
|
1852
|
+
: "";
|
|
1853
|
+
console.log(`${msg.id || "-"} ${role}: ${text}${suffix}`);
|
|
1854
|
+
}
|
|
1855
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
1856
|
+
}
|
|
1857
|
+
});
|
|
1858
|
+
|
|
1859
|
+
program.command("help").description("Show help").action(() => {
|
|
1860
|
+
program.outputHelp();
|
|
1861
|
+
});
|
|
1862
|
+
|
|
1863
|
+
if (!process.argv.slice(2).length) {
|
|
1864
|
+
console.error(
|
|
1865
|
+
"[vibe80] Missing command. Use `vibe80 run --codex`, `vibe80 workspace --help`, `vibe80 session --help`, or `vibe80 worktree --help`."
|
|
1866
|
+
);
|
|
1867
|
+
program.outputHelp();
|
|
1868
|
+
process.exit(1);
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
program.parseAsync(process.argv).catch((error) => {
|
|
1872
|
+
console.error(`[vibe80] ${error?.message || error}`);
|
|
1873
|
+
if (error?.payload) {
|
|
1874
|
+
console.error(JSON.stringify(error.payload, null, 2));
|
|
1875
|
+
}
|
|
1876
|
+
process.exit(1);
|
|
1877
|
+
});
|
|
166
1878
|
|
|
167
1879
|
process.on("SIGINT", () => shutdown(0));
|
|
168
1880
|
process.on("SIGTERM", () => shutdown(0));
|