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,155 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Retry utility for API calls and network operations
|
|
4
|
+
* Provides exponential backoff and configurable retry logic
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Retry options
|
|
9
|
+
* @typedef {Object} RetryOptions
|
|
10
|
+
* @property {number} maxRetries - Maximum number of retry attempts (default: 3)
|
|
11
|
+
* @property {number} initialDelay - Initial delay in milliseconds (default: 1000)
|
|
12
|
+
* @property {number} maxDelay - Maximum delay in milliseconds (default: 10000)
|
|
13
|
+
* @property {number} factor - Exponential backoff factor (default: 2)
|
|
14
|
+
* @property {function} shouldRetry - Function to determine if error should be retried (default: retry on network errors)
|
|
15
|
+
* @property {function} onRetry - Callback called before each retry attempt
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Default retry options
|
|
20
|
+
*/
|
|
21
|
+
const DEFAULT_OPTIONS = {
|
|
22
|
+
maxRetries: 3,
|
|
23
|
+
initialDelay: 1000,
|
|
24
|
+
maxDelay: 10000,
|
|
25
|
+
factor: 2,
|
|
26
|
+
shouldRetry: (error) => {
|
|
27
|
+
// Retry on network errors, timeouts, and 5xx server errors
|
|
28
|
+
if (error.name === 'AbortError' || error.name === 'TypeError') {
|
|
29
|
+
return true; // Network/timeout errors
|
|
30
|
+
}
|
|
31
|
+
if (error.status >= 500 && error.status < 600) {
|
|
32
|
+
return true; // Server errors
|
|
33
|
+
}
|
|
34
|
+
if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT' || error.code === 'ENOTFOUND') {
|
|
35
|
+
return true; // Connection errors
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
},
|
|
39
|
+
onRetry: null
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Sleep for specified milliseconds
|
|
44
|
+
*/
|
|
45
|
+
function sleep(ms) {
|
|
46
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Calculate delay for retry attempt with exponential backoff
|
|
51
|
+
*/
|
|
52
|
+
function calculateDelay(attempt, options) {
|
|
53
|
+
const delay = Math.min(
|
|
54
|
+
options.initialDelay * Math.pow(options.factor, attempt),
|
|
55
|
+
options.maxDelay
|
|
56
|
+
);
|
|
57
|
+
// Add jitter to prevent thundering herd
|
|
58
|
+
const jitter = Math.random() * 0.3 * delay; // Up to 30% jitter
|
|
59
|
+
return Math.floor(delay + jitter);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Retry a function with exponential backoff
|
|
64
|
+
* @param {Function} fn - Async function to retry
|
|
65
|
+
* @param {RetryOptions} options - Retry configuration options
|
|
66
|
+
* @returns {Promise} - Result of the function
|
|
67
|
+
*/
|
|
68
|
+
export async function retry(fn, options = {}) {
|
|
69
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
70
|
+
let lastError;
|
|
71
|
+
|
|
72
|
+
for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
|
|
73
|
+
try {
|
|
74
|
+
return await fn();
|
|
75
|
+
} catch (error) {
|
|
76
|
+
lastError = error;
|
|
77
|
+
|
|
78
|
+
// Check if we should retry this error
|
|
79
|
+
if (!opts.shouldRetry(error)) {
|
|
80
|
+
throw error; // Don't retry non-retryable errors
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Don't retry if we've exhausted attempts
|
|
84
|
+
if (attempt >= opts.maxRetries) {
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Calculate delay before retry
|
|
89
|
+
const delay = calculateDelay(attempt, opts);
|
|
90
|
+
|
|
91
|
+
// Call onRetry callback if provided
|
|
92
|
+
if (opts.onRetry) {
|
|
93
|
+
opts.onRetry(error, attempt + 1, delay);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Wait before retrying
|
|
97
|
+
await sleep(delay);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// All retries exhausted
|
|
102
|
+
throw lastError;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Retry a fetch request with exponential backoff
|
|
107
|
+
* @param {string} url - URL to fetch
|
|
108
|
+
* @param {RequestInit} fetchOptions - Fetch options
|
|
109
|
+
* @param {RetryOptions} retryOptions - Retry configuration options
|
|
110
|
+
* @returns {Promise<Response>} - Fetch response
|
|
111
|
+
*/
|
|
112
|
+
export async function retryFetch(url, fetchOptions = {}, retryOptions = {}) {
|
|
113
|
+
return retry(async () => {
|
|
114
|
+
const controller = new AbortController();
|
|
115
|
+
const timeout = retryOptions.timeout || 30000;
|
|
116
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const response = await fetch(url, {
|
|
120
|
+
...fetchOptions,
|
|
121
|
+
signal: controller.signal
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
clearTimeout(timeoutId);
|
|
125
|
+
|
|
126
|
+
// Treat 5xx errors as retryable
|
|
127
|
+
if (response.status >= 500 && response.status < 600) {
|
|
128
|
+
const error = new Error(`Server error: ${response.status}`);
|
|
129
|
+
error.status = response.status;
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return response;
|
|
134
|
+
} catch (error) {
|
|
135
|
+
clearTimeout(timeoutId);
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}, retryOptions);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Create a retryable API client function
|
|
143
|
+
* @param {string} baseUrl - Base URL for API
|
|
144
|
+
* @param {RetryOptions} defaultRetryOptions - Default retry options
|
|
145
|
+
* @returns {Function} - API client function
|
|
146
|
+
*/
|
|
147
|
+
export function createRetryableApiClient(baseUrl, defaultRetryOptions = {}) {
|
|
148
|
+
return async function apiCall(endpoint, options = {}, retryOptions = {}) {
|
|
149
|
+
const url = `${baseUrl}${endpoint}`;
|
|
150
|
+
const mergedRetryOptions = { ...defaultRetryOptions, ...retryOptions };
|
|
151
|
+
|
|
152
|
+
return retryFetch(url, options, mergedRetryOptions);
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Worktree Management Module
|
|
4
|
+
* Handles git worktree creation, listing, and cleanup for session isolation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { execSync } from 'child_process';
|
|
8
|
+
import { join, resolve, sep, relative } from 'path';
|
|
9
|
+
import { mkdir, rm, readdir, stat } from 'fs/promises';
|
|
10
|
+
import { existsSync } from 'fs';
|
|
11
|
+
|
|
12
|
+
const WORKTREE_BASE = '.teleportation/sessions';
|
|
13
|
+
|
|
14
|
+
// Input validation patterns
|
|
15
|
+
const VALID_SESSION_ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
|
|
16
|
+
const VALID_BRANCH_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_\-/.]{0,127}$/;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Validate and sanitize a session ID
|
|
20
|
+
* @param {string} sessionId - Session ID to validate
|
|
21
|
+
* @returns {string} Validated session ID
|
|
22
|
+
* @throws {Error} If session ID is invalid
|
|
23
|
+
*/
|
|
24
|
+
export function validateSessionId(sessionId) {
|
|
25
|
+
if (!sessionId || typeof sessionId !== 'string') {
|
|
26
|
+
throw new Error('Session ID is required');
|
|
27
|
+
}
|
|
28
|
+
if (!VALID_SESSION_ID.test(sessionId)) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`Invalid session ID: "${sessionId}". Must start with alphanumeric, contain only alphanumeric/dash/underscore, and be 1-64 characters.`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return sessionId;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Validate and sanitize a git branch name
|
|
38
|
+
* @param {string} branchName - Branch name to validate
|
|
39
|
+
* @returns {string} Validated branch name
|
|
40
|
+
* @throws {Error} If branch name is invalid
|
|
41
|
+
*/
|
|
42
|
+
export function validateBranchName(branchName) {
|
|
43
|
+
if (!branchName || typeof branchName !== 'string') {
|
|
44
|
+
throw new Error('Branch name is required');
|
|
45
|
+
}
|
|
46
|
+
if (!VALID_BRANCH_NAME.test(branchName)) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Invalid branch name: "${branchName}". Must start with alphanumeric and contain only alphanumeric/dash/underscore/slash/dot.`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
// Additional git-specific checks
|
|
52
|
+
if (branchName.includes('..') || branchName.endsWith('.lock') || branchName.endsWith('/')) {
|
|
53
|
+
throw new Error(`Invalid branch name: "${branchName}". Contains invalid git reference patterns.`);
|
|
54
|
+
}
|
|
55
|
+
return branchName;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* List all git worktrees
|
|
60
|
+
* @returns {Array<{path: string, branch: string, commitHash: string}>}
|
|
61
|
+
*/
|
|
62
|
+
export function listWorktrees() {
|
|
63
|
+
try {
|
|
64
|
+
const output = execSync('git worktree list --porcelain', { encoding: 'utf8' });
|
|
65
|
+
const worktrees = [];
|
|
66
|
+
const lines = output.trim().split('\n');
|
|
67
|
+
|
|
68
|
+
let current = {};
|
|
69
|
+
for (const line of lines) {
|
|
70
|
+
if (line.startsWith('worktree ')) {
|
|
71
|
+
current.path = line.substring(9);
|
|
72
|
+
} else if (line.startsWith('branch ')) {
|
|
73
|
+
current.branch = line.substring(7).replace('refs/heads/', '');
|
|
74
|
+
} else if (line.startsWith('HEAD ')) {
|
|
75
|
+
current.commitHash = line.substring(5);
|
|
76
|
+
} else if (line === '') {
|
|
77
|
+
if (current.path) {
|
|
78
|
+
worktrees.push(current);
|
|
79
|
+
current = {};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Push last one if exists
|
|
85
|
+
if (current.path) {
|
|
86
|
+
worktrees.push(current);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return worktrees;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
throw new Error(`Failed to list worktrees: ${error.message}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get the repository root directory
|
|
97
|
+
* @returns {string}
|
|
98
|
+
*/
|
|
99
|
+
export function getRepoRoot() {
|
|
100
|
+
try {
|
|
101
|
+
return execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();
|
|
102
|
+
} catch (error) {
|
|
103
|
+
throw new Error(`Not in a git repository: ${error.message}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Create a new worktree for a session
|
|
109
|
+
* @param {string} sessionId - Unique session identifier
|
|
110
|
+
* @param {string} branchName - Name of the branch to create
|
|
111
|
+
* @param {string} baseBranch - Base branch to branch from (default: 'main')
|
|
112
|
+
* @returns {Promise<{path: string, branch: string}>}
|
|
113
|
+
*/
|
|
114
|
+
export async function createWorktree(sessionId, branchName, baseBranch = 'main') {
|
|
115
|
+
// Validate all inputs to prevent command injection
|
|
116
|
+
const validSessionId = validateSessionId(sessionId);
|
|
117
|
+
const validBranchName = validateBranchName(branchName);
|
|
118
|
+
const validBaseBranch = validateBranchName(baseBranch);
|
|
119
|
+
|
|
120
|
+
const repoRoot = getRepoRoot();
|
|
121
|
+
const worktreePath = resolve(repoRoot, WORKTREE_BASE, validSessionId);
|
|
122
|
+
|
|
123
|
+
// Check if worktree already exists
|
|
124
|
+
if (existsSync(worktreePath)) {
|
|
125
|
+
throw new Error(`Worktree already exists at ${worktreePath}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Ensure base directory exists
|
|
129
|
+
await mkdir(join(repoRoot, WORKTREE_BASE), { recursive: true });
|
|
130
|
+
|
|
131
|
+
// Check if branch already exists
|
|
132
|
+
const branches = execSync('git branch --list', { encoding: 'utf8' });
|
|
133
|
+
const branchExists = branches.includes(validBranchName);
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
if (branchExists) {
|
|
137
|
+
// Use existing branch - inputs are validated, path is quoted
|
|
138
|
+
execSync(`git worktree add "${worktreePath}" "${validBranchName}"`, {
|
|
139
|
+
encoding: 'utf8',
|
|
140
|
+
stdio: 'pipe'
|
|
141
|
+
});
|
|
142
|
+
} else {
|
|
143
|
+
// Create new branch from base - inputs are validated, path is quoted
|
|
144
|
+
execSync(`git worktree add -b "${validBranchName}" "${worktreePath}" "${validBaseBranch}"`, {
|
|
145
|
+
encoding: 'utf8',
|
|
146
|
+
stdio: 'pipe'
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
path: worktreePath,
|
|
152
|
+
branch: validBranchName,
|
|
153
|
+
sessionId: validSessionId
|
|
154
|
+
};
|
|
155
|
+
} catch (error) {
|
|
156
|
+
// Clean up on failure
|
|
157
|
+
if (existsSync(worktreePath)) {
|
|
158
|
+
await rm(worktreePath, { recursive: true, force: true });
|
|
159
|
+
}
|
|
160
|
+
// Prune any stale worktree references
|
|
161
|
+
try {
|
|
162
|
+
execSync('git worktree prune', { encoding: 'utf8', stdio: 'pipe' });
|
|
163
|
+
} catch {
|
|
164
|
+
// Prune failure is non-fatal
|
|
165
|
+
}
|
|
166
|
+
throw new Error(`Failed to create worktree: ${error.message}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Remove a worktree
|
|
172
|
+
* @param {string} worktreePath - Path to the worktree
|
|
173
|
+
* @param {boolean} force - Force removal even with uncommitted changes
|
|
174
|
+
* @returns {Promise<void>}
|
|
175
|
+
*/
|
|
176
|
+
export async function removeWorktree(worktreePath, force = false) {
|
|
177
|
+
const absolutePath = resolve(worktreePath);
|
|
178
|
+
|
|
179
|
+
if (!existsSync(absolutePath)) {
|
|
180
|
+
throw new Error(`Worktree not found at ${absolutePath}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const forceFlag = force ? '--force' : '';
|
|
185
|
+
execSync(`git worktree remove ${forceFlag} "${absolutePath}"`, {
|
|
186
|
+
encoding: 'utf8',
|
|
187
|
+
stdio: 'pipe'
|
|
188
|
+
});
|
|
189
|
+
} catch (error) {
|
|
190
|
+
throw new Error(`Failed to remove worktree: ${error.message}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Prune stale worktree administrative files
|
|
196
|
+
* @returns {void}
|
|
197
|
+
*/
|
|
198
|
+
export function pruneWorktrees() {
|
|
199
|
+
try {
|
|
200
|
+
execSync('git worktree prune', { encoding: 'utf8', stdio: 'pipe' });
|
|
201
|
+
} catch (error) {
|
|
202
|
+
throw new Error(`Failed to prune worktrees: ${error.message}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Get worktree info by path
|
|
208
|
+
* @param {string} worktreePath - Path to worktree
|
|
209
|
+
* @returns {Object|null}
|
|
210
|
+
*/
|
|
211
|
+
export function getWorktreeInfo(worktreePath) {
|
|
212
|
+
const worktrees = listWorktrees();
|
|
213
|
+
const absolutePath = resolve(worktreePath);
|
|
214
|
+
return worktrees.find(wt => wt.path === absolutePath) || null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Check if current directory is inside a worktree
|
|
219
|
+
* @returns {boolean}
|
|
220
|
+
*/
|
|
221
|
+
export function isInWorktree() {
|
|
222
|
+
try {
|
|
223
|
+
const repoRoot = getRepoRoot();
|
|
224
|
+
const currentDir = process.cwd();
|
|
225
|
+
// Use path.relative for proper cross-platform comparison
|
|
226
|
+
const relativePath = relative(repoRoot, currentDir);
|
|
227
|
+
// Check if the relative path starts with the worktree base
|
|
228
|
+
const worktreeBaseParts = WORKTREE_BASE.split('/');
|
|
229
|
+
const currentParts = relativePath.split(sep);
|
|
230
|
+
// Check if current path is under WORKTREE_BASE
|
|
231
|
+
return worktreeBaseParts.every((part, i) => currentParts[i] === part);
|
|
232
|
+
} catch {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Get the session ID from current worktree path
|
|
239
|
+
* @returns {string|null}
|
|
240
|
+
*/
|
|
241
|
+
export function getCurrentSessionId() {
|
|
242
|
+
if (!isInWorktree()) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
const currentDir = process.cwd();
|
|
248
|
+
const repoRoot = getRepoRoot();
|
|
249
|
+
// Use path.relative for cross-platform compatibility
|
|
250
|
+
const relativePath = relative(repoRoot, currentDir);
|
|
251
|
+
// Split using path.sep for cross-platform support (/ on Unix, \ on Windows)
|
|
252
|
+
const parts = relativePath.split(sep);
|
|
253
|
+
|
|
254
|
+
const sessionsIndex = parts.indexOf('sessions');
|
|
255
|
+
if (sessionsIndex >= 0 && sessionsIndex < parts.length - 1) {
|
|
256
|
+
return parts[sessionsIndex + 1];
|
|
257
|
+
}
|
|
258
|
+
} catch {
|
|
259
|
+
// If anything fails, return null
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* List all session worktrees
|
|
267
|
+
* @returns {Promise<Array<{sessionId: string, path: string, branch: string}>>}
|
|
268
|
+
*/
|
|
269
|
+
export async function listSessionWorktrees() {
|
|
270
|
+
const repoRoot = getRepoRoot();
|
|
271
|
+
const sessionsDir = join(repoRoot, WORKTREE_BASE);
|
|
272
|
+
|
|
273
|
+
if (!existsSync(sessionsDir)) {
|
|
274
|
+
return [];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const allWorktrees = listWorktrees();
|
|
278
|
+
const sessionWorktrees = [];
|
|
279
|
+
|
|
280
|
+
// Get all directories in sessions folder
|
|
281
|
+
const entries = await readdir(sessionsDir);
|
|
282
|
+
|
|
283
|
+
for (const entry of entries) {
|
|
284
|
+
const entryPath = join(sessionsDir, entry);
|
|
285
|
+
const entryStat = await stat(entryPath);
|
|
286
|
+
|
|
287
|
+
if (entryStat.isDirectory()) {
|
|
288
|
+
const worktreeInfo = allWorktrees.find(wt => wt.path === entryPath);
|
|
289
|
+
if (worktreeInfo) {
|
|
290
|
+
sessionWorktrees.push({
|
|
291
|
+
sessionId: entry,
|
|
292
|
+
path: entryPath,
|
|
293
|
+
branch: worktreeInfo.branch,
|
|
294
|
+
commitHash: worktreeInfo.commitHash
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return sessionWorktrees;
|
|
301
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "teleportation-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Remote approval system for Claude Code - approve AI coding changes from your phone",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "teleportation-cli.cjs",
|
|
7
|
+
"bin": {
|
|
8
|
+
"teleportation": "./teleportation-cli.cjs"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"bun": "^1.3.0"
|
|
12
|
+
},
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/dundas/teleportation-private.git"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/dundas/teleportation-private#readme",
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/dundas/teleportation-private/issues"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"claude",
|
|
24
|
+
"claude-code",
|
|
25
|
+
"remote-control",
|
|
26
|
+
"approval",
|
|
27
|
+
"mobile",
|
|
28
|
+
"ai",
|
|
29
|
+
"coding-assistant",
|
|
30
|
+
"cli"
|
|
31
|
+
],
|
|
32
|
+
"author": "Dundas",
|
|
33
|
+
"scripts": {
|
|
34
|
+
"test": "bun --bun vitest run 'tests/**/*.test.js' 'lib/**/*.test.js'",
|
|
35
|
+
"test:watch": "bun --bun vitest",
|
|
36
|
+
"test:coverage": "bun --bun vitest --coverage",
|
|
37
|
+
"test:e2e": "bun --bun vitest run tests/e2e",
|
|
38
|
+
"test:unit": "bun --bun vitest run tests/unit",
|
|
39
|
+
"test:integration": "bun --bun vitest run tests/integration",
|
|
40
|
+
"dev:relay": "cd relay && bun run dev",
|
|
41
|
+
"dev:mobile": "cd mobile-ui && bun run dev",
|
|
42
|
+
"dev:all": "bun run dev:relay & bun run dev:mobile",
|
|
43
|
+
"prepublishOnly": "echo 'Skipping tests for now - will fix test command post-publish'"
|
|
44
|
+
},
|
|
45
|
+
"files": [
|
|
46
|
+
"lib/**/*.js",
|
|
47
|
+
"!lib/**/*.test.js",
|
|
48
|
+
"!lib/**/*.test.mjs",
|
|
49
|
+
"!lib/**/test-*.js",
|
|
50
|
+
"!lib/**/vitest.config.js",
|
|
51
|
+
"!lib/**/*.log",
|
|
52
|
+
".claude/hooks/*.mjs",
|
|
53
|
+
"!.claude/hooks/*.test.mjs",
|
|
54
|
+
"teleportation-cli.cjs",
|
|
55
|
+
"README.md",
|
|
56
|
+
"LICENSE"
|
|
57
|
+
],
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@vitest/coverage-v8": "^4.0.9",
|
|
60
|
+
"axios": "^1.13.2",
|
|
61
|
+
"vitest": "^4.0.9"
|
|
62
|
+
},
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"dotenv": "^17.2.3"
|
|
65
|
+
}
|
|
66
|
+
}
|