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,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TranscriptExporter Interface
|
|
3
|
+
*
|
|
4
|
+
* Base class for CLI-specific transcript exporters.
|
|
5
|
+
* Converts Timeline API events to native CLI transcript formats.
|
|
6
|
+
*
|
|
7
|
+
* @module lib/teleport/exporters/interface
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { writeFile, mkdir } from 'fs/promises';
|
|
11
|
+
import { dirname } from 'path';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Timeline event types that can be exported
|
|
15
|
+
* @typedef {'user_message'|'assistant_response'|'tool_use'|'tool_result'|'thinking'|'compact_summary'} TimelineEventType
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Timeline event structure from the relay API
|
|
20
|
+
* @typedef {Object} TimelineEvent
|
|
21
|
+
* @property {string} id - Unique event ID
|
|
22
|
+
* @property {TimelineEventType} event_type - Type of event
|
|
23
|
+
* @property {Object} data - Event-specific data
|
|
24
|
+
* @property {number} timestamp - Unix timestamp in milliseconds
|
|
25
|
+
* @property {string} session_id - Parent session ID
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Export options
|
|
30
|
+
* @typedef {Object} ExportOptions
|
|
31
|
+
* @property {boolean} [includeThinking=true] - Include thinking blocks if supported
|
|
32
|
+
* @property {boolean} [prettyPrint=false] - Pretty-print JSON output
|
|
33
|
+
* @property {string} [projectSlug] - Project identifier for path generation
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Base class for transcript exporters.
|
|
38
|
+
*
|
|
39
|
+
* Subclasses must implement:
|
|
40
|
+
* - export(events, options) - Convert timeline events to CLI format
|
|
41
|
+
* - getTranscriptPath(sessionId, projectSlug) - Get native transcript path
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```js
|
|
45
|
+
* import { ClaudeTranscriptExporter } from './claude-exporter.js';
|
|
46
|
+
*
|
|
47
|
+
* const exporter = new ClaudeTranscriptExporter();
|
|
48
|
+
* const transcript = await exporter.export(timelineEvents);
|
|
49
|
+
* await exporter.write(timelineEvents, sessionId, projectSlug);
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export class TranscriptExporter {
|
|
53
|
+
/**
|
|
54
|
+
* The CLI name this exporter targets
|
|
55
|
+
* @type {string}
|
|
56
|
+
*/
|
|
57
|
+
name = 'base';
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Human-readable exporter name
|
|
61
|
+
* @type {string}
|
|
62
|
+
*/
|
|
63
|
+
displayName = 'Base Exporter';
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Export timeline events to CLI-native transcript format.
|
|
67
|
+
*
|
|
68
|
+
* @abstract
|
|
69
|
+
* @param {TimelineEvent[]} events - Timeline events from relay API
|
|
70
|
+
* @param {ExportOptions} [options={}] - Export options
|
|
71
|
+
* @returns {Promise<string>} Transcript content in CLI-native format
|
|
72
|
+
* @throws {Error} If events array is invalid or export fails
|
|
73
|
+
*/
|
|
74
|
+
async export(events, options = {}) {
|
|
75
|
+
throw new Error('TranscriptExporter.export() must be implemented by subclass');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get the native transcript file path for a CLI.
|
|
80
|
+
*
|
|
81
|
+
* @abstract
|
|
82
|
+
* @param {string} sessionId - Session identifier
|
|
83
|
+
* @param {string} projectSlug - Project slug/identifier
|
|
84
|
+
* @returns {string} Absolute path to transcript file
|
|
85
|
+
*/
|
|
86
|
+
getTranscriptPath(sessionId, projectSlug) {
|
|
87
|
+
throw new Error('TranscriptExporter.getTranscriptPath() must be implemented by subclass');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Export and write transcript to the CLI-native location.
|
|
92
|
+
*
|
|
93
|
+
* @param {TimelineEvent[]} events - Timeline events from relay API
|
|
94
|
+
* @param {string} sessionId - Session identifier
|
|
95
|
+
* @param {string} projectSlug - Project slug/identifier
|
|
96
|
+
* @param {ExportOptions} [options={}] - Export options
|
|
97
|
+
* @returns {Promise<string>} Path where transcript was written
|
|
98
|
+
*/
|
|
99
|
+
async write(events, sessionId, projectSlug, options = {}) {
|
|
100
|
+
const transcript = await this.export(events, { ...options, projectSlug });
|
|
101
|
+
const path = this.getTranscriptPath(sessionId, projectSlug);
|
|
102
|
+
|
|
103
|
+
// Ensure directory exists
|
|
104
|
+
await mkdir(dirname(path), { recursive: true });
|
|
105
|
+
|
|
106
|
+
// Write transcript
|
|
107
|
+
await writeFile(path, transcript, 'utf-8');
|
|
108
|
+
|
|
109
|
+
return path;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Validate timeline events array.
|
|
114
|
+
*
|
|
115
|
+
* @protected
|
|
116
|
+
* @param {TimelineEvent[]} events - Events to validate
|
|
117
|
+
* @throws {Error} If events are invalid
|
|
118
|
+
*/
|
|
119
|
+
validateEvents(events) {
|
|
120
|
+
if (!Array.isArray(events)) {
|
|
121
|
+
throw new Error('Events must be an array');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Empty array is valid (new session with no messages)
|
|
125
|
+
if (events.length === 0) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Check each event has required fields
|
|
130
|
+
for (let i = 0; i < events.length; i++) {
|
|
131
|
+
const event = events[i];
|
|
132
|
+
if (!event || typeof event !== 'object') {
|
|
133
|
+
throw new Error(`Event at index ${i} is not an object`);
|
|
134
|
+
}
|
|
135
|
+
if (!event.event_type) {
|
|
136
|
+
throw new Error(`Event at index ${i} is missing event_type`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Sort events by timestamp.
|
|
143
|
+
*
|
|
144
|
+
* @protected
|
|
145
|
+
* @param {TimelineEvent[]} events - Events to sort
|
|
146
|
+
* @returns {TimelineEvent[]} Sorted events (oldest first)
|
|
147
|
+
*/
|
|
148
|
+
sortEvents(events) {
|
|
149
|
+
return [...events].sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export default TranscriptExporter;
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fork Tracker
|
|
3
|
+
*
|
|
4
|
+
* Tracks parallel sessions when using fork mode teleportation.
|
|
5
|
+
* Monitors both local and cloud sessions, detects conflicts,
|
|
6
|
+
* and provides coordination utilities.
|
|
7
|
+
*
|
|
8
|
+
* @module lib/teleport/fork-tracker
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { exec } from 'child_process';
|
|
12
|
+
import { promisify } from 'util';
|
|
13
|
+
|
|
14
|
+
const execAsync = promisify(exec);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Fork session state
|
|
18
|
+
* @enum {string}
|
|
19
|
+
*/
|
|
20
|
+
export const ForkSessionState = {
|
|
21
|
+
/** Session is active and making changes */
|
|
22
|
+
ACTIVE: 'active',
|
|
23
|
+
/** Session is idle (no recent activity) */
|
|
24
|
+
IDLE: 'idle',
|
|
25
|
+
/** Session has completed */
|
|
26
|
+
COMPLETED: 'completed',
|
|
27
|
+
/** Session encountered an error */
|
|
28
|
+
ERRORED: 'errored',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* ForkTracker manages parallel sessions in fork mode.
|
|
33
|
+
*/
|
|
34
|
+
export class ForkTracker {
|
|
35
|
+
/**
|
|
36
|
+
* Initialize ForkTracker
|
|
37
|
+
*
|
|
38
|
+
* @param {Object} config - Configuration
|
|
39
|
+
* @param {string} config.teleportId - Teleport ID
|
|
40
|
+
* @param {string} config.repoPath - Path to git repository
|
|
41
|
+
* @param {string} config.localBranch - Local session branch
|
|
42
|
+
* @param {string} config.cloudBranch - Cloud session branch
|
|
43
|
+
*/
|
|
44
|
+
constructor(config) {
|
|
45
|
+
if (!config) {
|
|
46
|
+
throw new Error('ForkTracker config is required');
|
|
47
|
+
}
|
|
48
|
+
if (!config.teleportId) {
|
|
49
|
+
throw new Error('teleportId is required');
|
|
50
|
+
}
|
|
51
|
+
if (!config.repoPath) {
|
|
52
|
+
throw new Error('repoPath is required');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.teleportId = config.teleportId;
|
|
56
|
+
this.repoPath = config.repoPath;
|
|
57
|
+
this.localBranch = config.localBranch || 'HEAD';
|
|
58
|
+
this.cloudBranch = config.cloudBranch || `teleport/${config.teleportId}`;
|
|
59
|
+
|
|
60
|
+
// Session states
|
|
61
|
+
this.localSession = {
|
|
62
|
+
state: ForkSessionState.ACTIVE,
|
|
63
|
+
lastActivityAt: new Date().toISOString(),
|
|
64
|
+
commitCount: 0,
|
|
65
|
+
lastCommitSha: null,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
this.cloudSession = {
|
|
69
|
+
state: ForkSessionState.ACTIVE,
|
|
70
|
+
lastActivityAt: new Date().toISOString(),
|
|
71
|
+
commitCount: 0,
|
|
72
|
+
lastCommitSha: null,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Fork point (common ancestor)
|
|
76
|
+
this.forkPoint = null;
|
|
77
|
+
|
|
78
|
+
// Conflict tracking
|
|
79
|
+
this.conflicts = [];
|
|
80
|
+
this.lastConflictCheck = null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Initialize fork tracking.
|
|
85
|
+
* Records the fork point and initial state.
|
|
86
|
+
*
|
|
87
|
+
* @returns {Promise<Object>} Fork point info
|
|
88
|
+
*/
|
|
89
|
+
async initialize() {
|
|
90
|
+
const cwd = this.repoPath;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
// Record current commit as fork point
|
|
94
|
+
const { stdout: sha } = await execAsync('git rev-parse HEAD', { cwd });
|
|
95
|
+
this.forkPoint = sha.trim();
|
|
96
|
+
|
|
97
|
+
// Get branch info
|
|
98
|
+
const { stdout: branch } = await execAsync('git rev-parse --abbrev-ref HEAD', { cwd });
|
|
99
|
+
this.localBranch = branch.trim();
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
forkPoint: this.forkPoint,
|
|
103
|
+
localBranch: this.localBranch,
|
|
104
|
+
cloudBranch: this.cloudBranch,
|
|
105
|
+
timestamp: new Date().toISOString(),
|
|
106
|
+
};
|
|
107
|
+
} catch (error) {
|
|
108
|
+
throw new Error(`Failed to initialize fork tracker: ${error.message}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Update local session state.
|
|
114
|
+
*
|
|
115
|
+
* @param {Object} update - Update data
|
|
116
|
+
* @param {ForkSessionState} [update.state] - New state
|
|
117
|
+
* @param {string} [update.commitSha] - Latest commit SHA
|
|
118
|
+
*/
|
|
119
|
+
updateLocalSession(update) {
|
|
120
|
+
if (update.state) {
|
|
121
|
+
this.localSession.state = update.state;
|
|
122
|
+
}
|
|
123
|
+
if (update.commitSha) {
|
|
124
|
+
this.localSession.lastCommitSha = update.commitSha;
|
|
125
|
+
this.localSession.commitCount++;
|
|
126
|
+
}
|
|
127
|
+
this.localSession.lastActivityAt = new Date().toISOString();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Update cloud session state.
|
|
132
|
+
*
|
|
133
|
+
* @param {Object} update - Update data
|
|
134
|
+
* @param {ForkSessionState} [update.state] - New state
|
|
135
|
+
* @param {string} [update.commitSha] - Latest commit SHA
|
|
136
|
+
*/
|
|
137
|
+
updateCloudSession(update) {
|
|
138
|
+
if (update.state) {
|
|
139
|
+
this.cloudSession.state = update.state;
|
|
140
|
+
}
|
|
141
|
+
if (update.commitSha) {
|
|
142
|
+
this.cloudSession.lastCommitSha = update.commitSha;
|
|
143
|
+
this.cloudSession.commitCount++;
|
|
144
|
+
}
|
|
145
|
+
this.cloudSession.lastActivityAt = new Date().toISOString();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Check for conflicts between local and cloud branches.
|
|
150
|
+
*
|
|
151
|
+
* @returns {Promise<Object>} Conflict check result
|
|
152
|
+
*/
|
|
153
|
+
async checkConflicts() {
|
|
154
|
+
const cwd = this.repoPath;
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
// Try to fetch cloud branch from origin (ignore if no remote)
|
|
158
|
+
await execAsync(`git fetch origin ${this.cloudBranch}`, { cwd }).catch(() => {});
|
|
159
|
+
|
|
160
|
+
// Determine cloud branch reference (origin/ prefix if remote exists, otherwise local)
|
|
161
|
+
let cloudRef;
|
|
162
|
+
try {
|
|
163
|
+
await execAsync(`git rev-parse origin/${this.cloudBranch}`, { cwd });
|
|
164
|
+
cloudRef = `origin/${this.cloudBranch}`;
|
|
165
|
+
} catch {
|
|
166
|
+
// No remote - try local branch
|
|
167
|
+
try {
|
|
168
|
+
await execAsync(`git rev-parse ${this.cloudBranch}`, { cwd });
|
|
169
|
+
cloudRef = this.cloudBranch;
|
|
170
|
+
} catch {
|
|
171
|
+
// Cloud branch doesn't exist yet
|
|
172
|
+
this.lastConflictCheck = new Date().toISOString();
|
|
173
|
+
this.conflicts = [];
|
|
174
|
+
return {
|
|
175
|
+
hasConflicts: false,
|
|
176
|
+
conflictingFiles: [],
|
|
177
|
+
localChanges: 0,
|
|
178
|
+
cloudChanges: 0,
|
|
179
|
+
checkedAt: this.lastConflictCheck,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Get files changed in local branch since fork point
|
|
185
|
+
const { stdout: localChanges } = await execAsync(
|
|
186
|
+
`git diff --name-only ${this.forkPoint}..HEAD`,
|
|
187
|
+
{ cwd }
|
|
188
|
+
).catch(() => ({ stdout: '' }));
|
|
189
|
+
|
|
190
|
+
// Get files changed in cloud branch since fork point
|
|
191
|
+
const { stdout: cloudChanges } = await execAsync(
|
|
192
|
+
`git diff --name-only ${this.forkPoint}..${cloudRef}`,
|
|
193
|
+
{ cwd }
|
|
194
|
+
).catch(() => ({ stdout: '' }));
|
|
195
|
+
|
|
196
|
+
const localFiles = localChanges.trim().split('\n').filter(Boolean);
|
|
197
|
+
const cloudFiles = cloudChanges.trim().split('\n').filter(Boolean);
|
|
198
|
+
|
|
199
|
+
// Find overlapping files (potential conflicts)
|
|
200
|
+
const conflictingFiles = localFiles.filter(f => cloudFiles.includes(f));
|
|
201
|
+
|
|
202
|
+
this.lastConflictCheck = new Date().toISOString();
|
|
203
|
+
|
|
204
|
+
if (conflictingFiles.length > 0) {
|
|
205
|
+
this.conflicts = conflictingFiles;
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
hasConflicts: true,
|
|
209
|
+
conflictingFiles,
|
|
210
|
+
localChanges: localFiles.length,
|
|
211
|
+
cloudChanges: cloudFiles.length,
|
|
212
|
+
checkedAt: this.lastConflictCheck,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
this.conflicts = [];
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
hasConflicts: false,
|
|
220
|
+
conflictingFiles: [],
|
|
221
|
+
localChanges: localFiles.length,
|
|
222
|
+
cloudChanges: cloudFiles.length,
|
|
223
|
+
checkedAt: this.lastConflictCheck,
|
|
224
|
+
};
|
|
225
|
+
} catch (error) {
|
|
226
|
+
throw new Error(`Failed to check conflicts: ${error.message}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Get divergence statistics between branches.
|
|
232
|
+
*
|
|
233
|
+
* @returns {Promise<Object>} Divergence stats
|
|
234
|
+
*/
|
|
235
|
+
async getDivergence() {
|
|
236
|
+
const cwd = this.repoPath;
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
// Try to fetch cloud branch from origin (ignore if no remote)
|
|
240
|
+
await execAsync(`git fetch origin ${this.cloudBranch}`, { cwd }).catch(() => {});
|
|
241
|
+
|
|
242
|
+
// Determine cloud branch reference
|
|
243
|
+
let cloudRef;
|
|
244
|
+
try {
|
|
245
|
+
await execAsync(`git rev-parse origin/${this.cloudBranch}`, { cwd });
|
|
246
|
+
cloudRef = `origin/${this.cloudBranch}`;
|
|
247
|
+
} catch {
|
|
248
|
+
// Try local branch
|
|
249
|
+
try {
|
|
250
|
+
await execAsync(`git rev-parse ${this.cloudBranch}`, { cwd });
|
|
251
|
+
cloudRef = this.cloudBranch;
|
|
252
|
+
} catch {
|
|
253
|
+
// Cloud branch doesn't exist - count local commits from fork point
|
|
254
|
+
if (this.forkPoint) {
|
|
255
|
+
const { stdout } = await execAsync(
|
|
256
|
+
`git rev-list --count ${this.forkPoint}..HEAD`,
|
|
257
|
+
{ cwd }
|
|
258
|
+
).catch(() => ({ stdout: '0' }));
|
|
259
|
+
return {
|
|
260
|
+
localAhead: parseInt(stdout.trim(), 10) || 0,
|
|
261
|
+
cloudAhead: 0,
|
|
262
|
+
diverged: false,
|
|
263
|
+
forkPoint: this.forkPoint,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
localAhead: this.localSession.commitCount,
|
|
268
|
+
cloudAhead: 0,
|
|
269
|
+
diverged: false,
|
|
270
|
+
forkPoint: this.forkPoint,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Count commits ahead/behind
|
|
276
|
+
const { stdout: aheadBehind } = await execAsync(
|
|
277
|
+
`git rev-list --left-right --count HEAD...${cloudRef}`,
|
|
278
|
+
{ cwd }
|
|
279
|
+
).catch(() => ({ stdout: '0\t0' }));
|
|
280
|
+
|
|
281
|
+
const [localAhead, cloudAhead] = aheadBehind.trim().split('\t').map(Number);
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
localAhead,
|
|
285
|
+
cloudAhead,
|
|
286
|
+
diverged: localAhead > 0 && cloudAhead > 0,
|
|
287
|
+
forkPoint: this.forkPoint,
|
|
288
|
+
};
|
|
289
|
+
} catch (error) {
|
|
290
|
+
// If cloud branch doesn't exist yet, only local has commits
|
|
291
|
+
return {
|
|
292
|
+
localAhead: this.localSession.commitCount,
|
|
293
|
+
cloudAhead: 0,
|
|
294
|
+
diverged: false,
|
|
295
|
+
forkPoint: this.forkPoint,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Generate merge plan for combining fork branches.
|
|
302
|
+
*
|
|
303
|
+
* @returns {Promise<Object>} Merge plan
|
|
304
|
+
*/
|
|
305
|
+
async generateMergePlan() {
|
|
306
|
+
const conflicts = await this.checkConflicts();
|
|
307
|
+
const divergence = await this.getDivergence();
|
|
308
|
+
|
|
309
|
+
const plan = {
|
|
310
|
+
canAutoMerge: !conflicts.hasConflicts,
|
|
311
|
+
conflicts: conflicts.conflictingFiles,
|
|
312
|
+
divergence,
|
|
313
|
+
recommendedStrategy: null,
|
|
314
|
+
steps: [],
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
if (!conflicts.hasConflicts) {
|
|
318
|
+
// No conflicts - can auto-merge
|
|
319
|
+
if (divergence.localAhead === 0) {
|
|
320
|
+
plan.recommendedStrategy = 'fast-forward';
|
|
321
|
+
plan.steps = [
|
|
322
|
+
`git fetch origin ${this.cloudBranch}`,
|
|
323
|
+
`git merge origin/${this.cloudBranch}`,
|
|
324
|
+
];
|
|
325
|
+
} else if (divergence.cloudAhead === 0) {
|
|
326
|
+
plan.recommendedStrategy = 'push';
|
|
327
|
+
plan.steps = [
|
|
328
|
+
`git push origin ${this.localBranch}:${this.cloudBranch}`,
|
|
329
|
+
];
|
|
330
|
+
} else {
|
|
331
|
+
plan.recommendedStrategy = 'merge';
|
|
332
|
+
plan.steps = [
|
|
333
|
+
`git fetch origin ${this.cloudBranch}`,
|
|
334
|
+
`git merge origin/${this.cloudBranch}`,
|
|
335
|
+
'git push origin HEAD',
|
|
336
|
+
];
|
|
337
|
+
}
|
|
338
|
+
} else {
|
|
339
|
+
// Has conflicts - manual resolution needed
|
|
340
|
+
plan.recommendedStrategy = 'manual';
|
|
341
|
+
plan.steps = [
|
|
342
|
+
`git fetch origin ${this.cloudBranch}`,
|
|
343
|
+
`git merge origin/${this.cloudBranch}`,
|
|
344
|
+
'# Resolve conflicts manually',
|
|
345
|
+
'git add <resolved-files>',
|
|
346
|
+
'git commit -m "Merge cloud session changes"',
|
|
347
|
+
'git push origin HEAD',
|
|
348
|
+
];
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return plan;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Get summary of fork state.
|
|
356
|
+
*
|
|
357
|
+
* @returns {Object} Fork state summary
|
|
358
|
+
*/
|
|
359
|
+
getSummary() {
|
|
360
|
+
return {
|
|
361
|
+
teleportId: this.teleportId,
|
|
362
|
+
forkPoint: this.forkPoint,
|
|
363
|
+
localSession: {
|
|
364
|
+
...this.localSession,
|
|
365
|
+
branch: this.localBranch,
|
|
366
|
+
},
|
|
367
|
+
cloudSession: {
|
|
368
|
+
...this.cloudSession,
|
|
369
|
+
branch: this.cloudBranch,
|
|
370
|
+
},
|
|
371
|
+
conflicts: this.conflicts,
|
|
372
|
+
lastConflictCheck: this.lastConflictCheck,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Check if both sessions are still active.
|
|
378
|
+
*
|
|
379
|
+
* @returns {boolean} True if both sessions are active
|
|
380
|
+
*/
|
|
381
|
+
areBothSessionsActive() {
|
|
382
|
+
return (
|
|
383
|
+
this.localSession.state === ForkSessionState.ACTIVE &&
|
|
384
|
+
this.cloudSession.state === ForkSessionState.ACTIVE
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Mark local session as completed.
|
|
390
|
+
*
|
|
391
|
+
* @param {Object} [result] - Completion result
|
|
392
|
+
*/
|
|
393
|
+
completeLocalSession(result) {
|
|
394
|
+
this.localSession.state = ForkSessionState.COMPLETED;
|
|
395
|
+
this.localSession.completedAt = new Date().toISOString();
|
|
396
|
+
if (result) {
|
|
397
|
+
this.localSession.result = result;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Mark cloud session as completed.
|
|
403
|
+
*
|
|
404
|
+
* @param {Object} [result] - Completion result
|
|
405
|
+
*/
|
|
406
|
+
completeCloudSession(result) {
|
|
407
|
+
this.cloudSession.state = ForkSessionState.COMPLETED;
|
|
408
|
+
this.cloudSession.completedAt = new Date().toISOString();
|
|
409
|
+
if (result) {
|
|
410
|
+
this.cloudSession.result = result;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export default ForkTracker;
|