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,390 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Snapshot Management Module
4
+ * Handles creation, restoration, and management of code snapshots
5
+ */
6
+
7
+ import { execSync } from 'child_process';
8
+ import { join, resolve, basename } from 'path';
9
+ import { mkdir, writeFile, readFile, readdir, rm } from 'fs/promises';
10
+ import { existsSync } from 'fs';
11
+ import { getRepoRoot, validateSessionId } from '../worktree/manager.js';
12
+
13
+ const SNAPSHOT_BASE = '.teleportation/snapshots';
14
+
15
+ // Validation pattern for snapshot IDs (session-type-timestamp format)
16
+ const VALID_SNAPSHOT_ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}-[a-z-]+-\d+$/;
17
+
18
+ /**
19
+ * Validate a snapshot ID format
20
+ * @param {string} snapshotId - Snapshot ID to validate
21
+ * @returns {string} Validated snapshot ID
22
+ * @throws {Error} If snapshot ID is invalid
23
+ */
24
+ function validateSnapshotId(snapshotId) {
25
+ if (!snapshotId || typeof snapshotId !== 'string') {
26
+ throw new Error('Snapshot ID is required');
27
+ }
28
+ if (!VALID_SNAPSHOT_ID.test(snapshotId)) {
29
+ throw new Error(
30
+ `Invalid snapshot ID format: "${snapshotId}". Expected format: sessionId-type-timestamp`
31
+ );
32
+ }
33
+ return snapshotId;
34
+ }
35
+
36
+ /**
37
+ * Validate a commit hash (SHA-1 or abbreviated)
38
+ * @param {string} commitHash - Commit hash to validate
39
+ * @returns {string} Validated commit hash
40
+ * @throws {Error} If commit hash is invalid
41
+ */
42
+ function validateCommitHash(commitHash) {
43
+ if (!commitHash || typeof commitHash !== 'string') {
44
+ throw new Error('Commit hash is required');
45
+ }
46
+ // SHA-1 hashes are 40 hex chars, but abbreviated can be 7+
47
+ if (!/^[a-f0-9]{7,40}$/i.test(commitHash)) {
48
+ throw new Error(`Invalid commit hash: "${commitHash}"`);
49
+ }
50
+ return commitHash;
51
+ }
52
+
53
+ /**
54
+ * Validate a stash reference
55
+ * @param {string} stashRef - Stash reference to validate
56
+ * @returns {string} Validated stash reference
57
+ * @throws {Error} If stash reference is invalid
58
+ */
59
+ function validateStashRef(stashRef) {
60
+ if (!stashRef || typeof stashRef !== 'string') {
61
+ throw new Error('Stash reference is required');
62
+ }
63
+ // stash@{n} format
64
+ if (!/^stash@\{\d+\}$/.test(stashRef)) {
65
+ throw new Error(`Invalid stash reference: "${stashRef}"`);
66
+ }
67
+ return stashRef;
68
+ }
69
+
70
+ /**
71
+ * Snapshot types
72
+ */
73
+ export const SnapshotType = {
74
+ BASELINE: 'baseline', // Initial state when session starts
75
+ CHECKPOINT: 'checkpoint', // Manual checkpoint
76
+ PRE_MERGE: 'pre-merge', // Before merging from another branch
77
+ PRE_COMMIT: 'pre-commit', // Before committing
78
+ AUTO: 'auto', // Automatic periodic snapshot
79
+ PRE_DESTROY: 'pre-destroy' // Before destroying worktree
80
+ };
81
+
82
+ /**
83
+ * Create a snapshot of the current working directory state
84
+ * @param {string} sessionId - Session identifier
85
+ * @param {string} type - Snapshot type
86
+ * @param {string} message - Optional message describing the snapshot
87
+ * @returns {Promise<{id: string, type: string, timestamp: number, message: string, stashRef: string}>}
88
+ */
89
+ export async function createSnapshot(sessionId, type = SnapshotType.AUTO, message = '') {
90
+ // Validate session ID to prevent command injection
91
+ const validSessionId = validateSessionId(sessionId);
92
+
93
+ // Validate snapshot type
94
+ const validTypes = Object.values(SnapshotType);
95
+ if (!validTypes.includes(type)) {
96
+ throw new Error(`Invalid snapshot type: "${type}". Valid types: ${validTypes.join(', ')}`);
97
+ }
98
+
99
+ const repoRoot = getRepoRoot();
100
+ const timestamp = Date.now();
101
+ const snapshotId = `${validSessionId}-${type}-${timestamp}`;
102
+
103
+ // Create snapshot metadata
104
+ const metadata = {
105
+ id: snapshotId,
106
+ sessionId,
107
+ type,
108
+ timestamp,
109
+ message,
110
+ branch: getCurrentBranch(),
111
+ commitHash: getCurrentCommitHash(),
112
+ hasUncommittedChanges: hasUncommittedChanges(),
113
+ hasUntrackedFiles: hasUntrackedFiles()
114
+ };
115
+
116
+ // Ensure snapshot directory exists
117
+ const snapshotDir = join(repoRoot, SNAPSHOT_BASE, sessionId);
118
+ await mkdir(snapshotDir, { recursive: true });
119
+
120
+ // If there are uncommitted changes, create a git stash
121
+ let stashRef = null;
122
+ if (metadata.hasUncommittedChanges || metadata.hasUntrackedFiles) {
123
+ stashRef = createStash(snapshotId);
124
+ metadata.stashRef = stashRef;
125
+ }
126
+
127
+ // Save metadata
128
+ const metadataPath = join(snapshotDir, `${snapshotId}.json`);
129
+ await writeFile(metadataPath, JSON.stringify(metadata, null, 2));
130
+
131
+ return metadata;
132
+ }
133
+
134
+ /**
135
+ * Restore a snapshot
136
+ * @param {string} snapshotId - Snapshot ID to restore
137
+ * @param {boolean} force - Force restore even with uncommitted changes
138
+ * @returns {Promise<void>}
139
+ */
140
+ export async function restoreSnapshot(snapshotId, force = false) {
141
+ const repoRoot = getRepoRoot();
142
+
143
+ // Find snapshot metadata
144
+ const metadata = await getSnapshotMetadata(snapshotId);
145
+ if (!metadata) {
146
+ throw new Error(`Snapshot not found: ${snapshotId}`);
147
+ }
148
+
149
+ // Validate commit hash before using in command
150
+ const validCommitHash = validateCommitHash(metadata.commitHash);
151
+
152
+ // Check for uncommitted changes
153
+ if (!force && (hasUncommittedChanges() || hasUntrackedFiles())) {
154
+ throw new Error(
155
+ 'Cannot restore snapshot: uncommitted changes present. Use --force to override.'
156
+ );
157
+ }
158
+
159
+ // Restore commit state
160
+ try {
161
+ execSync(`git checkout "${validCommitHash}"`, {
162
+ encoding: 'utf8',
163
+ stdio: 'pipe'
164
+ });
165
+ } catch (error) {
166
+ throw new Error(`Failed to checkout commit ${validCommitHash}: ${error.message}`);
167
+ }
168
+
169
+ // Restore stash if exists - use apply to allow multiple restores
170
+ if (metadata.stashRef) {
171
+ const validStashRef = validateStashRef(metadata.stashRef);
172
+ try {
173
+ // Use 'apply' instead of 'pop' to keep stash for multiple restores
174
+ // The stash is only dropped when the snapshot is deleted
175
+ execSync(`git stash apply "${validStashRef}"`, {
176
+ encoding: 'utf8',
177
+ stdio: 'pipe'
178
+ });
179
+
180
+ // Track restoration in metadata (but keep stashRef for future restores)
181
+ const metadataPath = join(
182
+ repoRoot,
183
+ SNAPSHOT_BASE,
184
+ metadata.sessionId,
185
+ `${snapshotId}.json`
186
+ );
187
+ metadata.lastRestoredAt = Date.now();
188
+ metadata.restoreCount = (metadata.restoreCount || 0) + 1;
189
+ await writeFile(metadataPath, JSON.stringify(metadata, null, 2));
190
+ } catch (error) {
191
+ throw new Error(`Failed to apply stash ${validStashRef}: ${error.message}`);
192
+ }
193
+ }
194
+ }
195
+
196
+ /**
197
+ * List all snapshots for a session
198
+ * @param {string} sessionId - Session identifier
199
+ * @returns {Promise<Array<Object>>}
200
+ */
201
+ export async function listSnapshots(sessionId) {
202
+ const repoRoot = getRepoRoot();
203
+ const snapshotDir = join(repoRoot, SNAPSHOT_BASE, sessionId);
204
+
205
+ if (!existsSync(snapshotDir)) {
206
+ return [];
207
+ }
208
+
209
+ const files = await readdir(snapshotDir);
210
+ const snapshots = [];
211
+
212
+ for (const file of files) {
213
+ if (file.endsWith('.json')) {
214
+ const metadataPath = join(snapshotDir, file);
215
+ const content = await readFile(metadataPath, 'utf8');
216
+ snapshots.push(JSON.parse(content));
217
+ }
218
+ }
219
+
220
+ // Sort by timestamp (newest first)
221
+ snapshots.sort((a, b) => b.timestamp - a.timestamp);
222
+
223
+ return snapshots;
224
+ }
225
+
226
+ /**
227
+ * Delete a snapshot
228
+ * @param {string} snapshotId - Snapshot ID to delete
229
+ * @returns {Promise<void>}
230
+ */
231
+ export async function deleteSnapshot(snapshotId) {
232
+ const metadata = await getSnapshotMetadata(snapshotId);
233
+ if (!metadata) {
234
+ throw new Error(`Snapshot not found: ${snapshotId}`);
235
+ }
236
+
237
+ const repoRoot = getRepoRoot();
238
+ const metadataPath = join(
239
+ repoRoot,
240
+ SNAPSHOT_BASE,
241
+ metadata.sessionId,
242
+ `${snapshotId}.json`
243
+ );
244
+
245
+ // Delete stash if it exists
246
+ if (metadata.stashRef) {
247
+ const validStashRef = validateStashRef(metadata.stashRef);
248
+ try {
249
+ execSync(`git stash drop "${validStashRef}"`, {
250
+ encoding: 'utf8',
251
+ stdio: 'pipe'
252
+ });
253
+ } catch (error) {
254
+ // Check if stash still exists - if yes, it's a real error
255
+ const stashList = execSync('git stash list', { encoding: 'utf8' });
256
+ if (stashList.includes(validStashRef)) {
257
+ throw new Error(`Failed to drop stash ${validStashRef}: stash still exists`);
258
+ }
259
+ // Stash already gone (possibly from earlier operation), safe to continue
260
+ }
261
+ }
262
+
263
+ // Now safe to delete metadata file
264
+ await rm(metadataPath, { force: true });
265
+ }
266
+
267
+ /**
268
+ * Delete all snapshots for a session
269
+ * @param {string} sessionId - Session identifier
270
+ * @returns {Promise<number>} Number of snapshots deleted
271
+ */
272
+ export async function deleteAllSnapshots(sessionId) {
273
+ const snapshots = await listSnapshots(sessionId);
274
+
275
+ for (const snapshot of snapshots) {
276
+ await deleteSnapshot(snapshot.id);
277
+ }
278
+
279
+ // Remove session directory if empty
280
+ const repoRoot = getRepoRoot();
281
+ const snapshotDir = join(repoRoot, SNAPSHOT_BASE, sessionId);
282
+
283
+ try {
284
+ await rm(snapshotDir, { recursive: true, force: true });
285
+ } catch {
286
+ // Directory might not be empty or might not exist, that's ok
287
+ }
288
+
289
+ return snapshots.length;
290
+ }
291
+
292
+ /**
293
+ * Get diff between current state and a snapshot
294
+ * @param {string} snapshotId - Snapshot ID
295
+ * @returns {Promise<string>}
296
+ */
297
+ export async function getSnapshotDiff(snapshotId) {
298
+ const metadata = await getSnapshotMetadata(snapshotId);
299
+ if (!metadata) {
300
+ throw new Error(`Snapshot not found: ${snapshotId}`);
301
+ }
302
+
303
+ // Validate commit hash before using in command
304
+ const validCommitHash = validateCommitHash(metadata.commitHash);
305
+
306
+ try {
307
+ const diff = execSync(`git diff "${validCommitHash}"`, {
308
+ encoding: 'utf8'
309
+ });
310
+ return diff;
311
+ } catch (error) {
312
+ throw new Error(`Failed to get diff: ${error.message}`);
313
+ }
314
+ }
315
+
316
+ // Helper functions
317
+
318
+ function getCurrentBranch() {
319
+ try {
320
+ return execSync('git branch --show-current', { encoding: 'utf8' }).trim();
321
+ } catch {
322
+ return null;
323
+ }
324
+ }
325
+
326
+ function getCurrentCommitHash() {
327
+ try {
328
+ return execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim();
329
+ } catch {
330
+ return null;
331
+ }
332
+ }
333
+
334
+ function hasUncommittedChanges() {
335
+ try {
336
+ const status = execSync('git status --porcelain', { encoding: 'utf8' });
337
+ // Check for modified or staged files (lines starting with M, A, D, etc.)
338
+ return status.split('\n').some(line => {
339
+ const trimmed = line.trim();
340
+ return trimmed && !trimmed.startsWith('??');
341
+ });
342
+ } catch {
343
+ return false;
344
+ }
345
+ }
346
+
347
+ function hasUntrackedFiles() {
348
+ try {
349
+ const status = execSync('git status --porcelain', { encoding: 'utf8' });
350
+ // Check for untracked files (lines starting with ??)
351
+ return status.includes('??');
352
+ } catch {
353
+ return false;
354
+ }
355
+ }
356
+
357
+ function createStash(snapshotId) {
358
+ // snapshotId is already validated in createSnapshot, but double-check format
359
+ // to ensure no shell metacharacters in stash message
360
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*-[a-z-]+-\d+$/.test(snapshotId)) {
361
+ throw new Error(`Invalid snapshot ID for stash: ${snapshotId}`);
362
+ }
363
+
364
+ try {
365
+ execSync(`git stash push -u -m "snapshot:${snapshotId}"`, {
366
+ encoding: 'utf8',
367
+ stdio: 'pipe'
368
+ });
369
+
370
+ // Get the stash reference
371
+ const stashList = execSync('git stash list', { encoding: 'utf8' });
372
+ const match = stashList.match(/stash@\{0\}/);
373
+ return match ? match[0] : null;
374
+ } catch (error) {
375
+ throw new Error(`Failed to create stash: ${error.message}`);
376
+ }
377
+ }
378
+
379
+ async function getSnapshotMetadata(snapshotId) {
380
+ const repoRoot = getRepoRoot();
381
+ const sessionId = snapshotId.split('-')[0];
382
+ const metadataPath = join(repoRoot, SNAPSHOT_BASE, sessionId, `${snapshotId}.json`);
383
+
384
+ if (!existsSync(metadataPath)) {
385
+ return null;
386
+ }
387
+
388
+ const content = await readFile(metadataPath, 'utf8');
389
+ return JSON.parse(content);
390
+ }
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Custom error classes for Teleportation CLI
4
+ * Provides structured error handling with user-friendly messages
5
+ */
6
+
7
+ class TeleportationError extends Error {
8
+ constructor(message, code = 'UNKNOWN_ERROR', details = null) {
9
+ super(message);
10
+ this.name = 'TeleportationError';
11
+ this.code = code;
12
+ this.details = details;
13
+ Error.captureStackTrace(this, this.constructor);
14
+ }
15
+
16
+ toJSON() {
17
+ return {
18
+ name: this.name,
19
+ message: this.message,
20
+ code: this.code,
21
+ details: this.details
22
+ };
23
+ }
24
+ }
25
+
26
+ class ConfigurationError extends TeleportationError {
27
+ constructor(message, details = null) {
28
+ super(message, 'CONFIG_ERROR', details);
29
+ this.name = 'ConfigurationError';
30
+ }
31
+ }
32
+
33
+ class AuthenticationError extends TeleportationError {
34
+ constructor(message, details = null) {
35
+ super(message, 'AUTH_ERROR', details);
36
+ this.name = 'AuthenticationError';
37
+ }
38
+ }
39
+
40
+ class NetworkError extends TeleportationError {
41
+ constructor(message, details = null) {
42
+ super(message, 'NETWORK_ERROR', details);
43
+ this.name = 'NetworkError';
44
+ }
45
+ }
46
+
47
+ class ValidationError extends TeleportationError {
48
+ constructor(message, details = null) {
49
+ super(message, 'VALIDATION_ERROR', details);
50
+ this.name = 'ValidationError';
51
+ }
52
+ }
53
+
54
+ class FileSystemError extends TeleportationError {
55
+ constructor(message, details = null) {
56
+ super(message, 'FILESYSTEM_ERROR', details);
57
+ this.name = 'FileSystemError';
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Format error for user display
63
+ */
64
+ function formatError(error) {
65
+ if (error instanceof TeleportationError) {
66
+ return {
67
+ message: error.message,
68
+ code: error.code,
69
+ details: error.details,
70
+ userFriendly: getUserFriendlyMessage(error)
71
+ };
72
+ }
73
+
74
+ // Handle standard errors
75
+ return {
76
+ message: error.message,
77
+ code: 'UNKNOWN_ERROR',
78
+ details: null,
79
+ userFriendly: getUserFriendlyMessage(error)
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Get user-friendly error message
85
+ */
86
+ function getUserFriendlyMessage(error) {
87
+ if (error instanceof ConfigurationError) {
88
+ return `Configuration error: ${error.message}. Please check your settings with 'teleportation config list'.`;
89
+ }
90
+
91
+ if (error instanceof AuthenticationError) {
92
+ return `Authentication failed: ${error.message}. Please try 'teleportation login' again.`;
93
+ }
94
+
95
+ if (error instanceof NetworkError) {
96
+ return `Network error: ${error.message}. Please check your internet connection and try again.`;
97
+ }
98
+
99
+ if (error instanceof ValidationError) {
100
+ return `Invalid input: ${error.message}. Please check your command syntax.`;
101
+ }
102
+
103
+ if (error instanceof FileSystemError) {
104
+ return `File system error: ${error.message}. Please check file permissions.`;
105
+ }
106
+
107
+ // Handle common system errors
108
+ if (error.code === 'ENOENT') {
109
+ return `File not found: ${error.message}. The file may have been moved or deleted.`;
110
+ }
111
+
112
+ if (error.code === 'EACCES') {
113
+ return `Permission denied: ${error.message}. Please check file permissions.`;
114
+ }
115
+
116
+ if (error.code === 'ECONNREFUSED') {
117
+ return `Connection refused: ${error.message}. The server may be down or unreachable.`;
118
+ }
119
+
120
+ if (error.code === 'ETIMEDOUT') {
121
+ return `Connection timeout: ${error.message}. The server took too long to respond.`;
122
+ }
123
+
124
+ // Default message
125
+ return error.message || 'An unexpected error occurred. Please try again.';
126
+ }
127
+
128
+ /**
129
+ * Handle error and exit with appropriate code
130
+ */
131
+ function handleError(error, logger = null) {
132
+ const formatted = formatError(error);
133
+
134
+ if (logger) {
135
+ logger.error('Error occurred', formatted);
136
+ } else {
137
+ console.error(`❌ Error [${formatted.code}]: ${formatted.userFriendly}`);
138
+ if (formatted.details && process.env.DEBUG) {
139
+ console.error('Details:', formatted.details);
140
+ }
141
+ }
142
+
143
+ // Exit with appropriate code
144
+ const exitCodes = {
145
+ 'CONFIG_ERROR': 2,
146
+ 'AUTH_ERROR': 3,
147
+ 'NETWORK_ERROR': 4,
148
+ 'VALIDATION_ERROR': 5,
149
+ 'FILESYSTEM_ERROR': 6
150
+ };
151
+
152
+ process.exit(exitCodes[formatted.code] || 1);
153
+ }
154
+
155
+ export {
156
+ TeleportationError,
157
+ ConfigurationError,
158
+ AuthenticationError,
159
+ NetworkError,
160
+ ValidationError,
161
+ FileSystemError,
162
+ formatError,
163
+ getUserFriendlyMessage,
164
+ handleError
165
+ };
166
+
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Structured logging utility for Teleportation CLI
4
+ * Supports different log levels and output formats
5
+ */
6
+
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ import os from 'os';
10
+
11
+ const LOG_LEVELS = {
12
+ DEBUG: 0,
13
+ INFO: 1,
14
+ WARN: 2,
15
+ ERROR: 3,
16
+ NONE: 4
17
+ };
18
+
19
+ const LOG_LEVEL_NAMES = {
20
+ 0: 'DEBUG',
21
+ 1: 'INFO',
22
+ 2: 'WARN',
23
+ 3: 'ERROR',
24
+ 4: 'NONE'
25
+ };
26
+
27
+ class Logger {
28
+ constructor(options = {}) {
29
+ this.level = options.level || (process.env.DEBUG ? LOG_LEVELS.DEBUG : LOG_LEVELS.INFO);
30
+ this.logFile = options.logFile || path.join(os.homedir(), '.teleportation', 'logs', 'cli.log');
31
+ this.enableFileLogging = options.enableFileLogging !== false;
32
+ this.enableColors = options.enableColors !== false && process.stdout.isTTY;
33
+
34
+ // Ensure log directory exists
35
+ if (this.enableFileLogging) {
36
+ const logDir = path.dirname(this.logFile);
37
+ if (!fs.existsSync(logDir)) {
38
+ fs.mkdirSync(logDir, { recursive: true });
39
+ }
40
+ }
41
+ }
42
+
43
+ _colorize(level, message) {
44
+ if (!this.enableColors) return message;
45
+
46
+ const colors = {
47
+ DEBUG: '\x1b[0;36m', // Cyan
48
+ INFO: '\x1b[0;32m', // Green
49
+ WARN: '\x1b[1;33m', // Yellow
50
+ ERROR: '\x1b[0;31m' // Red
51
+ };
52
+ const reset = '\x1b[0m';
53
+
54
+ return `${colors[level] || ''}${message}${reset}`;
55
+ }
56
+
57
+ _formatMessage(level, message, data = null) {
58
+ const timestamp = new Date().toISOString();
59
+ const levelName = LOG_LEVEL_NAMES[level];
60
+ const prefix = `[${timestamp}] [${levelName}]`;
61
+
62
+ let formatted = `${prefix} ${message}`;
63
+ if (data) {
64
+ formatted += ` ${JSON.stringify(data)}`;
65
+ }
66
+
67
+ return formatted;
68
+ }
69
+
70
+ _write(level, message, data = null) {
71
+ if (level < this.level) {
72
+ return;
73
+ }
74
+
75
+ const formatted = this._formatMessage(level, message, data);
76
+ const levelName = LOG_LEVEL_NAMES[level];
77
+
78
+ // Console output (with colors)
79
+ const consoleMessage = this._colorize(levelName, formatted);
80
+ if (level >= LOG_LEVELS.ERROR) {
81
+ console.error(consoleMessage);
82
+ } else {
83
+ console.log(consoleMessage);
84
+ }
85
+
86
+ // File output (without colors)
87
+ if (this.enableFileLogging) {
88
+ try {
89
+ fs.appendFileSync(this.logFile, formatted + '\n', { flag: 'a' });
90
+ } catch (e) {
91
+ // Silently fail if log file can't be written
92
+ }
93
+ }
94
+ }
95
+
96
+ debug(message, data) {
97
+ this._write(LOG_LEVELS.DEBUG, message, data);
98
+ }
99
+
100
+ info(message, data) {
101
+ this._write(LOG_LEVELS.INFO, message, data);
102
+ }
103
+
104
+ warn(message, data) {
105
+ this._write(LOG_LEVELS.WARN, message, data);
106
+ }
107
+
108
+ error(message, data) {
109
+ this._write(LOG_LEVELS.ERROR, message, data);
110
+ }
111
+
112
+ // Convenience methods for common patterns
113
+ success(message) {
114
+ if (this.enableColors) {
115
+ console.log(`\x1b[0;32m✓\x1b[0m ${message}`);
116
+ } else {
117
+ console.log(`✓ ${message}`);
118
+ }
119
+ }
120
+
121
+ failure(message) {
122
+ if (this.enableColors) {
123
+ console.error(`\x1b[0;31m✗\x1b[0m ${message}`);
124
+ } else {
125
+ console.error(`✗ ${message}`);
126
+ }
127
+ }
128
+
129
+ // Get log file path
130
+ getLogFile() {
131
+ return this.logFile;
132
+ }
133
+
134
+ // Set log level
135
+ setLevel(level) {
136
+ if (typeof level === 'string') {
137
+ level = LOG_LEVELS[level.toUpperCase()] || LOG_LEVELS.INFO;
138
+ }
139
+ this.level = level;
140
+ }
141
+ }
142
+
143
+ // Create default logger instance
144
+ const logger = new Logger();
145
+
146
+ export default logger;
147
+ export { Logger, LOG_LEVELS };
148
+