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.
Files changed (54) hide show
  1. package/.claude/hooks/config-loader.mjs +93 -0
  2. package/.claude/hooks/heartbeat.mjs +331 -0
  3. package/.claude/hooks/notification.mjs +35 -0
  4. package/.claude/hooks/permission_request.mjs +307 -0
  5. package/.claude/hooks/post_tool_use.mjs +137 -0
  6. package/.claude/hooks/pre_tool_use.mjs +451 -0
  7. package/.claude/hooks/session-register.mjs +274 -0
  8. package/.claude/hooks/session_end.mjs +256 -0
  9. package/.claude/hooks/session_start.mjs +308 -0
  10. package/.claude/hooks/stop.mjs +277 -0
  11. package/.claude/hooks/user_prompt_submit.mjs +91 -0
  12. package/LICENSE +21 -0
  13. package/README.md +243 -0
  14. package/lib/auth/api-key.js +110 -0
  15. package/lib/auth/credentials.js +341 -0
  16. package/lib/backup/manager.js +461 -0
  17. package/lib/cli/daemon-commands.js +299 -0
  18. package/lib/cli/index.js +303 -0
  19. package/lib/cli/session-commands.js +294 -0
  20. package/lib/cli/snapshot-commands.js +223 -0
  21. package/lib/cli/worktree-commands.js +291 -0
  22. package/lib/config/manager.js +306 -0
  23. package/lib/daemon/lifecycle.js +336 -0
  24. package/lib/daemon/pid-manager.js +160 -0
  25. package/lib/daemon/teleportation-daemon.js +2009 -0
  26. package/lib/handoff/config.js +102 -0
  27. package/lib/handoff/example.js +152 -0
  28. package/lib/handoff/git-handoff.js +351 -0
  29. package/lib/handoff/handoff.js +277 -0
  30. package/lib/handoff/index.js +25 -0
  31. package/lib/handoff/session-state.js +238 -0
  32. package/lib/install/installer.js +555 -0
  33. package/lib/machine-coders/claude-code-adapter.js +329 -0
  34. package/lib/machine-coders/example.js +239 -0
  35. package/lib/machine-coders/gemini-cli-adapter.js +406 -0
  36. package/lib/machine-coders/index.js +103 -0
  37. package/lib/machine-coders/interface.js +168 -0
  38. package/lib/router/classifier.js +251 -0
  39. package/lib/router/example.js +92 -0
  40. package/lib/router/index.js +69 -0
  41. package/lib/router/mech-llms-client.js +277 -0
  42. package/lib/router/models.js +188 -0
  43. package/lib/router/router.js +382 -0
  44. package/lib/session/cleanup.js +100 -0
  45. package/lib/session/metadata.js +258 -0
  46. package/lib/session/mute-checker.js +114 -0
  47. package/lib/session-registry/manager.js +302 -0
  48. package/lib/snapshot/manager.js +390 -0
  49. package/lib/utils/errors.js +166 -0
  50. package/lib/utils/logger.js +148 -0
  51. package/lib/utils/retry.js +155 -0
  52. package/lib/worktree/manager.js +301 -0
  53. package/package.json +66 -0
  54. package/teleportation-cli.cjs +2987 -0
