teleportation-cli 1.0.0 → 1.0.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.
@@ -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;
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Robust Init Script Generator
3
+ *
4
+ * Generates an init script with:
5
+ * - Network readiness checks
6
+ * - Detailed error logging
7
+ * - Retry logic for network calls
8
+ * - Step-by-step validation
9
+ */
10
+
11
+ export function generateRobustInitScript(sessionConfig, ssh, tunnel, vaultClient, repoUrl, branch) {
12
+ const safeSessionId = escapeShellArg(sessionConfig.id);
13
+ const safeTask = escapeShellArg(sessionConfig.task || '');
14
+ const safePublicKey = escapeShellArg(ssh.publicKey);
15
+ const safeBranch = escapeShellArg(branch);
16
+ const safeRepoUrl = escapeShellArg(repoUrl);
17
+ const safeVaultUrl = escapeShellArg(vaultClient.apiUrl);
18
+ const safeVaultKey = escapeShellArg(vaultClient.apiKey);
19
+ const safeAppId = escapeShellArg(vaultClient.appId);
20
+ const namespace = sessionConfig.vaultNamespace;
21
+
22
+ return `
23
+ set -e
24
+
25
+ log_step() {
26
+ echo "=== [$1] $2 ==="
27
+ }
28
+
29
+ log_error() {
30
+ echo "❌ ERROR: $1" >&2
31
+ }
32
+
33
+ log_success() {
34
+ echo "✅ $1"
35
+ }
36
+
37
+ # Wait for network to be ready
38
+ wait_for_network() {
39
+ log_step "0" "Waiting for network readiness"
40
+ local max_attempts=30
41
+ local attempt=1
42
+
43
+ while [ $attempt -le $max_attempts ]; do
44
+ if curl -fsSL --max-time 5 https://vault.mechdna.net/api/health >/dev/null 2>&1; then
45
+ log_success "Network is ready (attempt $attempt)"
46
+ return 0
47
+ fi
48
+ echo "Network not ready yet (attempt $attempt/$max_attempts)"
49
+ sleep 2
50
+ attempt=$((attempt + 1))
51
+ done
52
+
53
+ log_error "Network did not become ready after $max_attempts attempts"
54
+ return 1
55
+ }
56
+
57
+ # Retry a curl command with exponential backoff
58
+ retry_curl() {
59
+ local max_attempts=3
60
+ local attempt=1
61
+ local wait_time=2
62
+
63
+ while [ $attempt -le $max_attempts ]; do
64
+ if "$@"; then
65
+ return 0
66
+ fi
67
+ local exit_code=$?
68
+
69
+ if [ $attempt -lt $max_attempts ]; then
70
+ echo "⚠️ Curl failed (exit $exit_code), retrying in $wait_time seconds (attempt $attempt/$max_attempts)"
71
+ sleep $wait_time
72
+ wait_time=$((wait_time * 2))
73
+ attempt=$((attempt + 1))
74
+ else
75
+ log_error "Curl failed after $max_attempts attempts (exit code: $exit_code)"
76
+ return $exit_code
77
+ fi
78
+ done
79
+ }
80
+
81
+ log_step "1" "Installing dependencies"
82
+ apt-get update -qq && apt-get install -y -qq git openssh-server curl jq dnsutils
83
+ log_success "Dependencies installed"
84
+
85
+ log_step "2" "Checking DNS resolution"
86
+ if nslookup vault.mechdna.net >/dev/null 2>&1; then
87
+ log_success "DNS resolution working"
88
+ else
89
+ log_error "DNS resolution failed for vault.mechdna.net"
90
+ exit 1
91
+ fi
92
+
93
+ log_step "3" "Waiting for network connectivity"
94
+ wait_for_network || exit 1
95
+
96
+ log_step "4" "Setting up SSH"
97
+ mkdir -p ~/.ssh
98
+ echo ${safePublicKey} >> ~/.ssh/authorized_keys
99
+ chmod 600 ~/.ssh/authorized_keys
100
+ log_success "SSH configured"
101
+
102
+ log_step "5" "Configuring environment variables"
103
+ export VAULT_NAMESPACE="${namespace}"
104
+ export MECH_VAULT_URL=${safeVaultUrl}
105
+ export MECH_API_KEY=${safeVaultKey}
106
+ export MECH_APP_ID=${safeAppId}
107
+ export SESSION_ID=${safeSessionId}
108
+
109
+ echo "VAULT_NAMESPACE=$VAULT_NAMESPACE"
110
+ echo "MECH_VAULT_URL=$MECH_VAULT_URL"
111
+ echo "SESSION_ID=$SESSION_ID"
112
+ log_success "Environment configured"
113
+
114
+ log_step "6" "Fetching secrets from Vault"
115
+ ENV_FILE=/tmp/teleportation.env
116
+
117
+ retry_curl curl -fsSL -X POST "$MECH_VAULT_URL/env-files/export" \
118
+ -H "Content-Type: application/json" \
119
+ -H "X-API-Key: $MECH_API_KEY" \
120
+ -H "X-App-ID: $MECH_APP_ID" \
121
+ -d "$(jq -n --arg namespace "$VAULT_NAMESPACE" --arg format 'env' '{namespace:$namespace,format:$format}')" \
122
+ -o "$ENV_FILE" || exit 1
123
+
124
+ if [ ! -s "$ENV_FILE" ]; then
125
+ log_error "Vault env file is empty"
126
+ exit 1
127
+ fi
128
+
129
+ log_success "Secrets fetched ($(wc -l < "$ENV_FILE") lines)"
130
+
131
+ log_step "7" "Loading environment variables"
132
+ set +x
133
+ set -a
134
+ . "$ENV_FILE"
135
+ set +a
136
+ log_success "Environment loaded"
137
+
138
+ log_step "8" "Configuring Git authentication"
139
+ if [ -n "$GITHUB_TOKEN" ]; then
140
+ git config --global url."https://x-access-token:$GITHUB_TOKEN@github.com/".insteadOf "https://github.com/"
141
+ git config --global url."https://x-access-token:$GITHUB_TOKEN@github.com/".insteadOf "git@github.com:"
142
+ log_success "Git authentication configured"
143
+ else
144
+ echo "⚠️ No GITHUB_TOKEN found, proceeding without authentication"
145
+ fi
146
+
147
+ log_step "9" "Cloning repository"
148
+ git clone ${safeRepoUrl} /workspace || exit 1
149
+ cd /workspace
150
+ log_success "Repository cloned"
151
+
152
+ log_step "10" "Checking out branch"
153
+ git checkout ${safeBranch} || exit 1
154
+ log_success "Branch checked out: ${safeBranch}"
155
+
156
+ log_step "11" "Installing project dependencies"
157
+ bun install || exit 1
158
+ log_success "Dependencies installed"
159
+
160
+ log_step "12" "Starting LivePort tunnel"
161
+ ${tunnel.tunnelCommand} &
162
+ TUNNEL_PID=$!
163
+ log_success "Tunnel started (PID: $TUNNEL_PID)"
164
+
165
+ log_step "13" "Registering session with relay"
166
+ retry_curl curl -fsSL -X POST "$RELAY_API_URL/api/sessions/register" \
167
+ -H "Content-Type: application/json" \
168
+ -H "Authorization: Bearer $RELAY_API_KEY" \
169
+ -d "$(jq -n --arg session_id \"$SESSION_ID\" '{session_id:$session_id,meta:{provider:"fly",remote:true}}')" \
170
+ >/dev/null || echo "⚠️ Session registration failed (non-fatal)"
171
+
172
+ log_step "14" "Starting teleportation daemon"
173
+ echo "Task: ${safeTask}"
174
+ bun teleportation-cli.cjs daemon start --session-id ${safeSessionId} --task ${safeTask} || {
175
+ log_error "Daemon failed to start (exit code $?)"
176
+ exit 1
177
+ }
178
+
179
+ log_success "Initialization completed at $(date)"
180
+ echo "=== Daemon is running, container will stay alive ==="
181
+ tail -f /dev/null
182
+ `.trim();
183
+ }
184
+
185
+ function escapeShellArg(str) {
186
+ return "'" + String(str || '').replace(/'/g, "'\\''") + "'";
187
+ }