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
|
@@ -16,6 +16,51 @@ import { MachineCoder, CODER_NAMES } from './interface.js';
|
|
|
16
16
|
|
|
17
17
|
const execAsync = promisify(exec);
|
|
18
18
|
|
|
19
|
+
// Grace period before killing process after receiving result (allows hooks to finish)
|
|
20
|
+
// Configurable via environment variable for production tuning
|
|
21
|
+
const PROCESS_KILL_GRACE_MS = parseInt(process.env.TELEPORTATION_PROCESS_GRACE_MS || '50', 10);
|
|
22
|
+
|
|
23
|
+
// Environment variables to pass to Claude Code process (allowlist for security)
|
|
24
|
+
// Only pass essential env vars instead of spreading all of process.env
|
|
25
|
+
const ALLOWED_ENV_VARS = [
|
|
26
|
+
'HOME',
|
|
27
|
+
'PATH',
|
|
28
|
+
'SHELL',
|
|
29
|
+
'USER',
|
|
30
|
+
'TERM',
|
|
31
|
+
'LANG',
|
|
32
|
+
'LC_ALL',
|
|
33
|
+
'TMPDIR',
|
|
34
|
+
'XDG_CONFIG_HOME',
|
|
35
|
+
'XDG_DATA_HOME',
|
|
36
|
+
'XDG_CACHE_HOME',
|
|
37
|
+
// Claude Code specific
|
|
38
|
+
'ANTHROPIC_API_KEY',
|
|
39
|
+
'CLAUDE_CODE_USE_BEDROCK',
|
|
40
|
+
'CLAUDE_CODE_USE_VERTEX',
|
|
41
|
+
// Node.js
|
|
42
|
+
'NODE_ENV',
|
|
43
|
+
'NODE_OPTIONS',
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Build a filtered environment object for spawning Claude Code
|
|
48
|
+
* Uses allowlist approach for security - only passes essential env vars
|
|
49
|
+
* @param {string} sessionId - Teleportation session ID to pass
|
|
50
|
+
* @returns {Object} Environment object
|
|
51
|
+
*/
|
|
52
|
+
function buildSecureEnv(sessionId) {
|
|
53
|
+
const env = {};
|
|
54
|
+
for (const key of ALLOWED_ENV_VARS) {
|
|
55
|
+
if (process.env[key] !== undefined) {
|
|
56
|
+
env[key] = process.env[key];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Always pass session ID to hooks
|
|
60
|
+
env.TELEPORTATION_SESSION_ID = sessionId;
|
|
61
|
+
return env;
|
|
62
|
+
}
|
|
63
|
+
|
|
19
64
|
/**
|
|
20
65
|
* Check if a command exists on the system
|
|
21
66
|
* @param {string} command
|
|
@@ -36,9 +81,25 @@ async function commandExists(command) {
|
|
|
36
81
|
export class ClaudeCodeAdapter extends MachineCoder {
|
|
37
82
|
name = CODER_NAMES.CLAUDE_CODE;
|
|
38
83
|
displayName = 'Claude Code';
|
|
39
|
-
|
|
84
|
+
|
|
40
85
|
// Track running processes for stop()
|
|
41
86
|
#runningProcesses = new Map();
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Close stdin immediately to prevent Claude Code from hanging
|
|
90
|
+
* Claude Code waits for stdin to close when spawned - without this,
|
|
91
|
+
* the process hangs indefinitely waiting for possible input.
|
|
92
|
+
* @param {ChildProcess} proc - The spawned process
|
|
93
|
+
* @private
|
|
94
|
+
*/
|
|
95
|
+
#closeStdinSafely(proc) {
|
|
96
|
+
try {
|
|
97
|
+
proc.stdin.end();
|
|
98
|
+
} catch (err) {
|
|
99
|
+
// Log but don't throw - stdin might already be closed or in error state
|
|
100
|
+
console.warn('[claude-adapter] Failed to close stdin:', err.message);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
42
103
|
|
|
43
104
|
/**
|
|
44
105
|
* Check if Claude Code CLI is available
|
|
@@ -102,40 +163,39 @@ export class ClaudeCodeAdapter extends MachineCoder {
|
|
|
102
163
|
|
|
103
164
|
// Output format for parsing
|
|
104
165
|
args.push('--output-format', 'json');
|
|
105
|
-
|
|
166
|
+
|
|
106
167
|
return new Promise((resolve, reject) => {
|
|
107
168
|
const startTime = Date.now();
|
|
108
169
|
let stdout = '';
|
|
109
170
|
let stderr = '';
|
|
110
171
|
const toolCalls = [];
|
|
111
|
-
|
|
172
|
+
let resolved = false; // Prevent double-resolution race condition
|
|
173
|
+
let killTimeout = null; // Track kill timeout for cleanup
|
|
174
|
+
|
|
112
175
|
const proc = spawn('claude', args, {
|
|
113
176
|
cwd: projectPath,
|
|
114
|
-
env:
|
|
115
|
-
...process.env,
|
|
116
|
-
// Pass session ID to hooks
|
|
117
|
-
TELEPORTATION_SESSION_ID: sessionId,
|
|
118
|
-
},
|
|
177
|
+
env: buildSecureEnv(sessionId),
|
|
119
178
|
});
|
|
120
|
-
|
|
179
|
+
|
|
121
180
|
this.#runningProcesses.set(executionId, proc);
|
|
122
|
-
|
|
181
|
+
this.#closeStdinSafely(proc);
|
|
182
|
+
|
|
123
183
|
// Timeout handling
|
|
124
184
|
const timeout = setTimeout(() => {
|
|
125
185
|
proc.kill('SIGTERM');
|
|
126
186
|
reject(new Error(`Execution timed out after ${timeoutMs}ms`));
|
|
127
187
|
}, timeoutMs);
|
|
128
|
-
|
|
188
|
+
|
|
129
189
|
proc.stdout.on('data', (data) => {
|
|
130
190
|
const chunk = data.toString();
|
|
131
191
|
stdout += chunk;
|
|
132
|
-
|
|
192
|
+
|
|
133
193
|
// Try to parse JSON events for progress
|
|
134
194
|
try {
|
|
135
195
|
const lines = chunk.split('\n').filter(l => l.trim());
|
|
136
196
|
for (const line of lines) {
|
|
137
197
|
const event = JSON.parse(line);
|
|
138
|
-
|
|
198
|
+
|
|
139
199
|
// Track tool calls
|
|
140
200
|
if (event.type === 'tool_use') {
|
|
141
201
|
toolCalls.push({
|
|
@@ -145,12 +205,49 @@ export class ClaudeCodeAdapter extends MachineCoder {
|
|
|
145
205
|
timestamp: Date.now(),
|
|
146
206
|
});
|
|
147
207
|
}
|
|
148
|
-
|
|
208
|
+
|
|
149
209
|
onProgress?.({
|
|
150
210
|
type: event.type,
|
|
151
211
|
timestamp: Date.now(),
|
|
152
212
|
data: event,
|
|
153
213
|
});
|
|
214
|
+
|
|
215
|
+
// When we receive a final result, resolve early and kill the process
|
|
216
|
+
// This handles cases where hooks keep the process alive after completion
|
|
217
|
+
if (event.type === 'result') {
|
|
218
|
+
if (resolved) return; // Prevent double-resolution
|
|
219
|
+
resolved = true;
|
|
220
|
+
|
|
221
|
+
clearTimeout(timeout);
|
|
222
|
+
this.#runningProcesses.delete(executionId);
|
|
223
|
+
|
|
224
|
+
const durationMs = Date.now() - startTime;
|
|
225
|
+
const output = event.result || event.response || event.output || '';
|
|
226
|
+
const stats = { durationMs };
|
|
227
|
+
|
|
228
|
+
if (event.usage) {
|
|
229
|
+
stats.tokensUsed = event.usage.total_tokens;
|
|
230
|
+
}
|
|
231
|
+
if (event.cost || event.total_cost_usd) {
|
|
232
|
+
stats.cost = event.cost || event.total_cost_usd;
|
|
233
|
+
}
|
|
234
|
+
if (event.model) {
|
|
235
|
+
stats.model = event.model;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Kill the process since we have the result (small delay for hooks to finish)
|
|
239
|
+
killTimeout = setTimeout(() => proc.kill('SIGTERM'), PROCESS_KILL_GRACE_MS);
|
|
240
|
+
|
|
241
|
+
resolve({
|
|
242
|
+
success: !event.is_error,
|
|
243
|
+
output,
|
|
244
|
+
error: event.is_error ? (event.error || 'Unknown error') : undefined,
|
|
245
|
+
toolCalls,
|
|
246
|
+
stats,
|
|
247
|
+
executionId,
|
|
248
|
+
});
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
154
251
|
}
|
|
155
252
|
} catch {
|
|
156
253
|
// Not JSON, just raw output
|
|
@@ -172,15 +269,19 @@ export class ClaudeCodeAdapter extends MachineCoder {
|
|
|
172
269
|
});
|
|
173
270
|
|
|
174
271
|
proc.on('close', (code) => {
|
|
272
|
+
if (resolved) return; // Prevent double-resolution
|
|
273
|
+
resolved = true;
|
|
274
|
+
|
|
175
275
|
clearTimeout(timeout);
|
|
276
|
+
if (killTimeout) clearTimeout(killTimeout); // Clean up kill timeout if process exits naturally
|
|
176
277
|
this.#runningProcesses.delete(executionId);
|
|
177
|
-
|
|
278
|
+
|
|
178
279
|
const durationMs = Date.now() - startTime;
|
|
179
|
-
|
|
280
|
+
|
|
180
281
|
// Try to parse final JSON output
|
|
181
282
|
let output = stdout;
|
|
182
283
|
let stats = { durationMs };
|
|
183
|
-
|
|
284
|
+
|
|
184
285
|
try {
|
|
185
286
|
const result = JSON.parse(stdout);
|
|
186
287
|
output = result.result || result.response || result.output || stdout;
|
|
@@ -196,7 +297,7 @@ export class ClaudeCodeAdapter extends MachineCoder {
|
|
|
196
297
|
} catch {
|
|
197
298
|
// Not JSON, use raw output
|
|
198
299
|
}
|
|
199
|
-
|
|
300
|
+
|
|
200
301
|
resolve({
|
|
201
302
|
success: code === 0,
|
|
202
303
|
output,
|
|
@@ -238,45 +339,98 @@ export class ClaudeCodeAdapter extends MachineCoder {
|
|
|
238
339
|
}
|
|
239
340
|
|
|
240
341
|
args.push('--output-format', 'json');
|
|
241
|
-
|
|
342
|
+
|
|
242
343
|
return new Promise((resolve, reject) => {
|
|
243
344
|
const startTime = Date.now();
|
|
244
345
|
let stdout = '';
|
|
245
346
|
let stderr = '';
|
|
246
347
|
const toolCalls = [];
|
|
247
|
-
|
|
348
|
+
let resolved = false; // Prevent double-resolution race condition
|
|
349
|
+
let killTimeout = null; // Track kill timeout for cleanup
|
|
350
|
+
|
|
248
351
|
const proc = spawn('claude', args, {
|
|
249
352
|
cwd: projectPath,
|
|
250
|
-
env:
|
|
251
|
-
...process.env,
|
|
252
|
-
TELEPORTATION_SESSION_ID: sessionId,
|
|
253
|
-
},
|
|
353
|
+
env: buildSecureEnv(sessionId),
|
|
254
354
|
});
|
|
255
|
-
|
|
355
|
+
|
|
256
356
|
this.#runningProcesses.set(executionId, proc);
|
|
257
|
-
|
|
357
|
+
this.#closeStdinSafely(proc);
|
|
358
|
+
|
|
258
359
|
const timeout = setTimeout(() => {
|
|
259
360
|
proc.kill('SIGTERM');
|
|
260
361
|
reject(new Error(`Resume timed out after ${timeoutMs}ms`));
|
|
261
362
|
}, timeoutMs);
|
|
262
|
-
|
|
363
|
+
|
|
263
364
|
proc.stdout.on('data', (data) => {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
365
|
+
const chunk = data.toString();
|
|
366
|
+
stdout += chunk;
|
|
367
|
+
|
|
368
|
+
// Try to parse JSON result
|
|
369
|
+
try {
|
|
370
|
+
const lines = chunk.split('\n').filter(l => l.trim());
|
|
371
|
+
for (const line of lines) {
|
|
372
|
+
const event = JSON.parse(line);
|
|
373
|
+
|
|
374
|
+
onProgress?.({
|
|
375
|
+
type: event.type || 'output',
|
|
376
|
+
timestamp: Date.now(),
|
|
377
|
+
data: event,
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// When we receive a final result, resolve early and kill the process
|
|
381
|
+
if (event.type === 'result') {
|
|
382
|
+
if (resolved) return; // Prevent double-resolution
|
|
383
|
+
resolved = true;
|
|
384
|
+
|
|
385
|
+
clearTimeout(timeout);
|
|
386
|
+
this.#runningProcesses.delete(executionId);
|
|
387
|
+
|
|
388
|
+
const output = event.result || event.response || event.output || '';
|
|
389
|
+
const stats = { durationMs: Date.now() - startTime };
|
|
390
|
+
|
|
391
|
+
if (event.usage) {
|
|
392
|
+
stats.tokensUsed = event.usage.total_tokens;
|
|
393
|
+
}
|
|
394
|
+
if (event.cost || event.total_cost_usd) {
|
|
395
|
+
stats.cost = event.cost || event.total_cost_usd;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Kill the process since we have the result (small delay for hooks to finish)
|
|
399
|
+
killTimeout = setTimeout(() => proc.kill('SIGTERM'), PROCESS_KILL_GRACE_MS);
|
|
400
|
+
|
|
401
|
+
resolve({
|
|
402
|
+
success: !event.is_error,
|
|
403
|
+
output,
|
|
404
|
+
error: event.is_error ? (event.error || 'Unknown error') : undefined,
|
|
405
|
+
toolCalls,
|
|
406
|
+
stats,
|
|
407
|
+
executionId,
|
|
408
|
+
});
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
} catch {
|
|
413
|
+
// Not JSON, just raw output
|
|
414
|
+
onProgress?.({
|
|
415
|
+
type: 'output',
|
|
416
|
+
timestamp: Date.now(),
|
|
417
|
+
data: { text: chunk },
|
|
418
|
+
});
|
|
419
|
+
}
|
|
270
420
|
});
|
|
271
|
-
|
|
421
|
+
|
|
272
422
|
proc.stderr.on('data', (data) => {
|
|
273
423
|
stderr += data.toString();
|
|
274
424
|
});
|
|
275
|
-
|
|
425
|
+
|
|
276
426
|
proc.on('close', (code) => {
|
|
427
|
+
if (resolved) return; // Prevent double-resolution
|
|
428
|
+
resolved = true;
|
|
429
|
+
|
|
277
430
|
clearTimeout(timeout);
|
|
431
|
+
if (killTimeout) clearTimeout(killTimeout); // Clean up kill timeout if process exits naturally
|
|
278
432
|
this.#runningProcesses.delete(executionId);
|
|
279
|
-
|
|
433
|
+
|
|
280
434
|
resolve({
|
|
281
435
|
success: code === 0,
|
|
282
436
|
output: stdout,
|
|
@@ -286,7 +440,7 @@ export class ClaudeCodeAdapter extends MachineCoder {
|
|
|
286
440
|
executionId,
|
|
287
441
|
});
|
|
288
442
|
});
|
|
289
|
-
|
|
443
|
+
|
|
290
444
|
proc.on('error', (err) => {
|
|
291
445
|
clearTimeout(timeout);
|
|
292
446
|
this.#runningProcesses.delete(executionId);
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodeSync
|
|
3
|
+
*
|
|
4
|
+
* Manages git state synchronization between local and remote machines:
|
|
5
|
+
* - Capture local state (branch, commit, uncommitted changes)
|
|
6
|
+
* - Sync code to remote machine
|
|
7
|
+
* - Pull changes from remote
|
|
8
|
+
* - Conflict detection
|
|
9
|
+
*
|
|
10
|
+
* Integrates with existing GitHandoff system from lib/handoff/git-handoff.js
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { GitHandoff } from '../handoff/git-handoff.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Local state structure
|
|
17
|
+
* @typedef {Object} LocalState
|
|
18
|
+
* @property {string} branch - Current branch name
|
|
19
|
+
* @property {string} commit - Current commit hash
|
|
20
|
+
* @property {string} remoteUrl - Remote repository URL
|
|
21
|
+
* @property {Array<string>} uncommittedChanges - List of uncommitted files
|
|
22
|
+
* @property {boolean} hasUncommitted - Whether there are uncommitted changes
|
|
23
|
+
* @property {number} timestamp - Capture timestamp
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* CodeSync class
|
|
28
|
+
*/
|
|
29
|
+
export class CodeSync {
|
|
30
|
+
/**
|
|
31
|
+
* @param {Object} options
|
|
32
|
+
* @param {string} options.repoPath - Path to git repository
|
|
33
|
+
* @param {string} options.sessionId - Session identifier
|
|
34
|
+
* @param {Object} [options.gitHandoff] - GitHandoff instance (optional, will create if not provided)
|
|
35
|
+
*/
|
|
36
|
+
constructor(options) {
|
|
37
|
+
const { repoPath, sessionId, gitHandoff } = options;
|
|
38
|
+
|
|
39
|
+
if (!repoPath) {
|
|
40
|
+
throw new Error('repoPath is required');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!sessionId) {
|
|
44
|
+
throw new Error('sessionId is required');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
this.repoPath = repoPath;
|
|
48
|
+
this.sessionId = sessionId;
|
|
49
|
+
|
|
50
|
+
// Use provided GitHandoff or create new one
|
|
51
|
+
this.gitHandoff = gitHandoff || new GitHandoff({
|
|
52
|
+
repoPath,
|
|
53
|
+
sessionId,
|
|
54
|
+
branchPrefix: 'teleport/wip-'
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Capture current local git state
|
|
60
|
+
* @returns {Promise<LocalState>}
|
|
61
|
+
*/
|
|
62
|
+
async captureLocalState() {
|
|
63
|
+
// Verify git repository
|
|
64
|
+
const isRepo = await this.gitHandoff.isGitRepo();
|
|
65
|
+
if (!isRepo) {
|
|
66
|
+
throw new Error(`Not a git repository: ${this.repoPath}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Get current state
|
|
70
|
+
const branch = await this.gitHandoff.getCurrentBranch();
|
|
71
|
+
const commit = await this.gitHandoff.getCurrentCommit();
|
|
72
|
+
const remoteUrl = await this.gitHandoff.getRemoteUrl();
|
|
73
|
+
|
|
74
|
+
if (!remoteUrl) {
|
|
75
|
+
throw new Error('No remote URL configured');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const hasUncommitted = await this.gitHandoff.hasUncommittedChanges();
|
|
79
|
+
const uncommittedChanges = hasUncommitted
|
|
80
|
+
? await this.gitHandoff.getChangedFiles()
|
|
81
|
+
: [];
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
branch,
|
|
85
|
+
commit,
|
|
86
|
+
remoteUrl,
|
|
87
|
+
uncommittedChanges,
|
|
88
|
+
hasUncommitted,
|
|
89
|
+
timestamp: Date.now()
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Sync code to remote machine
|
|
95
|
+
* @param {Object} options
|
|
96
|
+
* @param {string} options.machineId - Remote machine ID
|
|
97
|
+
* @param {LocalState} [options.localState] - Pre-captured local state (optional)
|
|
98
|
+
* @returns {Promise<Object>}
|
|
99
|
+
*/
|
|
100
|
+
async syncToRemote(options) {
|
|
101
|
+
const { machineId, localState: providedState } = options;
|
|
102
|
+
|
|
103
|
+
// Use provided state or capture fresh
|
|
104
|
+
const localState = providedState || await this.captureLocalState();
|
|
105
|
+
|
|
106
|
+
// If there are uncommitted changes, commit them as WIP
|
|
107
|
+
let wipCommit = null;
|
|
108
|
+
if (localState.hasUncommitted) {
|
|
109
|
+
const hasChanges = await this.gitHandoff.hasUncommittedChanges();
|
|
110
|
+
if (hasChanges) {
|
|
111
|
+
const result = await this.gitHandoff.commitWip(
|
|
112
|
+
`Remote sync for session ${this.sessionId}`
|
|
113
|
+
);
|
|
114
|
+
wipCommit = result.commit;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Push to WIP branch
|
|
119
|
+
await this.gitHandoff.pushToWipBranch(true); // force push
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
success: true,
|
|
123
|
+
sessionId: this.sessionId,
|
|
124
|
+
machineId,
|
|
125
|
+
branch: localState.branch,
|
|
126
|
+
commit: localState.commit,
|
|
127
|
+
wipCommit,
|
|
128
|
+
syncedAt: Date.now()
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Pull changes from remote machine
|
|
134
|
+
* @param {Object} options
|
|
135
|
+
* @param {string} [options.strategy='merge'] - Sync strategy ('merge' or 'rebase')
|
|
136
|
+
* @param {boolean} [options.stashLocal=true] - Stash local changes before pulling
|
|
137
|
+
* @returns {Promise<Object>}
|
|
138
|
+
*/
|
|
139
|
+
async pullFromRemote(options = {}) {
|
|
140
|
+
const { strategy = 'merge', stashLocal = true } = options;
|
|
141
|
+
|
|
142
|
+
// Fetch WIP branch
|
|
143
|
+
await this.gitHandoff.fetchWipBranch();
|
|
144
|
+
|
|
145
|
+
// Check if there are remote changes
|
|
146
|
+
const remoteCheck = await this.gitHandoff.checkRemoteChanges();
|
|
147
|
+
if (!remoteCheck.hasChanges) {
|
|
148
|
+
return {
|
|
149
|
+
success: true,
|
|
150
|
+
hasChanges: false,
|
|
151
|
+
commit: remoteCheck.localCommit
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Sync from remote
|
|
156
|
+
const result = await this.gitHandoff.syncFromRemote({
|
|
157
|
+
strategy,
|
|
158
|
+
stashLocal
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
success: result.success,
|
|
163
|
+
hasChanges: true,
|
|
164
|
+
commit: result.commit,
|
|
165
|
+
conflicts: result.conflicts
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Get diff summary between local and remote
|
|
171
|
+
* @returns {Promise<Object>}
|
|
172
|
+
*/
|
|
173
|
+
async getDiffSummary() {
|
|
174
|
+
await this.gitHandoff.fetchWipBranch();
|
|
175
|
+
return this.gitHandoff.getDiffSummary();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Detect potential conflicts between local and remote
|
|
180
|
+
* @returns {Promise<Object>}
|
|
181
|
+
*/
|
|
182
|
+
async detectConflicts() {
|
|
183
|
+
// Check remote changes
|
|
184
|
+
const remoteCheck = await this.gitHandoff.checkRemoteChanges();
|
|
185
|
+
|
|
186
|
+
// Check local uncommitted changes
|
|
187
|
+
const hasUncommitted = await this.gitHandoff.hasUncommittedChanges();
|
|
188
|
+
const localChanges = hasUncommitted
|
|
189
|
+
? await this.gitHandoff.getChangedFiles()
|
|
190
|
+
: [];
|
|
191
|
+
|
|
192
|
+
// Get diff summary
|
|
193
|
+
const diff = await this.getDiffSummary();
|
|
194
|
+
|
|
195
|
+
// Determine if branches diverged
|
|
196
|
+
const diverged = diff.ahead > 0 && diff.behind > 0;
|
|
197
|
+
|
|
198
|
+
// Find conflicting files (overlap between remote changes and local uncommitted)
|
|
199
|
+
const conflictingFiles = localChanges.filter((file) =>
|
|
200
|
+
diff.files.includes(file)
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
hasConflicts: conflictingFiles.length > 0 || (remoteCheck.hasChanges && hasUncommitted),
|
|
205
|
+
diverged,
|
|
206
|
+
conflictingFiles,
|
|
207
|
+
remoteChanges: diff.files,
|
|
208
|
+
localChanges
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export default CodeSync;
|