teleportation-cli 1.1.5 → 1.2.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.
- package/.claude/hooks/permission_request.mjs +326 -59
- package/.claude/hooks/post_tool_use.mjs +90 -0
- package/.claude/hooks/pre_tool_use.mjs +212 -293
- package/.claude/hooks/session-register.mjs +89 -104
- package/.claude/hooks/session_end.mjs +41 -42
- package/.claude/hooks/session_start.mjs +45 -60
- package/.claude/hooks/stop.mjs +752 -99
- package/.claude/hooks/user_prompt_submit.mjs +26 -3
- package/lib/cli/daemon-commands.js +1 -1
- package/lib/cli/teleport-commands.js +469 -0
- package/lib/daemon/daemon-v2.js +104 -0
- package/lib/daemon/lifecycle.js +56 -171
- package/lib/daemon/services/index.js +3 -0
- package/lib/daemon/services/polling-service.js +173 -0
- package/lib/daemon/services/queue-service.js +318 -0
- package/lib/daemon/services/session-service.js +115 -0
- package/lib/daemon/state.js +35 -0
- package/lib/daemon/task-executor-v2.js +413 -0
- package/lib/daemon/task-executor.js +270 -96
- package/lib/daemon/teleportation-daemon.js +709 -126
- package/lib/daemon/timeline-analyzer.js +215 -0
- package/lib/daemon/transcript-ingestion.js +696 -0
- package/lib/daemon/utils.js +91 -0
- package/lib/install/installer.js +184 -20
- package/lib/install/uhr-installer.js +136 -0
- package/lib/remote/providers/base-provider.js +46 -0
- package/lib/remote/providers/daytona-provider.js +58 -0
- package/lib/remote/providers/provider-factory.js +90 -19
- package/lib/remote/providers/sprites-provider.js +711 -0
- package/lib/teleport/exporters/claude-exporter.js +302 -0
- package/lib/teleport/exporters/gemini-exporter.js +307 -0
- package/lib/teleport/exporters/index.js +93 -0
- package/lib/teleport/exporters/interface.js +153 -0
- package/lib/teleport/fork-tracker.js +415 -0
- package/lib/teleport/git-committer.js +337 -0
- package/lib/teleport/index.js +48 -0
- package/lib/teleport/manager.js +620 -0
- package/lib/teleport/session-capture.js +282 -0
- package/package.json +6 -2
- package/teleportation-cli.cjs +488 -453
- package/.claude/hooks/heartbeat.mjs +0 -396
- package/lib/daemon/pid-manager.js +0 -183
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon Utilities
|
|
3
|
+
*
|
|
4
|
+
* Helper functions extracted from teleportation-daemon.js
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { appendFileSync } from 'fs';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Cross-platform debug logging utility
|
|
12
|
+
*/
|
|
13
|
+
export function debugLog(filename, message, options = {}) {
|
|
14
|
+
const { enabled, logDir } = options;
|
|
15
|
+
if (!enabled) return;
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const logPath = join(logDir || '/tmp', filename);
|
|
19
|
+
appendFileSync(logPath, `[${new Date().toISOString()}] ${message}\n`);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
console.error(`[daemon] Failed to write debug log: ${error.message}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Truncate output for logging and storage
|
|
27
|
+
*/
|
|
28
|
+
export function truncateOutput(output, label) {
|
|
29
|
+
const MAX_OUTPUT_SIZE = 100_000; // 100KB
|
|
30
|
+
if (!output || output.length <= MAX_OUTPUT_SIZE) return output;
|
|
31
|
+
|
|
32
|
+
console.log(`[daemon] Truncating ${label} (${output.length} -> ${MAX_OUTPUT_SIZE} chars)`);
|
|
33
|
+
return output.slice(0, MAX_OUTPUT_SIZE) + `\n\n... (output truncated, total size: ${output.length} characters) ...`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Sanitize for log (remove sensitive info)
|
|
38
|
+
*/
|
|
39
|
+
export function sanitizeForLog(data) {
|
|
40
|
+
if (!data) return data;
|
|
41
|
+
// Simple sanitization - in a real app, use a more robust library
|
|
42
|
+
return String(data).replace(/Bearer\s+[a-zA-Z0-9._-]+/g, 'Bearer [REDACTED]');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Validation helpers
|
|
47
|
+
*/
|
|
48
|
+
export function validateSessionId(session_id) {
|
|
49
|
+
if (!session_id || typeof session_id !== 'string') {
|
|
50
|
+
throw new Error('session_id must be a non-empty string');
|
|
51
|
+
}
|
|
52
|
+
if (session_id.length > 256) {
|
|
53
|
+
throw new Error('session_id too long (max 256 characters)');
|
|
54
|
+
}
|
|
55
|
+
// Allow @ and . for user@host format
|
|
56
|
+
if (!/^[a-zA-Z0-9_@.-]+$/.test(session_id)) {
|
|
57
|
+
throw new Error('session_id contains invalid characters (only alphanumeric, dash, underscore, @, . allowed)');
|
|
58
|
+
}
|
|
59
|
+
return session_id;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function validateApprovalId(approval_id) {
|
|
63
|
+
if (!approval_id || typeof approval_id !== 'string') {
|
|
64
|
+
throw new Error('approval_id must be a non-empty string');
|
|
65
|
+
}
|
|
66
|
+
if (approval_id.length > 256) {
|
|
67
|
+
throw new Error('approval_id too long (max 256 characters)');
|
|
68
|
+
}
|
|
69
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(approval_id)) {
|
|
70
|
+
throw new Error('approval_id contains invalid characters');
|
|
71
|
+
}
|
|
72
|
+
return approval_id;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function validateToolName(tool_name) {
|
|
76
|
+
if (!tool_name || typeof tool_name !== 'string') {
|
|
77
|
+
throw new Error('tool_name must be a non-empty string');
|
|
78
|
+
}
|
|
79
|
+
return tool_name;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Build tool execution prompt
|
|
84
|
+
*/
|
|
85
|
+
export function buildToolPrompt(tool_name, tool_input) {
|
|
86
|
+
const input = tool_input || {};
|
|
87
|
+
return JSON.stringify({
|
|
88
|
+
tool: tool_name,
|
|
89
|
+
parameters: input
|
|
90
|
+
});
|
|
91
|
+
}
|
package/lib/install/installer.js
CHANGED
|
@@ -79,6 +79,25 @@ export function checkClaudeCode() {
|
|
|
79
79
|
};
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Check if Gemini CLI is installed
|
|
84
|
+
*/
|
|
85
|
+
export function checkGeminiCli() {
|
|
86
|
+
try {
|
|
87
|
+
const geminiPath = execSync('which gemini', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
88
|
+
if (geminiPath) {
|
|
89
|
+
return { valid: true, path: geminiPath };
|
|
90
|
+
}
|
|
91
|
+
} catch (e) {
|
|
92
|
+
// Gemini not found
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
valid: false,
|
|
97
|
+
error: 'Gemini CLI not found in PATH.'
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
82
101
|
/**
|
|
83
102
|
* Ensure required directories exist
|
|
84
103
|
*/
|
|
@@ -91,7 +110,8 @@ export async function ensureDirectories() {
|
|
|
91
110
|
settingsDir,
|
|
92
111
|
hooksDir,
|
|
93
112
|
join(HOME_DIR, '.teleportation'),
|
|
94
|
-
join(HOME_DIR, '.teleportation', 'daemon')
|
|
113
|
+
join(HOME_DIR, '.teleportation', 'daemon'),
|
|
114
|
+
join(HOME_DIR, '.gemini', 'hooks')
|
|
95
115
|
];
|
|
96
116
|
|
|
97
117
|
for (const dir of dirs) {
|
|
@@ -121,8 +141,43 @@ export async function verifyHooks(sourceHooksDir) {
|
|
|
121
141
|
'notification.mjs',
|
|
122
142
|
'user_prompt_submit.mjs', // Handles /model command detection
|
|
123
143
|
'config-loader.mjs',
|
|
124
|
-
'session-register.mjs'
|
|
125
|
-
|
|
144
|
+
'session-register.mjs'
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
const found = [];
|
|
148
|
+
const missing = [];
|
|
149
|
+
|
|
150
|
+
for (const hook of hooks) {
|
|
151
|
+
const hookPath = join(sourceHooksDir, hook);
|
|
152
|
+
try {
|
|
153
|
+
await stat(hookPath);
|
|
154
|
+
// Set executable permissions (755)
|
|
155
|
+
try {
|
|
156
|
+
await chmod(hookPath, 0o755);
|
|
157
|
+
} catch (_) {
|
|
158
|
+
// Ignore chmod errors
|
|
159
|
+
}
|
|
160
|
+
found.push(hook);
|
|
161
|
+
} catch (e) {
|
|
162
|
+
if (e.code === 'ENOENT') {
|
|
163
|
+
missing.push(hook);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { found, missing };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Verify Gemini hooks exist in source directory
|
|
173
|
+
*/
|
|
174
|
+
export async function verifyGeminiHooks(sourceHooksDir) {
|
|
175
|
+
const hooks = [
|
|
176
|
+
'before_tool.mjs',
|
|
177
|
+
'after_tool.mjs',
|
|
178
|
+
'session_start.mjs',
|
|
179
|
+
'session_end.mjs',
|
|
180
|
+
'after_agent.mjs'
|
|
126
181
|
];
|
|
127
182
|
|
|
128
183
|
const found = [];
|
|
@@ -186,6 +241,72 @@ export async function installHooks(sourceHooksDir) {
|
|
|
186
241
|
};
|
|
187
242
|
}
|
|
188
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Install Gemini hooks to ~/.gemini/hooks and update settings
|
|
246
|
+
*/
|
|
247
|
+
export async function installGeminiHooks(sourceGeminiHooksDir) {
|
|
248
|
+
const result = await verifyGeminiHooks(sourceGeminiHooksDir);
|
|
249
|
+
const destHooksDir = join(HOME_DIR, '.gemini', 'hooks');
|
|
250
|
+
const copyFailed = [];
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
await mkdir(destHooksDir, { recursive: true });
|
|
254
|
+
} catch (e) {
|
|
255
|
+
if (e.code !== 'EEXIST') {
|
|
256
|
+
throw e;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Copy hooks
|
|
261
|
+
for (const hook of result.found) {
|
|
262
|
+
const sourcePath = join(sourceGeminiHooksDir, hook);
|
|
263
|
+
const destPath = join(destHooksDir, hook);
|
|
264
|
+
try {
|
|
265
|
+
await copyFile(sourcePath, destPath);
|
|
266
|
+
await chmod(destPath, 0o755);
|
|
267
|
+
} catch (e) {
|
|
268
|
+
copyFailed.push({ file: hook, error: e.message });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Update ~/.gemini/settings.json
|
|
273
|
+
const settingsPath = join(HOME_DIR, '.gemini', 'settings.json');
|
|
274
|
+
let settings = {};
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
if (await stat(settingsPath).catch(() => false)) {
|
|
278
|
+
const content = await readFile(settingsPath, 'utf8');
|
|
279
|
+
settings = JSON.parse(content);
|
|
280
|
+
}
|
|
281
|
+
} catch (e) {
|
|
282
|
+
// Ignore error, start with empty settings
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Merge hooks config
|
|
286
|
+
settings.tools = { ...settings.tools, enableHooks: true };
|
|
287
|
+
settings.hooks = settings.hooks || {};
|
|
288
|
+
|
|
289
|
+
const hooksConfig = {
|
|
290
|
+
BeforeTool: [{ command: `node ${join(destHooksDir, 'before_tool.mjs')}`, timeout: 65000 }],
|
|
291
|
+
AfterTool: [{ command: `node ${join(destHooksDir, 'after_tool.mjs')}`, timeout: 10000 }],
|
|
292
|
+
SessionStart: [{ command: `node ${join(destHooksDir, 'session_start.mjs')}`, timeout: 15000 }],
|
|
293
|
+
AfterAgent: [{ command: `node ${join(destHooksDir, 'after_agent.mjs')}`, timeout: 15000 }],
|
|
294
|
+
SessionEnd: [{ command: `node ${join(destHooksDir, 'session_end.mjs')}`, timeout: 10000 }]
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// Standardize: Ensure BeforeAgent is also set if needed (parity with Claude's UserPromptSubmit)
|
|
298
|
+
// For now, we follow the PRD-0024 spec
|
|
299
|
+
Object.assign(settings.hooks, hooksConfig);
|
|
300
|
+
|
|
301
|
+
await writeFile(settingsPath, JSON.stringify(settings, null, 2));
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
installed: result.found,
|
|
305
|
+
failed: result.missing.map(hook => ({ file: hook, error: 'File not found' })).concat(copyFailed),
|
|
306
|
+
settingsPath
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
189
310
|
/**
|
|
190
311
|
* Copy daemon files to ~/.teleportation/daemon/
|
|
191
312
|
*/
|
|
@@ -195,7 +316,6 @@ export async function installDaemon() {
|
|
|
195
316
|
|
|
196
317
|
const daemonFiles = [
|
|
197
318
|
'teleportation-daemon.js',
|
|
198
|
-
'pid-manager.js',
|
|
199
319
|
'lifecycle.js'
|
|
200
320
|
];
|
|
201
321
|
|
|
@@ -311,7 +431,7 @@ export async function installLibFiles() {
|
|
|
311
431
|
{ subdir: 'auth', files: ['credentials.js', 'api-key.js'] },
|
|
312
432
|
{ subdir: 'session', files: ['metadata.js'] },
|
|
313
433
|
{ subdir: 'config', files: ['manager.js'] },
|
|
314
|
-
{ subdir: 'daemon', files: ['lifecycle.js', '
|
|
434
|
+
{ subdir: 'daemon', files: ['lifecycle.js', 'transcript-ingestion.js'] } // Required for daemon auto-start and real-time transcript ingestion
|
|
315
435
|
];
|
|
316
436
|
|
|
317
437
|
const installed = [];
|
|
@@ -503,26 +623,70 @@ export async function verifyInstallation() {
|
|
|
503
623
|
* - Hooks stay in PROJECT/.claude/hooks/ (source files, just verify they exist)
|
|
504
624
|
* - Settings created in PROJECT/.claude/settings.json with absolute paths
|
|
505
625
|
* - Daemon copied to ~/.teleportation/daemon/ (shared across projects)
|
|
626
|
+
*
|
|
627
|
+
* Global installation (Gemini):
|
|
628
|
+
* - Gemini hooks copied to ~/.gemini/hooks/
|
|
629
|
+
* - Global ~/.gemini/settings.json updated
|
|
630
|
+
*
|
|
631
|
+
* @param {string} sourceHooksDir - Source directory for Claude hooks
|
|
632
|
+
* @param {Object} [options] - Installation options
|
|
633
|
+
* @param {boolean} [options.includeClaude] - Force include Claude hooks (overrides detection)
|
|
634
|
+
* @param {boolean} [options.includeGemini] - Force include Gemini hooks (overrides detection)
|
|
506
635
|
*/
|
|
507
|
-
export async function install(sourceHooksDir) {
|
|
636
|
+
export async function install(sourceHooksDir, options = {}) {
|
|
508
637
|
// Pre-flight checks
|
|
509
638
|
const nodeCheck = checkNodeVersion();
|
|
510
639
|
if (!nodeCheck.valid) {
|
|
511
640
|
throw new Error(nodeCheck.error);
|
|
512
641
|
}
|
|
513
642
|
|
|
643
|
+
// Check available CLIs
|
|
514
644
|
const claudeCheck = checkClaudeCode();
|
|
515
|
-
|
|
516
|
-
|
|
645
|
+
const geminiCheck = checkGeminiCli();
|
|
646
|
+
|
|
647
|
+
// Resolve what to install based on options and detection
|
|
648
|
+
const shouldInstallClaude = options.includeClaude !== undefined
|
|
649
|
+
? options.includeClaude
|
|
650
|
+
: claudeCheck.valid;
|
|
651
|
+
|
|
652
|
+
const shouldInstallGemini = options.includeGemini !== undefined
|
|
653
|
+
? options.includeGemini
|
|
654
|
+
: geminiCheck.valid;
|
|
655
|
+
|
|
656
|
+
if (!shouldInstallClaude && !shouldInstallGemini) {
|
|
657
|
+
if (options.includeClaude === false && options.includeGemini === false) {
|
|
658
|
+
throw new Error('No targets selected for installation.');
|
|
659
|
+
}
|
|
660
|
+
throw new Error('Neither Claude Code nor Gemini CLI found. Please install one of them first, or specify a target.');
|
|
517
661
|
}
|
|
518
662
|
|
|
519
663
|
// Create directories
|
|
520
664
|
await ensureDirectories();
|
|
521
665
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
666
|
+
let hooksInstalled = 0;
|
|
667
|
+
let geminiHooksInstalled = 0;
|
|
668
|
+
|
|
669
|
+
// 1. Install Claude hooks
|
|
670
|
+
if (shouldInstallClaude) {
|
|
671
|
+
const hookResult = await installHooks(sourceHooksDir);
|
|
672
|
+
if (hookResult.failed.length > 0) {
|
|
673
|
+
throw new Error(`Failed to install Claude hooks: ${hookResult.failed.map(f => f.file).join(', ')}`);
|
|
674
|
+
}
|
|
675
|
+
hooksInstalled = hookResult.installed.length;
|
|
676
|
+
// Create project-level settings with absolute paths for Claude
|
|
677
|
+
await createSettings();
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// 2. Install Gemini hooks
|
|
681
|
+
if (shouldInstallGemini) {
|
|
682
|
+
// Determine Gemini hooks source directory (sibling to .claude/hooks)
|
|
683
|
+
const sourceGeminiHooksDir = join(dirname(sourceHooksDir), '..', '.gemini', 'hooks');
|
|
684
|
+
const geminiResult = await installGeminiHooks(sourceGeminiHooksDir);
|
|
685
|
+
if (geminiResult.failed.length > 0) {
|
|
686
|
+
console.warn(`Warning: Some Gemini hooks failed to install: ${geminiResult.failed.map(f => f.file).join(', ')}`);
|
|
687
|
+
} else {
|
|
688
|
+
geminiHooksInstalled = geminiResult.installed.length;
|
|
689
|
+
}
|
|
526
690
|
}
|
|
527
691
|
|
|
528
692
|
// Install daemon (still goes to ~/.teleportation/daemon/)
|
|
@@ -544,21 +708,21 @@ export async function install(sourceHooksDir) {
|
|
|
544
708
|
console.warn(`Warning: Some lib files failed to install: ${libResult.failed.map(f => f.file).join(', ')}`);
|
|
545
709
|
}
|
|
546
710
|
|
|
547
|
-
// Create project-level settings with absolute paths
|
|
548
|
-
await createSettings();
|
|
549
|
-
|
|
550
711
|
// Write version file
|
|
551
712
|
await writeVersionFile();
|
|
552
713
|
|
|
553
|
-
// Verify
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
714
|
+
// Verify (only checking Claude verification if Claude is installed)
|
|
715
|
+
if (claudeCheck.valid) {
|
|
716
|
+
const verification = await verifyInstallation();
|
|
717
|
+
if (!verification.valid) {
|
|
718
|
+
throw new Error(`Installation verification failed: ${verification.error}`);
|
|
719
|
+
}
|
|
557
720
|
}
|
|
558
721
|
|
|
559
722
|
return {
|
|
560
723
|
success: true,
|
|
561
|
-
hooksInstalled
|
|
724
|
+
hooksInstalled,
|
|
725
|
+
geminiHooksInstalled,
|
|
562
726
|
daemonInstalled: daemonResult.installed.length,
|
|
563
727
|
libFilesInstalled: libResult.installed.length,
|
|
564
728
|
settingsFile: getProjectSettings(),
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UHR (Universal Hook Registry) installer wrapper for Teleportation.
|
|
3
|
+
*
|
|
4
|
+
* Provides functions to install and uninstall teleportation hooks via the UHR CLI.
|
|
5
|
+
* Falls back gracefully when UHR is not available -- callers can check with
|
|
6
|
+
* `isUhrAvailable()` and use direct installation (installer.js) as a fallback.
|
|
7
|
+
*
|
|
8
|
+
* The manifest file (teleportation.uhr.json) uses `__HOOKS_DIR__` as a placeholder
|
|
9
|
+
* in hook commands. This module replaces that placeholder with the actual absolute
|
|
10
|
+
* path to the hooks directory before passing the manifest to UHR.
|
|
11
|
+
*
|
|
12
|
+
* @module lib/install/uhr-installer
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { execSync } from 'child_process';
|
|
16
|
+
import { join } from 'path';
|
|
17
|
+
import { tmpdir } from 'os';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if the UHR CLI is available on this system.
|
|
21
|
+
*
|
|
22
|
+
* Checks two locations:
|
|
23
|
+
* 1. `uhr` on the system PATH (via `which uhr`)
|
|
24
|
+
* 2. `node_modules/.bin/uhr` (local project install)
|
|
25
|
+
*
|
|
26
|
+
* @returns {Promise<boolean>} true if UHR CLI is reachable
|
|
27
|
+
*/
|
|
28
|
+
export async function isUhrAvailable() {
|
|
29
|
+
// Check PATH first
|
|
30
|
+
try {
|
|
31
|
+
execSync('which uhr', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
32
|
+
return true;
|
|
33
|
+
} catch (_) {
|
|
34
|
+
// Not in PATH
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Check local node_modules
|
|
38
|
+
try {
|
|
39
|
+
const localBin = join('node_modules', '.bin', 'uhr');
|
|
40
|
+
const file = Bun.file(localBin);
|
|
41
|
+
return await file.exists();
|
|
42
|
+
} catch (_) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Install hooks via the UHR CLI.
|
|
49
|
+
*
|
|
50
|
+
* Reads the manifest at `manifestPath`, replaces every occurrence of the
|
|
51
|
+
* `__HOOKS_DIR__` placeholder with the absolute `hooksDir` path, writes a
|
|
52
|
+
* temporary manifest, and runs `uhr install <tempManifest>`.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} manifestPath - Absolute path to the UHR manifest file (e.g. teleportation.uhr.json)
|
|
55
|
+
* @param {string} hooksDir - Absolute path to the hooks directory
|
|
56
|
+
* @param {object} [options] - Reserved for future use (e.g. dryRun)
|
|
57
|
+
* @returns {Promise<{success: true, warnings: string[]} | {success: false, reason: string}>}
|
|
58
|
+
*/
|
|
59
|
+
export async function installViaUhr(manifestPath, hooksDir, options = {}) {
|
|
60
|
+
// 1. Read the manifest
|
|
61
|
+
let manifestText;
|
|
62
|
+
try {
|
|
63
|
+
const file = Bun.file(manifestPath);
|
|
64
|
+
manifestText = await file.text();
|
|
65
|
+
} catch (err) {
|
|
66
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
67
|
+
return { success: false, reason: `Failed to read manifest file: ${msg}` };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 2. Parse to validate JSON
|
|
71
|
+
let manifest;
|
|
72
|
+
try {
|
|
73
|
+
manifest = JSON.parse(manifestText);
|
|
74
|
+
} catch (err) {
|
|
75
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
76
|
+
return { success: false, reason: `Failed to parse manifest JSON: ${msg}` };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 3. Replace __HOOKS_DIR__ placeholder with the actual absolute path
|
|
80
|
+
const templated = JSON.stringify(manifest, null, 2).replaceAll('__HOOKS_DIR__', hooksDir);
|
|
81
|
+
|
|
82
|
+
// 4. Write to a temp file
|
|
83
|
+
const tempManifestPath = join(tmpdir(), `teleportation-uhr-${Date.now()}.json`);
|
|
84
|
+
try {
|
|
85
|
+
await Bun.write(tempManifestPath, templated);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
88
|
+
return { success: false, reason: `Failed to write temp manifest: ${msg}` };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 5. Run uhr install
|
|
92
|
+
const warnings = [];
|
|
93
|
+
try {
|
|
94
|
+
const output = execSync(`uhr install ${tempManifestPath}`, {
|
|
95
|
+
encoding: 'utf8',
|
|
96
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
97
|
+
timeout: 30000,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Collect any warning lines from stdout
|
|
101
|
+
if (output) {
|
|
102
|
+
const lines = output.split('\n').filter(Boolean);
|
|
103
|
+
for (const line of lines) {
|
|
104
|
+
if (/warn/i.test(line)) {
|
|
105
|
+
warnings.push(line.trim());
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { success: true, warnings };
|
|
111
|
+
} catch (err) {
|
|
112
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
113
|
+
return { success: false, reason: msg || 'uhr install failed' };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Uninstall hooks via the UHR CLI.
|
|
119
|
+
*
|
|
120
|
+
* Runs `uhr uninstall <serviceName>` to remove previously installed hooks.
|
|
121
|
+
*
|
|
122
|
+
* @param {string} serviceName - The service name to uninstall (e.g. "teleportation")
|
|
123
|
+
* @returns {Promise<{success: boolean}>}
|
|
124
|
+
*/
|
|
125
|
+
export async function uninstallViaUhr(serviceName) {
|
|
126
|
+
try {
|
|
127
|
+
execSync(`uhr uninstall ${serviceName}`, {
|
|
128
|
+
encoding: 'utf8',
|
|
129
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
130
|
+
timeout: 30000,
|
|
131
|
+
});
|
|
132
|
+
return { success: true };
|
|
133
|
+
} catch (_) {
|
|
134
|
+
return { success: false };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -404,4 +404,50 @@ export class BaseProvider {
|
|
|
404
404
|
errors,
|
|
405
405
|
};
|
|
406
406
|
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Check provider health and connectivity.
|
|
410
|
+
*
|
|
411
|
+
* Tests connection to the provider's API and returns health status.
|
|
412
|
+
* Subclasses should override this to add provider-specific checks.
|
|
413
|
+
*
|
|
414
|
+
* @returns {Promise<Object>} Health check result
|
|
415
|
+
* @returns {boolean} return.healthy - Whether the provider is healthy
|
|
416
|
+
* @returns {string} return.provider - Provider name
|
|
417
|
+
* @returns {Object} [return.details] - Additional health details
|
|
418
|
+
* @returns {string} [return.error] - Error message if unhealthy
|
|
419
|
+
*/
|
|
420
|
+
async checkHealth() {
|
|
421
|
+
// Default implementation - subclasses should override
|
|
422
|
+
return {
|
|
423
|
+
healthy: true,
|
|
424
|
+
provider: this.constructor.name,
|
|
425
|
+
details: {
|
|
426
|
+
message: 'Health check not implemented for this provider',
|
|
427
|
+
},
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Get provider capabilities.
|
|
433
|
+
*
|
|
434
|
+
* Returns information about what this provider supports.
|
|
435
|
+
* Subclasses should override to provide accurate capabilities.
|
|
436
|
+
*
|
|
437
|
+
* @returns {Object} Provider capabilities
|
|
438
|
+
* @returns {boolean} return.supportsCheckpoints - Can create/restore checkpoints
|
|
439
|
+
* @returns {boolean} return.supportsSnapshots - Can create/restore snapshots
|
|
440
|
+
* @returns {boolean} return.supportsHibernation - Can hibernate/wake machines
|
|
441
|
+
* @returns {boolean} return.supportsAutoStop - Supports auto-stop when idle
|
|
442
|
+
* @returns {number} [return.checkpointTime] - Estimated checkpoint time in ms
|
|
443
|
+
* @returns {number} [return.coldStartTime] - Estimated cold start time in ms
|
|
444
|
+
*/
|
|
445
|
+
getCapabilities() {
|
|
446
|
+
return {
|
|
447
|
+
supportsCheckpoints: false,
|
|
448
|
+
supportsSnapshots: false,
|
|
449
|
+
supportsHibernation: false,
|
|
450
|
+
supportsAutoStop: false,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
407
453
|
}
|
|
@@ -273,6 +273,8 @@ for i in 1 2 3 4 5; do
|
|
|
273
273
|
done
|
|
274
274
|
|
|
275
275
|
# Start teleportation daemon
|
|
276
|
+
# Mark this as a cloud session so the frontend can show the cloud badge
|
|
277
|
+
export TELEPORTATION_IS_CLOUD=true
|
|
276
278
|
bun teleportation-cli.cjs daemon start --session-id ${safeSessionId} --task ${safeTask}
|
|
277
279
|
`.trim();
|
|
278
280
|
}
|
|
@@ -503,4 +505,60 @@ bun teleportation-cli.cjs daemon start --session-id ${safeSessionId} --task ${sa
|
|
|
503
505
|
|
|
504
506
|
return response.json();
|
|
505
507
|
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Get provider capabilities
|
|
511
|
+
*
|
|
512
|
+
* @returns {Object} Provider capabilities
|
|
513
|
+
*/
|
|
514
|
+
getCapabilities() {
|
|
515
|
+
return {
|
|
516
|
+
supportsCheckpoints: false,
|
|
517
|
+
supportsSnapshots: true,
|
|
518
|
+
supportsHibernation: false,
|
|
519
|
+
supportsAutoStop: true,
|
|
520
|
+
autoStopTimeout: 1800000, // 30 minutes default
|
|
521
|
+
provider: 'daytona',
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Check provider health
|
|
527
|
+
*
|
|
528
|
+
* @returns {Promise<Object>} Health check result
|
|
529
|
+
*/
|
|
530
|
+
async checkHealth() {
|
|
531
|
+
try {
|
|
532
|
+
// Try to list workspaces - if this works, the API is healthy
|
|
533
|
+
const url = `${this.apiUrl}/workspace`;
|
|
534
|
+
const response = await this._fetchWithTimeout(url, {
|
|
535
|
+
method: 'GET',
|
|
536
|
+
headers: this._headers(),
|
|
537
|
+
}, 10000); // 10 second timeout for health check
|
|
538
|
+
|
|
539
|
+
if (!response.ok) {
|
|
540
|
+
return {
|
|
541
|
+
healthy: false,
|
|
542
|
+
provider: 'daytona',
|
|
543
|
+
error: `API returned status ${response.status}`,
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return {
|
|
548
|
+
healthy: true,
|
|
549
|
+
provider: 'daytona',
|
|
550
|
+
details: {
|
|
551
|
+
apiUrl: this.apiUrl,
|
|
552
|
+
profileId: this.profileId,
|
|
553
|
+
timestamp: new Date().toISOString(),
|
|
554
|
+
},
|
|
555
|
+
};
|
|
556
|
+
} catch (error) {
|
|
557
|
+
return {
|
|
558
|
+
healthy: false,
|
|
559
|
+
provider: 'daytona',
|
|
560
|
+
error: error.message,
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
}
|
|
506
564
|
}
|