teleportation-cli 1.0.0
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/config-loader.mjs +93 -0
- package/.claude/hooks/heartbeat.mjs +331 -0
- package/.claude/hooks/notification.mjs +35 -0
- package/.claude/hooks/permission_request.mjs +307 -0
- package/.claude/hooks/post_tool_use.mjs +137 -0
- package/.claude/hooks/pre_tool_use.mjs +451 -0
- package/.claude/hooks/session-register.mjs +274 -0
- package/.claude/hooks/session_end.mjs +256 -0
- package/.claude/hooks/session_start.mjs +308 -0
- package/.claude/hooks/stop.mjs +277 -0
- package/.claude/hooks/user_prompt_submit.mjs +91 -0
- package/LICENSE +21 -0
- package/README.md +243 -0
- package/lib/auth/api-key.js +110 -0
- package/lib/auth/credentials.js +341 -0
- package/lib/backup/manager.js +461 -0
- package/lib/cli/daemon-commands.js +299 -0
- package/lib/cli/index.js +303 -0
- package/lib/cli/session-commands.js +294 -0
- package/lib/cli/snapshot-commands.js +223 -0
- package/lib/cli/worktree-commands.js +291 -0
- package/lib/config/manager.js +306 -0
- package/lib/daemon/lifecycle.js +336 -0
- package/lib/daemon/pid-manager.js +160 -0
- package/lib/daemon/teleportation-daemon.js +2009 -0
- package/lib/handoff/config.js +102 -0
- package/lib/handoff/example.js +152 -0
- package/lib/handoff/git-handoff.js +351 -0
- package/lib/handoff/handoff.js +277 -0
- package/lib/handoff/index.js +25 -0
- package/lib/handoff/session-state.js +238 -0
- package/lib/install/installer.js +555 -0
- package/lib/machine-coders/claude-code-adapter.js +329 -0
- package/lib/machine-coders/example.js +239 -0
- package/lib/machine-coders/gemini-cli-adapter.js +406 -0
- package/lib/machine-coders/index.js +103 -0
- package/lib/machine-coders/interface.js +168 -0
- package/lib/router/classifier.js +251 -0
- package/lib/router/example.js +92 -0
- package/lib/router/index.js +69 -0
- package/lib/router/mech-llms-client.js +277 -0
- package/lib/router/models.js +188 -0
- package/lib/router/router.js +382 -0
- package/lib/session/cleanup.js +100 -0
- package/lib/session/metadata.js +258 -0
- package/lib/session/mute-checker.js +114 -0
- package/lib/session-registry/manager.js +302 -0
- package/lib/snapshot/manager.js +390 -0
- package/lib/utils/errors.js +166 -0
- package/lib/utils/logger.js +148 -0
- package/lib/utils/retry.js +155 -0
- package/lib/worktree/manager.js +301 -0
- package/package.json +66 -0
- package/teleportation-cli.cjs +2987 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main Handoff Operations
|
|
3
|
+
*
|
|
4
|
+
* High-level functions for handoff and sync operations.
|
|
5
|
+
* Coordinates git operations, session state, and cloud provider.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { GitHandoff } from './git-handoff.js';
|
|
9
|
+
import { SessionState, loadSessionState, saveSessionState, getOrCreateSessionState } from './session-state.js';
|
|
10
|
+
import { getHandoffConfig } from './config.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Perform a handoff from local to cloud
|
|
14
|
+
*
|
|
15
|
+
* @param {Object} options
|
|
16
|
+
* @param {string} options.projectPath - Path to the project
|
|
17
|
+
* @param {string} options.sessionId - Teleportation session ID
|
|
18
|
+
* @param {string} [options.message] - Optional message describing what was being worked on
|
|
19
|
+
* @param {boolean} [options.skipCommit] - Skip committing (if already committed)
|
|
20
|
+
* @param {boolean} [options.skipPush] - Skip pushing (dry run)
|
|
21
|
+
* @param {Function} [options.onProgress] - Progress callback
|
|
22
|
+
* @returns {Promise<{success: boolean, commit?: string, branch?: string, error?: string}>}
|
|
23
|
+
*/
|
|
24
|
+
export async function handoff(options) {
|
|
25
|
+
const {
|
|
26
|
+
projectPath,
|
|
27
|
+
sessionId,
|
|
28
|
+
message = '',
|
|
29
|
+
skipCommit = false,
|
|
30
|
+
skipPush = false,
|
|
31
|
+
onProgress = () => {},
|
|
32
|
+
} = options;
|
|
33
|
+
|
|
34
|
+
const config = getHandoffConfig(projectPath);
|
|
35
|
+
|
|
36
|
+
if (!config.enabled) {
|
|
37
|
+
return { success: false, error: 'Handoff is disabled in configuration' };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
onProgress({ step: 'init', message: 'Initializing handoff...' });
|
|
41
|
+
|
|
42
|
+
// Initialize git handoff
|
|
43
|
+
const git = new GitHandoff({
|
|
44
|
+
repoPath: projectPath,
|
|
45
|
+
sessionId,
|
|
46
|
+
branchPrefix: config.wip_branch_prefix,
|
|
47
|
+
verbose: true,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Load or create session state
|
|
51
|
+
const state = getOrCreateSessionState(projectPath, sessionId);
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
// Check for remote URL
|
|
55
|
+
const remoteUrl = await git.getRemoteUrl();
|
|
56
|
+
if (!remoteUrl) {
|
|
57
|
+
return { success: false, error: 'No git remote configured. Cannot handoff.' };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let commit = await git.getCurrentCommit();
|
|
61
|
+
let files = [];
|
|
62
|
+
|
|
63
|
+
// Commit WIP changes if needed
|
|
64
|
+
if (!skipCommit && config.commit_uncommitted) {
|
|
65
|
+
onProgress({ step: 'commit', message: 'Committing work in progress...' });
|
|
66
|
+
|
|
67
|
+
const hasChanges = await git.hasUncommittedChanges();
|
|
68
|
+
if (hasChanges) {
|
|
69
|
+
const result = await git.commitWip(message);
|
|
70
|
+
commit = result.commit;
|
|
71
|
+
files = result.files;
|
|
72
|
+
onProgress({ step: 'commit', message: `Committed ${files.length} files` });
|
|
73
|
+
} else {
|
|
74
|
+
onProgress({ step: 'commit', message: 'No uncommitted changes' });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Push to WIP branch
|
|
79
|
+
let branch = git.wipBranch;
|
|
80
|
+
if (!skipPush) {
|
|
81
|
+
onProgress({ step: 'push', message: `Pushing to ${branch}...` });
|
|
82
|
+
const pushResult = await git.pushToWipBranch();
|
|
83
|
+
branch = pushResult.branch;
|
|
84
|
+
commit = pushResult.commit;
|
|
85
|
+
onProgress({ step: 'push', message: 'Pushed successfully' });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Update session state
|
|
89
|
+
state.recordHandoff({
|
|
90
|
+
type: 'local_to_cloud',
|
|
91
|
+
commit,
|
|
92
|
+
branch,
|
|
93
|
+
message,
|
|
94
|
+
files,
|
|
95
|
+
});
|
|
96
|
+
saveSessionState(projectPath, state);
|
|
97
|
+
|
|
98
|
+
onProgress({ step: 'done', message: 'Handoff complete' });
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
success: true,
|
|
102
|
+
commit,
|
|
103
|
+
branch,
|
|
104
|
+
files,
|
|
105
|
+
sessionId,
|
|
106
|
+
};
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return {
|
|
109
|
+
success: false,
|
|
110
|
+
error: error.message,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Sync changes from cloud back to local
|
|
117
|
+
*
|
|
118
|
+
* @param {Object} options
|
|
119
|
+
* @param {string} options.projectPath - Path to the project
|
|
120
|
+
* @param {string} options.sessionId - Teleportation session ID
|
|
121
|
+
* @param {string} [options.strategy='merge'] - 'merge' or 'rebase'
|
|
122
|
+
* @param {boolean} [options.force] - Force sync even if conflicts
|
|
123
|
+
* @param {Function} [options.onProgress] - Progress callback
|
|
124
|
+
* @returns {Promise<{success: boolean, commit?: string, files?: string[], conflicts?: string[], error?: string}>}
|
|
125
|
+
*/
|
|
126
|
+
export async function sync(options) {
|
|
127
|
+
const {
|
|
128
|
+
projectPath,
|
|
129
|
+
sessionId,
|
|
130
|
+
strategy = 'merge',
|
|
131
|
+
force = false,
|
|
132
|
+
onProgress = () => {},
|
|
133
|
+
} = options;
|
|
134
|
+
|
|
135
|
+
const config = getHandoffConfig(projectPath);
|
|
136
|
+
|
|
137
|
+
onProgress({ step: 'init', message: 'Checking for remote changes...' });
|
|
138
|
+
|
|
139
|
+
// Initialize git handoff
|
|
140
|
+
const git = new GitHandoff({
|
|
141
|
+
repoPath: projectPath,
|
|
142
|
+
sessionId,
|
|
143
|
+
branchPrefix: config.wip_branch_prefix,
|
|
144
|
+
verbose: true,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Load session state
|
|
148
|
+
const state = loadSessionState(projectPath);
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
// Check if there are remote changes
|
|
152
|
+
const { hasChanges, localCommit, remoteCommit } = await git.checkRemoteChanges();
|
|
153
|
+
|
|
154
|
+
if (!hasChanges) {
|
|
155
|
+
onProgress({ step: 'done', message: 'Already up to date' });
|
|
156
|
+
return {
|
|
157
|
+
success: true,
|
|
158
|
+
commit: localCommit,
|
|
159
|
+
files: [],
|
|
160
|
+
message: 'Already up to date',
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Get diff summary
|
|
165
|
+
const diff = await git.getDiffSummary();
|
|
166
|
+
onProgress({
|
|
167
|
+
step: 'diff',
|
|
168
|
+
message: `${diff.behind} commits behind, ${diff.files.length} files changed`
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Perform sync
|
|
172
|
+
onProgress({ step: 'sync', message: `Syncing with ${strategy}...` });
|
|
173
|
+
const result = await git.syncFromRemote({ strategy, stashLocal: true });
|
|
174
|
+
|
|
175
|
+
if (!result.success && result.conflicts.length > 0) {
|
|
176
|
+
if (!force) {
|
|
177
|
+
return {
|
|
178
|
+
success: false,
|
|
179
|
+
conflicts: result.conflicts,
|
|
180
|
+
error: `Merge conflicts in: ${result.conflicts.join(', ')}`,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
// Force mode: accept theirs
|
|
184
|
+
onProgress({ step: 'resolve', message: 'Force resolving conflicts...' });
|
|
185
|
+
// TODO: Implement force resolution
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Update session state
|
|
189
|
+
if (state) {
|
|
190
|
+
state.recordHandoff({
|
|
191
|
+
type: 'cloud_to_local',
|
|
192
|
+
commit: result.commit,
|
|
193
|
+
files: diff.files,
|
|
194
|
+
});
|
|
195
|
+
saveSessionState(projectPath, state);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
onProgress({ step: 'done', message: 'Sync complete' });
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
success: true,
|
|
202
|
+
commit: result.commit,
|
|
203
|
+
files: diff.files,
|
|
204
|
+
previousCommit: localCommit,
|
|
205
|
+
};
|
|
206
|
+
} catch (error) {
|
|
207
|
+
return {
|
|
208
|
+
success: false,
|
|
209
|
+
error: error.message,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Get the current handoff status
|
|
216
|
+
*
|
|
217
|
+
* @param {Object} options
|
|
218
|
+
* @param {string} options.projectPath - Path to the project
|
|
219
|
+
* @param {string} options.sessionId - Teleportation session ID
|
|
220
|
+
* @returns {Promise<{location: string, hasRemoteChanges: boolean, localCommit: string, remoteCommit?: string, diff?: Object}>}
|
|
221
|
+
*/
|
|
222
|
+
export async function getHandoffStatus(options) {
|
|
223
|
+
const { projectPath, sessionId } = options;
|
|
224
|
+
const config = getHandoffConfig(projectPath);
|
|
225
|
+
|
|
226
|
+
const git = new GitHandoff({
|
|
227
|
+
repoPath: projectPath,
|
|
228
|
+
sessionId,
|
|
229
|
+
branchPrefix: config.wip_branch_prefix,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const state = loadSessionState(projectPath);
|
|
233
|
+
|
|
234
|
+
const localCommit = await git.getCurrentCommit();
|
|
235
|
+
const currentBranch = await git.getCurrentBranch();
|
|
236
|
+
const hasUncommitted = await git.hasUncommittedChanges();
|
|
237
|
+
|
|
238
|
+
// Check remote
|
|
239
|
+
const { hasChanges, remoteCommit } = await git.checkRemoteChanges();
|
|
240
|
+
|
|
241
|
+
let diff = null;
|
|
242
|
+
if (hasChanges) {
|
|
243
|
+
diff = await git.getDiffSummary();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
location: state?.location || 'local',
|
|
248
|
+
sessionId: state?.sessionId || sessionId,
|
|
249
|
+
currentBranch,
|
|
250
|
+
wipBranch: git.wipBranch,
|
|
251
|
+
localCommit,
|
|
252
|
+
remoteCommit,
|
|
253
|
+
hasUncommittedChanges: hasUncommitted,
|
|
254
|
+
hasRemoteChanges: hasChanges,
|
|
255
|
+
diff,
|
|
256
|
+
lastHandoff: state?.handoffHistory?.slice(-1)[0] || null,
|
|
257
|
+
cloudContainer: state?.cloudContainer || null,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Check if a handoff is currently in progress
|
|
263
|
+
*
|
|
264
|
+
* @param {string} projectPath
|
|
265
|
+
* @returns {boolean}
|
|
266
|
+
*/
|
|
267
|
+
export function isHandoffInProgress(projectPath) {
|
|
268
|
+
const state = loadSessionState(projectPath);
|
|
269
|
+
return state?.location === 'syncing';
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export default {
|
|
273
|
+
handoff,
|
|
274
|
+
sync,
|
|
275
|
+
getHandoffStatus,
|
|
276
|
+
isHandoffInProgress,
|
|
277
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Teleportation Handoff Module
|
|
3
|
+
*
|
|
4
|
+
* Handles seamless handoff between local and cloud dev environments.
|
|
5
|
+
*
|
|
6
|
+
* Key features:
|
|
7
|
+
* - Auto-commit WIP changes to teleport/wip-<session> branch
|
|
8
|
+
* - Push to remote for cloud pickup
|
|
9
|
+
* - Sync changes back from cloud
|
|
10
|
+
* - Session state persistence in .teleportation/ directory
|
|
11
|
+
*
|
|
12
|
+
* @module lib/handoff
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export { GitHandoff, createGitHandoff } from './git-handoff.js';
|
|
16
|
+
export { SessionState, loadSessionState, saveSessionState } from './session-state.js';
|
|
17
|
+
export {
|
|
18
|
+
handoff,
|
|
19
|
+
sync,
|
|
20
|
+
getHandoffStatus,
|
|
21
|
+
isHandoffInProgress
|
|
22
|
+
} from './handoff.js';
|
|
23
|
+
|
|
24
|
+
// Re-export config helpers
|
|
25
|
+
export { getHandoffConfig, DEFAULT_HANDOFF_CONFIG } from './config.js';
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session State Management
|
|
3
|
+
*
|
|
4
|
+
* Persists session state to .teleportation/ directory so it can be
|
|
5
|
+
* synced between local and cloud environments.
|
|
6
|
+
*
|
|
7
|
+
* State includes:
|
|
8
|
+
* - Session metadata (ID, timestamps, location)
|
|
9
|
+
* - Handoff history
|
|
10
|
+
* - Task progress
|
|
11
|
+
* - Conversation context (optional)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
15
|
+
import { join } from 'path';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Session state structure
|
|
19
|
+
*/
|
|
20
|
+
export class SessionState {
|
|
21
|
+
constructor(data = {}) {
|
|
22
|
+
// Core identifiers
|
|
23
|
+
this.sessionId = data.sessionId || null;
|
|
24
|
+
this.projectPath = data.projectPath || null;
|
|
25
|
+
|
|
26
|
+
// Location tracking
|
|
27
|
+
this.location = data.location || 'local'; // 'local' | 'cloud' | 'syncing'
|
|
28
|
+
this.lastLocalActivity = data.lastLocalActivity || null;
|
|
29
|
+
this.lastCloudActivity = data.lastCloudActivity || null;
|
|
30
|
+
|
|
31
|
+
// Handoff tracking
|
|
32
|
+
this.handoffHistory = data.handoffHistory || [];
|
|
33
|
+
this.lastHandoffCommit = data.lastHandoffCommit || null;
|
|
34
|
+
this.lastSyncCommit = data.lastSyncCommit || null;
|
|
35
|
+
|
|
36
|
+
// Cloud container info (when in cloud)
|
|
37
|
+
this.cloudContainer = data.cloudContainer || null;
|
|
38
|
+
|
|
39
|
+
// Task/workflow state
|
|
40
|
+
this.currentTask = data.currentTask || null;
|
|
41
|
+
this.taskProgress = data.taskProgress || {};
|
|
42
|
+
|
|
43
|
+
// Timestamps
|
|
44
|
+
this.createdAt = data.createdAt || new Date().toISOString();
|
|
45
|
+
this.updatedAt = data.updatedAt || new Date().toISOString();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Record a handoff event
|
|
50
|
+
* @param {Object} event
|
|
51
|
+
* @param {string} event.type - 'local_to_cloud' | 'cloud_to_local'
|
|
52
|
+
* @param {string} event.commit - Git commit hash
|
|
53
|
+
* @param {string} [event.message] - Optional message
|
|
54
|
+
*/
|
|
55
|
+
recordHandoff(event) {
|
|
56
|
+
this.handoffHistory.push({
|
|
57
|
+
...event,
|
|
58
|
+
timestamp: new Date().toISOString(),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (event.type === 'local_to_cloud') {
|
|
62
|
+
this.lastHandoffCommit = event.commit;
|
|
63
|
+
this.location = 'cloud';
|
|
64
|
+
this.lastLocalActivity = new Date().toISOString();
|
|
65
|
+
} else if (event.type === 'cloud_to_local') {
|
|
66
|
+
this.lastSyncCommit = event.commit;
|
|
67
|
+
this.location = 'local';
|
|
68
|
+
this.lastCloudActivity = new Date().toISOString();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
this.updatedAt = new Date().toISOString();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Update location
|
|
76
|
+
* @param {'local'|'cloud'|'syncing'} location
|
|
77
|
+
*/
|
|
78
|
+
setLocation(location) {
|
|
79
|
+
this.location = location;
|
|
80
|
+
if (location === 'local') {
|
|
81
|
+
this.lastLocalActivity = new Date().toISOString();
|
|
82
|
+
} else if (location === 'cloud') {
|
|
83
|
+
this.lastCloudActivity = new Date().toISOString();
|
|
84
|
+
}
|
|
85
|
+
this.updatedAt = new Date().toISOString();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Update cloud container info
|
|
90
|
+
* @param {Object|null} container
|
|
91
|
+
*/
|
|
92
|
+
setCloudContainer(container) {
|
|
93
|
+
this.cloudContainer = container;
|
|
94
|
+
this.updatedAt = new Date().toISOString();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Update current task
|
|
99
|
+
* @param {Object|null} task
|
|
100
|
+
*/
|
|
101
|
+
setCurrentTask(task) {
|
|
102
|
+
this.currentTask = task;
|
|
103
|
+
this.updatedAt = new Date().toISOString();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Update task progress
|
|
108
|
+
* @param {string} taskId
|
|
109
|
+
* @param {Object} progress
|
|
110
|
+
*/
|
|
111
|
+
updateTaskProgress(taskId, progress) {
|
|
112
|
+
this.taskProgress[taskId] = {
|
|
113
|
+
...this.taskProgress[taskId],
|
|
114
|
+
...progress,
|
|
115
|
+
updatedAt: new Date().toISOString(),
|
|
116
|
+
};
|
|
117
|
+
this.updatedAt = new Date().toISOString();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Serialize to JSON
|
|
122
|
+
* @returns {Object}
|
|
123
|
+
*/
|
|
124
|
+
toJSON() {
|
|
125
|
+
return {
|
|
126
|
+
sessionId: this.sessionId,
|
|
127
|
+
projectPath: this.projectPath,
|
|
128
|
+
location: this.location,
|
|
129
|
+
lastLocalActivity: this.lastLocalActivity,
|
|
130
|
+
lastCloudActivity: this.lastCloudActivity,
|
|
131
|
+
handoffHistory: this.handoffHistory,
|
|
132
|
+
lastHandoffCommit: this.lastHandoffCommit,
|
|
133
|
+
lastSyncCommit: this.lastSyncCommit,
|
|
134
|
+
cloudContainer: this.cloudContainer,
|
|
135
|
+
currentTask: this.currentTask,
|
|
136
|
+
taskProgress: this.taskProgress,
|
|
137
|
+
createdAt: this.createdAt,
|
|
138
|
+
updatedAt: this.updatedAt,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get the .teleportation directory path for a project
|
|
145
|
+
* @param {string} projectPath
|
|
146
|
+
* @returns {string}
|
|
147
|
+
*/
|
|
148
|
+
function getTeleportationDir(projectPath) {
|
|
149
|
+
return join(projectPath, '.teleportation');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get the session state file path
|
|
154
|
+
* @param {string} projectPath
|
|
155
|
+
* @returns {string}
|
|
156
|
+
*/
|
|
157
|
+
function getSessionStatePath(projectPath) {
|
|
158
|
+
return join(getTeleportationDir(projectPath), 'session.json');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Ensure .teleportation directory exists
|
|
163
|
+
* @param {string} projectPath
|
|
164
|
+
*/
|
|
165
|
+
function ensureTeleportationDir(projectPath) {
|
|
166
|
+
const dir = getTeleportationDir(projectPath);
|
|
167
|
+
if (!existsSync(dir)) {
|
|
168
|
+
// Create with 700 permissions (owner only) for security
|
|
169
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Load session state from disk
|
|
175
|
+
* @param {string} projectPath - Project directory
|
|
176
|
+
* @returns {SessionState|null}
|
|
177
|
+
*/
|
|
178
|
+
export function loadSessionState(projectPath) {
|
|
179
|
+
const statePath = getSessionStatePath(projectPath);
|
|
180
|
+
|
|
181
|
+
if (!existsSync(statePath)) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
const data = JSON.parse(readFileSync(statePath, 'utf-8'));
|
|
187
|
+
return new SessionState(data);
|
|
188
|
+
} catch (error) {
|
|
189
|
+
console.error(`[session-state] Failed to load state: ${error.message}`);
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Save session state to disk
|
|
196
|
+
* @param {string} projectPath - Project directory
|
|
197
|
+
* @param {SessionState} state - State to save
|
|
198
|
+
*/
|
|
199
|
+
export function saveSessionState(projectPath, state) {
|
|
200
|
+
ensureTeleportationDir(projectPath);
|
|
201
|
+
const statePath = getSessionStatePath(projectPath);
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
// Write with 600 permissions (owner read/write only) for security
|
|
205
|
+
// Session state may contain sensitive info like cloud container credentials
|
|
206
|
+
writeFileSync(statePath, JSON.stringify(state.toJSON(), null, 2), { mode: 0o600 });
|
|
207
|
+
} catch (error) {
|
|
208
|
+
console.error(`[session-state] Failed to save state: ${error.message}`);
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Create or load session state
|
|
215
|
+
* @param {string} projectPath
|
|
216
|
+
* @param {string} sessionId
|
|
217
|
+
* @returns {SessionState}
|
|
218
|
+
*/
|
|
219
|
+
export function getOrCreateSessionState(projectPath, sessionId) {
|
|
220
|
+
let state = loadSessionState(projectPath);
|
|
221
|
+
|
|
222
|
+
if (!state || state.sessionId !== sessionId) {
|
|
223
|
+
state = new SessionState({
|
|
224
|
+
sessionId,
|
|
225
|
+
projectPath,
|
|
226
|
+
});
|
|
227
|
+
saveSessionState(projectPath, state);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return state;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export default {
|
|
234
|
+
SessionState,
|
|
235
|
+
loadSessionState,
|
|
236
|
+
saveSessionState,
|
|
237
|
+
getOrCreateSessionState,
|
|
238
|
+
};
|