teleportation-cli 1.1.5 → 1.2.1
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/.claude/hooks/permission_request.mjs +326 -59
- package/.claude/hooks/post_tool_use.mjs +90 -0
- package/.claude/hooks/pre_tool_use.mjs +212 -293
- package/.claude/hooks/session-register.mjs +89 -104
- package/.claude/hooks/session_end.mjs +41 -42
- package/.claude/hooks/session_start.mjs +45 -60
- package/.claude/hooks/stop.mjs +752 -99
- package/.claude/hooks/user_prompt_submit.mjs +26 -3
- package/lib/cli/daemon-commands.js +1 -1
- package/lib/cli/teleport-commands.js +469 -0
- package/lib/daemon/daemon-v2.js +104 -0
- package/lib/daemon/lifecycle.js +56 -171
- package/lib/daemon/services/index.js +3 -0
- package/lib/daemon/services/polling-service.js +173 -0
- package/lib/daemon/services/queue-service.js +318 -0
- package/lib/daemon/services/session-service.js +115 -0
- package/lib/daemon/state.js +35 -0
- package/lib/daemon/task-executor-v2.js +413 -0
- package/lib/daemon/task-executor.js +270 -96
- package/lib/daemon/teleportation-daemon.js +709 -126
- package/lib/daemon/timeline-analyzer.js +215 -0
- package/lib/daemon/transcript-ingestion.js +696 -0
- package/lib/daemon/utils.js +91 -0
- package/lib/install/installer.js +184 -20
- package/lib/install/uhr-installer.js +136 -0
- package/lib/remote/providers/base-provider.js +46 -0
- package/lib/remote/providers/daytona-provider.js +58 -0
- package/lib/remote/providers/provider-factory.js +90 -19
- package/lib/remote/providers/sprites-provider.js +711 -0
- package/lib/teleport/exporters/claude-exporter.js +302 -0
- package/lib/teleport/exporters/gemini-exporter.js +307 -0
- package/lib/teleport/exporters/index.js +93 -0
- package/lib/teleport/exporters/interface.js +153 -0
- package/lib/teleport/fork-tracker.js +415 -0
- package/lib/teleport/git-committer.js +337 -0
- package/lib/teleport/index.js +48 -0
- package/lib/teleport/manager.js +620 -0
- package/lib/teleport/session-capture.js +282 -0
- package/package.json +6 -2
- package/teleportation-cli.cjs +488 -453
- package/.claude/hooks/heartbeat.mjs +0 -396
- package/lib/daemon/pid-manager.js +0 -183
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Teleport Manager
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates the full teleportation flow:
|
|
5
|
+
* 1. Capture local session state
|
|
6
|
+
* 2. Upload state to relay API
|
|
7
|
+
* 3. Provision cloud environment
|
|
8
|
+
* 4. Export and upload transcript
|
|
9
|
+
* 5. Resume session on cloud
|
|
10
|
+
*
|
|
11
|
+
* @module lib/teleport/manager
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { nanoid } from 'nanoid';
|
|
15
|
+
import { captureSessionState } from './session-capture.js';
|
|
16
|
+
import { getExporterForCoder } from './exporters/index.js';
|
|
17
|
+
import { GitCommitter } from './git-committer.js';
|
|
18
|
+
import { ResultsDelivery } from '../remote/results-delivery.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Teleport modes
|
|
22
|
+
* @enum {string}
|
|
23
|
+
*/
|
|
24
|
+
export const TeleportMode = {
|
|
25
|
+
/** Pause local session, continue on cloud */
|
|
26
|
+
PAUSE: 'pause',
|
|
27
|
+
/** Fork: local continues, cloud runs in parallel */
|
|
28
|
+
FORK: 'fork',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Teleport status
|
|
33
|
+
* @enum {string}
|
|
34
|
+
*/
|
|
35
|
+
export const TeleportStatus = {
|
|
36
|
+
PENDING: 'pending',
|
|
37
|
+
CAPTURING: 'capturing',
|
|
38
|
+
UPLOADING: 'uploading',
|
|
39
|
+
PROVISIONING: 'provisioning',
|
|
40
|
+
EXPORTING: 'exporting',
|
|
41
|
+
RESUMING: 'resuming',
|
|
42
|
+
RUNNING: 'running',
|
|
43
|
+
COMPLETED: 'completed',
|
|
44
|
+
FAILED: 'failed',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* TeleportManager orchestrates session teleportation to cloud environments.
|
|
49
|
+
*/
|
|
50
|
+
export class TeleportManager {
|
|
51
|
+
/**
|
|
52
|
+
* Initialize TeleportManager
|
|
53
|
+
*
|
|
54
|
+
* @param {Object} config - Manager configuration
|
|
55
|
+
* @param {Object} config.providerFactory - Provider factory instance
|
|
56
|
+
* @param {string} config.relayApiUrl - Relay API URL
|
|
57
|
+
* @param {string} config.relayApiKey - Relay API key
|
|
58
|
+
* @param {Object} [config.vaultClient] - Vault client for secrets
|
|
59
|
+
* @param {Object} [config.livePortClient] - LivePort client for tunnels
|
|
60
|
+
*/
|
|
61
|
+
constructor(config) {
|
|
62
|
+
if (!config) {
|
|
63
|
+
throw new Error('TeleportManager config is required');
|
|
64
|
+
}
|
|
65
|
+
if (!config.providerFactory) {
|
|
66
|
+
throw new Error('providerFactory is required');
|
|
67
|
+
}
|
|
68
|
+
if (!config.relayApiUrl) {
|
|
69
|
+
throw new Error('relayApiUrl is required');
|
|
70
|
+
}
|
|
71
|
+
if (!config.relayApiKey) {
|
|
72
|
+
throw new Error('relayApiKey is required');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this.providerFactory = config.providerFactory;
|
|
76
|
+
this.relayApiUrl = config.relayApiUrl;
|
|
77
|
+
this.relayApiKey = config.relayApiKey;
|
|
78
|
+
this.vaultClient = config.vaultClient;
|
|
79
|
+
this.livePortClient = config.livePortClient;
|
|
80
|
+
|
|
81
|
+
// Track active teleports
|
|
82
|
+
this.activeTeleports = new Map();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Initiate a teleportation session.
|
|
87
|
+
*
|
|
88
|
+
* @param {string} sessionId - Local session ID
|
|
89
|
+
* @param {Object} options - Teleport options
|
|
90
|
+
* @param {string} [options.provider] - Force specific provider
|
|
91
|
+
* @param {string} [options.targetCoder='claude-code'] - Target CLI (claude-code, gemini-cli)
|
|
92
|
+
* @param {TeleportMode} [options.mode='pause'] - Teleport mode
|
|
93
|
+
* @param {string} [options.task] - Task description for the cloud session
|
|
94
|
+
* @param {string} [options.cwd] - Working directory
|
|
95
|
+
* @param {string} [options.claudeSessionId] - Claude session ID for transcript
|
|
96
|
+
* @param {string} [options.githubToken] - GitHub token for repo access
|
|
97
|
+
* @param {Function} [options.onProgress] - Progress callback
|
|
98
|
+
* @returns {Promise<Object>} Teleport result
|
|
99
|
+
*/
|
|
100
|
+
async initiateTeleport(sessionId, options = {}) {
|
|
101
|
+
const {
|
|
102
|
+
provider,
|
|
103
|
+
targetCoder = 'claude-code',
|
|
104
|
+
mode = TeleportMode.PAUSE,
|
|
105
|
+
task,
|
|
106
|
+
cwd,
|
|
107
|
+
claudeSessionId,
|
|
108
|
+
githubToken,
|
|
109
|
+
onProgress,
|
|
110
|
+
commitInterval,
|
|
111
|
+
} = options;
|
|
112
|
+
|
|
113
|
+
// Generate teleport ID
|
|
114
|
+
const teleportId = `tel_${nanoid(12)}`;
|
|
115
|
+
|
|
116
|
+
// Initialize teleport state
|
|
117
|
+
const teleportState = {
|
|
118
|
+
teleportId,
|
|
119
|
+
sessionId,
|
|
120
|
+
status: TeleportStatus.PENDING,
|
|
121
|
+
targetCoder,
|
|
122
|
+
mode,
|
|
123
|
+
task,
|
|
124
|
+
createdAt: new Date().toISOString(),
|
|
125
|
+
provider: null,
|
|
126
|
+
machineId: null,
|
|
127
|
+
error: null,
|
|
128
|
+
gitCommitter: null,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
this.activeTeleports.set(teleportId, teleportState);
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
// Step 1: Capture session state
|
|
135
|
+
this._updateStatus(teleportId, TeleportStatus.CAPTURING, onProgress);
|
|
136
|
+
const sessionState = await this._captureSession(sessionId, {
|
|
137
|
+
cwd,
|
|
138
|
+
claudeSessionId,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Store session state for later use (e.g., deliverResults)
|
|
142
|
+
teleportState.sessionState = sessionState;
|
|
143
|
+
|
|
144
|
+
// Step 2: Upload session state to relay
|
|
145
|
+
this._updateStatus(teleportId, TeleportStatus.UPLOADING, onProgress);
|
|
146
|
+
const teleportRecord = await this._uploadSessionState(teleportId, sessionId, sessionState, {
|
|
147
|
+
targetCoder,
|
|
148
|
+
mode,
|
|
149
|
+
task,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Step 3: Provision cloud environment
|
|
153
|
+
this._updateStatus(teleportId, TeleportStatus.PROVISIONING, onProgress);
|
|
154
|
+
const cloudMachine = await this._provisionCloud(teleportId, sessionState, {
|
|
155
|
+
provider,
|
|
156
|
+
targetCoder,
|
|
157
|
+
task,
|
|
158
|
+
githubToken,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
teleportState.provider = cloudMachine.provider;
|
|
162
|
+
teleportState.machineId = cloudMachine.machineId;
|
|
163
|
+
|
|
164
|
+
// Step 4: Export and upload transcript
|
|
165
|
+
this._updateStatus(teleportId, TeleportStatus.EXPORTING, onProgress);
|
|
166
|
+
const transcriptPath = await this._prepareTranscript(sessionId, targetCoder, cloudMachine);
|
|
167
|
+
|
|
168
|
+
// Step 5: Resume session on cloud
|
|
169
|
+
this._updateStatus(teleportId, TeleportStatus.RESUMING, onProgress);
|
|
170
|
+
await this._resumeSession(teleportId, cloudMachine, transcriptPath, {
|
|
171
|
+
targetCoder,
|
|
172
|
+
task,
|
|
173
|
+
claudeSessionId,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Success!
|
|
177
|
+
this._updateStatus(teleportId, TeleportStatus.RUNNING, onProgress);
|
|
178
|
+
|
|
179
|
+
// Start auto-commit if we have repo info
|
|
180
|
+
if (sessionState.git && sessionState.cwd) {
|
|
181
|
+
const gitCommitter = new GitCommitter({
|
|
182
|
+
repoPath: sessionState.cwd,
|
|
183
|
+
branch: `teleport/${teleportId}`,
|
|
184
|
+
teleportId,
|
|
185
|
+
interval: options.commitInterval,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
teleportState.gitCommitter = gitCommitter;
|
|
189
|
+
gitCommitter.startAutoCommit();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
teleportId,
|
|
194
|
+
sessionId,
|
|
195
|
+
provider: cloudMachine.provider,
|
|
196
|
+
machineId: cloudMachine.machineId,
|
|
197
|
+
spriteUrl: cloudMachine.spriteUrl,
|
|
198
|
+
tunnelUrl: cloudMachine.tunnelUrl,
|
|
199
|
+
targetCoder,
|
|
200
|
+
mode,
|
|
201
|
+
status: TeleportStatus.RUNNING,
|
|
202
|
+
timelineUrl: `${this.relayApiUrl}/sessions/${sessionId}/timeline`,
|
|
203
|
+
};
|
|
204
|
+
} catch (error) {
|
|
205
|
+
teleportState.status = TeleportStatus.FAILED;
|
|
206
|
+
teleportState.error = error.message;
|
|
207
|
+
|
|
208
|
+
// Attempt cleanup on failure
|
|
209
|
+
await this._cleanupOnError(teleportId, teleportState);
|
|
210
|
+
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Capture local session state.
|
|
217
|
+
*
|
|
218
|
+
* @private
|
|
219
|
+
* @param {string} sessionId - Session ID
|
|
220
|
+
* @param {Object} options - Capture options
|
|
221
|
+
* @returns {Promise<Object>} Session state
|
|
222
|
+
*/
|
|
223
|
+
async _captureSession(sessionId, options = {}) {
|
|
224
|
+
return captureSessionState(sessionId, {
|
|
225
|
+
cwd: options.cwd,
|
|
226
|
+
claudeSessionId: options.claudeSessionId,
|
|
227
|
+
includeUntracked: false, // Don't include untracked files by default
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Upload session state to relay API.
|
|
233
|
+
*
|
|
234
|
+
* @private
|
|
235
|
+
* @param {string} teleportId - Teleport ID
|
|
236
|
+
* @param {string} sessionId - Session ID
|
|
237
|
+
* @param {Object} sessionState - Captured session state
|
|
238
|
+
* @param {Object} options - Upload options
|
|
239
|
+
* @returns {Promise<Object>} Teleport record from relay
|
|
240
|
+
*/
|
|
241
|
+
async _uploadSessionState(teleportId, sessionId, sessionState, options = {}) {
|
|
242
|
+
const response = await fetch(`${this.relayApiUrl}/api/sessions/${sessionId}/teleport`, {
|
|
243
|
+
method: 'POST',
|
|
244
|
+
headers: {
|
|
245
|
+
'Content-Type': 'application/json',
|
|
246
|
+
'Authorization': `Bearer ${this.relayApiKey}`,
|
|
247
|
+
},
|
|
248
|
+
body: JSON.stringify({
|
|
249
|
+
teleport_id: teleportId,
|
|
250
|
+
git_state: sessionState.git,
|
|
251
|
+
patch: sessionState.patch,
|
|
252
|
+
target_coder: options.targetCoder,
|
|
253
|
+
mode: options.mode,
|
|
254
|
+
task: options.task,
|
|
255
|
+
meta: sessionState.meta,
|
|
256
|
+
}),
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
if (!response.ok) {
|
|
260
|
+
const error = await response.text();
|
|
261
|
+
throw new Error(`Failed to upload session state: ${response.status} ${error}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return response.json();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Provision cloud environment.
|
|
269
|
+
*
|
|
270
|
+
* @private
|
|
271
|
+
* @param {string} teleportId - Teleport ID
|
|
272
|
+
* @param {Object} sessionState - Session state
|
|
273
|
+
* @param {Object} options - Provisioning options
|
|
274
|
+
* @returns {Promise<Object>} Cloud machine details
|
|
275
|
+
*/
|
|
276
|
+
async _provisionCloud(teleportId, sessionState, options = {}) {
|
|
277
|
+
// Select provider based on task or explicit choice
|
|
278
|
+
const providerType = this.providerFactory.selectProvider(
|
|
279
|
+
options.task || '',
|
|
280
|
+
{ provider: options.provider }
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
// Create provider instance
|
|
284
|
+
const provider = this.providerFactory.createProvider(providerType);
|
|
285
|
+
|
|
286
|
+
// Configure session for cloud
|
|
287
|
+
const sessionConfig = {
|
|
288
|
+
sessionId: teleportId,
|
|
289
|
+
repoUrl: sessionState.git.repoUrl,
|
|
290
|
+
branch: sessionState.git.branch,
|
|
291
|
+
relayApiUrl: this.relayApiUrl,
|
|
292
|
+
relayApiKey: this.relayApiKey,
|
|
293
|
+
githubToken: options.githubToken || process.env.GITHUB_TOKEN,
|
|
294
|
+
mechApiKey: process.env.MECH_API_KEY,
|
|
295
|
+
task: options.task,
|
|
296
|
+
patch: sessionState.patch,
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// Create the machine
|
|
300
|
+
const machine = await provider.createMachine(sessionConfig);
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
provider: providerType,
|
|
304
|
+
machineId: machine.machineId,
|
|
305
|
+
spriteUrl: machine.spriteUrl,
|
|
306
|
+
tunnelUrl: machine.tunnelUrl,
|
|
307
|
+
namespace: machine.namespace,
|
|
308
|
+
sshKeyId: machine.sshKeyId,
|
|
309
|
+
bridgeKeyId: machine.bridgeKeyId,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Prepare and upload transcript for target CLI.
|
|
315
|
+
*
|
|
316
|
+
* @private
|
|
317
|
+
* @param {string} sessionId - Session ID
|
|
318
|
+
* @param {string} targetCoder - Target CLI name
|
|
319
|
+
* @param {Object} cloudMachine - Cloud machine details
|
|
320
|
+
* @returns {Promise<string>} Transcript path on cloud
|
|
321
|
+
*/
|
|
322
|
+
async _prepareTranscript(sessionId, targetCoder, cloudMachine) {
|
|
323
|
+
// Fetch timeline events from relay
|
|
324
|
+
const response = await fetch(`${this.relayApiUrl}/api/sessions/${sessionId}/timeline`, {
|
|
325
|
+
headers: {
|
|
326
|
+
'Authorization': `Bearer ${this.relayApiKey}`,
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
if (!response.ok) {
|
|
331
|
+
// Timeline might not exist yet - that's OK
|
|
332
|
+
if (response.status === 404) {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
throw new Error(`Failed to fetch timeline: ${response.status}`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const timeline = await response.json();
|
|
339
|
+
const events = timeline.events || [];
|
|
340
|
+
|
|
341
|
+
if (events.length === 0) {
|
|
342
|
+
return null; // No transcript to export
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Get appropriate exporter
|
|
346
|
+
const exporter = await getExporterForCoder(targetCoder);
|
|
347
|
+
|
|
348
|
+
// Export to CLI format
|
|
349
|
+
const transcript = await exporter.export(events);
|
|
350
|
+
|
|
351
|
+
// Upload transcript to relay for cloud to download
|
|
352
|
+
const uploadResponse = await fetch(
|
|
353
|
+
`${this.relayApiUrl}/api/teleports/${cloudMachine.machineId}/transcript`,
|
|
354
|
+
{
|
|
355
|
+
method: 'PUT',
|
|
356
|
+
headers: {
|
|
357
|
+
'Content-Type': 'application/json',
|
|
358
|
+
'Authorization': `Bearer ${this.relayApiKey}`,
|
|
359
|
+
},
|
|
360
|
+
body: JSON.stringify({
|
|
361
|
+
content: transcript,
|
|
362
|
+
format: targetCoder,
|
|
363
|
+
}),
|
|
364
|
+
}
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
if (!uploadResponse.ok) {
|
|
368
|
+
throw new Error(`Failed to upload transcript: ${uploadResponse.status}`);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const result = await uploadResponse.json();
|
|
372
|
+
return result.path;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Resume session on cloud environment.
|
|
377
|
+
*
|
|
378
|
+
* @private
|
|
379
|
+
* @param {string} teleportId - Teleport ID
|
|
380
|
+
* @param {Object} cloudMachine - Cloud machine details
|
|
381
|
+
* @param {string|null} transcriptPath - Path to transcript on cloud
|
|
382
|
+
* @param {Object} options - Resume options
|
|
383
|
+
* @returns {Promise<void>}
|
|
384
|
+
*/
|
|
385
|
+
async _resumeSession(teleportId, cloudMachine, transcriptPath, options = {}) {
|
|
386
|
+
// The bootstrap script on the cloud should handle:
|
|
387
|
+
// 1. Writing transcript to CLI-native location
|
|
388
|
+
// 2. Starting the CLI with --resume flag
|
|
389
|
+
// 3. Starting the teleportation daemon
|
|
390
|
+
|
|
391
|
+
// Notify relay that teleport is ready
|
|
392
|
+
await fetch(`${this.relayApiUrl}/api/teleports/${teleportId}/ready`, {
|
|
393
|
+
method: 'POST',
|
|
394
|
+
headers: {
|
|
395
|
+
'Content-Type': 'application/json',
|
|
396
|
+
'Authorization': `Bearer ${this.relayApiKey}`,
|
|
397
|
+
},
|
|
398
|
+
body: JSON.stringify({
|
|
399
|
+
machine_id: cloudMachine.machineId,
|
|
400
|
+
provider: cloudMachine.provider,
|
|
401
|
+
transcript_path: transcriptPath,
|
|
402
|
+
target_coder: options.targetCoder,
|
|
403
|
+
}),
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Update teleport status and notify callback.
|
|
409
|
+
*
|
|
410
|
+
* @private
|
|
411
|
+
* @param {string} teleportId - Teleport ID
|
|
412
|
+
* @param {TeleportStatus} status - New status
|
|
413
|
+
* @param {Function} [onProgress] - Progress callback
|
|
414
|
+
*/
|
|
415
|
+
_updateStatus(teleportId, status, onProgress) {
|
|
416
|
+
const state = this.activeTeleports.get(teleportId);
|
|
417
|
+
if (state) {
|
|
418
|
+
state.status = status;
|
|
419
|
+
state.updatedAt = new Date().toISOString();
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (onProgress) {
|
|
423
|
+
onProgress({ teleportId, status });
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Cleanup resources on error.
|
|
429
|
+
*
|
|
430
|
+
* @private
|
|
431
|
+
* @param {string} teleportId - Teleport ID
|
|
432
|
+
* @param {Object} teleportState - Teleport state
|
|
433
|
+
*/
|
|
434
|
+
async _cleanupOnError(teleportId, teleportState) {
|
|
435
|
+
try {
|
|
436
|
+
// Stop auto-commits
|
|
437
|
+
if (teleportState.gitCommitter) {
|
|
438
|
+
teleportState.gitCommitter.stopAutoCommit();
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (teleportState.machineId && teleportState.provider) {
|
|
442
|
+
const provider = this.providerFactory.createProvider(teleportState.provider);
|
|
443
|
+
await provider.destroyMachine(teleportState.machineId);
|
|
444
|
+
}
|
|
445
|
+
} catch (cleanupError) {
|
|
446
|
+
console.error(`[teleport-manager] Cleanup failed for ${teleportId}:`, cleanupError.message);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Get teleport status.
|
|
452
|
+
*
|
|
453
|
+
* @param {string} teleportId - Teleport ID
|
|
454
|
+
* @returns {Object|null} Teleport state or null if not found
|
|
455
|
+
*/
|
|
456
|
+
getTeleportStatus(teleportId) {
|
|
457
|
+
return this.activeTeleports.get(teleportId) || null;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Stop a running teleport and destroy cloud resources.
|
|
462
|
+
*
|
|
463
|
+
* @param {string} teleportId - Teleport ID
|
|
464
|
+
* @param {Object} [options] - Stop options
|
|
465
|
+
* @param {boolean} [options.success=true] - Whether task completed successfully
|
|
466
|
+
* @param {string} [options.description] - Final commit description
|
|
467
|
+
* @param {string} [options.reason] - Reason for stopping (if not success)
|
|
468
|
+
* @param {boolean} [options.createPR=false] - Whether to create PR with results
|
|
469
|
+
* @param {string} [options.baseBranch] - Base branch for PR
|
|
470
|
+
* @returns {Promise<Object>} Stop result with optional PR details
|
|
471
|
+
*/
|
|
472
|
+
async stopTeleport(teleportId, options = {}) {
|
|
473
|
+
const state = this.activeTeleports.get(teleportId);
|
|
474
|
+
if (!state) {
|
|
475
|
+
return { success: false, error: 'Teleport not found' };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const { success = true, description, reason, createPR = false, baseBranch } = options;
|
|
479
|
+
|
|
480
|
+
try {
|
|
481
|
+
// Stop auto-commits and create final commit
|
|
482
|
+
if (state.gitCommitter) {
|
|
483
|
+
state.gitCommitter.stopAutoCommit();
|
|
484
|
+
|
|
485
|
+
if (success) {
|
|
486
|
+
await state.gitCommitter.createFinalCommit(
|
|
487
|
+
description || state.task || 'Task completed'
|
|
488
|
+
);
|
|
489
|
+
} else {
|
|
490
|
+
await state.gitCommitter.createPartialCommit(
|
|
491
|
+
description || 'Partial work completed',
|
|
492
|
+
reason || 'Session stopped'
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Push to remote before destroying machine
|
|
497
|
+
if (createPR) {
|
|
498
|
+
try {
|
|
499
|
+
await state.gitCommitter.push();
|
|
500
|
+
} catch (pushError) {
|
|
501
|
+
console.error(`[teleport-manager] Push failed: ${pushError.message}`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (state.machineId && state.provider) {
|
|
507
|
+
const provider = this.providerFactory.createProvider(state.provider);
|
|
508
|
+
await provider.destroyMachine(state.machineId);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
state.status = TeleportStatus.COMPLETED;
|
|
512
|
+
state.completedAt = new Date().toISOString();
|
|
513
|
+
|
|
514
|
+
const result = { success: true };
|
|
515
|
+
|
|
516
|
+
// Create PR if requested
|
|
517
|
+
if (createPR) {
|
|
518
|
+
try {
|
|
519
|
+
const deliveryResult = await this.deliverResults(teleportId, { baseBranch });
|
|
520
|
+
result.delivery = deliveryResult;
|
|
521
|
+
} catch (deliveryError) {
|
|
522
|
+
result.deliveryError = deliveryError.message;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return result;
|
|
527
|
+
} catch (error) {
|
|
528
|
+
state.error = error.message;
|
|
529
|
+
throw error;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* List all active teleports.
|
|
535
|
+
*
|
|
536
|
+
* @returns {Object[]} Array of teleport states
|
|
537
|
+
*/
|
|
538
|
+
listActiveTeleports() {
|
|
539
|
+
return Array.from(this.activeTeleports.values());
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Update activity description for WIP commits.
|
|
544
|
+
*
|
|
545
|
+
* @param {string} teleportId - Teleport ID
|
|
546
|
+
* @param {string} activity - Current activity description
|
|
547
|
+
*/
|
|
548
|
+
updateActivity(teleportId, activity) {
|
|
549
|
+
const state = this.activeTeleports.get(teleportId);
|
|
550
|
+
if (state && state.gitCommitter) {
|
|
551
|
+
state.gitCommitter.updateActivity(activity);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Get commit statistics for a teleport session.
|
|
557
|
+
*
|
|
558
|
+
* @param {string} teleportId - Teleport ID
|
|
559
|
+
* @returns {Object|null} Commit stats or null if not found
|
|
560
|
+
*/
|
|
561
|
+
getCommitStats(teleportId) {
|
|
562
|
+
const state = this.activeTeleports.get(teleportId);
|
|
563
|
+
if (state && state.gitCommitter) {
|
|
564
|
+
return state.gitCommitter.getStats();
|
|
565
|
+
}
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Deliver results from a completed teleport session.
|
|
571
|
+
*
|
|
572
|
+
* Creates a PR (preferred) or falls back to a local branch.
|
|
573
|
+
*
|
|
574
|
+
* @param {string} teleportId - Teleport ID
|
|
575
|
+
* @param {Object} [options] - Delivery options
|
|
576
|
+
* @param {string} [options.baseBranch] - Base branch for PR (defaults to 'main')
|
|
577
|
+
* @returns {Promise<Object>} Delivery result
|
|
578
|
+
*/
|
|
579
|
+
async deliverResults(teleportId, options = {}) {
|
|
580
|
+
const state = this.activeTeleports.get(teleportId);
|
|
581
|
+
if (!state) {
|
|
582
|
+
throw new Error(`Teleport ${teleportId} not found`);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (!state.sessionState || !state.sessionState.cwd) {
|
|
586
|
+
throw new Error(`No repository path for teleport ${teleportId}`);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const remoteBranch = state.gitCommitter?.branch || `teleport/${teleportId}`;
|
|
590
|
+
|
|
591
|
+
// Create ResultsDelivery instance
|
|
592
|
+
const resultsDelivery = new ResultsDelivery({
|
|
593
|
+
repoPath: state.sessionState.cwd,
|
|
594
|
+
sessionId: state.sessionId,
|
|
595
|
+
remoteBranch,
|
|
596
|
+
verbose: options.verbose,
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
// Deliver results with fallback strategy
|
|
600
|
+
const result = await resultsDelivery.deliverResults({
|
|
601
|
+
task: state.task,
|
|
602
|
+
baseBranch: options.baseBranch,
|
|
603
|
+
timelineUrl: `${this.relayApiUrl}/sessions/${state.sessionId}/timeline`,
|
|
604
|
+
metadata: {
|
|
605
|
+
provider: state.provider,
|
|
606
|
+
machineId: state.machineId,
|
|
607
|
+
duration: state.completedAt && state.createdAt
|
|
608
|
+
? Math.floor((new Date(state.completedAt) - new Date(state.createdAt)) / 1000)
|
|
609
|
+
: undefined,
|
|
610
|
+
},
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
// Update state with delivery result
|
|
614
|
+
state.deliveryResult = result;
|
|
615
|
+
|
|
616
|
+
return result;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
export default TeleportManager;
|