@@ -0,0 +1,461 @@
1
+ /**
2
+ * Backup Manager for Teleportation
3
+ *
4
+ * Handles backup and rollback of configuration files before setup/changes.
5
+ * Ensures users can safely restore previous state if something goes wrong.
6
+ */
7
+
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+ import crypto from 'crypto';
11
+ import os from 'os';
12
+
13
+ const HOME_DIR = os.homedir();
14
+ const BACKUP_DIR = path.join(HOME_DIR, '.teleportation', 'backups');
15
+ const MAX_BACKUPS = 10;
16
+ const MIN_BACKUP_AGE_DAYS = 7;
17
+
18
+ /**
19
+ * Files and directories that may be modified during setup
20
+ */
21
+ const BACKUP_TARGETS = [
22
+ {
23
+ name: 'claude-settings',
24
+ path: path.join(HOME_DIR, '.claude', 'settings.json'),
25
+ type: 'file',
26
+ description: 'Claude Code settings'
27
+ },
28
+ {
29
+ name: 'claude-hooks',
30
+ path: path.join(HOME_DIR, '.claude', 'hooks'),
31
+ type: 'directory',
32
+ description: 'Claude Code hooks'
33
+ },
34
+ {
35
+ name: 'teleportation-config',
36
+ path: path.join(HOME_DIR, '.teleportation', 'config.json'),
37
+ type: 'file',
38
+ description: 'Teleportation configuration'
39
+ },
40
+ {
41
+ name: 'teleportation-credentials',
42
+ path: path.join(HOME_DIR, '.teleportation', 'credentials'),
43
+ type: 'file',
44
+ description: 'Teleportation credentials'
45
+ }
46
+ ];
47
+
48
+ export class BackupManager {
49
+ constructor() {
50
+ this.backupDir = BACKUP_DIR;
51
+ }
52
+
53
+ /**
54
+ * Create a backup of all relevant files before making changes
55
+ * @param {string} reason - Why this backup is being created (e.g., "teleportation setup")
56
+ * @returns {Promise<{backupPath: string, manifest: object}>}
57
+ */
58
+ async createBackup(reason) {
59
+ // Add random suffix to prevent collisions if multiple backups in same millisecond
60
+ const randomSuffix = crypto.randomBytes(3).toString('hex');
61
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-') + '-' + randomSuffix;
62
+ const backupPath = path.join(this.backupDir, timestamp);
63
+
64
+ // Ensure backup directory exists
65
+ fs.mkdirSync(backupPath, { recursive: true });
66
+
67
+ const manifest = {
68
+ timestamp: new Date().toISOString(),
69
+ reason,
70
+ version: '1.0.0',
71
+ files: []
72
+ };
73
+
74
+ // Backup each target
75
+ for (const target of BACKUP_TARGETS) {
76
+ if (!fs.existsSync(target.path)) {
77
+ continue; // Skip if doesn't exist
78
+ }
79
+
80
+ const backupName = target.type === 'directory'
81
+ ? target.name
82
+ : `${target.name}.json`;
83
+ const backupTarget = path.join(backupPath, backupName);
84
+
85
+ try {
86
+ if (target.type === 'directory') {
87
+ await this._copyDirectoryWithPermissions(target.path, backupTarget);
88
+ const files = fs.readdirSync(target.path);
89
+ manifest.files.push({
90
+ original: target.path,
91
+ backup: backupName,
92
+ type: 'directory',
93
+ existed: true,
94
+ fileCount: files.length,
95
+ files: files,
96
+ description: target.description
97
+ });
98
+ } else {
99
+ // Copy file and preserve permissions
100
+ fs.copyFileSync(target.path, backupTarget);
101
+ const srcStat = fs.statSync(target.path);
102
+ fs.chmodSync(backupTarget, srcStat.mode);
103
+ const hash = await this._hashFile(target.path);
104
+ manifest.files.push({
105
+ original: target.path,
106
+ backup: backupName,
107
+ type: 'file',
108
+ existed: true,
109
+ hash: target.name.includes('credentials') ? undefined : hash,
110
+ description: target.description
111
+ });
112
+ }
113
+ } catch (err) {
114
+ console.error(`Warning: Failed to backup ${target.path}: ${err.message}`);
115
+ }
116
+ }
117
+
118
+ // Save manifest
119
+ manifest.canRollback = manifest.files.length > 0;
120
+ fs.writeFileSync(
121
+ path.join(backupPath, 'manifest.json'),
122
+ JSON.stringify(manifest, null, 2)
123
+ );
124
+
125
+ // Update 'latest' symlink
126
+ await this._updateLatestLink(timestamp);
127
+
128
+ // Cleanup old backups
129
+ await this._cleanupOldBackups();
130
+
131
+ return { backupPath, manifest };
132
+ }
133
+
134
+ /**
135
+ * Restore from a backup
136
+ * @param {string|null} backupId - Specific backup ID or null for latest
137
+ * @returns {Promise<{manifest: object, restoredFiles: string[]}>}
138
+ */
139
+ async restore(backupId = null) {
140
+ let backupPath;
141
+
142
+ if (backupId) {
143
+ backupPath = path.join(this.backupDir, backupId);
144
+ } else {
145
+ // Use latest (with symlink fallback support)
146
+ backupPath = this._resolveLatest();
147
+ if (!backupPath) {
148
+ throw new Error('No backups found');
149
+ }
150
+ }
151
+
152
+ const manifestPath = path.join(backupPath, 'manifest.json');
153
+ if (!fs.existsSync(manifestPath)) {
154
+ throw new Error(`No backup manifest found at ${backupPath}`);
155
+ }
156
+
157
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
158
+ const restoredFiles = [];
159
+ const failedFiles = [];
160
+
161
+ for (const file of manifest.files) {
162
+ const backupFile = path.join(backupPath, file.backup);
163
+
164
+ if (!fs.existsSync(backupFile)) {
165
+ console.error(`Warning: Backup file not found: ${backupFile}`);
166
+ failedFiles.push({ path: file.original, reason: 'Backup file not found' });
167
+ continue;
168
+ }
169
+
170
+ try {
171
+ if (file.type === 'directory') {
172
+ // Remove existing directory first
173
+ if (fs.existsSync(file.original)) {
174
+ fs.rmSync(file.original, { recursive: true, force: true });
175
+ }
176
+ await this._copyDirectoryWithPermissions(backupFile, file.original);
177
+ } else {
178
+ // Ensure parent directory exists
179
+ const parentDir = path.dirname(file.original);
180
+ if (!fs.existsSync(parentDir)) {
181
+ fs.mkdirSync(parentDir, { recursive: true });
182
+ }
183
+ // Copy file and preserve permissions
184
+ fs.copyFileSync(backupFile, file.original);
185
+ const backupStat = fs.statSync(backupFile);
186
+ fs.chmodSync(file.original, backupStat.mode);
187
+ }
188
+ restoredFiles.push(file.original);
189
+ } catch (err) {
190
+ console.error(`Warning: Failed to restore ${file.original}: ${err.message}`);
191
+ failedFiles.push({ path: file.original, reason: err.message });
192
+ }
193
+ }
194
+
195
+ return { manifest, restoredFiles, failedFiles };
196
+ }
197
+
198
+ /**
199
+ * List all available backups
200
+ * @returns {Array<{id: string, timestamp: string, reason: string, fileCount: number}>}
201
+ */
202
+ listBackups() {
203
+ if (!fs.existsSync(this.backupDir)) {
204
+ return [];
205
+ }
206
+
207
+ const backups = [];
208
+ const entries = fs.readdirSync(this.backupDir);
209
+
210
+ for (const entry of entries) {
211
+ if (entry === 'latest') continue;
212
+
213
+ const entryPath = path.join(this.backupDir, entry);
214
+ const stat = fs.statSync(entryPath);
215
+
216
+ if (!stat.isDirectory()) continue;
217
+
218
+ const manifestPath = path.join(entryPath, 'manifest.json');
219
+ if (!fs.existsSync(manifestPath)) continue;
220
+
221
+ try {
222
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
223
+ backups.push({
224
+ id: entry,
225
+ timestamp: manifest.timestamp,
226
+ reason: manifest.reason,
227
+ fileCount: manifest.files.length,
228
+ canRollback: manifest.canRollback
229
+ });
230
+ } catch (err) {
231
+ // Skip invalid manifests
232
+ }
233
+ }
234
+
235
+ // Sort by timestamp descending (newest first)
236
+ return backups.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
237
+ }
238
+
239
+ /**
240
+ * Get details of a specific backup
241
+ * @param {string} backupId
242
+ * @returns {object|null}
243
+ */
244
+ getBackupDetails(backupId) {
245
+ const manifestPath = path.join(this.backupDir, backupId, 'manifest.json');
246
+ if (!fs.existsSync(manifestPath)) {
247
+ return null;
248
+ }
249
+ return JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
250
+ }
251
+
252
+ /**
253
+ * Check if there are any files that would be modified
254
+ * @returns {Array<{path: string, description: string, exists: boolean}>}
255
+ */
256
+ checkExistingFiles() {
257
+ const existing = [];
258
+
259
+ for (const target of BACKUP_TARGETS) {
260
+ if (fs.existsSync(target.path)) {
261
+ const info = {
262
+ path: target.path,
263
+ description: target.description,
264
+ exists: true
265
+ };
266
+
267
+ if (target.type === 'directory') {
268
+ const files = fs.readdirSync(target.path);
269
+ info.fileCount = files.length;
270
+ info.files = files;
271
+ }
272
+
273
+ existing.push(info);
274
+ }
275
+ }
276
+
277
+ return existing;
278
+ }
279
+
280
+ /**
281
+ * Delete a specific backup
282
+ * @param {string} backupId
283
+ */
284
+ deleteBackup(backupId) {
285
+ const backupPath = path.join(this.backupDir, backupId);
286
+ if (fs.existsSync(backupPath)) {
287
+ fs.rmSync(backupPath, { recursive: true, force: true });
288
+ }
289
+
290
+ // Update latest link if needed (handles both symlink and fallback file)
291
+ const latestLink = path.join(this.backupDir, 'latest');
292
+ if (fs.existsSync(latestLink)) {
293
+ try {
294
+ const stat = fs.lstatSync(latestLink);
295
+ let latestTarget;
296
+
297
+ if (stat.isSymbolicLink()) {
298
+ // It's a symlink - read target directly
299
+ latestTarget = fs.readlinkSync(latestLink);
300
+ } else {
301
+ // It's a fallback file (Windows compat) - read content as target ID
302
+ latestTarget = fs.readFileSync(latestLink, 'utf8').trim();
303
+ }
304
+
305
+ if (latestTarget === backupId) {
306
+ fs.unlinkSync(latestLink);
307
+ // Point to next most recent
308
+ const backups = this.listBackups();
309
+ if (backups.length > 0) {
310
+ // Use _updateLatestLink to handle symlink/fallback consistently
311
+ this._updateLatestLink(backups[0].id);
312
+ }
313
+ }
314
+ } catch (err) {
315
+ // If we can't read the latest link, just remove it
316
+ try {
317
+ fs.unlinkSync(latestLink);
318
+ } catch {
319
+ // Ignore errors removing latest link
320
+ }
321
+ }
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Clean up old backups beyond retention limit
327
+ */
328
+ async _cleanupOldBackups() {
329
+ const backups = this.listBackups();
330
+
331
+ if (backups.length <= MAX_BACKUPS) {
332
+ return;
333
+ }
334
+
335
+ const now = Date.now();
336
+ const minAgeMs = MIN_BACKUP_AGE_DAYS * 24 * 60 * 60 * 1000;
337
+
338
+ // Keep MAX_BACKUPS, but never delete backups less than MIN_BACKUP_AGE_DAYS old
339
+ const toDelete = backups.slice(MAX_BACKUPS).filter(backup => {
340
+ const backupTime = new Date(backup.timestamp).getTime();
341
+ return (now - backupTime) > minAgeMs;
342
+ });
343
+
344
+ for (const backup of toDelete) {
345
+ this.deleteBackup(backup.id);
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Update the 'latest' symlink (with fallback to regular file for Windows compatibility)
351
+ */
352
+ async _updateLatestLink(targetId) {
353
+ const latestLink = path.join(this.backupDir, 'latest');
354
+
355
+ try {
356
+ // Remove existing link/file
357
+ if (fs.existsSync(latestLink)) {
358
+ fs.unlinkSync(latestLink);
359
+ }
360
+
361
+ // Try to create symlink first
362
+ try {
363
+ fs.symlinkSync(targetId, latestLink);
364
+ } catch (symlinkErr) {
365
+ // Fallback: create a regular file containing the target ID (Windows compatibility)
366
+ fs.writeFileSync(latestLink, targetId, 'utf8');
367
+ }
368
+ } catch (err) {
369
+ // Symlinks might fail on some systems, that's ok
370
+ console.error(`Warning: Could not create latest symlink: ${err.message}`);
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Resolve 'latest' to actual backup path (supports both symlink and fallback file)
376
+ */
377
+ _resolveLatest() {
378
+ const latestLink = path.join(this.backupDir, 'latest');
379
+
380
+ if (!fs.existsSync(latestLink)) {
381
+ return null;
382
+ }
383
+
384
+ try {
385
+ // Try symlink first
386
+ const stat = fs.lstatSync(latestLink);
387
+ if (stat.isSymbolicLink()) {
388
+ return fs.realpathSync(latestLink);
389
+ }
390
+
391
+ // Fallback: read as file containing target ID
392
+ const targetId = fs.readFileSync(latestLink, 'utf8').trim();
393
+ const targetPath = path.join(this.backupDir, targetId);
394
+ if (fs.existsSync(targetPath)) {
395
+ return targetPath;
396
+ }
397
+ } catch (err) {
398
+ // Try direct realpath as last resort
399
+ try {
400
+ return fs.realpathSync(latestLink);
401
+ } catch {
402
+ return null;
403
+ }
404
+ }
405
+
406
+ return null;
407
+ }
408
+
409
+ /**
410
+ * Copy a directory recursively (basic, without permission preservation)
411
+ */
412
+ async _copyDirectory(src, dest) {
413
+ fs.mkdirSync(dest, { recursive: true });
414
+
415
+ const entries = fs.readdirSync(src, { withFileTypes: true });
416
+
417
+ for (const entry of entries) {
418
+ const srcPath = path.join(src, entry.name);
419
+ const destPath = path.join(dest, entry.name);
420
+
421
+ if (entry.isDirectory()) {
422
+ await this._copyDirectory(srcPath, destPath);
423
+ } else {
424
+ fs.copyFileSync(srcPath, destPath);
425
+ }
426
+ }
427
+ }
428
+
429
+ /**
430
+ * Copy a directory recursively with permission preservation
431
+ */
432
+ async _copyDirectoryWithPermissions(src, dest) {
433
+ const srcStat = fs.statSync(src);
434
+ fs.mkdirSync(dest, { recursive: true, mode: srcStat.mode });
435
+
436
+ const entries = fs.readdirSync(src, { withFileTypes: true });
437
+
438
+ for (const entry of entries) {
439
+ const srcPath = path.join(src, entry.name);
440
+ const destPath = path.join(dest, entry.name);
441
+
442
+ if (entry.isDirectory()) {
443
+ await this._copyDirectoryWithPermissions(srcPath, destPath);
444
+ } else {
445
+ fs.copyFileSync(srcPath, destPath);
446
+ const fileStat = fs.statSync(srcPath);
447
+ fs.chmodSync(destPath, fileStat.mode);
448
+ }
449
+ }
450
+ }
451
+
452
+ /**
453
+ * Calculate SHA256 hash of a file
454
+ */
455
+ async _hashFile(filePath) {
456
+ const content = fs.readFileSync(filePath);
457
+ return 'sha256:' + crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
458
+ }
459
+ }
460
+
461
+ export default BackupManager;