agentuity-vscode 0.1.5 → 0.1.7
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/package.json +475 -2
- package/src/core/cliClient.ts +477 -0
- package/src/core/index.ts +1 -0
- package/src/core/readonlyDocument.ts +11 -0
- package/src/core/sandboxManager.ts +483 -0
- package/src/extension.ts +14 -0
- package/src/features/chat/agentTools.ts +254 -1
- package/src/features/sandboxExplorer/index.ts +1375 -0
- package/src/features/sandboxExplorer/sandboxTreeData.ts +803 -0
- package/src/features/sandboxExplorer/statusBar.ts +383 -0
|
@@ -0,0 +1,1375 @@
|
|
|
1
|
+
import * as vscode from 'vscode';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import { SandboxTreeDataProvider, SandboxTreeItem } from './sandboxTreeData';
|
|
6
|
+
import { onAuthStatusChanged } from '../../core/auth';
|
|
7
|
+
import {
|
|
8
|
+
getCliClient,
|
|
9
|
+
type SandboxCreateOptions,
|
|
10
|
+
type SnapshotInfo,
|
|
11
|
+
CliClient,
|
|
12
|
+
} from '../../core/cliClient';
|
|
13
|
+
import {
|
|
14
|
+
getSandboxManager,
|
|
15
|
+
initSandboxManager,
|
|
16
|
+
onLinkedSandboxesChanged,
|
|
17
|
+
formatBytes,
|
|
18
|
+
DEFAULT_SANDBOX_PATH,
|
|
19
|
+
} from '../../core/sandboxManager';
|
|
20
|
+
import { openReadonlyDocument } from '../../core/readonlyDocument';
|
|
21
|
+
import {
|
|
22
|
+
createSandboxStatusBar,
|
|
23
|
+
updateStatusBar,
|
|
24
|
+
showSyncProgress,
|
|
25
|
+
showSyncSuccess,
|
|
26
|
+
showSyncError,
|
|
27
|
+
disposeSandboxStatusBar,
|
|
28
|
+
} from './statusBar';
|
|
29
|
+
|
|
30
|
+
const sandboxTerminals: Map<string, vscode.Terminal> = new Map();
|
|
31
|
+
|
|
32
|
+
// Track sandbox files opened for editing: localPath -> { sandboxId, remotePath }
|
|
33
|
+
interface SandboxFileMapping {
|
|
34
|
+
sandboxId: string;
|
|
35
|
+
remotePath: string;
|
|
36
|
+
}
|
|
37
|
+
const sandboxFileMap: Map<string, SandboxFileMapping> = new Map();
|
|
38
|
+
let saveListener: vscode.Disposable | undefined;
|
|
39
|
+
|
|
40
|
+
// Debounce timers for file uploads
|
|
41
|
+
const uploadDebounceTimers: Map<string, NodeJS.Timeout> = new Map();
|
|
42
|
+
const UPLOAD_DEBOUNCE_MS = 1000; // 1 second debounce
|
|
43
|
+
|
|
44
|
+
export function registerSandboxExplorer(
|
|
45
|
+
context: vscode.ExtensionContext
|
|
46
|
+
): SandboxTreeDataProvider {
|
|
47
|
+
// Initialize sandbox manager
|
|
48
|
+
initSandboxManager(context);
|
|
49
|
+
|
|
50
|
+
const provider = new SandboxTreeDataProvider();
|
|
51
|
+
|
|
52
|
+
const treeView = vscode.window.createTreeView('agentuity.sandboxes', {
|
|
53
|
+
treeDataProvider: provider,
|
|
54
|
+
showCollapseAll: true,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Create status bar
|
|
58
|
+
createSandboxStatusBar(context);
|
|
59
|
+
|
|
60
|
+
// Refresh on auth changes
|
|
61
|
+
const authSub = onAuthStatusChanged(() => {
|
|
62
|
+
provider.refresh();
|
|
63
|
+
updateStatusBar();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Refresh when linked sandboxes change
|
|
67
|
+
const linkedSub = onLinkedSandboxesChanged(() => {
|
|
68
|
+
provider.refresh();
|
|
69
|
+
updateStatusBar();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Register sandbox-specific commands (refresh is registered in extension.ts)
|
|
73
|
+
registerCommands(context, provider);
|
|
74
|
+
|
|
75
|
+
// Set up save listener for sandbox files with debouncing
|
|
76
|
+
saveListener = vscode.workspace.onDidSaveTextDocument((doc) => {
|
|
77
|
+
const mapping = sandboxFileMap.get(doc.uri.fsPath);
|
|
78
|
+
if (mapping) {
|
|
79
|
+
// Clear any existing timer for this file
|
|
80
|
+
const existingTimer = uploadDebounceTimers.get(doc.uri.fsPath);
|
|
81
|
+
if (existingTimer) {
|
|
82
|
+
clearTimeout(existingTimer);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Set new debounced upload
|
|
86
|
+
const timer = setTimeout(async () => {
|
|
87
|
+
uploadDebounceTimers.delete(doc.uri.fsPath);
|
|
88
|
+
await uploadSavedFile(mapping.sandboxId, doc.uri.fsPath, mapping.remotePath, provider);
|
|
89
|
+
}, UPLOAD_DEBOUNCE_MS);
|
|
90
|
+
|
|
91
|
+
uploadDebounceTimers.set(doc.uri.fsPath, timer);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Clean up file mappings and pending uploads when documents are closed
|
|
96
|
+
const closeListener = vscode.workspace.onDidCloseTextDocument((doc) => {
|
|
97
|
+
sandboxFileMap.delete(doc.uri.fsPath);
|
|
98
|
+
const timer = uploadDebounceTimers.get(doc.uri.fsPath);
|
|
99
|
+
if (timer) {
|
|
100
|
+
clearTimeout(timer);
|
|
101
|
+
uploadDebounceTimers.delete(doc.uri.fsPath);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
context.subscriptions.push(
|
|
106
|
+
treeView,
|
|
107
|
+
authSub,
|
|
108
|
+
linkedSub,
|
|
109
|
+
saveListener,
|
|
110
|
+
closeListener,
|
|
111
|
+
{ dispose: () => provider.dispose() },
|
|
112
|
+
{ dispose: () => disposeTerminals() },
|
|
113
|
+
{ dispose: () => disposeSandboxStatusBar() },
|
|
114
|
+
{ dispose: () => sandboxFileMap.clear() },
|
|
115
|
+
{
|
|
116
|
+
dispose: () => {
|
|
117
|
+
// Clear all pending upload timers
|
|
118
|
+
for (const timer of uploadDebounceTimers.values()) {
|
|
119
|
+
clearTimeout(timer);
|
|
120
|
+
}
|
|
121
|
+
uploadDebounceTimers.clear();
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
return provider;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function registerCommands(
|
|
130
|
+
context: vscode.ExtensionContext,
|
|
131
|
+
provider: SandboxTreeDataProvider
|
|
132
|
+
): void {
|
|
133
|
+
// Create sandbox
|
|
134
|
+
context.subscriptions.push(
|
|
135
|
+
vscode.commands.registerCommand('agentuity.sandbox.create', async () => {
|
|
136
|
+
await createSandbox(provider);
|
|
137
|
+
})
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// Create sandbox from snapshot
|
|
141
|
+
context.subscriptions.push(
|
|
142
|
+
vscode.commands.registerCommand(
|
|
143
|
+
'agentuity.sandbox.createFromSnapshot',
|
|
144
|
+
async (item?: SandboxTreeItem) => {
|
|
145
|
+
if (item?.snapshotData) {
|
|
146
|
+
await createSandboxFromSnapshot(item.snapshotData.snapshotId, provider);
|
|
147
|
+
} else {
|
|
148
|
+
// Prompt for snapshot
|
|
149
|
+
const snapshotId = await vscode.window.showInputBox({
|
|
150
|
+
prompt: 'Enter snapshot ID or tag',
|
|
151
|
+
placeHolder: 'snp_xxx or tag-name',
|
|
152
|
+
});
|
|
153
|
+
if (snapshotId) {
|
|
154
|
+
await createSandboxFromSnapshot(snapshotId, provider);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
// Delete sandbox
|
|
162
|
+
context.subscriptions.push(
|
|
163
|
+
vscode.commands.registerCommand(
|
|
164
|
+
'agentuity.sandbox.delete',
|
|
165
|
+
async (item?: SandboxTreeItem) => {
|
|
166
|
+
if (!item?.sandboxData) return;
|
|
167
|
+
await deleteSandbox(item.sandboxData.sandboxId, provider);
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// Link sandbox
|
|
173
|
+
context.subscriptions.push(
|
|
174
|
+
vscode.commands.registerCommand(
|
|
175
|
+
'agentuity.sandbox.link',
|
|
176
|
+
async (item?: SandboxTreeItem) => {
|
|
177
|
+
if (!item?.sandboxData) return;
|
|
178
|
+
await linkSandbox(item.sandboxData.sandboxId, provider);
|
|
179
|
+
}
|
|
180
|
+
)
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
// Unlink sandbox
|
|
184
|
+
context.subscriptions.push(
|
|
185
|
+
vscode.commands.registerCommand(
|
|
186
|
+
'agentuity.sandbox.unlink',
|
|
187
|
+
async (item?: SandboxTreeItem) => {
|
|
188
|
+
if (!item?.sandboxData) return;
|
|
189
|
+
await unlinkSandbox(item.sandboxData.sandboxId, provider);
|
|
190
|
+
}
|
|
191
|
+
)
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
// Sync project to sandbox
|
|
195
|
+
context.subscriptions.push(
|
|
196
|
+
vscode.commands.registerCommand(
|
|
197
|
+
'agentuity.sandbox.sync',
|
|
198
|
+
async (item?: SandboxTreeItem) => {
|
|
199
|
+
let sandboxId: string | undefined;
|
|
200
|
+
|
|
201
|
+
if (item?.sandboxData) {
|
|
202
|
+
sandboxId = item.sandboxData.sandboxId;
|
|
203
|
+
} else {
|
|
204
|
+
// Try to get from linked sandboxes
|
|
205
|
+
const linked = getSandboxManager().getLinkedSandboxes();
|
|
206
|
+
if (linked.length === 0) {
|
|
207
|
+
vscode.window.showWarningMessage(
|
|
208
|
+
'No sandbox linked to this workspace. Link a sandbox first.'
|
|
209
|
+
);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (linked.length === 1) {
|
|
213
|
+
sandboxId = linked[0].sandboxId;
|
|
214
|
+
} else {
|
|
215
|
+
// Pick one
|
|
216
|
+
const picked = await vscode.window.showQuickPick(
|
|
217
|
+
linked.map((l) => ({
|
|
218
|
+
label: l.name || l.sandboxId,
|
|
219
|
+
description: l.sandboxId,
|
|
220
|
+
sandboxId: l.sandboxId,
|
|
221
|
+
})),
|
|
222
|
+
{ placeHolder: 'Select sandbox to sync to' }
|
|
223
|
+
);
|
|
224
|
+
if (picked) {
|
|
225
|
+
sandboxId = picked.sandboxId;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (sandboxId) {
|
|
231
|
+
await syncToSandbox(sandboxId, provider);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
)
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
// Execute command in sandbox
|
|
238
|
+
context.subscriptions.push(
|
|
239
|
+
vscode.commands.registerCommand(
|
|
240
|
+
'agentuity.sandbox.exec',
|
|
241
|
+
async (itemOrOptions?: SandboxTreeItem | { sandboxId: string; command?: string }) => {
|
|
242
|
+
let sandboxId: string | undefined;
|
|
243
|
+
let command: string | undefined;
|
|
244
|
+
|
|
245
|
+
if (itemOrOptions instanceof SandboxTreeItem) {
|
|
246
|
+
sandboxId = itemOrOptions.sandboxData?.sandboxId;
|
|
247
|
+
} else if (itemOrOptions && 'sandboxId' in itemOrOptions) {
|
|
248
|
+
sandboxId = itemOrOptions.sandboxId;
|
|
249
|
+
command = itemOrOptions.command;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (!sandboxId) return;
|
|
253
|
+
await execInSandbox(sandboxId, command);
|
|
254
|
+
}
|
|
255
|
+
)
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
// View file
|
|
259
|
+
context.subscriptions.push(
|
|
260
|
+
vscode.commands.registerCommand(
|
|
261
|
+
'agentuity.sandbox.viewFile',
|
|
262
|
+
async (item?: SandboxTreeItem) => {
|
|
263
|
+
if (!item?.parentSandboxId || !item?.filePath) return;
|
|
264
|
+
await viewSandboxFile(item.parentSandboxId, item.filePath);
|
|
265
|
+
}
|
|
266
|
+
)
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
// Download file/directory
|
|
270
|
+
context.subscriptions.push(
|
|
271
|
+
vscode.commands.registerCommand(
|
|
272
|
+
'agentuity.sandbox.download',
|
|
273
|
+
async (item?: SandboxTreeItem) => {
|
|
274
|
+
if (!item?.parentSandboxId || !item?.filePath) return;
|
|
275
|
+
await downloadFromSandbox(item.parentSandboxId, item.filePath, item.itemType === 'directory');
|
|
276
|
+
}
|
|
277
|
+
)
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
// Delete file
|
|
281
|
+
context.subscriptions.push(
|
|
282
|
+
vscode.commands.registerCommand(
|
|
283
|
+
'agentuity.sandbox.deleteFile',
|
|
284
|
+
async (item?: SandboxTreeItem) => {
|
|
285
|
+
if (!item?.parentSandboxId || !item?.filePath) return;
|
|
286
|
+
await deleteFile(item.parentSandboxId, item.filePath, item.itemType === 'directory', provider);
|
|
287
|
+
}
|
|
288
|
+
)
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
// Create new file in sandbox
|
|
292
|
+
context.subscriptions.push(
|
|
293
|
+
vscode.commands.registerCommand(
|
|
294
|
+
'agentuity.sandbox.createFile',
|
|
295
|
+
async (item?: SandboxTreeItem) => {
|
|
296
|
+
// Get sandbox ID from item or category
|
|
297
|
+
const sandboxId = item?.parentSandboxId || item?.sandboxData?.sandboxId;
|
|
298
|
+
if (!sandboxId) return;
|
|
299
|
+
|
|
300
|
+
// Get parent directory path
|
|
301
|
+
let parentDir = '';
|
|
302
|
+
if (item?.itemType === 'directory' && item.filePath) {
|
|
303
|
+
parentDir = item.filePath;
|
|
304
|
+
} else if (item?.itemType === 'file' && item.filePath) {
|
|
305
|
+
parentDir = path.dirname(item.filePath);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
await createSandboxFile(sandboxId, parentDir, provider);
|
|
309
|
+
}
|
|
310
|
+
)
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
// Create new folder in sandbox
|
|
314
|
+
context.subscriptions.push(
|
|
315
|
+
vscode.commands.registerCommand(
|
|
316
|
+
'agentuity.sandbox.createFolder',
|
|
317
|
+
async (item?: SandboxTreeItem) => {
|
|
318
|
+
// Get sandbox ID from item or category
|
|
319
|
+
const sandboxId = item?.parentSandboxId || item?.sandboxData?.sandboxId;
|
|
320
|
+
if (!sandboxId) return;
|
|
321
|
+
|
|
322
|
+
// Get parent directory path
|
|
323
|
+
let parentDir = '';
|
|
324
|
+
if (item?.itemType === 'directory' && item.filePath) {
|
|
325
|
+
parentDir = item.filePath;
|
|
326
|
+
} else if (item?.itemType === 'file' && item.filePath) {
|
|
327
|
+
parentDir = path.dirname(item.filePath);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
await createSandboxFolder(sandboxId, parentDir, provider);
|
|
331
|
+
}
|
|
332
|
+
)
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
// Copy path
|
|
336
|
+
context.subscriptions.push(
|
|
337
|
+
vscode.commands.registerCommand(
|
|
338
|
+
'agentuity.sandbox.copyPath',
|
|
339
|
+
async (item?: SandboxTreeItem) => {
|
|
340
|
+
if (!item?.filePath) return;
|
|
341
|
+
await vscode.env.clipboard.writeText(item.filePath);
|
|
342
|
+
vscode.window.showInformationMessage(`Copied: ${item.filePath}`);
|
|
343
|
+
}
|
|
344
|
+
)
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
// View execution
|
|
348
|
+
context.subscriptions.push(
|
|
349
|
+
vscode.commands.registerCommand(
|
|
350
|
+
'agentuity.sandbox.viewExecution',
|
|
351
|
+
async (item?: SandboxTreeItem) => {
|
|
352
|
+
if (!item?.executionData) return;
|
|
353
|
+
await viewExecution(item.executionData.executionId);
|
|
354
|
+
}
|
|
355
|
+
)
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
// Set environment variable
|
|
359
|
+
context.subscriptions.push(
|
|
360
|
+
vscode.commands.registerCommand(
|
|
361
|
+
'agentuity.sandbox.setEnv',
|
|
362
|
+
async (item?: SandboxTreeItem) => {
|
|
363
|
+
if (!item?.sandboxData) return;
|
|
364
|
+
await setEnvVar(item.sandboxData.sandboxId);
|
|
365
|
+
}
|
|
366
|
+
)
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
// View environment
|
|
370
|
+
context.subscriptions.push(
|
|
371
|
+
vscode.commands.registerCommand(
|
|
372
|
+
'agentuity.sandbox.viewEnv',
|
|
373
|
+
async (item?: SandboxTreeItem) => {
|
|
374
|
+
if (!item?.sandboxData) return;
|
|
375
|
+
await viewEnv(item.sandboxData.sandboxId);
|
|
376
|
+
}
|
|
377
|
+
)
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
// Sync .env file
|
|
381
|
+
context.subscriptions.push(
|
|
382
|
+
vscode.commands.registerCommand(
|
|
383
|
+
'agentuity.sandbox.syncEnvFile',
|
|
384
|
+
async (item?: SandboxTreeItem) => {
|
|
385
|
+
if (!item?.sandboxData) return;
|
|
386
|
+
await syncEnvFile(item.sandboxData.sandboxId);
|
|
387
|
+
}
|
|
388
|
+
)
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
// Create snapshot
|
|
392
|
+
context.subscriptions.push(
|
|
393
|
+
vscode.commands.registerCommand(
|
|
394
|
+
'agentuity.sandbox.snapshot.create',
|
|
395
|
+
async (item?: SandboxTreeItem) => {
|
|
396
|
+
if (!item?.sandboxData) return;
|
|
397
|
+
await createSnapshot(item.sandboxData.sandboxId, provider);
|
|
398
|
+
}
|
|
399
|
+
)
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
// Delete snapshot
|
|
403
|
+
context.subscriptions.push(
|
|
404
|
+
vscode.commands.registerCommand(
|
|
405
|
+
'agentuity.sandbox.snapshot.delete',
|
|
406
|
+
async (item?: SandboxTreeItem) => {
|
|
407
|
+
if (!item?.snapshotData) return;
|
|
408
|
+
await deleteSnapshot(item.snapshotData.snapshotId, provider);
|
|
409
|
+
}
|
|
410
|
+
)
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
// Tag snapshot
|
|
414
|
+
context.subscriptions.push(
|
|
415
|
+
vscode.commands.registerCommand(
|
|
416
|
+
'agentuity.sandbox.snapshot.tag',
|
|
417
|
+
async (item?: SandboxTreeItem) => {
|
|
418
|
+
if (!item?.snapshotData) return;
|
|
419
|
+
await tagSnapshot(item.snapshotData.snapshotId, provider);
|
|
420
|
+
}
|
|
421
|
+
)
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
// View snapshot details (JSON)
|
|
425
|
+
context.subscriptions.push(
|
|
426
|
+
vscode.commands.registerCommand(
|
|
427
|
+
'agentuity.sandbox.snapshot.viewDetails',
|
|
428
|
+
async (item?: SandboxTreeItem) => {
|
|
429
|
+
if (!item?.snapshotData) return;
|
|
430
|
+
await viewSnapshotDetails(item.snapshotData.snapshotId);
|
|
431
|
+
}
|
|
432
|
+
)
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
// View snapshot file (readonly)
|
|
436
|
+
context.subscriptions.push(
|
|
437
|
+
vscode.commands.registerCommand(
|
|
438
|
+
'agentuity.sandbox.snapshot.viewFile',
|
|
439
|
+
async (item?: SandboxTreeItem) => {
|
|
440
|
+
if (!item?.snapshotData || !item?.filePath) return;
|
|
441
|
+
await viewSnapshotFile(item.snapshotData, item.filePath);
|
|
442
|
+
}
|
|
443
|
+
)
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
// Upload from explorer (context menu)
|
|
447
|
+
context.subscriptions.push(
|
|
448
|
+
vscode.commands.registerCommand(
|
|
449
|
+
'agentuity.sandbox.upload',
|
|
450
|
+
async (uri?: vscode.Uri) => {
|
|
451
|
+
if (!uri) return;
|
|
452
|
+
await uploadToSandbox(uri);
|
|
453
|
+
}
|
|
454
|
+
)
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ==================== Command Implementations ====================
|
|
459
|
+
|
|
460
|
+
async function createSandbox(provider: SandboxTreeDataProvider): Promise<void> {
|
|
461
|
+
const config = vscode.workspace.getConfiguration('agentuity');
|
|
462
|
+
const defaultMemory = config.get<string>('sandbox.defaultMemory', '512Mi');
|
|
463
|
+
const defaultCpu = config.get<string>('sandbox.defaultCpu', '500m');
|
|
464
|
+
const defaultNetwork = config.get<boolean>('sandbox.defaultNetwork', false);
|
|
465
|
+
|
|
466
|
+
// Quick pick for basic vs advanced
|
|
467
|
+
const mode = await vscode.window.showQuickPick(
|
|
468
|
+
[
|
|
469
|
+
{ label: 'Quick Create', description: 'Use default settings' },
|
|
470
|
+
{ label: 'Custom', description: 'Configure resources and options' },
|
|
471
|
+
],
|
|
472
|
+
{ placeHolder: 'How do you want to create the sandbox?' }
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
if (!mode) return;
|
|
476
|
+
|
|
477
|
+
let options: SandboxCreateOptions = {};
|
|
478
|
+
|
|
479
|
+
if (mode.label === 'Custom') {
|
|
480
|
+
// Memory
|
|
481
|
+
const memory = await vscode.window.showInputBox({
|
|
482
|
+
prompt: 'Memory limit',
|
|
483
|
+
value: defaultMemory,
|
|
484
|
+
placeHolder: 'e.g., 512Mi, 1Gi, 2Gi',
|
|
485
|
+
});
|
|
486
|
+
if (memory === undefined) return;
|
|
487
|
+
options.memory = memory || undefined;
|
|
488
|
+
|
|
489
|
+
// CPU
|
|
490
|
+
const cpu = await vscode.window.showInputBox({
|
|
491
|
+
prompt: 'CPU limit (millicores)',
|
|
492
|
+
value: defaultCpu,
|
|
493
|
+
placeHolder: 'e.g., 500m, 1000m',
|
|
494
|
+
});
|
|
495
|
+
if (cpu === undefined) return;
|
|
496
|
+
options.cpu = cpu || undefined;
|
|
497
|
+
|
|
498
|
+
// Network
|
|
499
|
+
const network = await vscode.window.showQuickPick(
|
|
500
|
+
[
|
|
501
|
+
{ label: 'Disabled', description: 'No outbound network access', value: false },
|
|
502
|
+
{ label: 'Enabled', description: 'Allow outbound network access', value: true },
|
|
503
|
+
],
|
|
504
|
+
{ placeHolder: 'Network access' }
|
|
505
|
+
);
|
|
506
|
+
if (!network) return;
|
|
507
|
+
options.network = network.value;
|
|
508
|
+
|
|
509
|
+
// Dependencies
|
|
510
|
+
const deps = await vscode.window.showInputBox({
|
|
511
|
+
prompt: 'APT packages to install (optional)',
|
|
512
|
+
placeHolder: 'e.g., python3 nodejs git',
|
|
513
|
+
});
|
|
514
|
+
if (deps) {
|
|
515
|
+
options.dependencies = deps.split(/\s+/).filter(Boolean);
|
|
516
|
+
}
|
|
517
|
+
} else {
|
|
518
|
+
options = {
|
|
519
|
+
memory: defaultMemory,
|
|
520
|
+
cpu: defaultCpu,
|
|
521
|
+
network: defaultNetwork,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
await vscode.window.withProgress(
|
|
526
|
+
{
|
|
527
|
+
location: vscode.ProgressLocation.Notification,
|
|
528
|
+
title: 'Creating sandbox...',
|
|
529
|
+
cancellable: false,
|
|
530
|
+
},
|
|
531
|
+
async () => {
|
|
532
|
+
const cli = getCliClient();
|
|
533
|
+
const result = await cli.sandboxCreate(options);
|
|
534
|
+
|
|
535
|
+
if (result.success && result.data) {
|
|
536
|
+
vscode.window.showInformationMessage(`Sandbox created: ${result.data.sandboxId}`);
|
|
537
|
+
await provider.forceRefresh();
|
|
538
|
+
} else {
|
|
539
|
+
vscode.window.showErrorMessage(`Failed to create sandbox: ${result.error}`);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
async function createSandboxFromSnapshot(
|
|
546
|
+
snapshotId: string,
|
|
547
|
+
provider: SandboxTreeDataProvider
|
|
548
|
+
): Promise<void> {
|
|
549
|
+
await vscode.window.withProgress(
|
|
550
|
+
{
|
|
551
|
+
location: vscode.ProgressLocation.Notification,
|
|
552
|
+
title: 'Creating sandbox from snapshot...',
|
|
553
|
+
cancellable: false,
|
|
554
|
+
},
|
|
555
|
+
async () => {
|
|
556
|
+
const cli = getCliClient();
|
|
557
|
+
const result = await cli.sandboxCreate({ snapshot: snapshotId });
|
|
558
|
+
|
|
559
|
+
if (result.success && result.data) {
|
|
560
|
+
vscode.window.showInformationMessage(`Sandbox created: ${result.data.sandboxId}`);
|
|
561
|
+
await provider.forceRefresh();
|
|
562
|
+
} else {
|
|
563
|
+
vscode.window.showErrorMessage(`Failed to create sandbox: ${result.error}`);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
async function deleteSandbox(sandboxId: string, provider: SandboxTreeDataProvider): Promise<void> {
|
|
570
|
+
const confirm = await vscode.window.showWarningMessage(
|
|
571
|
+
`Are you sure you want to delete sandbox ${sandboxId}?`,
|
|
572
|
+
{ modal: true },
|
|
573
|
+
'Delete'
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
if (confirm !== 'Delete') return;
|
|
577
|
+
|
|
578
|
+
await vscode.window.withProgress(
|
|
579
|
+
{
|
|
580
|
+
location: vscode.ProgressLocation.Notification,
|
|
581
|
+
title: 'Deleting sandbox...',
|
|
582
|
+
cancellable: false,
|
|
583
|
+
},
|
|
584
|
+
async () => {
|
|
585
|
+
const cli = getCliClient();
|
|
586
|
+
const result = await cli.sandboxDelete(sandboxId);
|
|
587
|
+
|
|
588
|
+
if (result.success) {
|
|
589
|
+
// Also unlink if linked
|
|
590
|
+
try {
|
|
591
|
+
await getSandboxManager().unlinkSandbox(sandboxId);
|
|
592
|
+
} catch {
|
|
593
|
+
// Ignore if not linked
|
|
594
|
+
}
|
|
595
|
+
vscode.window.showInformationMessage('Sandbox deleted');
|
|
596
|
+
await provider.forceRefresh();
|
|
597
|
+
} else {
|
|
598
|
+
vscode.window.showErrorMessage(`Failed to delete sandbox: ${result.error}`);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async function linkSandbox(sandboxId: string, provider: SandboxTreeDataProvider): Promise<void> {
|
|
605
|
+
const name = await vscode.window.showInputBox({
|
|
606
|
+
prompt: 'Enter a friendly name for this sandbox (optional)',
|
|
607
|
+
placeHolder: 'my-dev-sandbox',
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
const remotePath = await vscode.window.showInputBox({
|
|
611
|
+
prompt: 'Remote path for synced files',
|
|
612
|
+
value: DEFAULT_SANDBOX_PATH,
|
|
613
|
+
placeHolder: DEFAULT_SANDBOX_PATH,
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
if (remotePath === undefined) return;
|
|
617
|
+
|
|
618
|
+
try {
|
|
619
|
+
await getSandboxManager().linkSandbox(sandboxId, {
|
|
620
|
+
name: name || undefined,
|
|
621
|
+
remotePath: remotePath || DEFAULT_SANDBOX_PATH,
|
|
622
|
+
});
|
|
623
|
+
vscode.window.showInformationMessage(`Sandbox linked to workspace`);
|
|
624
|
+
provider.refresh();
|
|
625
|
+
} catch (err) {
|
|
626
|
+
vscode.window.showErrorMessage(
|
|
627
|
+
`Failed to link sandbox: ${err instanceof Error ? err.message : 'Unknown error'}`
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
async function unlinkSandbox(sandboxId: string, provider: SandboxTreeDataProvider): Promise<void> {
|
|
633
|
+
try {
|
|
634
|
+
await getSandboxManager().unlinkSandbox(sandboxId);
|
|
635
|
+
vscode.window.showInformationMessage('Sandbox unlinked from workspace');
|
|
636
|
+
provider.refresh();
|
|
637
|
+
} catch (err) {
|
|
638
|
+
vscode.window.showErrorMessage(
|
|
639
|
+
`Failed to unlink sandbox: ${err instanceof Error ? err.message : 'Unknown error'}`
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
async function syncToSandbox(sandboxId: string, provider: SandboxTreeDataProvider): Promise<void> {
|
|
645
|
+
// Show status bar sync indicator
|
|
646
|
+
showSyncProgress('Syncing to sandbox...');
|
|
647
|
+
|
|
648
|
+
await vscode.window.withProgress(
|
|
649
|
+
{
|
|
650
|
+
location: vscode.ProgressLocation.Notification,
|
|
651
|
+
title: 'Syncing files to sandbox...',
|
|
652
|
+
cancellable: false,
|
|
653
|
+
},
|
|
654
|
+
async () => {
|
|
655
|
+
try {
|
|
656
|
+
const manager = getSandboxManager();
|
|
657
|
+
const result = await manager.syncToSandbox(sandboxId);
|
|
658
|
+
|
|
659
|
+
vscode.window.showInformationMessage(
|
|
660
|
+
`Synced ${result.filesUploaded} files (${formatBytes(result.bytesTransferred)}) in ${(result.duration / 1000).toFixed(1)}s`
|
|
661
|
+
);
|
|
662
|
+
|
|
663
|
+
// Show success in status bar
|
|
664
|
+
showSyncSuccess(result.filesUploaded, result.bytesTransferred);
|
|
665
|
+
|
|
666
|
+
// Refresh files cache
|
|
667
|
+
provider.clearSandboxCache(sandboxId);
|
|
668
|
+
provider.refresh();
|
|
669
|
+
updateStatusBar();
|
|
670
|
+
} catch (err) {
|
|
671
|
+
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
|
672
|
+
vscode.window.showErrorMessage(`Failed to sync: ${errorMessage}`);
|
|
673
|
+
showSyncError(errorMessage);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
async function execInSandbox(sandboxId: string, prefilledCommand?: string): Promise<void> {
|
|
680
|
+
const command = prefilledCommand ?? await vscode.window.showInputBox({
|
|
681
|
+
prompt: 'Enter command to execute',
|
|
682
|
+
placeHolder: 'npm test',
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
if (!command) return;
|
|
686
|
+
|
|
687
|
+
executeInTerminal(sandboxId, command);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function executeInTerminal(sandboxId: string, command: string): void {
|
|
691
|
+
const cli = getCliClient();
|
|
692
|
+
const cliPath = cli.getCliPath();
|
|
693
|
+
|
|
694
|
+
// Get or create terminal
|
|
695
|
+
let terminal = sandboxTerminals.get(sandboxId);
|
|
696
|
+
if (!terminal || terminal.exitStatus !== undefined) {
|
|
697
|
+
terminal = vscode.window.createTerminal({
|
|
698
|
+
name: `Sandbox: ${sandboxId.slice(0, 8)}`,
|
|
699
|
+
iconPath: new vscode.ThemeIcon('vm'),
|
|
700
|
+
});
|
|
701
|
+
sandboxTerminals.set(sandboxId, terminal);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
terminal.show();
|
|
705
|
+
terminal.sendText(`${cliPath} cloud sandbox exec ${sandboxId} --region ${cli.getSandboxRegion()} -- ${command}`);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function disposeTerminals(): void {
|
|
709
|
+
for (const terminal of sandboxTerminals.values()) {
|
|
710
|
+
terminal.dispose();
|
|
711
|
+
}
|
|
712
|
+
sandboxTerminals.clear();
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
async function viewSandboxFile(sandboxId: string, filePath: string): Promise<void> {
|
|
716
|
+
await vscode.window.withProgress(
|
|
717
|
+
{
|
|
718
|
+
location: vscode.ProgressLocation.Notification,
|
|
719
|
+
title: 'Fetching file...',
|
|
720
|
+
cancellable: false,
|
|
721
|
+
},
|
|
722
|
+
async () => {
|
|
723
|
+
const cli = getCliClient();
|
|
724
|
+
// Use a stable temp directory for sandbox files
|
|
725
|
+
const sandboxTmpDir = path.join(os.tmpdir(), 'agentuity-sandbox', sandboxId.slice(0, 12));
|
|
726
|
+
fs.mkdirSync(sandboxTmpDir, { recursive: true });
|
|
727
|
+
|
|
728
|
+
const fileName = path.basename(filePath);
|
|
729
|
+
const localPath = path.join(sandboxTmpDir, fileName);
|
|
730
|
+
|
|
731
|
+
// Build full remote path under sandbox home
|
|
732
|
+
const fullRemotePath = filePath.startsWith('/')
|
|
733
|
+
? filePath
|
|
734
|
+
: `${CliClient.SANDBOX_HOME}/${filePath}`;
|
|
735
|
+
|
|
736
|
+
const result = await cli.sandboxCpFromSandbox(sandboxId, fullRemotePath, localPath);
|
|
737
|
+
|
|
738
|
+
if (result.success) {
|
|
739
|
+
// Track this file for save-back
|
|
740
|
+
sandboxFileMap.set(localPath, {
|
|
741
|
+
sandboxId,
|
|
742
|
+
remotePath: fullRemotePath,
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
const doc = await vscode.workspace.openTextDocument(localPath);
|
|
746
|
+
await vscode.window.showTextDocument(doc, { preview: false });
|
|
747
|
+
} else {
|
|
748
|
+
vscode.window.showErrorMessage(`Failed to fetch file: ${result.error}`);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
async function uploadSavedFile(
|
|
755
|
+
sandboxId: string,
|
|
756
|
+
localPath: string,
|
|
757
|
+
remotePath: string,
|
|
758
|
+
provider: SandboxTreeDataProvider
|
|
759
|
+
): Promise<void> {
|
|
760
|
+
const cli = getCliClient();
|
|
761
|
+
const result = await cli.sandboxCpToSandbox(sandboxId, localPath, remotePath);
|
|
762
|
+
|
|
763
|
+
if (result.success) {
|
|
764
|
+
vscode.window.showInformationMessage(`Saved to sandbox: ${path.basename(remotePath)}`);
|
|
765
|
+
provider.clearSandboxCache(sandboxId);
|
|
766
|
+
provider.refresh();
|
|
767
|
+
} else {
|
|
768
|
+
vscode.window.showErrorMessage(`Failed to save to sandbox: ${result.error}`);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
async function createSandboxFile(
|
|
773
|
+
sandboxId: string,
|
|
774
|
+
parentDir: string,
|
|
775
|
+
_provider: SandboxTreeDataProvider
|
|
776
|
+
): Promise<void> {
|
|
777
|
+
const fileName = await vscode.window.showInputBox({
|
|
778
|
+
prompt: 'Enter new file name',
|
|
779
|
+
placeHolder: 'newfile.ts',
|
|
780
|
+
validateInput: (value) => {
|
|
781
|
+
if (!value || value.trim() === '') {
|
|
782
|
+
return 'File name cannot be empty';
|
|
783
|
+
}
|
|
784
|
+
if (value.includes('/') || value.includes('\\')) {
|
|
785
|
+
return 'File name cannot contain path separators';
|
|
786
|
+
}
|
|
787
|
+
return undefined;
|
|
788
|
+
},
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
if (!fileName) return;
|
|
792
|
+
|
|
793
|
+
// Create temp file locally
|
|
794
|
+
const sandboxTmpDir = path.join(os.tmpdir(), 'agentuity-sandbox', sandboxId.slice(0, 12));
|
|
795
|
+
fs.mkdirSync(sandboxTmpDir, { recursive: true });
|
|
796
|
+
const localPath = path.join(sandboxTmpDir, fileName);
|
|
797
|
+
|
|
798
|
+
// Create empty file
|
|
799
|
+
fs.writeFileSync(localPath, '');
|
|
800
|
+
|
|
801
|
+
// Build remote path
|
|
802
|
+
const remotePath = parentDir
|
|
803
|
+
? `${CliClient.SANDBOX_HOME}/${parentDir}/${fileName}`
|
|
804
|
+
: `${CliClient.SANDBOX_HOME}/${fileName}`;
|
|
805
|
+
|
|
806
|
+
// Track this file for save-back
|
|
807
|
+
sandboxFileMap.set(localPath, {
|
|
808
|
+
sandboxId,
|
|
809
|
+
remotePath,
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
// Open in editor - file will be uploaded on first save
|
|
813
|
+
const doc = await vscode.workspace.openTextDocument(localPath);
|
|
814
|
+
await vscode.window.showTextDocument(doc, { preview: false });
|
|
815
|
+
|
|
816
|
+
vscode.window.showInformationMessage(
|
|
817
|
+
`New file will be created at ${remotePath} when you save`
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
async function createSandboxFolder(
|
|
822
|
+
sandboxId: string,
|
|
823
|
+
parentDir: string,
|
|
824
|
+
provider: SandboxTreeDataProvider
|
|
825
|
+
): Promise<void> {
|
|
826
|
+
const folderName = await vscode.window.showInputBox({
|
|
827
|
+
prompt: 'Enter new folder name',
|
|
828
|
+
placeHolder: 'newfolder',
|
|
829
|
+
validateInput: (value) => {
|
|
830
|
+
if (!value || value.trim() === '') {
|
|
831
|
+
return 'Folder name cannot be empty';
|
|
832
|
+
}
|
|
833
|
+
if (value.includes('/') || value.includes('\\')) {
|
|
834
|
+
return 'Folder name cannot contain path separators';
|
|
835
|
+
}
|
|
836
|
+
return undefined;
|
|
837
|
+
},
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
if (!folderName) return;
|
|
841
|
+
|
|
842
|
+
const remotePath = parentDir
|
|
843
|
+
? `${CliClient.SANDBOX_HOME}/${parentDir}/${folderName}`
|
|
844
|
+
: `${CliClient.SANDBOX_HOME}/${folderName}`;
|
|
845
|
+
|
|
846
|
+
const cli = getCliClient();
|
|
847
|
+
const result = await cli.sandboxMkdir(sandboxId, remotePath, true);
|
|
848
|
+
|
|
849
|
+
if (result.success) {
|
|
850
|
+
vscode.window.showInformationMessage(`Created folder: ${folderName}`);
|
|
851
|
+
provider.clearSandboxCache(sandboxId);
|
|
852
|
+
provider.refresh();
|
|
853
|
+
} else {
|
|
854
|
+
vscode.window.showErrorMessage(`Failed to create folder: ${result.error}`);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
async function downloadFromSandbox(
|
|
859
|
+
sandboxId: string,
|
|
860
|
+
remotePath: string,
|
|
861
|
+
isDirectory: boolean
|
|
862
|
+
): Promise<void> {
|
|
863
|
+
const defaultUri = vscode.workspace.workspaceFolders?.[0]?.uri;
|
|
864
|
+
|
|
865
|
+
const saveUri = await vscode.window.showSaveDialog({
|
|
866
|
+
defaultUri: defaultUri
|
|
867
|
+
? vscode.Uri.joinPath(defaultUri, path.basename(remotePath))
|
|
868
|
+
: undefined,
|
|
869
|
+
saveLabel: 'Download',
|
|
870
|
+
filters: isDirectory ? { Archives: ['tar.gz', 'zip'] } : {},
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
if (!saveUri) return;
|
|
874
|
+
|
|
875
|
+
await vscode.window.withProgress(
|
|
876
|
+
{
|
|
877
|
+
location: vscode.ProgressLocation.Notification,
|
|
878
|
+
title: 'Downloading...',
|
|
879
|
+
cancellable: false,
|
|
880
|
+
},
|
|
881
|
+
async () => {
|
|
882
|
+
const cli = getCliClient();
|
|
883
|
+
const result = await cli.sandboxCpFromSandbox(
|
|
884
|
+
sandboxId,
|
|
885
|
+
remotePath,
|
|
886
|
+
saveUri.fsPath,
|
|
887
|
+
isDirectory
|
|
888
|
+
);
|
|
889
|
+
|
|
890
|
+
if (result.success) {
|
|
891
|
+
vscode.window.showInformationMessage(`Downloaded to ${saveUri.fsPath}`);
|
|
892
|
+
} else {
|
|
893
|
+
vscode.window.showErrorMessage(`Failed to download: ${result.error}`);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
async function deleteFile(
|
|
900
|
+
sandboxId: string,
|
|
901
|
+
filePath: string,
|
|
902
|
+
isDirectory: boolean,
|
|
903
|
+
provider: SandboxTreeDataProvider
|
|
904
|
+
): Promise<void> {
|
|
905
|
+
const confirm = await vscode.window.showWarningMessage(
|
|
906
|
+
`Delete ${isDirectory ? 'directory' : 'file'} ${filePath}?`,
|
|
907
|
+
{ modal: true },
|
|
908
|
+
'Delete'
|
|
909
|
+
);
|
|
910
|
+
|
|
911
|
+
if (confirm !== 'Delete') return;
|
|
912
|
+
|
|
913
|
+
const cli = getCliClient();
|
|
914
|
+
const result = isDirectory
|
|
915
|
+
? await cli.sandboxRmdir(sandboxId, filePath, true)
|
|
916
|
+
: await cli.sandboxRm(sandboxId, filePath);
|
|
917
|
+
|
|
918
|
+
if (result.success) {
|
|
919
|
+
vscode.window.showInformationMessage(`Deleted ${filePath}`);
|
|
920
|
+
provider.clearSandboxCache(sandboxId);
|
|
921
|
+
provider.refresh();
|
|
922
|
+
} else {
|
|
923
|
+
vscode.window.showErrorMessage(`Failed to delete: ${result.error}`);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
async function viewExecution(executionId: string): Promise<void> {
|
|
928
|
+
await vscode.window.withProgress(
|
|
929
|
+
{
|
|
930
|
+
location: vscode.ProgressLocation.Notification,
|
|
931
|
+
title: 'Fetching execution details...',
|
|
932
|
+
cancellable: false,
|
|
933
|
+
},
|
|
934
|
+
async () => {
|
|
935
|
+
const cli = getCliClient();
|
|
936
|
+
const result = await cli.executionGet(executionId);
|
|
937
|
+
|
|
938
|
+
if (result.success && result.data) {
|
|
939
|
+
const exec = result.data;
|
|
940
|
+
|
|
941
|
+
// Build execution details
|
|
942
|
+
const lines: string[] = [
|
|
943
|
+
'='.repeat(60),
|
|
944
|
+
'EXECUTION DETAILS',
|
|
945
|
+
'='.repeat(60),
|
|
946
|
+
`Execution: ${exec.executionId}`,
|
|
947
|
+
`Sandbox: ${exec.sandboxId || 'N/A'}`,
|
|
948
|
+
`Status: ${exec.status}`,
|
|
949
|
+
`Exit Code: ${exec.exitCode ?? 'N/A'}`,
|
|
950
|
+
`Duration: ${exec.durationMs ? `${exec.durationMs}ms` : 'N/A'}`,
|
|
951
|
+
`Started: ${exec.startedAt || 'N/A'}`,
|
|
952
|
+
`Completed: ${exec.completedAt || 'N/A'}`,
|
|
953
|
+
`Command: ${exec.command || 'N/A'}`,
|
|
954
|
+
];
|
|
955
|
+
|
|
956
|
+
// Fetch stdout stream if available
|
|
957
|
+
if (exec.stdoutStreamUrl) {
|
|
958
|
+
lines.push('', '='.repeat(60), 'STDOUT', '='.repeat(60));
|
|
959
|
+
try {
|
|
960
|
+
const stdoutContent = await fetchStreamContent(exec.stdoutStreamUrl);
|
|
961
|
+
lines.push(stdoutContent || '(empty)');
|
|
962
|
+
} catch (err) {
|
|
963
|
+
lines.push(`(failed to fetch: ${err instanceof Error ? err.message : 'unknown error'})`);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Fetch stderr stream if available
|
|
968
|
+
if (exec.stderrStreamUrl) {
|
|
969
|
+
lines.push('', '='.repeat(60), 'STDERR', '='.repeat(60));
|
|
970
|
+
try {
|
|
971
|
+
const stderrContent = await fetchStreamContent(exec.stderrStreamUrl);
|
|
972
|
+
lines.push(stderrContent || '(empty)');
|
|
973
|
+
} catch (err) {
|
|
974
|
+
lines.push(`(failed to fetch: ${err instanceof Error ? err.message : 'unknown error'})`);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
await openReadonlyDocument(lines.join('\n'), 'log', `execution-${executionId.slice(0, 8)}`);
|
|
979
|
+
} else {
|
|
980
|
+
vscode.window.showErrorMessage(`Failed to get execution: ${result.error}`);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
async function fetchStreamContent(url: string): Promise<string> {
|
|
987
|
+
// Use https module to fetch stream content
|
|
988
|
+
const https = await import('https');
|
|
989
|
+
const http = await import('http');
|
|
990
|
+
|
|
991
|
+
return new Promise((resolve, reject) => {
|
|
992
|
+
const protocol = url.startsWith('https') ? https : http;
|
|
993
|
+
const request = protocol.get(url, (response) => {
|
|
994
|
+
if (response.statusCode !== 200) {
|
|
995
|
+
reject(new Error(`HTTP ${response.statusCode}`));
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
let data = '';
|
|
1000
|
+
response.on('data', (chunk: Buffer) => {
|
|
1001
|
+
data += chunk.toString();
|
|
1002
|
+
});
|
|
1003
|
+
response.on('end', () => {
|
|
1004
|
+
resolve(data);
|
|
1005
|
+
});
|
|
1006
|
+
response.on('error', reject);
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
request.on('error', reject);
|
|
1010
|
+
request.setTimeout(10000, () => {
|
|
1011
|
+
request.destroy();
|
|
1012
|
+
reject(new Error('Request timeout'));
|
|
1013
|
+
});
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
async function setEnvVar(sandboxId: string): Promise<void> {
|
|
1018
|
+
const input = await vscode.window.showInputBox({
|
|
1019
|
+
prompt: 'Enter environment variable (KEY=value)',
|
|
1020
|
+
placeHolder: 'MY_VAR=my_value',
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
if (!input) return;
|
|
1024
|
+
|
|
1025
|
+
const [key, ...valueParts] = input.split('=');
|
|
1026
|
+
const value = valueParts.join('=');
|
|
1027
|
+
|
|
1028
|
+
if (!key || value === undefined) {
|
|
1029
|
+
vscode.window.showErrorMessage('Invalid format. Use KEY=value');
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
const cli = getCliClient();
|
|
1034
|
+
const result = await cli.sandboxEnvSet(sandboxId, { [key]: value });
|
|
1035
|
+
|
|
1036
|
+
if (result.success) {
|
|
1037
|
+
vscode.window.showInformationMessage(`Set ${key}=${value}`);
|
|
1038
|
+
} else {
|
|
1039
|
+
vscode.window.showErrorMessage(`Failed to set env var: ${result.error}`);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
async function viewEnv(sandboxId: string): Promise<void> {
|
|
1044
|
+
await vscode.window.withProgress(
|
|
1045
|
+
{
|
|
1046
|
+
location: vscode.ProgressLocation.Notification,
|
|
1047
|
+
title: 'Fetching environment variables...',
|
|
1048
|
+
cancellable: false,
|
|
1049
|
+
},
|
|
1050
|
+
async () => {
|
|
1051
|
+
const cli = getCliClient();
|
|
1052
|
+
// Use exec to run 'env' command to get actual runtime environment
|
|
1053
|
+
const result = await cli.sandboxExec(sandboxId, ['env']);
|
|
1054
|
+
|
|
1055
|
+
if (result.success && result.data) {
|
|
1056
|
+
const content = result.data.output || '(no environment variables)';
|
|
1057
|
+
await openReadonlyDocument(
|
|
1058
|
+
content,
|
|
1059
|
+
'properties',
|
|
1060
|
+
`sandbox-env-${sandboxId.slice(0, 8)}`
|
|
1061
|
+
);
|
|
1062
|
+
} else {
|
|
1063
|
+
vscode.window.showErrorMessage(`Failed to get env: ${result.error}`);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
async function syncEnvFile(sandboxId: string): Promise<void> {
|
|
1070
|
+
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
|
1071
|
+
if (!workspaceFolder) {
|
|
1072
|
+
vscode.window.showWarningMessage('No workspace folder open');
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
const envPath = path.join(workspaceFolder.uri.fsPath, '.env');
|
|
1077
|
+
try {
|
|
1078
|
+
const content = await vscode.workspace.fs.readFile(vscode.Uri.file(envPath));
|
|
1079
|
+
const text = new TextDecoder().decode(content);
|
|
1080
|
+
|
|
1081
|
+
const vars: Record<string, string> = {};
|
|
1082
|
+
for (const line of text.split('\n')) {
|
|
1083
|
+
const trimmed = line.trim();
|
|
1084
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
1085
|
+
const [key, ...valueParts] = trimmed.split('=');
|
|
1086
|
+
if (key && valueParts.length > 0) {
|
|
1087
|
+
vars[key] = valueParts.join('=');
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
if (Object.keys(vars).length === 0) {
|
|
1092
|
+
vscode.window.showWarningMessage('No variables found in .env file');
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
const cli = getCliClient();
|
|
1097
|
+
const result = await cli.sandboxEnvSet(sandboxId, vars);
|
|
1098
|
+
|
|
1099
|
+
if (result.success) {
|
|
1100
|
+
vscode.window.showInformationMessage(
|
|
1101
|
+
`Synced ${Object.keys(vars).length} environment variables`
|
|
1102
|
+
);
|
|
1103
|
+
} else {
|
|
1104
|
+
vscode.window.showErrorMessage(`Failed to sync env: ${result.error}`);
|
|
1105
|
+
}
|
|
1106
|
+
} catch {
|
|
1107
|
+
vscode.window.showWarningMessage('No .env file found in workspace root');
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
async function createSnapshot(
|
|
1112
|
+
sandboxId: string,
|
|
1113
|
+
provider: SandboxTreeDataProvider
|
|
1114
|
+
): Promise<void> {
|
|
1115
|
+
const tag = await vscode.window.showInputBox({
|
|
1116
|
+
prompt: 'Enter a tag for this snapshot (optional)',
|
|
1117
|
+
placeHolder: 'v1.0 or latest',
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
await vscode.window.withProgress(
|
|
1121
|
+
{
|
|
1122
|
+
location: vscode.ProgressLocation.Notification,
|
|
1123
|
+
title: 'Creating snapshot...',
|
|
1124
|
+
cancellable: false,
|
|
1125
|
+
},
|
|
1126
|
+
async () => {
|
|
1127
|
+
const cli = getCliClient();
|
|
1128
|
+
const result = await cli.snapshotCreate(sandboxId, tag || undefined);
|
|
1129
|
+
|
|
1130
|
+
if (result.success && result.data) {
|
|
1131
|
+
vscode.window.showInformationMessage(
|
|
1132
|
+
`Snapshot created: ${result.data.snapshotId}${tag ? ` [${tag}]` : ''}`
|
|
1133
|
+
);
|
|
1134
|
+
provider.clearSandboxCache(sandboxId);
|
|
1135
|
+
provider.refresh();
|
|
1136
|
+
} else {
|
|
1137
|
+
vscode.window.showErrorMessage(`Failed to create snapshot: ${result.error}`);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
async function deleteSnapshot(
|
|
1144
|
+
snapshotId: string,
|
|
1145
|
+
provider: SandboxTreeDataProvider
|
|
1146
|
+
): Promise<void> {
|
|
1147
|
+
const confirm = await vscode.window.showWarningMessage(
|
|
1148
|
+
`Are you sure you want to delete snapshot ${snapshotId}?`,
|
|
1149
|
+
{ modal: true },
|
|
1150
|
+
'Delete'
|
|
1151
|
+
);
|
|
1152
|
+
|
|
1153
|
+
if (confirm !== 'Delete') return;
|
|
1154
|
+
|
|
1155
|
+
const cli = getCliClient();
|
|
1156
|
+
const result = await cli.snapshotDelete(snapshotId);
|
|
1157
|
+
|
|
1158
|
+
if (result.success) {
|
|
1159
|
+
vscode.window.showInformationMessage('Snapshot deleted');
|
|
1160
|
+
await provider.forceRefresh();
|
|
1161
|
+
} else {
|
|
1162
|
+
vscode.window.showErrorMessage(`Failed to delete snapshot: ${result.error}`);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
async function tagSnapshot(snapshotId: string, provider: SandboxTreeDataProvider): Promise<void> {
|
|
1167
|
+
const tag = await vscode.window.showInputBox({
|
|
1168
|
+
prompt: 'Enter new tag (leave empty to remove tag)',
|
|
1169
|
+
placeHolder: 'v1.0 or latest',
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
if (tag === undefined) return;
|
|
1173
|
+
|
|
1174
|
+
const cli = getCliClient();
|
|
1175
|
+
const result = await cli.snapshotTag(snapshotId, tag || null);
|
|
1176
|
+
|
|
1177
|
+
if (result.success) {
|
|
1178
|
+
vscode.window.showInformationMessage(tag ? `Tagged as: ${tag}` : 'Tag removed');
|
|
1179
|
+
await provider.forceRefresh();
|
|
1180
|
+
} else {
|
|
1181
|
+
vscode.window.showErrorMessage(`Failed to update tag: ${result.error}`);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
async function viewSnapshotDetails(snapshotId: string): Promise<void> {
|
|
1186
|
+
await vscode.window.withProgress(
|
|
1187
|
+
{
|
|
1188
|
+
location: vscode.ProgressLocation.Notification,
|
|
1189
|
+
title: 'Fetching snapshot details...',
|
|
1190
|
+
cancellable: false,
|
|
1191
|
+
},
|
|
1192
|
+
async () => {
|
|
1193
|
+
const cli = getCliClient();
|
|
1194
|
+
const result = await cli.snapshotGet(snapshotId);
|
|
1195
|
+
|
|
1196
|
+
if (result.success && result.data) {
|
|
1197
|
+
const content = JSON.stringify(result.data, null, 2);
|
|
1198
|
+
await openReadonlyDocument(content, 'json', `snapshot-${snapshotId.slice(0, 8)}`);
|
|
1199
|
+
} else {
|
|
1200
|
+
vscode.window.showErrorMessage(`Failed to get snapshot: ${result.error}`);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
async function viewSnapshotFile(snapshot: SnapshotInfo, filePath: string): Promise<void> {
|
|
1207
|
+
if (!snapshot.downloadUrl) {
|
|
1208
|
+
vscode.window.showErrorMessage('Snapshot does not have a download URL');
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
await vscode.window.withProgress(
|
|
1213
|
+
{
|
|
1214
|
+
location: vscode.ProgressLocation.Notification,
|
|
1215
|
+
title: `Fetching ${path.basename(filePath)}...`,
|
|
1216
|
+
cancellable: false,
|
|
1217
|
+
},
|
|
1218
|
+
async () => {
|
|
1219
|
+
try {
|
|
1220
|
+
// Create temp directory for snapshot files
|
|
1221
|
+
const snapshotTmpDir = path.join(os.tmpdir(), 'agentuity-snapshots', snapshot.snapshotId.slice(0, 12));
|
|
1222
|
+
fs.mkdirSync(snapshotTmpDir, { recursive: true });
|
|
1223
|
+
|
|
1224
|
+
const archivePath = path.join(snapshotTmpDir, 'snapshot.tar.gz');
|
|
1225
|
+
const extractDir = path.join(snapshotTmpDir, 'files');
|
|
1226
|
+
|
|
1227
|
+
// Download and extract the archive if not already cached
|
|
1228
|
+
if (!fs.existsSync(extractDir)) {
|
|
1229
|
+
// Download tar.gz
|
|
1230
|
+
await downloadFile(snapshot.downloadUrl!, archivePath);
|
|
1231
|
+
|
|
1232
|
+
// Extract using tar module
|
|
1233
|
+
fs.mkdirSync(extractDir, { recursive: true });
|
|
1234
|
+
const { extract } = await import('tar');
|
|
1235
|
+
await extract({
|
|
1236
|
+
file: archivePath,
|
|
1237
|
+
cwd: extractDir,
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// Read the specific file
|
|
1242
|
+
const targetFile = path.join(extractDir, filePath);
|
|
1243
|
+
if (!fs.existsSync(targetFile)) {
|
|
1244
|
+
vscode.window.showErrorMessage(`File not found in snapshot: ${filePath}`);
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
const content = fs.readFileSync(targetFile, 'utf-8');
|
|
1249
|
+
const ext = path.extname(filePath).slice(1) || 'txt';
|
|
1250
|
+
await openReadonlyDocument(
|
|
1251
|
+
content,
|
|
1252
|
+
ext,
|
|
1253
|
+
`snapshot-${snapshot.snapshotId.slice(0, 8)}-${path.basename(filePath)}`
|
|
1254
|
+
);
|
|
1255
|
+
} catch (err) {
|
|
1256
|
+
vscode.window.showErrorMessage(
|
|
1257
|
+
`Failed to view snapshot file: ${err instanceof Error ? err.message : 'unknown error'}`
|
|
1258
|
+
);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
async function downloadFile(url: string, destPath: string): Promise<void> {
|
|
1265
|
+
const https = await import('https');
|
|
1266
|
+
const http = await import('http');
|
|
1267
|
+
|
|
1268
|
+
return new Promise((resolve, reject) => {
|
|
1269
|
+
const protocol = url.startsWith('https') ? https : http;
|
|
1270
|
+
const file = fs.createWriteStream(destPath);
|
|
1271
|
+
|
|
1272
|
+
const request = protocol.get(url, (response) => {
|
|
1273
|
+
// Handle redirects
|
|
1274
|
+
if (response.statusCode === 301 || response.statusCode === 302) {
|
|
1275
|
+
const redirectUrl = response.headers.location;
|
|
1276
|
+
if (redirectUrl) {
|
|
1277
|
+
file.close();
|
|
1278
|
+
fs.unlinkSync(destPath);
|
|
1279
|
+
downloadFile(redirectUrl, destPath).then(resolve).catch(reject);
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
if (response.statusCode !== 200) {
|
|
1285
|
+
file.close();
|
|
1286
|
+
fs.unlinkSync(destPath);
|
|
1287
|
+
reject(new Error(`HTTP ${response.statusCode}`));
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
response.pipe(file);
|
|
1292
|
+
file.on('finish', () => {
|
|
1293
|
+
file.close();
|
|
1294
|
+
resolve();
|
|
1295
|
+
});
|
|
1296
|
+
file.on('error', (err) => {
|
|
1297
|
+
fs.unlinkSync(destPath);
|
|
1298
|
+
reject(err);
|
|
1299
|
+
});
|
|
1300
|
+
});
|
|
1301
|
+
|
|
1302
|
+
request.on('error', (err) => {
|
|
1303
|
+
file.close();
|
|
1304
|
+
try { fs.unlinkSync(destPath); } catch { /* ignore cleanup errors */ }
|
|
1305
|
+
reject(err);
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1308
|
+
request.setTimeout(60000, () => {
|
|
1309
|
+
request.destroy();
|
|
1310
|
+
file.close();
|
|
1311
|
+
try { fs.unlinkSync(destPath); } catch { /* ignore cleanup errors */ }
|
|
1312
|
+
reject(new Error('Download timeout'));
|
|
1313
|
+
});
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
async function uploadToSandbox(uri: vscode.Uri): Promise<void> {
|
|
1318
|
+
const linked = getSandboxManager().getLinkedSandboxes();
|
|
1319
|
+
|
|
1320
|
+
if (linked.length === 0) {
|
|
1321
|
+
vscode.window.showWarningMessage('No sandbox linked. Link a sandbox first.');
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
let sandboxId: string;
|
|
1326
|
+
if (linked.length === 1) {
|
|
1327
|
+
sandboxId = linked[0].sandboxId;
|
|
1328
|
+
} else {
|
|
1329
|
+
const picked = await vscode.window.showQuickPick(
|
|
1330
|
+
linked.map((l) => ({
|
|
1331
|
+
label: l.name || l.sandboxId,
|
|
1332
|
+
description: l.sandboxId,
|
|
1333
|
+
sandboxId: l.sandboxId,
|
|
1334
|
+
})),
|
|
1335
|
+
{ placeHolder: 'Select sandbox to upload to' }
|
|
1336
|
+
);
|
|
1337
|
+
if (!picked) return;
|
|
1338
|
+
sandboxId = picked.sandboxId;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
const remotePath = await vscode.window.showInputBox({
|
|
1342
|
+
prompt: 'Remote path',
|
|
1343
|
+
value: linked.find((l) => l.sandboxId === sandboxId)?.remotePath || DEFAULT_SANDBOX_PATH,
|
|
1344
|
+
});
|
|
1345
|
+
|
|
1346
|
+
if (!remotePath) return;
|
|
1347
|
+
|
|
1348
|
+
await vscode.window.withProgress(
|
|
1349
|
+
{
|
|
1350
|
+
location: vscode.ProgressLocation.Notification,
|
|
1351
|
+
title: 'Uploading...',
|
|
1352
|
+
cancellable: false,
|
|
1353
|
+
},
|
|
1354
|
+
async () => {
|
|
1355
|
+
const cli = getCliClient();
|
|
1356
|
+
const stats = await vscode.workspace.fs.stat(uri);
|
|
1357
|
+
const isDir = stats.type === vscode.FileType.Directory;
|
|
1358
|
+
|
|
1359
|
+
const result = await cli.sandboxCpToSandbox(
|
|
1360
|
+
sandboxId,
|
|
1361
|
+
uri.fsPath,
|
|
1362
|
+
remotePath,
|
|
1363
|
+
isDir
|
|
1364
|
+
);
|
|
1365
|
+
|
|
1366
|
+
if (result.success) {
|
|
1367
|
+
vscode.window.showInformationMessage(`Uploaded to ${remotePath}`);
|
|
1368
|
+
} else {
|
|
1369
|
+
vscode.window.showErrorMessage(`Failed to upload: ${result.error}`);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
);
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
export { SandboxTreeDataProvider };
|