teleportation-cli 1.0.0 → 1.0.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/.claude/hooks/heartbeat.mjs +67 -2
- package/.claude/hooks/permission_request.mjs +55 -26
- package/.claude/hooks/pre_tool_use.mjs +29 -2
- package/.claude/hooks/session-register.mjs +64 -5
- package/.claude/hooks/stop.mjs +205 -1
- package/.claude/hooks/user_prompt_submit.mjs +111 -0
- package/README.md +36 -12
- package/lib/auth/claude-key-extractor.js +196 -0
- package/lib/auth/credentials.js +7 -2
- package/lib/cli/remote-commands.js +649 -0
- package/lib/daemon/teleportation-daemon.js +131 -41
- package/lib/install/installer.js +22 -7
- package/lib/machine-coders/claude-code-adapter.js +191 -37
- package/lib/remote/code-sync.js +213 -0
- package/lib/remote/init-script-robust.js +187 -0
- package/lib/remote/liveport-client.js +417 -0
- package/lib/remote/orchestrator.js +480 -0
- package/lib/remote/pr-creator.js +382 -0
- package/lib/remote/providers/base-provider.js +407 -0
- package/lib/remote/providers/daytona-provider.js +506 -0
- package/lib/remote/providers/fly-provider.js +611 -0
- package/lib/remote/providers/provider-factory.js +228 -0
- package/lib/remote/results-delivery.js +333 -0
- package/lib/remote/session-manager.js +273 -0
- package/lib/remote/state-capture.js +324 -0
- package/lib/remote/vault-client.js +478 -0
- package/lib/session/metadata.js +80 -49
- package/lib/session/mute-checker.js +2 -1
- package/lib/utils/vault-errors.js +353 -0
- package/package.json +5 -5
- package/teleportation-cli.cjs +417 -7
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI Commands for Remote Session Management
|
|
4
|
+
*
|
|
5
|
+
* Provides command-line interface for:
|
|
6
|
+
* - Starting remote sessions
|
|
7
|
+
* - Listing and querying sessions
|
|
8
|
+
* - Viewing logs
|
|
9
|
+
* - Pulling results
|
|
10
|
+
* - Session control (stop, resume, destroy)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { RemoteOrchestrator } from '../remote/orchestrator.js';
|
|
14
|
+
import { RemoteSessionManager } from '../remote/session-manager.js';
|
|
15
|
+
import { ProviderFactory } from '../remote/providers/provider-factory.js';
|
|
16
|
+
import { VaultClient } from '../remote/vault-client.js';
|
|
17
|
+
import { LivePortClient } from '../remote/liveport-client.js';
|
|
18
|
+
import { CodeSync } from '../remote/code-sync.js';
|
|
19
|
+
import { randomUUID } from 'crypto';
|
|
20
|
+
|
|
21
|
+
async function loadSavedRelayCredentials() {
|
|
22
|
+
try {
|
|
23
|
+
const { CredentialManager } = await import('../auth/credentials.js');
|
|
24
|
+
const manager = new CredentialManager();
|
|
25
|
+
const creds = await manager.load();
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
relayApiUrl: creds?.relayApiUrl || '',
|
|
29
|
+
relayApiKey: creds?.relayApiKey || creds?.apiKey || '',
|
|
30
|
+
githubToken: creds?.githubToken || '',
|
|
31
|
+
};
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (process.env.TELEPORTATION_DEBUG === 'true') {
|
|
34
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
35
|
+
console.error(`[remote] Failed to load saved credentials: ${message}`);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
relayApiUrl: '',
|
|
39
|
+
relayApiKey: '',
|
|
40
|
+
githubToken: '',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Color helpers for terminal output
|
|
47
|
+
*/
|
|
48
|
+
const c = {
|
|
49
|
+
red: (text) => `\x1b[0;31m${text}\x1b[0m`,
|
|
50
|
+
green: (text) => `\x1b[0;32m${text}\x1b[0m`,
|
|
51
|
+
yellow: (text) => `\x1b[1;33m${text}\x1b[0m`,
|
|
52
|
+
blue: (text) => `\x1b[0;34m${text}\x1b[0m`,
|
|
53
|
+
cyan: (text) => `\x1b[0;36m${text}\x1b[0m`,
|
|
54
|
+
purple: (text) => `\x1b[0;35m${text}\x1b[0m`
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Format uptime from milliseconds to human-readable string
|
|
59
|
+
* @private
|
|
60
|
+
*/
|
|
61
|
+
function formatUptime(ms) {
|
|
62
|
+
const seconds = Math.floor(ms / 1000);
|
|
63
|
+
const minutes = Math.floor(seconds / 60);
|
|
64
|
+
const hours = Math.floor(minutes / 60);
|
|
65
|
+
const days = Math.floor(hours / 24);
|
|
66
|
+
|
|
67
|
+
if (days > 0) return `${days}d ${hours % 24}h`;
|
|
68
|
+
if (hours > 0) return `${hours}h ${minutes % 60}m`;
|
|
69
|
+
if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
|
|
70
|
+
return `${seconds}s`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function createStubVaultClient() {
|
|
74
|
+
return {
|
|
75
|
+
apiUrl: process.env.MECH_VAULT_URL || process.env.VAULT_API_URL || 'https://vault.mechdna.net/api',
|
|
76
|
+
apiKey: process.env.MECH_API_KEY,
|
|
77
|
+
appId: process.env.MECH_APP_ID,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function createStubLivePortClient() {
|
|
82
|
+
return {
|
|
83
|
+
apiKey: process.env.LIVEPORT_API_KEY,
|
|
84
|
+
apiUrl: process.env.LIVEPORT_API_URL || 'https://api.liveport.dev/v1',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getProviderForSession(session, deps = {}) {
|
|
89
|
+
if (deps.provider) return deps.provider;
|
|
90
|
+
|
|
91
|
+
const vaultClient = deps.vaultClient || createStubVaultClient();
|
|
92
|
+
const livePortClient = deps.livePortClient || createStubLivePortClient();
|
|
93
|
+
|
|
94
|
+
const providerFactory = deps.providerFactory || new ProviderFactory({
|
|
95
|
+
vaultClient,
|
|
96
|
+
livePortClient,
|
|
97
|
+
flyApiToken: process.env.FLY_API_TOKEN,
|
|
98
|
+
daytonaApiKey: process.env.DAYTONA_API_KEY,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return providerFactory.createProvider(session.provider);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Start a new remote session
|
|
106
|
+
*
|
|
107
|
+
* @param {Object} args - Command arguments
|
|
108
|
+
* @param {string} args.task - Task description
|
|
109
|
+
* @param {string} [args.branch] - Git branch name
|
|
110
|
+
* @param {string} [args.provider] - Provider type (fly or daytona)
|
|
111
|
+
* @param {boolean} [args.noPr] - Skip PR creation on completion
|
|
112
|
+
* @param {Object} [deps] - Injected dependencies (for testing)
|
|
113
|
+
* @returns {Promise<Object>} Session information
|
|
114
|
+
*/
|
|
115
|
+
export async function commandRemoteStart(args, deps = {}) {
|
|
116
|
+
const { task, branch, provider: providerOverride, noPr } = args;
|
|
117
|
+
|
|
118
|
+
// Validate required arguments
|
|
119
|
+
if (!task) {
|
|
120
|
+
throw new Error('Task description is required. Use --task "description"');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
console.log(c.cyan('\nStarting remote session...'));
|
|
124
|
+
console.log(`Task: ${task}`);
|
|
125
|
+
if (branch) console.log(`Branch: ${branch}`);
|
|
126
|
+
if (providerOverride) console.log(`Provider: ${providerOverride}`);
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
// Initialize dependencies (use injected for testing, or create real ones)
|
|
130
|
+
const sessionManager = deps.sessionManager || new RemoteSessionManager();
|
|
131
|
+
|
|
132
|
+
// Only create real clients if not in test mode (deps not provided)
|
|
133
|
+
let vaultClient, livePortClient, providerFactory, codeSync;
|
|
134
|
+
|
|
135
|
+
if (deps.orchestrator && deps.providerFactory) {
|
|
136
|
+
// Test mode: use all injected dependencies
|
|
137
|
+
vaultClient = deps.vaultClient;
|
|
138
|
+
livePortClient = deps.livePortClient;
|
|
139
|
+
providerFactory = deps.providerFactory;
|
|
140
|
+
codeSync = deps.codeSync;
|
|
141
|
+
} else {
|
|
142
|
+
// Production mode: create real clients
|
|
143
|
+
vaultClient = new VaultClient({
|
|
144
|
+
apiUrl: process.env.MECH_VAULT_URL || process.env.VAULT_API_URL || 'https://vault.mechdna.net/api',
|
|
145
|
+
apiKey: process.env.MECH_API_KEY,
|
|
146
|
+
appId: process.env.MECH_APP_ID
|
|
147
|
+
});
|
|
148
|
+
livePortClient = new LivePortClient({
|
|
149
|
+
apiKey: process.env.LIVEPORT_API_KEY,
|
|
150
|
+
apiUrl: process.env.LIVEPORT_API_URL || 'https://api.liveport.dev/v1'
|
|
151
|
+
});
|
|
152
|
+
providerFactory = new ProviderFactory({
|
|
153
|
+
vaultClient,
|
|
154
|
+
livePortClient,
|
|
155
|
+
flyApiToken: process.env.FLY_API_TOKEN,
|
|
156
|
+
daytonaApiKey: process.env.DAYTONA_API_KEY
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Select provider
|
|
161
|
+
const selectedProvider = providerFactory.selectAndCreate(task, {
|
|
162
|
+
provider: providerOverride
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const providerType = providerOverride || providerFactory.selectProvider(task);
|
|
166
|
+
console.log(c.blue(`\nSelected provider: ${providerType}`));
|
|
167
|
+
|
|
168
|
+
// Generate session ID
|
|
169
|
+
const sessionId = `remote-${randomUUID().slice(0, 8)}`;
|
|
170
|
+
|
|
171
|
+
// Resolve relay credentials (prefer env vars; fallback to saved credentials)
|
|
172
|
+
const savedCreds = await loadSavedRelayCredentials();
|
|
173
|
+
const relayApiUrl = process.env.RELAY_API_URL || savedCreds.relayApiUrl;
|
|
174
|
+
const relayApiKey = process.env.RELAY_API_KEY || savedCreds.relayApiKey;
|
|
175
|
+
|
|
176
|
+
const githubToken = process.env.GITHUB_TOKEN || savedCreds.githubToken;
|
|
177
|
+
|
|
178
|
+
if (!relayApiUrl) {
|
|
179
|
+
throw new Error('Missing RELAY_API_URL. Set RELAY_API_URL or run `teleportation login --api-key ...`');
|
|
180
|
+
}
|
|
181
|
+
if (!relayApiKey) {
|
|
182
|
+
throw new Error('Missing RELAY_API_KEY. Set RELAY_API_KEY or run `teleportation login --api-key ...`');
|
|
183
|
+
}
|
|
184
|
+
if (!githubToken) {
|
|
185
|
+
throw new Error('Missing GitHub credentials. Set GITHUB_TOKEN or run `teleportation github connect --token ...`');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const repoPath = process.cwd();
|
|
189
|
+
|
|
190
|
+
// Create orchestrator (use injected or create real one)
|
|
191
|
+
if (!codeSync && !deps.orchestrator) {
|
|
192
|
+
codeSync = new CodeSync({
|
|
193
|
+
repoPath,
|
|
194
|
+
sessionId
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const orchestrator = deps.orchestrator || new RemoteOrchestrator({
|
|
199
|
+
sessionId,
|
|
200
|
+
repoPath,
|
|
201
|
+
vaultClient,
|
|
202
|
+
livePortClient,
|
|
203
|
+
provider: selectedProvider
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Capture local state and secrets
|
|
207
|
+
console.log(c.yellow('\n→ Capturing local state...'));
|
|
208
|
+
const localState = await codeSync.captureLocalState();
|
|
209
|
+
|
|
210
|
+
// Start session
|
|
211
|
+
console.log(c.yellow('→ Provisioning remote machine...'));
|
|
212
|
+
await orchestrator.startSession({
|
|
213
|
+
task,
|
|
214
|
+
branch: branch || localState.branch,
|
|
215
|
+
repoUrl: localState.remoteUrl,
|
|
216
|
+
relayApiUrl,
|
|
217
|
+
relayApiKey,
|
|
218
|
+
githubToken,
|
|
219
|
+
mechApiKey: process.env.MECH_API_KEY
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Register session
|
|
223
|
+
const session = await sessionManager.registerSession({
|
|
224
|
+
id: sessionId,
|
|
225
|
+
provider: providerType,
|
|
226
|
+
machineId: orchestrator.machineId,
|
|
227
|
+
branch: localState.branch,
|
|
228
|
+
metadata: {
|
|
229
|
+
task,
|
|
230
|
+
tunnelUrl: orchestrator.tunnelUrl,
|
|
231
|
+
noPr,
|
|
232
|
+
startedAt: Date.now()
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Display results
|
|
237
|
+
console.log(c.green('\n✓ Remote session started successfully!\n'));
|
|
238
|
+
console.log(`Session ID: ${c.cyan(sessionId)}`);
|
|
239
|
+
console.log(`Provider: ${c.cyan(providerType)}`);
|
|
240
|
+
console.log(`Machine ID: ${c.cyan(orchestrator.machineId)}`);
|
|
241
|
+
console.log(`Tunnel URL: ${c.cyan(orchestrator.tunnelUrl)}`);
|
|
242
|
+
console.log(`Branch: ${c.cyan(localState.branch)}`);
|
|
243
|
+
console.log(`\nTimeline: ${c.cyan(`https://app.teleportation.dev/sessions/${sessionId}`)}`);
|
|
244
|
+
|
|
245
|
+
console.log(c.yellow('\n→ Remote AI agent is now working on your task'));
|
|
246
|
+
console.log(c.yellow('→ Approve actions from your mobile device'));
|
|
247
|
+
console.log(c.yellow(`→ Check status with: teleportation remote status ${sessionId}`));
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
sessionId,
|
|
251
|
+
tunnelUrl: orchestrator.tunnelUrl,
|
|
252
|
+
machineId: orchestrator.machineId,
|
|
253
|
+
provider: providerType,
|
|
254
|
+
session
|
|
255
|
+
};
|
|
256
|
+
} catch (error) {
|
|
257
|
+
console.error(c.red(`\n✗ Failed to start remote session: ${error.message}`));
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* List remote sessions
|
|
264
|
+
*
|
|
265
|
+
* @param {Object} args - Command arguments
|
|
266
|
+
* @param {string} [args.status] - Filter by status (running, paused, completed, failed)
|
|
267
|
+
* @param {string} [args.provider] - Filter by provider (fly, daytona)
|
|
268
|
+
* @param {Object} [deps] - Injected dependencies (for testing)
|
|
269
|
+
* @returns {Promise<Array>} List of sessions
|
|
270
|
+
*/
|
|
271
|
+
export async function commandRemoteList(args, deps = {}) {
|
|
272
|
+
const { status, provider } = args;
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
const sessionManager = deps.sessionManager || new RemoteSessionManager();
|
|
276
|
+
|
|
277
|
+
const filters = {};
|
|
278
|
+
if (status) filters.status = status;
|
|
279
|
+
if (provider) filters.provider = provider;
|
|
280
|
+
|
|
281
|
+
const sessions = await sessionManager.listSessions(filters);
|
|
282
|
+
|
|
283
|
+
if (sessions.length === 0) {
|
|
284
|
+
console.log('No remote sessions found.');
|
|
285
|
+
return [];
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
console.log(c.cyan('\nRemote Sessions:\n'));
|
|
289
|
+
console.log(
|
|
290
|
+
'SESSION ID'.padEnd(20),
|
|
291
|
+
'PROVIDER'.padEnd(12),
|
|
292
|
+
'STATUS'.padEnd(12),
|
|
293
|
+
'BRANCH'.padEnd(25),
|
|
294
|
+
'UPTIME'
|
|
295
|
+
);
|
|
296
|
+
console.log('-'.repeat(95));
|
|
297
|
+
|
|
298
|
+
for (const session of sessions) {
|
|
299
|
+
const uptime = formatUptime(Date.now() - session.createdAt);
|
|
300
|
+
const statusColor = session.status === 'running' ? c.green :
|
|
301
|
+
session.status === 'paused' ? c.yellow :
|
|
302
|
+
session.status === 'completed' ? c.blue :
|
|
303
|
+
c.red;
|
|
304
|
+
|
|
305
|
+
console.log(
|
|
306
|
+
session.id.padEnd(20),
|
|
307
|
+
session.provider.padEnd(12),
|
|
308
|
+
statusColor(session.status.padEnd(12)),
|
|
309
|
+
session.branch.padEnd(25),
|
|
310
|
+
uptime
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
console.log(`\n${c.cyan(`Total: ${sessions.length} session(s)`)}`);
|
|
315
|
+
|
|
316
|
+
return sessions;
|
|
317
|
+
} catch (error) {
|
|
318
|
+
console.error(c.red(`Failed to list sessions: ${error.message}`));
|
|
319
|
+
throw error;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Show detailed status of a remote session
|
|
325
|
+
*
|
|
326
|
+
* @param {string} sessionId - Session ID
|
|
327
|
+
* @param {Object} [deps] - Injected dependencies (for testing)
|
|
328
|
+
* @returns {Promise<Object>} Session status details
|
|
329
|
+
*/
|
|
330
|
+
export async function commandRemoteStatus(sessionId, deps = {}) {
|
|
331
|
+
if (!sessionId) {
|
|
332
|
+
throw new Error('Session ID is required');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
const sessionManager = deps.sessionManager || new RemoteSessionManager();
|
|
337
|
+
|
|
338
|
+
const session = await sessionManager.getSession(sessionId);
|
|
339
|
+
|
|
340
|
+
if (!session) {
|
|
341
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
console.log(c.cyan('\nSession Status:\n'));
|
|
345
|
+
console.log(`Session ID: ${session.id}`);
|
|
346
|
+
console.log(`Provider: ${session.provider}`);
|
|
347
|
+
console.log(`Status: ${c.green(session.status)}`);
|
|
348
|
+
console.log(`Branch: ${session.branch}`);
|
|
349
|
+
console.log(`Machine ID: ${session.machineId}`);
|
|
350
|
+
|
|
351
|
+
if (session.metadata.tunnelUrl) {
|
|
352
|
+
console.log(`Tunnel URL: ${c.cyan(session.metadata.tunnelUrl)}`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
console.log(`\nCreated: ${new Date(session.createdAt).toLocaleString()}`);
|
|
356
|
+
console.log(`Last Active: ${new Date(session.lastActiveAt).toLocaleString()}`);
|
|
357
|
+
console.log(`Uptime: ${formatUptime(Date.now() - session.createdAt)}`);
|
|
358
|
+
|
|
359
|
+
if (session.metadata.task) {
|
|
360
|
+
console.log(`\nTask: ${session.metadata.task}`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
console.log(`\nTimeline: ${c.cyan(`https://app.teleportation.dev/sessions/${sessionId}`)}`);
|
|
364
|
+
|
|
365
|
+
// Get machine status from orchestrator if available, otherwise query provider
|
|
366
|
+
let status = null;
|
|
367
|
+
if (deps.orchestrator) {
|
|
368
|
+
status = await deps.orchestrator.getStatus();
|
|
369
|
+
} else {
|
|
370
|
+
const provider = getProviderForSession(session, deps);
|
|
371
|
+
status = { machine: await provider.getMachineStatus(session.machineId) };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (status?.machine) {
|
|
375
|
+
console.log(c.cyan('\nMachine Status:'));
|
|
376
|
+
console.log(` State: ${status.machine.state || status.machine.status || 'unknown'}`);
|
|
377
|
+
if (status.machine.cpu !== undefined) {
|
|
378
|
+
console.log(` CPU: ${status.machine.cpu}`);
|
|
379
|
+
}
|
|
380
|
+
if (status.machine.memory !== undefined) {
|
|
381
|
+
console.log(` Memory: ${status.machine.memory} MB`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
session,
|
|
387
|
+
status
|
|
388
|
+
};
|
|
389
|
+
} catch (error) {
|
|
390
|
+
console.error(c.red(`Failed to get session status: ${error.message}`));
|
|
391
|
+
throw error;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Stream logs from a remote session
|
|
397
|
+
*
|
|
398
|
+
* @param {string} sessionId - Session ID
|
|
399
|
+
* @param {Object} options - Log options
|
|
400
|
+
* @param {boolean} [options.follow] - Follow log output
|
|
401
|
+
* @param {number} [options.tail] - Number of lines to show from end
|
|
402
|
+
* @param {Object} [deps] - Injected dependencies (for testing)
|
|
403
|
+
* @returns {Promise<Array>} Log entries
|
|
404
|
+
*/
|
|
405
|
+
export async function commandRemoteLogs(sessionId, options = {}, deps = {}) {
|
|
406
|
+
if (!sessionId) {
|
|
407
|
+
throw new Error('Session ID is required');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
try {
|
|
411
|
+
const sessionManager = deps.sessionManager || new RemoteSessionManager();
|
|
412
|
+
|
|
413
|
+
const session = await sessionManager.getSession(sessionId);
|
|
414
|
+
|
|
415
|
+
if (!session) {
|
|
416
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
console.log(c.cyan(`\nLogs for session: ${sessionId}\n`));
|
|
420
|
+
|
|
421
|
+
const provider = getProviderForSession(session, deps);
|
|
422
|
+
const response = await provider.getLogs(session.machineId, options);
|
|
423
|
+
|
|
424
|
+
const entries = Array.isArray(response)
|
|
425
|
+
? response
|
|
426
|
+
: (response && typeof response === 'object' && Array.isArray(response.logs))
|
|
427
|
+
? response.logs
|
|
428
|
+
: [];
|
|
429
|
+
|
|
430
|
+
for (const entry of entries) {
|
|
431
|
+
if (entry && typeof entry === 'object' && entry.timestamp && entry.message) {
|
|
432
|
+
const timestamp = new Date(entry.timestamp).toLocaleTimeString();
|
|
433
|
+
console.log(`${c.cyan(timestamp)} ${entry.message}`);
|
|
434
|
+
} else {
|
|
435
|
+
console.log(String(entry));
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return entries;
|
|
440
|
+
} catch (error) {
|
|
441
|
+
console.error(c.red(`Failed to retrieve logs: ${error.message}`));
|
|
442
|
+
throw error;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Pull results from a remote session
|
|
448
|
+
*
|
|
449
|
+
* @param {string} sessionId - Session ID
|
|
450
|
+
* @param {Object} [deps] - Injected dependencies (for testing)
|
|
451
|
+
* @returns {Promise<Object>} Pull result
|
|
452
|
+
*/
|
|
453
|
+
export async function commandRemotePull(sessionId, deps = {}) {
|
|
454
|
+
if (!sessionId) {
|
|
455
|
+
throw new Error('Session ID is required');
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
try {
|
|
459
|
+
const sessionManager = deps.sessionManager || new RemoteSessionManager();
|
|
460
|
+
|
|
461
|
+
const session = await sessionManager.getSession(sessionId);
|
|
462
|
+
|
|
463
|
+
if (!session) {
|
|
464
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
console.log(c.cyan(`\nPulling results from session: ${sessionId}\n`));
|
|
468
|
+
|
|
469
|
+
// Initialize CodeSync
|
|
470
|
+
const codeSync = deps.codeSync || new CodeSync({
|
|
471
|
+
repoPath: process.cwd(),
|
|
472
|
+
sessionId
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// Check for conflicts
|
|
476
|
+
console.log(c.yellow('→ Checking for conflicts...'));
|
|
477
|
+
const conflicts = await codeSync.detectConflicts();
|
|
478
|
+
|
|
479
|
+
if (conflicts.hasConflicts) {
|
|
480
|
+
console.log(c.red('\n✗ Conflicts detected:'));
|
|
481
|
+
console.log(` Conflicting files: ${conflicts.conflictingFiles.join(', ')}`);
|
|
482
|
+
throw new Error('Conflicts detected. Resolve conflicts before pulling.');
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Show diff summary
|
|
486
|
+
console.log(c.yellow('→ Fetching changes...'));
|
|
487
|
+
const diff = await codeSync.getDiffSummary();
|
|
488
|
+
|
|
489
|
+
console.log(c.cyan('\nChanges:'));
|
|
490
|
+
console.log(` Behind remote: ${diff.behind} commit(s)`);
|
|
491
|
+
console.log(` Files changed: ${diff.files.length}`);
|
|
492
|
+
|
|
493
|
+
if (diff.files.length > 0) {
|
|
494
|
+
console.log('\n Modified files:');
|
|
495
|
+
diff.files.slice(0, 10).forEach(file => {
|
|
496
|
+
console.log(` - ${file}`);
|
|
497
|
+
});
|
|
498
|
+
if (diff.files.length > 10) {
|
|
499
|
+
console.log(` ... and ${diff.files.length - 10} more`);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Pull changes
|
|
504
|
+
console.log(c.yellow('\n→ Pulling changes...'));
|
|
505
|
+
const result = await codeSync.pullFromRemote({
|
|
506
|
+
strategy: 'merge',
|
|
507
|
+
stashLocal: true
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
if (result.success) {
|
|
511
|
+
console.log(c.green('\n✓ Successfully pulled changes from remote session'));
|
|
512
|
+
console.log(` Commit: ${result.commit}`);
|
|
513
|
+
} else {
|
|
514
|
+
console.log(c.red('\n✗ Failed to pull changes'));
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return result;
|
|
518
|
+
} catch (error) {
|
|
519
|
+
console.error(c.red(`Failed to pull results: ${error.message}`));
|
|
520
|
+
throw error;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Stop a remote session (pause)
|
|
526
|
+
*
|
|
527
|
+
* @param {string} sessionId - Session ID
|
|
528
|
+
* @param {Object} [deps] - Injected dependencies (for testing)
|
|
529
|
+
* @returns {Promise<void>}
|
|
530
|
+
*/
|
|
531
|
+
export async function commandRemoteStop(sessionId, deps = {}) {
|
|
532
|
+
if (!sessionId) {
|
|
533
|
+
throw new Error('Session ID is required');
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
try {
|
|
537
|
+
const sessionManager = deps.sessionManager || new RemoteSessionManager();
|
|
538
|
+
|
|
539
|
+
const session = await sessionManager.getSession(sessionId);
|
|
540
|
+
|
|
541
|
+
if (!session) {
|
|
542
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
console.log(c.yellow(`\nStopping session: ${sessionId}...`));
|
|
546
|
+
|
|
547
|
+
if (deps.orchestrator) {
|
|
548
|
+
await deps.orchestrator.stopSession();
|
|
549
|
+
} else {
|
|
550
|
+
const provider = getProviderForSession(session, deps);
|
|
551
|
+
await provider.stopMachine(session.machineId);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Update session status
|
|
555
|
+
await sessionManager.updateSession(sessionId, {
|
|
556
|
+
status: 'paused'
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
console.log(c.green('✓ Session stopped (paused)'));
|
|
560
|
+
} catch (error) {
|
|
561
|
+
console.error(c.red(`Failed to stop session: ${error.message}`));
|
|
562
|
+
throw error;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Resume a paused remote session
|
|
568
|
+
*
|
|
569
|
+
* @param {string} sessionId - Session ID
|
|
570
|
+
* @param {Object} [deps] - Injected dependencies (for testing)
|
|
571
|
+
* @returns {Promise<void>}
|
|
572
|
+
*/
|
|
573
|
+
export async function commandRemoteResume(sessionId, deps = {}) {
|
|
574
|
+
if (!sessionId) {
|
|
575
|
+
throw new Error('Session ID is required');
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
try {
|
|
579
|
+
const sessionManager = deps.sessionManager || new RemoteSessionManager();
|
|
580
|
+
|
|
581
|
+
const session = await sessionManager.getSession(sessionId);
|
|
582
|
+
|
|
583
|
+
if (!session) {
|
|
584
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
console.log(c.yellow(`\nResuming session: ${sessionId}...`));
|
|
588
|
+
|
|
589
|
+
const provider = getProviderForSession(session, deps);
|
|
590
|
+
await provider.startMachine(session.machineId);
|
|
591
|
+
|
|
592
|
+
// Update session status
|
|
593
|
+
await sessionManager.updateSession(sessionId, {
|
|
594
|
+
status: 'running'
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
console.log(c.green('✓ Session resumed'));
|
|
598
|
+
} catch (error) {
|
|
599
|
+
console.error(c.red(`Failed to resume session: ${error.message}`));
|
|
600
|
+
throw error;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Destroy a remote session and cleanup all resources
|
|
606
|
+
*
|
|
607
|
+
* @param {string} sessionId - Session ID
|
|
608
|
+
* @param {Object} [deps] - Injected dependencies (for testing)
|
|
609
|
+
* @returns {Promise<void>}
|
|
610
|
+
*/
|
|
611
|
+
export async function commandRemoteDestroy(sessionId, deps = {}) {
|
|
612
|
+
if (!sessionId) {
|
|
613
|
+
throw new Error('Session ID is required');
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Require confirmation for destructive action
|
|
617
|
+
if (!deps.confirm) {
|
|
618
|
+
throw new Error('Confirmation required. Use --confirm to destroy session.');
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
try {
|
|
622
|
+
const sessionManager = deps.sessionManager || new RemoteSessionManager();
|
|
623
|
+
|
|
624
|
+
const session = await sessionManager.getSession(sessionId);
|
|
625
|
+
|
|
626
|
+
if (!session) {
|
|
627
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
console.log(c.red(`\nDestroying session: ${sessionId}...`));
|
|
631
|
+
console.log(c.yellow('This will delete the remote machine and all resources.'));
|
|
632
|
+
|
|
633
|
+
if (deps.orchestrator) {
|
|
634
|
+
await deps.orchestrator.destroySession();
|
|
635
|
+
} else {
|
|
636
|
+
const provider = getProviderForSession(session, deps);
|
|
637
|
+
await provider.destroyMachine(session.machineId);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Unregister session
|
|
641
|
+
await sessionManager.unregisterSession(sessionId);
|
|
642
|
+
|
|
643
|
+
console.log(c.green('✓ Session destroyed'));
|
|
644
|
+
console.log(c.cyan('All remote resources have been cleaned up.'));
|
|
645
|
+
} catch (error) {
|
|
646
|
+
console.error(c.red(`Failed to destroy session: ${error.message}`));
|
|
647
|
+
throw error;
|
|
648
|
+
}
|
|
649
|
+
}
|