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.
@@ -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
+ }