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,2987 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// Teleportation CLI - Remote Claude Code Control Setup
|
|
3
|
+
// Version 1.0.0
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { execSync } = require('child_process');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
|
|
10
|
+
const CLI_VERSION = '1.0.0';
|
|
11
|
+
const HOME_DIR = os.homedir();
|
|
12
|
+
// Teleportation project directory (for development)
|
|
13
|
+
// In production, hooks will be installed globally
|
|
14
|
+
const TELEPORTATION_DIR = process.env.TELEPORTATION_DIR || path.join(__dirname);
|
|
15
|
+
|
|
16
|
+
// Color helpers
|
|
17
|
+
const c = {
|
|
18
|
+
red: (text) => '\x1b[0;31m' + text + '\x1b[0m',
|
|
19
|
+
green: (text) => '\x1b[0;32m' + text + '\x1b[0m',
|
|
20
|
+
yellow: (text) => '\x1b[1;33m' + text + '\x1b[0m',
|
|
21
|
+
blue: (text) => '\x1b[0;34m' + text + '\x1b[0m',
|
|
22
|
+
purple: (text) => '\x1b[0;35m' + text + '\x1b[0m',
|
|
23
|
+
cyan: (text) => '\x1b[0;36m' + text + '\x1b[0m'
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Configuration manager
|
|
27
|
+
class ConfigManager {
|
|
28
|
+
constructor() {
|
|
29
|
+
this.globalHooksDir = path.join(HOME_DIR, '.claude');
|
|
30
|
+
this.globalSettings = path.join(this.globalHooksDir, 'settings.json');
|
|
31
|
+
this.globalHooks = path.join(this.globalHooksDir, 'hooks');
|
|
32
|
+
this.projectHooksDir = path.join(TELEPORTATION_DIR, '.claude', 'hooks');
|
|
33
|
+
this.envFile = path.join(HOME_DIR, '.teleportation-env');
|
|
34
|
+
this.zshrc = path.join(HOME_DIR, '.zshrc');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
ensureDirectories() {
|
|
38
|
+
[this.globalHooksDir, this.globalHooks].forEach(dir => {
|
|
39
|
+
if (!fs.existsSync(dir)) {
|
|
40
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
isConfigured() {
|
|
46
|
+
return fs.existsSync(this.globalSettings) &&
|
|
47
|
+
fs.existsSync(this.globalHooks) &&
|
|
48
|
+
fs.readdirSync(this.globalHooks).length > 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
getEnvVars() {
|
|
52
|
+
// Synchronous version for backward compatibility
|
|
53
|
+
// For async credential loading, use getCredentials() instead
|
|
54
|
+
const vars = {
|
|
55
|
+
RELAY_API_URL: process.env.RELAY_API_URL || '',
|
|
56
|
+
RELAY_API_KEY: process.env.RELAY_API_KEY || '',
|
|
57
|
+
SLACK_WEBHOOK_URL: process.env.SLACK_WEBHOOK_URL || ''
|
|
58
|
+
};
|
|
59
|
+
return vars;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async getCredentials() {
|
|
63
|
+
return await getCredentials();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
areEnvVarsSet() {
|
|
67
|
+
const vars = this.getEnvVars();
|
|
68
|
+
return vars.RELAY_API_URL && vars.RELAY_API_KEY;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const config = new ConfigManager();
|
|
73
|
+
|
|
74
|
+
// Credential loader (async, uses ES module)
|
|
75
|
+
let credentialManager = null;
|
|
76
|
+
async function loadCredentialManager() {
|
|
77
|
+
if (!credentialManager) {
|
|
78
|
+
try {
|
|
79
|
+
const { CredentialManager } = await import('./lib/auth/credentials.js');
|
|
80
|
+
credentialManager = new CredentialManager();
|
|
81
|
+
} catch (e) {
|
|
82
|
+
// Credential manager not available, will fall back to env vars
|
|
83
|
+
credentialManager = null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return credentialManager;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Load credentials on startup
|
|
90
|
+
let loadedCredentials = null;
|
|
91
|
+
async function loadCredentials() {
|
|
92
|
+
if (loadedCredentials !== null) return loadedCredentials;
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const manager = await loadCredentialManager();
|
|
96
|
+
if (manager) {
|
|
97
|
+
loadedCredentials = await manager.load();
|
|
98
|
+
}
|
|
99
|
+
} catch (e) {
|
|
100
|
+
// Distinguish between different error types
|
|
101
|
+
if (e.code === 'ENOENT') {
|
|
102
|
+
// File doesn't exist - OK, fall back to env vars
|
|
103
|
+
loadedCredentials = null;
|
|
104
|
+
} else if (e.message && e.message.includes('decrypt')) {
|
|
105
|
+
// Decryption failed - warn user but fall back
|
|
106
|
+
console.warn('⚠️ Credential file exists but could not be decrypted. Using environment variables.');
|
|
107
|
+
loadedCredentials = null;
|
|
108
|
+
} else {
|
|
109
|
+
// Other error - log for debugging but don't fail
|
|
110
|
+
console.error('Failed to load credentials:', e.message);
|
|
111
|
+
loadedCredentials = null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return loadedCredentials;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Get credentials with fallback to environment variables
|
|
119
|
+
async function getCredentials() {
|
|
120
|
+
const creds = await loadCredentials();
|
|
121
|
+
if (creds) {
|
|
122
|
+
return {
|
|
123
|
+
RELAY_API_URL: creds.relayApiUrl || process.env.RELAY_API_URL || '',
|
|
124
|
+
RELAY_API_KEY: creds.relayApiKey || creds.apiKey || process.env.RELAY_API_KEY || '',
|
|
125
|
+
SLACK_WEBHOOK_URL: creds.slackWebhookUrl || process.env.SLACK_WEBHOOK_URL || ''
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Fall back to environment variables
|
|
130
|
+
return {
|
|
131
|
+
RELAY_API_URL: process.env.RELAY_API_URL || '',
|
|
132
|
+
RELAY_API_KEY: process.env.RELAY_API_KEY || '',
|
|
133
|
+
SLACK_WEBHOOK_URL: process.env.SLACK_WEBHOOK_URL || ''
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Parse command line flags
|
|
138
|
+
function parseFlags(args) {
|
|
139
|
+
const flags = {};
|
|
140
|
+
const positional = [];
|
|
141
|
+
|
|
142
|
+
for (let i = 0; i < args.length; i++) {
|
|
143
|
+
const arg = args[i];
|
|
144
|
+
if (arg.startsWith('--')) {
|
|
145
|
+
const key = arg.slice(2);
|
|
146
|
+
if (i + 1 < args.length && !args[i + 1].startsWith('--')) {
|
|
147
|
+
flags[key] = args[i + 1];
|
|
148
|
+
i++; // Skip next arg as it's the value
|
|
149
|
+
} else {
|
|
150
|
+
flags[key] = true; // Boolean flag
|
|
151
|
+
}
|
|
152
|
+
} else if (arg.startsWith('-')) {
|
|
153
|
+
// Short flags like -k
|
|
154
|
+
const key = arg.slice(1);
|
|
155
|
+
if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
|
|
156
|
+
flags[key] = args[i + 1];
|
|
157
|
+
i++;
|
|
158
|
+
} else {
|
|
159
|
+
flags[key] = true;
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
positional.push(arg);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return { flags, positional };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Service checker
|
|
170
|
+
function checkService(name, port) {
|
|
171
|
+
try {
|
|
172
|
+
const result = execSync(`lsof -i :${port} 2>/dev/null | grep LISTEN`, { encoding: 'utf8' });
|
|
173
|
+
return result.includes('LISTEN');
|
|
174
|
+
} catch (e) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function checkServiceHealth(url) {
|
|
180
|
+
try {
|
|
181
|
+
const result = execSync(`curl -s ${url}/health`, { encoding: 'utf8' });
|
|
182
|
+
return result.includes('healthy');
|
|
183
|
+
} catch (e) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Command handlers
|
|
189
|
+
function commandVersion() {
|
|
190
|
+
console.log(c.purple('Teleportation CLI'));
|
|
191
|
+
console.log(c.cyan(`Version: ${CLI_VERSION}`));
|
|
192
|
+
console.log(c.blue(`Node.js: ${process.version}`));
|
|
193
|
+
console.log(c.yellow(`Platform: ${process.platform} ${process.arch}`));
|
|
194
|
+
console.log(c.green(`Home: ${HOME_DIR}`));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function commandHelp() {
|
|
198
|
+
console.log(c.purple('Teleportation CLI v' + CLI_VERSION));
|
|
199
|
+
console.log(c.cyan('Remote Claude Code Control System\n'));
|
|
200
|
+
|
|
201
|
+
console.log(c.yellow('Usage:'));
|
|
202
|
+
console.log(' ./teleportation <command> [options]\n');
|
|
203
|
+
|
|
204
|
+
console.log(c.yellow('Getting Started:'));
|
|
205
|
+
console.log(' ' + c.green('setup') + ' ⭐ Guided setup wizard (recommended for new users)');
|
|
206
|
+
console.log(' ' + c.green('status') + ' Check system status and connectivity\n');
|
|
207
|
+
|
|
208
|
+
console.log(c.yellow('Authentication:'));
|
|
209
|
+
console.log(' ' + c.green('login') + ' Authenticate with API key or token');
|
|
210
|
+
console.log(' ' + c.green('logout') + ' Clear saved credentials\n');
|
|
211
|
+
|
|
212
|
+
console.log(c.yellow('Setup Commands:'));
|
|
213
|
+
console.log(' ' + c.green('on') + ' Enable remote control hooks');
|
|
214
|
+
console.log(' ' + c.green('off') + ' Disable remote control hooks');
|
|
215
|
+
console.log(' ' + c.green('install-hooks') + ' Install hooks globally to ~/.claude/hooks/');
|
|
216
|
+
console.log(' ' + c.green('update') + ' Update CLI and hooks to latest version');
|
|
217
|
+
console.log(' ' + c.green('test') + ' Run diagnostic tests');
|
|
218
|
+
console.log(' ' + c.green('doctor') + ' Run comprehensive diagnostics\n');
|
|
219
|
+
|
|
220
|
+
console.log(c.yellow('Backup & Restore:'));
|
|
221
|
+
console.log(' ' + c.green('backup list') + ' List all backups');
|
|
222
|
+
console.log(' ' + c.green('backup restore') + ' Restore from backup');
|
|
223
|
+
console.log(' ' + c.green('backup create') + ' Create a manual backup\n');
|
|
224
|
+
|
|
225
|
+
console.log(c.yellow('Service Management:'));
|
|
226
|
+
console.log(' ' + c.green('start') + ' Start relay and storage services');
|
|
227
|
+
console.log(' ' + c.green('stop') + ' Stop all services');
|
|
228
|
+
console.log(' ' + c.green('restart') + ' Restart all services');
|
|
229
|
+
console.log(' ' + c.green('logs') + ' View service logs\n');
|
|
230
|
+
|
|
231
|
+
console.log(c.yellow('Daemon Management:'));
|
|
232
|
+
console.log(' ' + c.green('daemon start') + ' Start the teleportation daemon');
|
|
233
|
+
console.log(' ' + c.green('daemon stop') + ' Stop the daemon');
|
|
234
|
+
console.log(' ' + c.green('daemon restart') + ' Restart the daemon');
|
|
235
|
+
console.log(' ' + c.green('daemon status') + ' Show daemon status');
|
|
236
|
+
console.log(' ' + c.green('daemon health') + ' Check daemon health\n');
|
|
237
|
+
|
|
238
|
+
console.log(c.yellow('Inbox & Messaging:'));
|
|
239
|
+
console.log(' ' + c.green('command "<text>"') + ' Enqueue a command message for this session');
|
|
240
|
+
console.log(' ' + c.green('inbox') + ' View next inbox message for this session');
|
|
241
|
+
console.log(' ' + c.green('inbox-ack <id>') + ' Acknowledge inbox message by id\n');
|
|
242
|
+
|
|
243
|
+
console.log(c.yellow('Configuration:'));
|
|
244
|
+
console.log(' ' + c.green('config') + ' Manage configuration');
|
|
245
|
+
console.log(' ' + c.green('config list') + ' Show all settings');
|
|
246
|
+
console.log(' ' + c.green('config get <key>') + ' Get specific setting');
|
|
247
|
+
console.log(' ' + c.green('config set <key> <value>') + ' Update setting');
|
|
248
|
+
console.log(' ' + c.green('config edit') + ' Open config in editor');
|
|
249
|
+
console.log(' ' + c.green('env') + ' Show environment variables\n');
|
|
250
|
+
|
|
251
|
+
console.log(c.yellow('Session Isolation:'));
|
|
252
|
+
console.log(' ' + c.green('worktree create') + ' Create isolated worktree for a session');
|
|
253
|
+
console.log(' ' + c.green('worktree list') + ' List all session worktrees');
|
|
254
|
+
console.log(' ' + c.green('worktree remove') + ' Remove a worktree');
|
|
255
|
+
console.log(' ' + c.green('worktree info') + ' Show worktree information');
|
|
256
|
+
console.log(' ' + c.green('snapshot create') + ' Create a code snapshot');
|
|
257
|
+
console.log(' ' + c.green('snapshot list') + ' List snapshots for a session');
|
|
258
|
+
console.log(' ' + c.green('snapshot restore') + ' Restore a previous snapshot');
|
|
259
|
+
console.log(' ' + c.green('session list') + ' List registered sessions');
|
|
260
|
+
console.log(' ' + c.green('session check-conflicts') + ' Check for file conflicts\n');
|
|
261
|
+
|
|
262
|
+
console.log(c.yellow('Information:'));
|
|
263
|
+
console.log(' ' + c.green('info') + ' Show detailed system info');
|
|
264
|
+
console.log(' ' + c.green('version') + ' Show version information');
|
|
265
|
+
console.log(' ' + c.green('help') + ' Show this help message\n');
|
|
266
|
+
|
|
267
|
+
console.log(c.purple('Examples:'));
|
|
268
|
+
console.log(' ./teleportation setup # ⭐ Recommended: guided setup');
|
|
269
|
+
console.log(' ./teleportation status # Check status');
|
|
270
|
+
console.log(' ./teleportation backup restore # Restore previous config');
|
|
271
|
+
console.log('');
|
|
272
|
+
console.log(c.purple('Quick Start (new users):'));
|
|
273
|
+
console.log(' Just run: ' + c.green('teleportation setup'));
|
|
274
|
+
console.log(' The wizard will guide you through everything!\n');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function commandOn() {
|
|
278
|
+
console.log(c.yellow('🚀 Enabling Teleportation Remote Control...\n'));
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
// Use installer module
|
|
282
|
+
const installerPath = path.join(TELEPORTATION_DIR, 'lib', 'install', 'installer.js');
|
|
283
|
+
const { install, checkNodeVersion, checkClaudeCode } = await import('file://' + installerPath);
|
|
284
|
+
|
|
285
|
+
// Pre-flight checks
|
|
286
|
+
const nodeCheck = checkNodeVersion();
|
|
287
|
+
if (!nodeCheck.valid) {
|
|
288
|
+
console.log(c.red(`❌ ${nodeCheck.error}\n`));
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
console.log(c.green(`✅ Node.js ${nodeCheck.version}\n`));
|
|
292
|
+
|
|
293
|
+
const claudeCheck = checkClaudeCode();
|
|
294
|
+
if (!claudeCheck.valid) {
|
|
295
|
+
console.log(c.yellow(`⚠️ ${claudeCheck.error}\n`));
|
|
296
|
+
console.log(c.cyan(' Continuing anyway...\n'));
|
|
297
|
+
} else {
|
|
298
|
+
console.log(c.green(`✅ Claude Code found: ${claudeCheck.path}\n`));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Install hooks
|
|
302
|
+
const sourceHooksDir = path.join(TELEPORTATION_DIR, '.claude', 'hooks');
|
|
303
|
+
if (!fs.existsSync(sourceHooksDir)) {
|
|
304
|
+
console.log(c.red(`❌ Hooks not found at ${sourceHooksDir}\n`));
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const result = await install(sourceHooksDir);
|
|
309
|
+
|
|
310
|
+
console.log(c.green('\n🎉 Teleportation Remote Control ENABLED!'));
|
|
311
|
+
console.log(c.cyan('\nInstallation Summary:'));
|
|
312
|
+
console.log(` Hooks verified: ${c.green(result.hooksVerified)}`);
|
|
313
|
+
console.log(` Daemon installed: ${c.green(result.daemonInstalled + ' files')}`);
|
|
314
|
+
console.log(` Settings file: ${c.green(result.settingsFile)}`);
|
|
315
|
+
console.log(` Hooks directory: ${c.green(result.hooksDir)}`);
|
|
316
|
+
console.log(` Daemon directory: ${c.green(result.daemonDir)}`);
|
|
317
|
+
console.log(c.cyan('\nNext steps:'));
|
|
318
|
+
console.log(' 1. Login: teleportation login');
|
|
319
|
+
console.log(' 2. Check status: teleportation status');
|
|
320
|
+
console.log(' 3. Run diagnostics: teleportation doctor\n');
|
|
321
|
+
|
|
322
|
+
} catch (error) {
|
|
323
|
+
console.log(c.red(`❌ Installation failed: ${error.message}\n`));
|
|
324
|
+
process.exit(1);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Setup wizard - guided onboarding for new users
|
|
330
|
+
* Creates backup before making changes, validates API key, installs hooks
|
|
331
|
+
*/
|
|
332
|
+
async function commandSetup() {
|
|
333
|
+
const readline = require('readline');
|
|
334
|
+
|
|
335
|
+
const rl = readline.createInterface({
|
|
336
|
+
input: process.stdin,
|
|
337
|
+
output: process.stdout
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
const question = (prompt) => new Promise(resolve => rl.question(prompt, resolve));
|
|
341
|
+
|
|
342
|
+
console.log('\n' + c.cyan('╭─────────────────────────────────────────────────────╮'));
|
|
343
|
+
console.log(c.cyan('│ │'));
|
|
344
|
+
console.log(c.cyan('│ 🚀 ') + c.purple('Teleportation Setup') + c.cyan(' │'));
|
|
345
|
+
console.log(c.cyan('│ │'));
|
|
346
|
+
console.log(c.cyan('│ Let\'s get you set up for remote Claude Code │'));
|
|
347
|
+
console.log(c.cyan('│ approvals in just a few steps. │'));
|
|
348
|
+
console.log(c.cyan('│ │'));
|
|
349
|
+
console.log(c.cyan('╰─────────────────────────────────────────────────────╯\n'));
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
// Step 0: Check for existing files and create backup
|
|
353
|
+
console.log(c.yellow('Checking existing configuration...\n'));
|
|
354
|
+
|
|
355
|
+
const backupManagerPath = path.join(TELEPORTATION_DIR, 'lib', 'backup', 'manager.js');
|
|
356
|
+
const { BackupManager } = await import('file://' + backupManagerPath);
|
|
357
|
+
const backupManager = new BackupManager();
|
|
358
|
+
|
|
359
|
+
const existingFiles = backupManager.checkExistingFiles();
|
|
360
|
+
|
|
361
|
+
if (existingFiles.length > 0) {
|
|
362
|
+
console.log(c.yellow(' ⚠️ Found existing files that will be modified:'));
|
|
363
|
+
for (const file of existingFiles) {
|
|
364
|
+
const shortPath = file.path.replace(HOME_DIR, '~');
|
|
365
|
+
if (file.fileCount !== undefined) {
|
|
366
|
+
console.log(` • ${shortPath} (contains ${file.fileCount} files)`);
|
|
367
|
+
} else {
|
|
368
|
+
console.log(` • ${shortPath}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
console.log('');
|
|
372
|
+
|
|
373
|
+
console.log(c.cyan(' Creating backup before proceeding...'));
|
|
374
|
+
const { backupPath, manifest } = await backupManager.createBackup('teleportation setup');
|
|
375
|
+
const shortBackupPath = backupPath.replace(HOME_DIR, '~');
|
|
376
|
+
console.log(c.green(` ✅ Backup saved to ${shortBackupPath}/`));
|
|
377
|
+
console.log(c.cyan(`\n To restore if needed: ${c.green('teleportation backup restore')}\n`));
|
|
378
|
+
|
|
379
|
+
const proceed = await question('Continue with setup? (Y/n): ');
|
|
380
|
+
if (proceed.toLowerCase() === 'n' || proceed.toLowerCase() === 'no') {
|
|
381
|
+
console.log(c.yellow('\nSetup cancelled.\n'));
|
|
382
|
+
rl.close();
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Step 1: Authentication
|
|
388
|
+
console.log('\n' + c.purple('Step 1 of 4: Authentication\n'));
|
|
389
|
+
console.log(' You\'ll need to sign in to get an API key.\n');
|
|
390
|
+
|
|
391
|
+
// Try to open browser
|
|
392
|
+
const appUrl = 'https://app.teleportation.dev/login';
|
|
393
|
+
console.log(c.cyan(` → Opening ${appUrl} in your browser...`));
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
const openCommand = process.platform === 'darwin' ? 'open' :
|
|
397
|
+
process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
398
|
+
execSync(`${openCommand} ${appUrl}`, { stdio: 'ignore' });
|
|
399
|
+
} catch (e) {
|
|
400
|
+
console.log(c.yellow(` ⚠️ Could not open browser. Please visit: ${appUrl}`));
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
console.log('\n After signing in:');
|
|
404
|
+
console.log(' 1. Click "Keys" in the bottom navigation');
|
|
405
|
+
console.log(' 2. Click "Create" to generate a new API key');
|
|
406
|
+
console.log(' 3. Copy the key and paste it below\n');
|
|
407
|
+
|
|
408
|
+
const apiKey = await question(' Paste your API key: ');
|
|
409
|
+
|
|
410
|
+
if (!apiKey || !apiKey.trim()) {
|
|
411
|
+
console.log(c.red('\n ❌ No API key provided. Setup cancelled.\n'));
|
|
412
|
+
rl.close();
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const trimmedKey = apiKey.trim();
|
|
417
|
+
const relayUrl = 'https://api.teleportation.dev';
|
|
418
|
+
|
|
419
|
+
// Validate API key
|
|
420
|
+
console.log(c.cyan('\n Validating API key...'));
|
|
421
|
+
try {
|
|
422
|
+
const apiKeyPath = path.join(TELEPORTATION_DIR, 'lib', 'auth', 'api-key.js');
|
|
423
|
+
const { validateApiKey } = await import('file://' + apiKeyPath);
|
|
424
|
+
const result = await validateApiKey(trimmedKey, relayUrl);
|
|
425
|
+
|
|
426
|
+
if (!result.valid) {
|
|
427
|
+
console.log(c.red(`\n ❌ ${result.error}`));
|
|
428
|
+
console.log(c.yellow(' Please check your API key and try again.\n'));
|
|
429
|
+
rl.close();
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
} catch (e) {
|
|
433
|
+
console.log(c.yellow(`\n ⚠️ Could not validate API key: ${e.message}`));
|
|
434
|
+
console.log(c.cyan(' Continuing anyway (will validate on first use)...'));
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
console.log(c.green(' ✅ API key validated successfully!'));
|
|
438
|
+
|
|
439
|
+
// Step 2: Configuration
|
|
440
|
+
console.log('\n' + c.purple('Step 2 of 4: Configuration\n'));
|
|
441
|
+
console.log(` Relay URL: ${c.cyan(relayUrl)}`);
|
|
442
|
+
|
|
443
|
+
// Test relay connectivity
|
|
444
|
+
console.log(c.cyan(' Testing connectivity...'));
|
|
445
|
+
try {
|
|
446
|
+
const start = Date.now();
|
|
447
|
+
const response = await fetch(`${relayUrl}/health`, {
|
|
448
|
+
signal: AbortSignal.timeout(5000)
|
|
449
|
+
});
|
|
450
|
+
const elapsed = Date.now() - start;
|
|
451
|
+
|
|
452
|
+
if (response.ok) {
|
|
453
|
+
console.log(c.green(` ✅ Relay is reachable (responded in ${elapsed}ms)`));
|
|
454
|
+
} else {
|
|
455
|
+
console.log(c.yellow(` ⚠️ Relay returned status ${response.status}`));
|
|
456
|
+
}
|
|
457
|
+
} catch (e) {
|
|
458
|
+
console.log(c.yellow(` ⚠️ Could not reach relay: ${e.message}`));
|
|
459
|
+
console.log(c.cyan(' Continuing anyway (will retry on use)...'));
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Save credentials
|
|
463
|
+
console.log(c.cyan('\n Saving credentials...'));
|
|
464
|
+
const manager = await loadCredentialManager();
|
|
465
|
+
if (manager) {
|
|
466
|
+
const credentials = {
|
|
467
|
+
apiKey: trimmedKey,
|
|
468
|
+
relayApiUrl: relayUrl,
|
|
469
|
+
authenticatedAt: Date.now(),
|
|
470
|
+
method: 'setup-wizard'
|
|
471
|
+
};
|
|
472
|
+
await manager.save(credentials);
|
|
473
|
+
console.log(c.green(' ✅ Credentials saved'));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Update config to match
|
|
477
|
+
const configManagerPath = path.join(TELEPORTATION_DIR, 'lib', 'config', 'manager.js');
|
|
478
|
+
try {
|
|
479
|
+
const { setConfigValue } = await import('file://' + configManagerPath);
|
|
480
|
+
await setConfigValue('relay.url', relayUrl);
|
|
481
|
+
console.log(c.green(' ✅ Configuration updated'));
|
|
482
|
+
} catch (e) {
|
|
483
|
+
console.log(c.yellow(` ⚠️ Could not update config: ${e.message}`));
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Step 3: Install Hooks
|
|
487
|
+
console.log('\n' + c.purple('Step 3 of 4: Installing Hooks\n'));
|
|
488
|
+
console.log(' Installing Claude Code hooks to ~/.claude/hooks/');
|
|
489
|
+
|
|
490
|
+
try {
|
|
491
|
+
const installerPath = path.join(TELEPORTATION_DIR, 'lib', 'install', 'installer.js');
|
|
492
|
+
const { install } = await import('file://' + installerPath);
|
|
493
|
+
const sourceHooksDir = path.join(TELEPORTATION_DIR, '.claude', 'hooks');
|
|
494
|
+
const result = await install(sourceHooksDir);
|
|
495
|
+
console.log(c.green(` ✅ ${result.hooksVerified} hooks installed successfully`));
|
|
496
|
+
} catch (e) {
|
|
497
|
+
console.log(c.red(` ❌ Failed to install hooks: ${e.message}`));
|
|
498
|
+
console.log(c.yellow('\n Would you like to restore your previous configuration?'));
|
|
499
|
+
const restore = await question(' Restore backup? (y/N): ');
|
|
500
|
+
if (restore.toLowerCase() === 'y' || restore.toLowerCase() === 'yes') {
|
|
501
|
+
console.log(c.cyan('\n Restoring from backup...'));
|
|
502
|
+
try {
|
|
503
|
+
await backupManager.restore();
|
|
504
|
+
console.log(c.green(' ✅ Previous configuration restored'));
|
|
505
|
+
} catch (restoreErr) {
|
|
506
|
+
console.log(c.red(` ❌ Failed to restore: ${restoreErr.message}`));
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
rl.close();
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Step 4: Verification
|
|
514
|
+
console.log('\n' + c.purple('Step 4 of 4: Verification\n'));
|
|
515
|
+
console.log(c.green(' ✅ Credentials saved'));
|
|
516
|
+
console.log(c.green(' ✅ Configuration saved'));
|
|
517
|
+
console.log(c.green(' ✅ Hooks installed'));
|
|
518
|
+
console.log(c.green(' ✅ Relay connectivity confirmed'));
|
|
519
|
+
|
|
520
|
+
// Success!
|
|
521
|
+
console.log('\n' + c.cyan('╭─────────────────────────────────────────────────────╮'));
|
|
522
|
+
console.log(c.cyan('│ │'));
|
|
523
|
+
console.log(c.cyan('│ 🎉 ') + c.green('Setup Complete!') + c.cyan(' │'));
|
|
524
|
+
console.log(c.cyan('│ │'));
|
|
525
|
+
console.log(c.cyan('│ ') + c.yellow('⚠️ Important:') + c.cyan(' Restart Claude Code to activate │'));
|
|
526
|
+
console.log(c.cyan('│ teleportation for your current session. │'));
|
|
527
|
+
console.log(c.cyan('│ │'));
|
|
528
|
+
console.log(c.cyan('│ Then open ') + c.green('https://app.teleportation.dev') + c.cyan(' on your │'));
|
|
529
|
+
console.log(c.cyan('│ phone to approve actions remotely. │'));
|
|
530
|
+
console.log(c.cyan('│ │'));
|
|
531
|
+
console.log(c.cyan('╰─────────────────────────────────────────────────────╯\n'));
|
|
532
|
+
|
|
533
|
+
rl.close();
|
|
534
|
+
|
|
535
|
+
} catch (error) {
|
|
536
|
+
console.log(c.red(`\n❌ Setup failed: ${error.message}\n`));
|
|
537
|
+
rl.close();
|
|
538
|
+
process.exit(1);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Backup command - list, create, restore backups
|
|
544
|
+
*/
|
|
545
|
+
async function commandBackup(args) {
|
|
546
|
+
const subcommand = args[0] || 'list';
|
|
547
|
+
|
|
548
|
+
const backupManagerPath = path.join(TELEPORTATION_DIR, 'lib', 'backup', 'manager.js');
|
|
549
|
+
const { BackupManager } = await import('file://' + backupManagerPath);
|
|
550
|
+
const backupManager = new BackupManager();
|
|
551
|
+
|
|
552
|
+
switch (subcommand) {
|
|
553
|
+
case 'list': {
|
|
554
|
+
console.log(c.purple('Teleportation Backups\n'));
|
|
555
|
+
|
|
556
|
+
const backups = backupManager.listBackups();
|
|
557
|
+
|
|
558
|
+
if (backups.length === 0) {
|
|
559
|
+
console.log(c.yellow(' No backups found.\n'));
|
|
560
|
+
console.log(c.cyan(' Backups are created automatically during setup.'));
|
|
561
|
+
console.log(c.cyan(' Run: teleportation backup create\n'));
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
backups.forEach((backup, index) => {
|
|
566
|
+
const isLatest = index === 0;
|
|
567
|
+
const date = new Date(backup.timestamp).toLocaleString();
|
|
568
|
+
console.log(` ${index + 1}. ${c.cyan(backup.id)}${isLatest ? c.green(' (latest)') : ''}`);
|
|
569
|
+
console.log(` Reason: ${backup.reason}`);
|
|
570
|
+
console.log(` Date: ${date}`);
|
|
571
|
+
console.log(` Files: ${backup.fileCount} backed up\n`);
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
console.log(c.cyan(' To restore: teleportation backup restore [backup-id]\n'));
|
|
575
|
+
break;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
case 'create': {
|
|
579
|
+
console.log(c.purple('Creating Backup\n'));
|
|
580
|
+
|
|
581
|
+
const reason = args[1] || 'manual backup';
|
|
582
|
+
console.log(c.cyan(' Creating backup...'));
|
|
583
|
+
|
|
584
|
+
try {
|
|
585
|
+
const { backupPath, manifest } = await backupManager.createBackup(reason);
|
|
586
|
+
const shortPath = backupPath.replace(HOME_DIR, '~');
|
|
587
|
+
console.log(c.green(`\n ✅ Backup created: ${shortPath}/`));
|
|
588
|
+
console.log(` Files backed up: ${manifest.files.length}\n`);
|
|
589
|
+
} catch (e) {
|
|
590
|
+
console.log(c.red(`\n ❌ Failed to create backup: ${e.message}\n`));
|
|
591
|
+
process.exit(1);
|
|
592
|
+
}
|
|
593
|
+
break;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
case 'restore': {
|
|
597
|
+
const backupId = args[1] || null;
|
|
598
|
+
|
|
599
|
+
console.log(c.purple('Restoring from Backup\n'));
|
|
600
|
+
|
|
601
|
+
if (!backupId) {
|
|
602
|
+
console.log(c.cyan(' Restoring from latest backup...'));
|
|
603
|
+
} else {
|
|
604
|
+
console.log(c.cyan(` Restoring from backup: ${backupId}...`));
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
try {
|
|
608
|
+
const { manifest, restoredFiles } = await backupManager.restore(backupId);
|
|
609
|
+
console.log(c.green(`\n ✅ Restored ${restoredFiles.length} files from backup\n`));
|
|
610
|
+
|
|
611
|
+
for (const file of restoredFiles) {
|
|
612
|
+
const shortPath = file.replace(HOME_DIR, '~');
|
|
613
|
+
console.log(c.green(` ✅ ${shortPath}`));
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
console.log(c.yellow('\n ⚠️ Restart Claude Code to apply restored settings.\n'));
|
|
617
|
+
} catch (e) {
|
|
618
|
+
console.log(c.red(`\n ❌ Failed to restore: ${e.message}\n`));
|
|
619
|
+
process.exit(1);
|
|
620
|
+
}
|
|
621
|
+
break;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
case 'delete': {
|
|
625
|
+
const backupId = args[1];
|
|
626
|
+
|
|
627
|
+
if (!backupId) {
|
|
628
|
+
console.log(c.red(' ❌ Please specify a backup ID to delete.\n'));
|
|
629
|
+
console.log(c.cyan(' Usage: teleportation backup delete <backup-id>\n'));
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
console.log(c.purple('Deleting Backup\n'));
|
|
634
|
+
console.log(c.cyan(` Deleting backup: ${backupId}...`));
|
|
635
|
+
|
|
636
|
+
try {
|
|
637
|
+
backupManager.deleteBackup(backupId);
|
|
638
|
+
console.log(c.green(`\n ✅ Backup deleted: ${backupId}\n`));
|
|
639
|
+
} catch (e) {
|
|
640
|
+
console.log(c.red(`\n ❌ Failed to delete: ${e.message}\n`));
|
|
641
|
+
}
|
|
642
|
+
break;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
default:
|
|
646
|
+
console.log(c.red(` Unknown backup subcommand: ${subcommand}\n`));
|
|
647
|
+
console.log(c.cyan(' Available commands:'));
|
|
648
|
+
console.log(' backup list - List all backups');
|
|
649
|
+
console.log(' backup create - Create a new backup');
|
|
650
|
+
console.log(' backup restore - Restore from backup');
|
|
651
|
+
console.log(' backup delete - Delete a backup\n');
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function commandOff() {
|
|
656
|
+
console.log(c.yellow('🛑 Disabling Teleportation Remote Control...\n'));
|
|
657
|
+
|
|
658
|
+
// Remove settings.json
|
|
659
|
+
if (fs.existsSync(config.globalSettings)) {
|
|
660
|
+
fs.unlinkSync(config.globalSettings);
|
|
661
|
+
console.log(c.green('✅ Removed ~/.claude/settings.json'));
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Remove hooks
|
|
665
|
+
if (fs.existsSync(config.globalHooks)) {
|
|
666
|
+
const hooks = fs.readdirSync(config.globalHooks).filter(f => f.endsWith('.mjs'));
|
|
667
|
+
hooks.forEach(hook => {
|
|
668
|
+
fs.unlinkSync(path.join(config.globalHooks, hook));
|
|
669
|
+
});
|
|
670
|
+
console.log(c.green(`✅ Removed ${hooks.length} hooks`));
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
console.log(c.yellow('\n🛑 Teleportation Remote Control DISABLED'));
|
|
674
|
+
console.log(c.cyan('Services are still running. Stop with: ./teleportation stop\n'));
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
async function commandStatus() {
|
|
678
|
+
console.log(c.purple('Teleportation System Status\n'));
|
|
679
|
+
|
|
680
|
+
let issues = [];
|
|
681
|
+
let warnings = [];
|
|
682
|
+
|
|
683
|
+
// Authentication status
|
|
684
|
+
console.log(c.yellow('Authentication:'));
|
|
685
|
+
const creds = await getCredentials();
|
|
686
|
+
const hasCredentials = await loadCredentials();
|
|
687
|
+
|
|
688
|
+
if (hasCredentials) {
|
|
689
|
+
console.log(' ' + c.green('✅') + ' Logged in');
|
|
690
|
+
console.log(' Source: Encrypted credentials file');
|
|
691
|
+
console.log(' API key: ' + c.cyan('***' + (creds.RELAY_API_KEY?.slice(-4) || '????')));
|
|
692
|
+
} else if (creds.RELAY_API_KEY) {
|
|
693
|
+
console.log(' ' + c.yellow('⚠️') + ' Using environment variables');
|
|
694
|
+
console.log(' API key: ' + c.cyan('***' + creds.RELAY_API_KEY.slice(-4)));
|
|
695
|
+
warnings.push('Consider running `teleportation login` for encrypted credential storage');
|
|
696
|
+
} else {
|
|
697
|
+
console.log(' ' + c.red('❌') + ' Not logged in');
|
|
698
|
+
issues.push('Run `teleportation setup` to configure authentication');
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Relay connection status
|
|
702
|
+
console.log('\n' + c.yellow('Relay Connection:'));
|
|
703
|
+
const relayUrl = creds.RELAY_API_URL;
|
|
704
|
+
|
|
705
|
+
if (!relayUrl) {
|
|
706
|
+
console.log(' ' + c.red('❌') + ' Relay URL not configured');
|
|
707
|
+
issues.push('Run `teleportation setup` to configure relay URL');
|
|
708
|
+
} else {
|
|
709
|
+
console.log(' URL: ' + c.cyan(relayUrl));
|
|
710
|
+
|
|
711
|
+
// Test connectivity
|
|
712
|
+
try {
|
|
713
|
+
const start = Date.now();
|
|
714
|
+
const response = await fetch(`${relayUrl}/health`, {
|
|
715
|
+
signal: AbortSignal.timeout(5000)
|
|
716
|
+
});
|
|
717
|
+
const elapsed = Date.now() - start;
|
|
718
|
+
|
|
719
|
+
if (response.ok) {
|
|
720
|
+
console.log(' ' + c.green('✅') + ` Reachable (${elapsed}ms)`);
|
|
721
|
+
try {
|
|
722
|
+
const health = await response.json();
|
|
723
|
+
if (health.version) {
|
|
724
|
+
console.log(' Version: ' + c.cyan(health.version));
|
|
725
|
+
}
|
|
726
|
+
} catch (e) {
|
|
727
|
+
// Ignore JSON parse errors
|
|
728
|
+
}
|
|
729
|
+
} else {
|
|
730
|
+
console.log(' ' + c.yellow('⚠️') + ` Returned status ${response.status}`);
|
|
731
|
+
warnings.push(`Relay returned HTTP ${response.status}`);
|
|
732
|
+
}
|
|
733
|
+
} catch (e) {
|
|
734
|
+
if (e.name === 'TimeoutError') {
|
|
735
|
+
console.log(' ' + c.red('❌') + ' Connection timed out');
|
|
736
|
+
issues.push('Relay is not responding. Check your internet connection.');
|
|
737
|
+
} else {
|
|
738
|
+
console.log(' ' + c.red('❌') + ` Cannot reach: ${e.message}`);
|
|
739
|
+
issues.push('Relay is unreachable. Check the URL and your internet connection.');
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Validate API key if we have one
|
|
744
|
+
if (creds.RELAY_API_KEY) {
|
|
745
|
+
try {
|
|
746
|
+
const apiKeyPath = path.join(TELEPORTATION_DIR, 'lib', 'auth', 'api-key.js');
|
|
747
|
+
const { validateApiKey } = await import('file://' + apiKeyPath);
|
|
748
|
+
const result = await validateApiKey(creds.RELAY_API_KEY, relayUrl);
|
|
749
|
+
|
|
750
|
+
if (result.valid) {
|
|
751
|
+
console.log(' ' + c.green('✅') + ' API key validated');
|
|
752
|
+
} else {
|
|
753
|
+
console.log(' ' + c.red('❌') + ` API key invalid: ${result.error}`);
|
|
754
|
+
issues.push('API key is invalid. Create a new one at app.teleportation.dev/api-keys');
|
|
755
|
+
}
|
|
756
|
+
} catch (e) {
|
|
757
|
+
console.log(' ' + c.yellow('⚠️') + ' Could not validate API key');
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// Hooks status
|
|
763
|
+
console.log('\n' + c.yellow('Hooks:'));
|
|
764
|
+
const hooksConfigured = config.isConfigured();
|
|
765
|
+
|
|
766
|
+
if (hooksConfigured) {
|
|
767
|
+
console.log(' ' + c.green('✅') + ' Enabled in Claude Code settings');
|
|
768
|
+
const hookFiles = fs.readdirSync(config.globalHooks).filter(f => f.endsWith('.mjs'));
|
|
769
|
+
console.log(' ' + c.green('✅') + ` ${hookFiles.length} hook files installed`);
|
|
770
|
+
console.log(' Directory: ~/.claude/hooks/');
|
|
771
|
+
} else {
|
|
772
|
+
console.log(' ' + c.red('❌') + ' Hooks not installed');
|
|
773
|
+
issues.push('Run `teleportation setup` to install hooks');
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Config/credentials sync check
|
|
777
|
+
console.log('\n' + c.yellow('Configuration:'));
|
|
778
|
+
try {
|
|
779
|
+
const configManagerPath = path.join(TELEPORTATION_DIR, 'lib', 'config', 'manager.js');
|
|
780
|
+
const { getConfigValue } = await import('file://' + configManagerPath);
|
|
781
|
+
const configRelayUrl = await getConfigValue('relay.url');
|
|
782
|
+
|
|
783
|
+
if (configRelayUrl && relayUrl && configRelayUrl !== relayUrl) {
|
|
784
|
+
console.log(' ' + c.yellow('⚠️') + ' Config/credentials mismatch');
|
|
785
|
+
console.log(' Config relay.url: ' + c.cyan(configRelayUrl));
|
|
786
|
+
console.log(' Credentials URL: ' + c.cyan(relayUrl));
|
|
787
|
+
warnings.push('Config and credentials have different relay URLs. Run `teleportation setup` to fix.');
|
|
788
|
+
} else {
|
|
789
|
+
console.log(' ' + c.green('✅') + ' Config and credentials in sync');
|
|
790
|
+
}
|
|
791
|
+
} catch (e) {
|
|
792
|
+
console.log(' ' + c.yellow('⚠️') + ' Could not check config sync');
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Session/restart check
|
|
796
|
+
const sessionMarkerPath = path.join(HOME_DIR, '.teleportation', '.session_marker');
|
|
797
|
+
const credentialsPath = path.join(HOME_DIR, '.teleportation', 'credentials');
|
|
798
|
+
|
|
799
|
+
if (fs.existsSync(sessionMarkerPath) && fs.existsSync(credentialsPath)) {
|
|
800
|
+
try {
|
|
801
|
+
const markerMtime = fs.statSync(sessionMarkerPath).mtimeMs;
|
|
802
|
+
const credsMtime = fs.statSync(credentialsPath).mtimeMs;
|
|
803
|
+
|
|
804
|
+
if (credsMtime > markerMtime) {
|
|
805
|
+
console.log(' ' + c.yellow('⚠️') + ' Credentials updated since session started');
|
|
806
|
+
console.log(' Session started: ' + c.cyan(new Date(markerMtime).toLocaleTimeString()));
|
|
807
|
+
console.log(' Credentials updated: ' + c.cyan(new Date(credsMtime).toLocaleTimeString()));
|
|
808
|
+
warnings.push('Credentials changed after session started. Restart Claude Code to apply changes.');
|
|
809
|
+
}
|
|
810
|
+
} catch (e) {
|
|
811
|
+
// Ignore errors checking session marker
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// Local services (only show if relevant)
|
|
816
|
+
const localRelayRunning = checkService('relay', 3030);
|
|
817
|
+
const localStorageRunning = checkService('storage', 3040);
|
|
818
|
+
|
|
819
|
+
if (localRelayRunning || localStorageRunning) {
|
|
820
|
+
console.log('\n' + c.yellow('Local Services:'));
|
|
821
|
+
if (localRelayRunning) {
|
|
822
|
+
console.log(' ' + c.green('✅') + ' Local relay running (port 3030)');
|
|
823
|
+
}
|
|
824
|
+
if (localStorageRunning) {
|
|
825
|
+
console.log(' ' + c.green('✅') + ' Local storage running (port 3040)');
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// Overall status
|
|
830
|
+
console.log('\n' + c.yellow('─────────────────────────────────────────'));
|
|
831
|
+
|
|
832
|
+
if (issues.length === 0 && warnings.length === 0) {
|
|
833
|
+
console.log(c.green('\n🎉 All systems operational!'));
|
|
834
|
+
console.log(c.cyan('\nOpen https://app.teleportation.dev on your phone to approve actions.\n'));
|
|
835
|
+
} else if (issues.length === 0) {
|
|
836
|
+
console.log(c.yellow('\n⚠️ System operational with warnings:\n'));
|
|
837
|
+
warnings.forEach(w => console.log(' • ' + w));
|
|
838
|
+
console.log();
|
|
839
|
+
} else {
|
|
840
|
+
console.log(c.red('\n❌ Issues found:\n'));
|
|
841
|
+
issues.forEach(i => console.log(' • ' + i));
|
|
842
|
+
if (warnings.length > 0) {
|
|
843
|
+
console.log(c.yellow('\nWarnings:'));
|
|
844
|
+
warnings.forEach(w => console.log(' • ' + w));
|
|
845
|
+
}
|
|
846
|
+
console.log(c.cyan('\n→ Run: teleportation setup\n'));
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function commandStart() {
|
|
851
|
+
console.log(c.yellow('🚀 Starting Teleportation services...\n'));
|
|
852
|
+
|
|
853
|
+
// Note: Relay and storage APIs are separate services
|
|
854
|
+
// These paths are for local development only
|
|
855
|
+
const internalRelayDir = path.join(TELEPORTATION_DIR, 'relay');
|
|
856
|
+
const relayDir = process.env.RELAY_DIR || (fs.existsSync(internalRelayDir) ? internalRelayDir : path.join(TELEPORTATION_DIR, '..', 'detach', 'relay'));
|
|
857
|
+
|
|
858
|
+
// Storage API is expected to be in the 'storage-api' directory within the teleportation project
|
|
859
|
+
const storageDir = process.env.STORAGE_DIR || path.join(TELEPORTATION_DIR, 'storage-api');
|
|
860
|
+
const logDir = path.join(HOME_DIR, 'Library', 'Logs');
|
|
861
|
+
|
|
862
|
+
// Check if already running
|
|
863
|
+
if (checkService('relay', 3030)) {
|
|
864
|
+
console.log(c.yellow('⚠️ Relay API already running'));
|
|
865
|
+
} else {
|
|
866
|
+
// Start relay - use environment file instead of command line to avoid credential exposure
|
|
867
|
+
try {
|
|
868
|
+
const { spawn } = require('child_process');
|
|
869
|
+
const envFile = path.join(relayDir, '.env.relay');
|
|
870
|
+
const envContent = `RELAY_API_KEY=dev-key-123\nPORT=3030\n`;
|
|
871
|
+
|
|
872
|
+
// Write env file with secure permissions
|
|
873
|
+
fs.writeFileSync(envFile, envContent, { mode: 0o600 });
|
|
874
|
+
|
|
875
|
+
// Use spawn with env file instead of execSync with command line
|
|
876
|
+
const child = spawn('node', ['server.js'], {
|
|
877
|
+
cwd: relayDir,
|
|
878
|
+
env: { ...process.env, RELAY_API_KEY: 'dev-key-123', PORT: '3030' },
|
|
879
|
+
detached: true,
|
|
880
|
+
stdio: ['ignore', 'ignore', 'ignore']
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
// Write PID and redirect output
|
|
884
|
+
const logFile = path.join(logDir, 'teleportation-relay.log');
|
|
885
|
+
const logStream = fs.createWriteStream(logFile, { flags: 'a' });
|
|
886
|
+
child.unref();
|
|
887
|
+
|
|
888
|
+
console.log(c.green('✅ Relay API started on port 3030'));
|
|
889
|
+
} catch (e) {
|
|
890
|
+
console.log(c.red('❌ Failed to start Relay API: ' + e.message));
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
if (checkService('storage', 3040)) {
|
|
895
|
+
console.log(c.yellow('⚠️ Storage API already running'));
|
|
896
|
+
} else {
|
|
897
|
+
// Start storage - use proper env parsing instead of shell injection
|
|
898
|
+
try {
|
|
899
|
+
const { spawn } = require('child_process');
|
|
900
|
+
const envFile = path.join(storageDir, '.env.local');
|
|
901
|
+
|
|
902
|
+
if (fs.existsSync(envFile)) {
|
|
903
|
+
// Parse .env file safely
|
|
904
|
+
const envContent = fs.readFileSync(envFile, 'utf8');
|
|
905
|
+
const envVars = {};
|
|
906
|
+
|
|
907
|
+
// Parse env file line by line (simple parser, no shell execution)
|
|
908
|
+
for (const line of envContent.split('\n')) {
|
|
909
|
+
const trimmed = line.trim();
|
|
910
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
911
|
+
|
|
912
|
+
const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
913
|
+
if (match) {
|
|
914
|
+
const key = match[1];
|
|
915
|
+
let value = match[2];
|
|
916
|
+
|
|
917
|
+
// Remove quotes if present
|
|
918
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
919
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
920
|
+
value = value.slice(1, -1);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
envVars[key] = value;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// Use spawn with parsed env vars instead of shell command
|
|
928
|
+
const child = spawn('node', ['server.js'], {
|
|
929
|
+
cwd: storageDir,
|
|
930
|
+
env: { ...process.env, ...envVars },
|
|
931
|
+
detached: true,
|
|
932
|
+
stdio: ['ignore', 'ignore', 'ignore']
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
child.unref();
|
|
936
|
+
console.log(c.green('✅ Storage API started on port 3040'));
|
|
937
|
+
} else {
|
|
938
|
+
console.log(c.red('❌ Storage API .env.local not found'));
|
|
939
|
+
}
|
|
940
|
+
} catch (e) {
|
|
941
|
+
console.log(c.red('❌ Failed to start Storage API: ' + e.message));
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
console.log(c.cyan('\nWait 2 seconds, then check: ./teleportation status\n'));
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function commandStop() {
|
|
949
|
+
console.log(c.yellow('🛑 Stopping Teleportation services...\n'));
|
|
950
|
+
|
|
951
|
+
try {
|
|
952
|
+
execSync('pkill -f "relay/server.js"', { stdio: 'ignore' });
|
|
953
|
+
console.log(c.green('✅ Stopped Relay API'));
|
|
954
|
+
} catch (e) {
|
|
955
|
+
console.log(c.yellow('⚠️ Relay API not running'));
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
try {
|
|
959
|
+
execSync('pkill -f "storage-api/server.js"', { stdio: 'ignore' });
|
|
960
|
+
console.log(c.green('✅ Stopped Storage API'));
|
|
961
|
+
} catch (e) {
|
|
962
|
+
console.log(c.yellow('⚠️ Storage API not running'));
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
console.log(c.cyan('\nServices stopped\n'));
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function commandRestart() {
|
|
969
|
+
commandStop();
|
|
970
|
+
setTimeout(() => {
|
|
971
|
+
commandStart();
|
|
972
|
+
}, 1000);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
async function commandEnv(args) {
|
|
976
|
+
const subcommand = args[0];
|
|
977
|
+
|
|
978
|
+
if (subcommand === 'set') {
|
|
979
|
+
console.log(c.yellow('Setting environment variables...\n'));
|
|
980
|
+
console.log(c.yellow('Note: Consider using "teleportation login" for encrypted credential storage instead.\n'));
|
|
981
|
+
|
|
982
|
+
const envContent = `
|
|
983
|
+
# Teleportation Remote Control Environment Variables
|
|
984
|
+
export RELAY_API_URL="http://localhost:3030"
|
|
985
|
+
export RELAY_API_KEY="dev-key-123"
|
|
986
|
+
export SLACK_WEBHOOK_URL=""
|
|
987
|
+
`;
|
|
988
|
+
|
|
989
|
+
// Check if already in .zshrc
|
|
990
|
+
if (fs.existsSync(config.zshrc)) {
|
|
991
|
+
const content = fs.readFileSync(config.zshrc, 'utf8');
|
|
992
|
+
if (content.includes('RELAY_API_URL')) {
|
|
993
|
+
console.log(c.yellow('⚠️ Environment variables already in ~/.zshrc'));
|
|
994
|
+
} else {
|
|
995
|
+
fs.appendFileSync(config.zshrc, envContent);
|
|
996
|
+
console.log(c.green('✅ Added to ~/.zshrc'));
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
console.log(c.cyan('\nTo apply now, run:'));
|
|
1001
|
+
console.log(c.green(' source ~/.zshrc\n'));
|
|
1002
|
+
console.log(c.cyan('Or restart your terminal\n'));
|
|
1003
|
+
} else {
|
|
1004
|
+
// Show current credentials (from file or env)
|
|
1005
|
+
const creds = await getCredentials();
|
|
1006
|
+
const hasFileCreds = await loadCredentials();
|
|
1007
|
+
|
|
1008
|
+
console.log(c.yellow('Credentials:\n'));
|
|
1009
|
+
if (hasFileCreds) {
|
|
1010
|
+
console.log(' Source:', c.green('Encrypted file (~/.teleportation/credentials)'));
|
|
1011
|
+
} else {
|
|
1012
|
+
console.log(' Source:', c.yellow('Environment variables'));
|
|
1013
|
+
}
|
|
1014
|
+
console.log(' RELAY_API_URL:', creds.RELAY_API_URL || c.red('not set'));
|
|
1015
|
+
console.log(' RELAY_API_KEY:', creds.RELAY_API_KEY ? '***' + creds.RELAY_API_KEY.slice(-4) : c.red('not set'));
|
|
1016
|
+
console.log(' SLACK_WEBHOOK_URL:', creds.SLACK_WEBHOOK_URL || c.yellow('not set (optional)'));
|
|
1017
|
+
console.log();
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
async function commandTest() {
|
|
1022
|
+
console.log(c.purple('Running Teleportation Diagnostics...\n'));
|
|
1023
|
+
|
|
1024
|
+
let passed = 0;
|
|
1025
|
+
let failed = 0;
|
|
1026
|
+
|
|
1027
|
+
// Test 1: Hooks configured
|
|
1028
|
+
console.log(c.yellow('Test 1: Hooks Configuration'));
|
|
1029
|
+
if (config.isConfigured()) {
|
|
1030
|
+
console.log(c.green(' ✅ PASS - Hooks configured in ~/.claude/\n'));
|
|
1031
|
+
passed++;
|
|
1032
|
+
} else {
|
|
1033
|
+
console.log(c.red(' ❌ FAIL - Hooks not configured\n'));
|
|
1034
|
+
failed++;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// Test 2: Credentials (file or env vars)
|
|
1038
|
+
console.log(c.yellow('Test 2: Credentials'));
|
|
1039
|
+
const creds = await getCredentials();
|
|
1040
|
+
if (creds.RELAY_API_URL && creds.RELAY_API_KEY) {
|
|
1041
|
+
const source = await loadCredentials() ? 'encrypted file' : 'environment variables';
|
|
1042
|
+
console.log(c.green(` ✅ PASS - Credentials loaded from ${source}\n`));
|
|
1043
|
+
passed++;
|
|
1044
|
+
} else {
|
|
1045
|
+
console.log(c.red(' ❌ FAIL - Credentials missing\n'));
|
|
1046
|
+
failed++;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// Test 3: Relay service
|
|
1050
|
+
console.log(c.yellow('Test 3: Relay API Service'));
|
|
1051
|
+
const relayUrl = creds.RELAY_API_URL || 'https://api.teleportation.dev';
|
|
1052
|
+
if (checkService('relay', 3030) && checkServiceHealth(relayUrl)) {
|
|
1053
|
+
console.log(c.green(' ✅ PASS - Relay API running and healthy\n'));
|
|
1054
|
+
passed++;
|
|
1055
|
+
} else {
|
|
1056
|
+
console.log(c.red(' ❌ FAIL - Relay API not running or unhealthy\n'));
|
|
1057
|
+
failed++;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// Test 4: Storage service
|
|
1061
|
+
console.log(c.yellow('Test 4: Storage API Service'));
|
|
1062
|
+
if (checkService('storage', 3040) && checkServiceHealth('http://localhost:3040')) {
|
|
1063
|
+
console.log(c.green(' ✅ PASS - Storage API running and healthy\n'));
|
|
1064
|
+
passed++;
|
|
1065
|
+
} else {
|
|
1066
|
+
console.log(c.red(' ❌ FAIL - Storage API not running or unhealthy\n'));
|
|
1067
|
+
failed++;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// Test 5: Hook execution
|
|
1071
|
+
console.log(c.yellow('Test 5: Hook Execution'));
|
|
1072
|
+
try {
|
|
1073
|
+
const testHook = path.join(config.globalHooks, 'pre_tool_use.mjs');
|
|
1074
|
+
if (fs.existsSync(testHook)) {
|
|
1075
|
+
const testInput = '{"session_id":"test","tool_name":"Read","tool_input":{}}';
|
|
1076
|
+
const envVars = `RELAY_API_URL="${creds.RELAY_API_URL || 'http://localhost:3030'}" RELAY_API_KEY="${creds.RELAY_API_KEY || 'dev-key-123'}"`;
|
|
1077
|
+
execSync(`echo '${testInput}' | ${envVars} node ${testHook}`, { stdio: 'ignore' });
|
|
1078
|
+
console.log(c.green(' ✅ PASS - Hook executes successfully\n'));
|
|
1079
|
+
passed++;
|
|
1080
|
+
} else {
|
|
1081
|
+
console.log(c.red(' ❌ FAIL - Hook file not found\n'));
|
|
1082
|
+
failed++;
|
|
1083
|
+
}
|
|
1084
|
+
} catch (e) {
|
|
1085
|
+
console.log(c.red(' ❌ FAIL - Hook execution error\n'));
|
|
1086
|
+
failed++;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// Summary
|
|
1090
|
+
console.log(c.purple('Test Summary:'));
|
|
1091
|
+
console.log(` Passed: ${c.green(passed)}`);
|
|
1092
|
+
console.log(` Failed: ${c.red(failed)}`);
|
|
1093
|
+
|
|
1094
|
+
if (failed === 0) {
|
|
1095
|
+
console.log(c.green('\n🎉 All tests passed! System is ready.\n'));
|
|
1096
|
+
} else {
|
|
1097
|
+
console.log(c.yellow('\n⚠️ Some tests failed. Run ./teleportation status for details.\n'));
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
async function commandDoctor() {
|
|
1102
|
+
console.log(c.purple('🔍 Teleportation Doctor - System Diagnostics\n'));
|
|
1103
|
+
|
|
1104
|
+
const issues = [];
|
|
1105
|
+
const recommendations = [];
|
|
1106
|
+
let checksPassed = 0;
|
|
1107
|
+
let checksFailed = 0;
|
|
1108
|
+
|
|
1109
|
+
// Check 1: Claude Code installation
|
|
1110
|
+
console.log(c.yellow('1. Claude Code Installation'));
|
|
1111
|
+
try {
|
|
1112
|
+
const claudeCodePath = execSync('which claude', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
1113
|
+
if (claudeCodePath) {
|
|
1114
|
+
console.log(c.green(` ✅ Found: ${claudeCodePath}\n`));
|
|
1115
|
+
checksPassed++;
|
|
1116
|
+
} else {
|
|
1117
|
+
console.log(c.yellow(' ⚠️ Claude Code not found in PATH\n'));
|
|
1118
|
+
issues.push('Claude Code not found');
|
|
1119
|
+
recommendations.push('Install Claude Code or add it to your PATH');
|
|
1120
|
+
checksFailed++;
|
|
1121
|
+
}
|
|
1122
|
+
} catch (e) {
|
|
1123
|
+
console.log(c.yellow(' ⚠️ Could not detect Claude Code installation\n'));
|
|
1124
|
+
issues.push('Claude Code detection failed');
|
|
1125
|
+
checksFailed++;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// Check 2: Hooks installation
|
|
1129
|
+
console.log(c.yellow('2. Hooks Installation'));
|
|
1130
|
+
const hooksConfigured = config.isConfigured();
|
|
1131
|
+
if (hooksConfigured) {
|
|
1132
|
+
const hookFiles = fs.readdirSync(config.globalHooks).filter(f => f.endsWith('.mjs'));
|
|
1133
|
+
console.log(c.green(` ✅ ${hookFiles.length} hooks installed\n`));
|
|
1134
|
+
hookFiles.forEach(f => {
|
|
1135
|
+
const hookPath = path.join(config.globalHooks, f);
|
|
1136
|
+
const stats = fs.statSync(hookPath);
|
|
1137
|
+
const isExecutable = (stats.mode & parseInt('111', 8)) !== 0;
|
|
1138
|
+
if (isExecutable) {
|
|
1139
|
+
console.log(c.green(` • ${f} (executable)\n`));
|
|
1140
|
+
} else {
|
|
1141
|
+
console.log(c.yellow(` • ${f} (not executable)\n`));
|
|
1142
|
+
issues.push(`Hook ${f} is not executable`);
|
|
1143
|
+
recommendations.push(`Run: chmod +x ${hookPath}`);
|
|
1144
|
+
}
|
|
1145
|
+
});
|
|
1146
|
+
checksPassed++;
|
|
1147
|
+
} else {
|
|
1148
|
+
console.log(c.red(' ❌ Hooks not configured\n'));
|
|
1149
|
+
issues.push('Hooks not installed');
|
|
1150
|
+
recommendations.push('Run: teleportation on');
|
|
1151
|
+
checksFailed++;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// Check 3: Credentials
|
|
1155
|
+
console.log(c.yellow('3. Credentials'));
|
|
1156
|
+
const manager = await loadCredentialManager();
|
|
1157
|
+
if (manager) {
|
|
1158
|
+
const credentials = await manager.load();
|
|
1159
|
+
if (credentials) {
|
|
1160
|
+
console.log(c.green(' ✅ Credentials found (encrypted file)\n'));
|
|
1161
|
+
|
|
1162
|
+
// Check if expired
|
|
1163
|
+
const isExpired = await manager.isExpired();
|
|
1164
|
+
if (isExpired) {
|
|
1165
|
+
console.log(c.red(' ❌ Credentials expired\n'));
|
|
1166
|
+
issues.push('Credentials expired');
|
|
1167
|
+
recommendations.push('Run: teleportation login');
|
|
1168
|
+
checksFailed++;
|
|
1169
|
+
} else {
|
|
1170
|
+
const daysUntil = await manager.daysUntilExpiry();
|
|
1171
|
+
if (daysUntil !== null) {
|
|
1172
|
+
if (daysUntil < 7) {
|
|
1173
|
+
console.log(c.yellow(` ⚠️ Credentials expire in ${daysUntil} days\n`));
|
|
1174
|
+
recommendations.push('Consider refreshing credentials soon');
|
|
1175
|
+
} else {
|
|
1176
|
+
console.log(c.green(` ✅ Credentials valid for ${daysUntil} more days\n`));
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
checksPassed++;
|
|
1180
|
+
}
|
|
1181
|
+
} else {
|
|
1182
|
+
// Check environment variables
|
|
1183
|
+
const envCreds = {
|
|
1184
|
+
RELAY_API_URL: process.env.RELAY_API_URL,
|
|
1185
|
+
RELAY_API_KEY: process.env.RELAY_API_KEY
|
|
1186
|
+
};
|
|
1187
|
+
if (envCreds.RELAY_API_URL && envCreds.RELAY_API_KEY) {
|
|
1188
|
+
console.log(c.yellow(' ⚠️ Using environment variables (not encrypted)\n'));
|
|
1189
|
+
recommendations.push('Consider using: teleportation login');
|
|
1190
|
+
checksPassed++;
|
|
1191
|
+
} else {
|
|
1192
|
+
console.log(c.red(' ❌ No credentials found\n'));
|
|
1193
|
+
issues.push('No credentials');
|
|
1194
|
+
recommendations.push('Run: teleportation login');
|
|
1195
|
+
checksFailed++;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
} else {
|
|
1199
|
+
console.log(c.red(' ❌ Credential manager unavailable\n'));
|
|
1200
|
+
checksFailed++;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// Check 4: Relay API connection
|
|
1204
|
+
console.log(c.yellow('4. Relay API Connection'));
|
|
1205
|
+
const creds = await getCredentials();
|
|
1206
|
+
const relayUrl = creds.RELAY_API_URL || 'https://api.teleportation.dev';
|
|
1207
|
+
|
|
1208
|
+
if (!relayUrl) {
|
|
1209
|
+
console.log(c.red(' ❌ Relay API URL not configured\n'));
|
|
1210
|
+
issues.push('Relay API URL missing');
|
|
1211
|
+
recommendations.push('Set RELAY_API_URL or run: teleportation login');
|
|
1212
|
+
checksFailed++;
|
|
1213
|
+
} else {
|
|
1214
|
+
const startTime = Date.now();
|
|
1215
|
+
try {
|
|
1216
|
+
const response = await fetch(`${relayUrl}/health`, {
|
|
1217
|
+
method: 'GET',
|
|
1218
|
+
signal: AbortSignal.timeout(5000)
|
|
1219
|
+
});
|
|
1220
|
+
const latency = Date.now() - startTime;
|
|
1221
|
+
|
|
1222
|
+
if (response.ok) {
|
|
1223
|
+
console.log(c.green(` ✅ Connected (${latency}ms latency)\n`));
|
|
1224
|
+
checksPassed++;
|
|
1225
|
+
|
|
1226
|
+
if (latency > 1000) {
|
|
1227
|
+
console.log(c.yellow(` ⚠️ High latency detected (${latency}ms)\n`));
|
|
1228
|
+
recommendations.push('Consider using a relay API closer to your location');
|
|
1229
|
+
}
|
|
1230
|
+
} else {
|
|
1231
|
+
console.log(c.red(` ❌ API returned status ${response.status}\n`));
|
|
1232
|
+
issues.push(`Relay API unhealthy (status ${response.status})`);
|
|
1233
|
+
checksFailed++;
|
|
1234
|
+
}
|
|
1235
|
+
} catch (error) {
|
|
1236
|
+
const latency = Date.now() - startTime;
|
|
1237
|
+
console.log(c.red(` ❌ Connection failed: ${error.message}\n`));
|
|
1238
|
+
issues.push(`Cannot connect to relay API at ${relayUrl}`);
|
|
1239
|
+
recommendations.push('Check if relay API is running: teleportation start');
|
|
1240
|
+
recommendations.push(`Verify URL is correct: ${relayUrl}`);
|
|
1241
|
+
checksFailed++;
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// Check 5: Environment variables
|
|
1246
|
+
console.log(c.yellow('5. Environment Variables'));
|
|
1247
|
+
const envVars = {
|
|
1248
|
+
RELAY_API_URL: process.env.RELAY_API_URL,
|
|
1249
|
+
RELAY_API_KEY: process.env.RELAY_API_KEY ? '***' + process.env.RELAY_API_KEY.slice(-4) : undefined,
|
|
1250
|
+
EDITOR: process.env.EDITOR,
|
|
1251
|
+
HOME: process.env.HOME
|
|
1252
|
+
};
|
|
1253
|
+
|
|
1254
|
+
const envSet = Object.entries(envVars).filter(([_, v]) => v).length;
|
|
1255
|
+
console.log(c.cyan(` ${envSet} environment variables set\n`));
|
|
1256
|
+
if (envVars.EDITOR) {
|
|
1257
|
+
console.log(c.green(` ✅ Editor: ${envVars.EDITOR}\n`));
|
|
1258
|
+
} else {
|
|
1259
|
+
console.log(c.yellow(' ⚠️ EDITOR not set (needed for config edit)\n'));
|
|
1260
|
+
recommendations.push('Set EDITOR environment variable for config editing');
|
|
1261
|
+
}
|
|
1262
|
+
checksPassed++;
|
|
1263
|
+
|
|
1264
|
+
// Check 6: File permissions
|
|
1265
|
+
console.log(c.yellow('6. File Permissions'));
|
|
1266
|
+
try {
|
|
1267
|
+
const credsPath = path.join(HOME_DIR, '.teleportation', 'credentials');
|
|
1268
|
+
if (fs.existsSync(credsPath)) {
|
|
1269
|
+
const stats = fs.statSync(credsPath);
|
|
1270
|
+
const mode = stats.mode & parseInt('777', 8);
|
|
1271
|
+
if (mode === parseInt('600', 8)) {
|
|
1272
|
+
console.log(c.green(' ✅ Credentials file permissions correct (600)\n'));
|
|
1273
|
+
checksPassed++;
|
|
1274
|
+
} else {
|
|
1275
|
+
console.log(c.yellow(` ⚠️ Credentials file permissions: ${mode.toString(8)}\n`));
|
|
1276
|
+
issues.push('Credentials file permissions not secure');
|
|
1277
|
+
recommendations.push(`Run: chmod 600 ${credsPath}`);
|
|
1278
|
+
checksPassed++; // Not critical, just a warning
|
|
1279
|
+
}
|
|
1280
|
+
} else {
|
|
1281
|
+
console.log(c.yellow(' ⚠️ Credentials file does not exist\n'));
|
|
1282
|
+
checksPassed++; // Not an error if using env vars
|
|
1283
|
+
}
|
|
1284
|
+
} catch (e) {
|
|
1285
|
+
console.log(c.yellow(` ⚠️ Could not check permissions: ${e.message}\n`));
|
|
1286
|
+
checksPassed++;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// Summary
|
|
1290
|
+
console.log(c.purple('\n📊 Diagnostic Summary\n'));
|
|
1291
|
+
console.log(` Checks passed: ${c.green(checksPassed)}`);
|
|
1292
|
+
console.log(` Checks failed: ${checksFailed > 0 ? c.red(checksFailed) : c.green(checksFailed)}`);
|
|
1293
|
+
|
|
1294
|
+
if (issues.length > 0) {
|
|
1295
|
+
console.log(c.red('\n⚠️ Issues Found:\n'));
|
|
1296
|
+
issues.forEach((issue, i) => {
|
|
1297
|
+
console.log(` ${i + 1}. ${issue}`);
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
if (recommendations.length > 0) {
|
|
1302
|
+
console.log(c.cyan('\n💡 Recommendations:\n'));
|
|
1303
|
+
recommendations.forEach((rec, i) => {
|
|
1304
|
+
console.log(` ${i + 1}. ${rec}`);
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
if (checksFailed === 0 && issues.length === 0) {
|
|
1309
|
+
console.log(c.green('\n🎉 All checks passed! System is healthy.\n'));
|
|
1310
|
+
} else {
|
|
1311
|
+
console.log(c.yellow('\n⚠️ Some issues detected. Review recommendations above.\n'));
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
async function commandUninstall() {
|
|
1316
|
+
console.log(c.purple('🗑️ Teleportation Uninstall\n'));
|
|
1317
|
+
|
|
1318
|
+
const readline = require('readline').createInterface({
|
|
1319
|
+
input: process.stdin,
|
|
1320
|
+
output: process.stdout
|
|
1321
|
+
});
|
|
1322
|
+
|
|
1323
|
+
return new Promise((resolve) => {
|
|
1324
|
+
readline.question('Are you sure you want to uninstall Teleportation? (y/N): ', async (answer) => {
|
|
1325
|
+
if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
|
|
1326
|
+
console.log(c.yellow('Uninstall cancelled.\n'));
|
|
1327
|
+
readline.close();
|
|
1328
|
+
resolve();
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
console.log(c.yellow('\nUninstalling Teleportation...\n'));
|
|
1333
|
+
|
|
1334
|
+
let removed = 0;
|
|
1335
|
+
let kept = 0;
|
|
1336
|
+
|
|
1337
|
+
// 1. Remove hooks
|
|
1338
|
+
console.log(c.yellow('1. Removing hooks...'));
|
|
1339
|
+
if (fs.existsSync(config.globalHooks)) {
|
|
1340
|
+
const hookFiles = fs.readdirSync(config.globalHooks).filter(f =>
|
|
1341
|
+
f.endsWith('.mjs') && ['pre_tool_use.mjs', 'permission_request.mjs', 'post_tool_use.mjs', 'session_start.mjs', 'session_end.mjs', 'stop.mjs', 'notification.mjs', 'config-loader.mjs'].includes(f)
|
|
1342
|
+
);
|
|
1343
|
+
|
|
1344
|
+
hookFiles.forEach(hook => {
|
|
1345
|
+
try {
|
|
1346
|
+
fs.unlinkSync(path.join(config.globalHooks, hook));
|
|
1347
|
+
console.log(c.green(` ✅ Removed ${hook}`));
|
|
1348
|
+
removed++;
|
|
1349
|
+
} catch (e) {
|
|
1350
|
+
console.log(c.red(` ❌ Failed to remove ${hook}: ${e.message}`));
|
|
1351
|
+
}
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
console.log();
|
|
1355
|
+
|
|
1356
|
+
// 2. Remove settings.json
|
|
1357
|
+
console.log(c.yellow('2. Removing Claude Code settings...'));
|
|
1358
|
+
if (fs.existsSync(config.globalSettings)) {
|
|
1359
|
+
try {
|
|
1360
|
+
fs.unlinkSync(config.globalSettings);
|
|
1361
|
+
console.log(c.green(' ✅ Removed ~/.claude/settings.json'));
|
|
1362
|
+
removed++;
|
|
1363
|
+
} catch (e) {
|
|
1364
|
+
console.log(c.red(` ❌ Failed to remove settings: ${e.message}`));
|
|
1365
|
+
}
|
|
1366
|
+
} else {
|
|
1367
|
+
console.log(c.yellow(' ⚠️ Settings file not found'));
|
|
1368
|
+
}
|
|
1369
|
+
console.log();
|
|
1370
|
+
|
|
1371
|
+
// 3. Ask about credentials
|
|
1372
|
+
console.log(c.yellow('3. Credentials...'));
|
|
1373
|
+
const manager = await loadCredentialManager();
|
|
1374
|
+
if (manager && await manager.exists()) {
|
|
1375
|
+
readline.question(' Delete saved credentials? (y/N): ', async (answer) => {
|
|
1376
|
+
if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
|
|
1377
|
+
try {
|
|
1378
|
+
await manager.delete();
|
|
1379
|
+
console.log(c.green(' ✅ Credentials deleted'));
|
|
1380
|
+
removed++;
|
|
1381
|
+
} catch (e) {
|
|
1382
|
+
console.log(c.red(` ❌ Failed to delete credentials: ${e.message}`));
|
|
1383
|
+
}
|
|
1384
|
+
} else {
|
|
1385
|
+
console.log(c.yellow(' ⚠️ Credentials kept'));
|
|
1386
|
+
kept++;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
// 4. Ask about config
|
|
1390
|
+
console.log(c.yellow('\n4. Configuration...'));
|
|
1391
|
+
const configPath = path.join(TELEPORTATION_DIR, 'lib', 'config', 'manager.js');
|
|
1392
|
+
try {
|
|
1393
|
+
const { configExists, DEFAULT_CONFIG_PATH } = await import('file://' + configPath);
|
|
1394
|
+
if (await configExists()) {
|
|
1395
|
+
readline.question(' Delete config file? (y/N): ', async (answer) => {
|
|
1396
|
+
if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
|
|
1397
|
+
try {
|
|
1398
|
+
fs.unlinkSync(DEFAULT_CONFIG_PATH);
|
|
1399
|
+
console.log(c.green(' ✅ Config deleted'));
|
|
1400
|
+
removed++;
|
|
1401
|
+
} catch (e) {
|
|
1402
|
+
console.log(c.red(` ❌ Failed to delete config: ${e.message}`));
|
|
1403
|
+
}
|
|
1404
|
+
} else {
|
|
1405
|
+
console.log(c.yellow(' ⚠️ Config kept'));
|
|
1406
|
+
kept++;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
// 5. Remove CLI binary (if installed globally)
|
|
1410
|
+
console.log(c.yellow('\n5. CLI binary...'));
|
|
1411
|
+
const cliPaths = [
|
|
1412
|
+
'/usr/local/bin/teleportation',
|
|
1413
|
+
'/usr/bin/teleportation',
|
|
1414
|
+
path.join(HOME_DIR, '.local', 'bin', 'teleportation')
|
|
1415
|
+
];
|
|
1416
|
+
|
|
1417
|
+
let cliRemoved = false;
|
|
1418
|
+
for (const cliPath of cliPaths) {
|
|
1419
|
+
if (fs.existsSync(cliPath)) {
|
|
1420
|
+
try {
|
|
1421
|
+
fs.unlinkSync(cliPath);
|
|
1422
|
+
console.log(c.green(` ✅ Removed ${cliPath}`));
|
|
1423
|
+
removed++;
|
|
1424
|
+
cliRemoved = true;
|
|
1425
|
+
break;
|
|
1426
|
+
} catch (e) {
|
|
1427
|
+
// Need sudo, inform user
|
|
1428
|
+
console.log(c.yellow(` ⚠️ ${cliPath} exists but requires sudo to remove`));
|
|
1429
|
+
console.log(c.cyan(` Run: sudo rm ${cliPath}`));
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
if (!cliRemoved) {
|
|
1435
|
+
console.log(c.yellow(' ⚠️ CLI binary not found in standard locations'));
|
|
1436
|
+
console.log(c.cyan(' If installed elsewhere, remove manually'));
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
// 6. Deregister from relay API (if credentials available)
|
|
1440
|
+
console.log(c.yellow('\n6. Relay API deregistration...'));
|
|
1441
|
+
const creds = await getCredentials();
|
|
1442
|
+
if (creds.RELAY_API_URL && creds.RELAY_API_KEY) {
|
|
1443
|
+
try {
|
|
1444
|
+
// Try to deregister (if endpoint exists)
|
|
1445
|
+
await fetch(`${creds.RELAY_API_URL}/api/sessions/deregister`, {
|
|
1446
|
+
method: 'POST',
|
|
1447
|
+
headers: {
|
|
1448
|
+
'Authorization': `Bearer ${creds.RELAY_API_KEY}`,
|
|
1449
|
+
'Content-Type': 'application/json'
|
|
1450
|
+
},
|
|
1451
|
+
body: JSON.stringify({ session_id: 'uninstall' })
|
|
1452
|
+
}).catch(() => {}); // Ignore errors
|
|
1453
|
+
console.log(c.green(' ✅ Attempted deregistration'));
|
|
1454
|
+
} catch (e) {
|
|
1455
|
+
console.log(c.yellow(' ⚠️ Could not deregister (non-critical)'));
|
|
1456
|
+
}
|
|
1457
|
+
} else {
|
|
1458
|
+
console.log(c.yellow(' ⚠️ No credentials available for deregistration'));
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// Summary
|
|
1462
|
+
console.log(c.purple('\n📊 Uninstall Summary\n'));
|
|
1463
|
+
console.log(` Removed: ${c.green(removed)} items`);
|
|
1464
|
+
if (kept > 0) {
|
|
1465
|
+
console.log(` Kept: ${c.yellow(kept)} items`);
|
|
1466
|
+
}
|
|
1467
|
+
console.log(c.green('\n✅ Uninstall complete!\n'));
|
|
1468
|
+
console.log(c.cyan('Note: Environment variables in ~/.zshrc were not removed.'));
|
|
1469
|
+
console.log(c.cyan(' Remove them manually if desired.\n'));
|
|
1470
|
+
|
|
1471
|
+
readline.close();
|
|
1472
|
+
resolve();
|
|
1473
|
+
});
|
|
1474
|
+
} else {
|
|
1475
|
+
console.log(c.yellow(' ⚠️ Config file does not exist'));
|
|
1476
|
+
readline.close();
|
|
1477
|
+
resolve();
|
|
1478
|
+
}
|
|
1479
|
+
} catch (e) {
|
|
1480
|
+
console.log(c.yellow(' ⚠️ Could not check config'));
|
|
1481
|
+
readline.close();
|
|
1482
|
+
resolve();
|
|
1483
|
+
}
|
|
1484
|
+
});
|
|
1485
|
+
} else {
|
|
1486
|
+
console.log(c.yellow(' ⚠️ No credentials found'));
|
|
1487
|
+
readline.close();
|
|
1488
|
+
resolve();
|
|
1489
|
+
}
|
|
1490
|
+
});
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
function commandInfo() {
|
|
1495
|
+
console.log(c.purple('Teleportation System Information\n'));
|
|
1496
|
+
|
|
1497
|
+
console.log(c.yellow('Project:'));
|
|
1498
|
+
console.log(' Location:', c.cyan(TELEPORTATION_DIR));
|
|
1499
|
+
console.log(' Home:', c.cyan(HOME_DIR));
|
|
1500
|
+
|
|
1501
|
+
console.log('\n' + c.yellow('Configuration:'));
|
|
1502
|
+
console.log(' Global hooks:', c.cyan(config.globalHooksDir));
|
|
1503
|
+
console.log(' Settings:', c.cyan(config.globalSettings));
|
|
1504
|
+
console.log(' Status:', config.isConfigured() ? c.green('CONFIGURED') : c.red('NOT CONFIGURED'));
|
|
1505
|
+
|
|
1506
|
+
console.log('\n' + c.yellow('Services:'));
|
|
1507
|
+
console.log(' Relay API:', checkService('relay', 3030) ? c.green('RUNNING') : c.red('STOPPED'));
|
|
1508
|
+
console.log(' Storage API:', checkService('storage', 3040) ? c.green('RUNNING') : c.red('STOPPED'));
|
|
1509
|
+
|
|
1510
|
+
console.log('\n' + c.yellow('Logs:'));
|
|
1511
|
+
console.log(' Relay:', c.cyan(path.join(HOME_DIR, 'Library/Logs/teleportation-relay.log')));
|
|
1512
|
+
console.log(' Storage:', c.cyan(path.join(HOME_DIR, 'Library/Logs/teleportation-storage.log')));
|
|
1513
|
+
|
|
1514
|
+
console.log();
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
function commandLogs(args) {
|
|
1518
|
+
const service = args[0] || 'relay';
|
|
1519
|
+
const logFile = path.join(HOME_DIR, 'Library/Logs', `teleportation-${service}.log`);
|
|
1520
|
+
|
|
1521
|
+
if (fs.existsSync(logFile)) {
|
|
1522
|
+
console.log(c.yellow(`Showing logs for ${service}:\n`));
|
|
1523
|
+
try {
|
|
1524
|
+
const logs = execSync(`tail -20 ${logFile}`, { encoding: 'utf8' });
|
|
1525
|
+
console.log(logs);
|
|
1526
|
+
} catch (e) {
|
|
1527
|
+
console.log(c.red('Error reading logs'));
|
|
1528
|
+
}
|
|
1529
|
+
} else {
|
|
1530
|
+
console.log(c.red(`Log file not found: ${logFile}`));
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
async function commandLogin(args) {
|
|
1535
|
+
const { flags, positional } = parseFlags(args);
|
|
1536
|
+
|
|
1537
|
+
console.log(c.purple('Teleportation Login\n'));
|
|
1538
|
+
|
|
1539
|
+
// Load credential manager
|
|
1540
|
+
const manager = await loadCredentialManager();
|
|
1541
|
+
if (!manager) {
|
|
1542
|
+
console.log(c.red('❌ Failed to load credential manager'));
|
|
1543
|
+
process.exit(1);
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
// Check for existing credentials
|
|
1547
|
+
const existing = await manager.load();
|
|
1548
|
+
if (existing) {
|
|
1549
|
+
console.log(c.yellow('⚠️ You are already logged in.'));
|
|
1550
|
+
console.log(c.cyan(' Run "teleportation logout" to clear existing credentials.\n'));
|
|
1551
|
+
|
|
1552
|
+
const readline = require('readline').createInterface({
|
|
1553
|
+
input: process.stdin,
|
|
1554
|
+
output: process.stdout
|
|
1555
|
+
});
|
|
1556
|
+
|
|
1557
|
+
return new Promise((resolve) => {
|
|
1558
|
+
readline.question('Do you want to overwrite existing credentials? (y/N): ', async (answer) => {
|
|
1559
|
+
readline.close();
|
|
1560
|
+
if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
|
|
1561
|
+
console.log(c.yellow('Login cancelled.\n'));
|
|
1562
|
+
resolve();
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
await performLogin(manager, flags, positional);
|
|
1566
|
+
resolve();
|
|
1567
|
+
});
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
await performLogin(manager, flags, positional);
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
async function performLogin(manager, flags, positional) {
|
|
1575
|
+
let apiKey = flags['api-key'] || flags.k;
|
|
1576
|
+
let token = flags.token || flags.t;
|
|
1577
|
+
const relayApiUrl = flags['relay-url'] || flags.r || process.env.RELAY_API_URL || 'https://api.teleportation.dev';
|
|
1578
|
+
|
|
1579
|
+
// Create backup before modifying credentials (only if credentials exist)
|
|
1580
|
+
const existingCreds = await manager.load().catch(() => null);
|
|
1581
|
+
if (existingCreds) {
|
|
1582
|
+
try {
|
|
1583
|
+
const backupManagerPath = path.join(TELEPORTATION_DIR, 'lib', 'backup', 'manager.js');
|
|
1584
|
+
const { BackupManager } = await import('file://' + backupManagerPath);
|
|
1585
|
+
const backupMgr = new BackupManager();
|
|
1586
|
+
await backupMgr.createBackup('teleportation login');
|
|
1587
|
+
console.log(c.dim('Backup created before credential update.\n'));
|
|
1588
|
+
} catch (backupErr) {
|
|
1589
|
+
// Non-fatal - warn but continue
|
|
1590
|
+
if (process.env.DEBUG) {
|
|
1591
|
+
console.log(c.yellow(`⚠️ Could not create backup: ${backupErr.message}`));
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
// If API key provided via flag
|
|
1597
|
+
if (apiKey) {
|
|
1598
|
+
console.log(c.yellow('Authenticating with API key...\n'));
|
|
1599
|
+
|
|
1600
|
+
try {
|
|
1601
|
+
const apiKeyPath = path.join(TELEPORTATION_DIR, 'lib', 'auth', 'api-key.js');
|
|
1602
|
+
const { validateApiKey } = await import('file://' + apiKeyPath);
|
|
1603
|
+
const result = await validateApiKey(apiKey, relayApiUrl);
|
|
1604
|
+
|
|
1605
|
+
if (!result.valid) {
|
|
1606
|
+
console.log(c.red(`❌ ${result.error}\n`));
|
|
1607
|
+
process.exit(1);
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
// Save credentials
|
|
1611
|
+
const credentials = {
|
|
1612
|
+
apiKey: apiKey,
|
|
1613
|
+
relayApiUrl: relayApiUrl,
|
|
1614
|
+
authenticatedAt: Date.now(),
|
|
1615
|
+
method: 'api-key'
|
|
1616
|
+
};
|
|
1617
|
+
|
|
1618
|
+
await manager.save(credentials);
|
|
1619
|
+
|
|
1620
|
+
// Also sync config to match credentials
|
|
1621
|
+
try {
|
|
1622
|
+
const configManagerPath = path.join(TELEPORTATION_DIR, 'lib', 'config', 'manager.js');
|
|
1623
|
+
const { setConfigValue } = await import('file://' + configManagerPath);
|
|
1624
|
+
await setConfigValue('relay.url', relayApiUrl);
|
|
1625
|
+
} catch (configErr) {
|
|
1626
|
+
// Non-fatal - just warn
|
|
1627
|
+
console.log(c.yellow(`⚠️ Could not sync config: ${configErr.message}`));
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
console.log(c.green('✅ Successfully authenticated with API key!\n'));
|
|
1631
|
+
console.log(c.cyan('Credentials saved to ~/.teleportation/credentials\n'));
|
|
1632
|
+
console.log(c.yellow('⚠️ Restart Claude Code to apply changes to current session.\n'));
|
|
1633
|
+
return;
|
|
1634
|
+
} catch (error) {
|
|
1635
|
+
console.log(c.red(`❌ Error: ${error.message}\n`));
|
|
1636
|
+
process.exit(1);
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
// If token provided via flag
|
|
1641
|
+
if (token) {
|
|
1642
|
+
console.log(c.yellow('Authenticating with token...\n'));
|
|
1643
|
+
|
|
1644
|
+
try {
|
|
1645
|
+
// Save credentials with token
|
|
1646
|
+
const credentials = {
|
|
1647
|
+
accessToken: token,
|
|
1648
|
+
relayApiUrl: relayApiUrl,
|
|
1649
|
+
authenticatedAt: Date.now(),
|
|
1650
|
+
method: 'token'
|
|
1651
|
+
};
|
|
1652
|
+
|
|
1653
|
+
await manager.save(credentials);
|
|
1654
|
+
|
|
1655
|
+
// Also sync config to match credentials
|
|
1656
|
+
try {
|
|
1657
|
+
const configManagerPath = path.join(TELEPORTATION_DIR, 'lib', 'config', 'manager.js');
|
|
1658
|
+
const { setConfigValue } = await import('file://' + configManagerPath);
|
|
1659
|
+
await setConfigValue('relay.url', relayApiUrl);
|
|
1660
|
+
} catch (configErr) {
|
|
1661
|
+
// Non-fatal - just warn
|
|
1662
|
+
console.log(c.yellow(`⚠️ Could not sync config: ${configErr.message}`));
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
console.log(c.green('✅ Successfully authenticated with token!\n'));
|
|
1666
|
+
console.log(c.cyan('Credentials saved to ~/.teleportation/credentials\n'));
|
|
1667
|
+
console.log(c.yellow('⚠️ Restart Claude Code to apply changes to current session.\n'));
|
|
1668
|
+
return;
|
|
1669
|
+
} catch (error) {
|
|
1670
|
+
console.log(c.red(`❌ Error: ${error.message}\n`));
|
|
1671
|
+
process.exit(1);
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
// Interactive mode - prompt for API key
|
|
1676
|
+
console.log(c.cyan('Interactive login mode\n'));
|
|
1677
|
+
console.log(c.yellow('Options:'));
|
|
1678
|
+
console.log(' 1. API Key authentication (recommended)');
|
|
1679
|
+
console.log(' 2. OAuth device code flow (coming soon)\n');
|
|
1680
|
+
|
|
1681
|
+
const readline = require('readline').createInterface({
|
|
1682
|
+
input: process.stdin,
|
|
1683
|
+
output: process.stdout
|
|
1684
|
+
});
|
|
1685
|
+
|
|
1686
|
+
return new Promise((resolve) => {
|
|
1687
|
+
readline.question('Enter your API key (or press Enter to skip): ', async (input) => {
|
|
1688
|
+
readline.close();
|
|
1689
|
+
|
|
1690
|
+
if (!input || input.trim() === '') {
|
|
1691
|
+
console.log(c.yellow('\n⚠️ No API key provided.'));
|
|
1692
|
+
console.log(c.cyan(' Use --api-key flag or --token flag for non-interactive login.\n'));
|
|
1693
|
+
console.log(c.cyan(' Example: teleportation login --api-key YOUR_KEY\n'));
|
|
1694
|
+
resolve();
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
apiKey = input.trim();
|
|
1699
|
+
console.log(c.yellow('\nValidating API key...\n'));
|
|
1700
|
+
|
|
1701
|
+
try {
|
|
1702
|
+
const apiKeyPath = path.join(TELEPORTATION_DIR, 'lib', 'auth', 'api-key.js');
|
|
1703
|
+
const { validateApiKey } = await import('file://' + apiKeyPath);
|
|
1704
|
+
const result = await validateApiKey(apiKey, relayApiUrl);
|
|
1705
|
+
|
|
1706
|
+
if (!result.valid) {
|
|
1707
|
+
console.log(c.red(`❌ ${result.error}\n`));
|
|
1708
|
+
process.exit(1);
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
// Save credentials
|
|
1712
|
+
const credentials = {
|
|
1713
|
+
apiKey: apiKey,
|
|
1714
|
+
relayApiUrl: relayApiUrl,
|
|
1715
|
+
authenticatedAt: Date.now(),
|
|
1716
|
+
method: 'api-key'
|
|
1717
|
+
};
|
|
1718
|
+
|
|
1719
|
+
await manager.save(credentials);
|
|
1720
|
+
console.log(c.green('✅ Successfully authenticated!\n'));
|
|
1721
|
+
console.log(c.cyan('Credentials saved to ~/.teleportation/credentials\n'));
|
|
1722
|
+
resolve();
|
|
1723
|
+
} catch (error) {
|
|
1724
|
+
console.log(c.red(`❌ Error: ${error.message}\n`));
|
|
1725
|
+
process.exit(1);
|
|
1726
|
+
}
|
|
1727
|
+
});
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
async function commandConfig(args) {
|
|
1732
|
+
const subcommand = args[0] || 'list';
|
|
1733
|
+
|
|
1734
|
+
try {
|
|
1735
|
+
const configPath = path.join(TELEPORTATION_DIR, 'lib', 'config', 'manager.js');
|
|
1736
|
+
const { loadConfig, getConfigValue, setConfigValue, configExists, DEFAULT_CONFIG_PATH } = await import('file://' + configPath);
|
|
1737
|
+
|
|
1738
|
+
if (subcommand === 'list') {
|
|
1739
|
+
console.log(c.purple('Teleportation Configuration\n'));
|
|
1740
|
+
|
|
1741
|
+
const config = await loadConfig();
|
|
1742
|
+
const exists = await configExists();
|
|
1743
|
+
|
|
1744
|
+
if (!exists) {
|
|
1745
|
+
console.log(c.yellow('⚠️ Config file does not exist. Using defaults.\n'));
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
console.log(c.cyan('Relay Settings:'));
|
|
1749
|
+
console.log(` URL: ${c.green(config.relay?.url || 'not set')}`);
|
|
1750
|
+
console.log(` Timeout: ${c.green((config.relay?.timeout || 0) + 'ms')}`);
|
|
1751
|
+
|
|
1752
|
+
console.log(c.cyan('\nHook Settings:'));
|
|
1753
|
+
console.log(` Auto-update: ${config.hooks?.autoUpdate ? c.green('enabled') : c.yellow('disabled')}`);
|
|
1754
|
+
console.log(` Update check interval: ${c.green((config.hooks?.updateCheckInterval || 0) / 1000 / 60 + ' minutes')}`);
|
|
1755
|
+
|
|
1756
|
+
console.log(c.cyan('\nSession Settings:'));
|
|
1757
|
+
console.log(` Timeout: ${c.green((config.session?.timeout || 0) / 1000 / 60 + ' minutes')}`);
|
|
1758
|
+
console.log(` Mute timeout: ${c.green((config.session?.muteTimeout || 0) / 1000 / 60 + ' minutes')}`);
|
|
1759
|
+
|
|
1760
|
+
console.log(c.cyan('\nNotification Settings:'));
|
|
1761
|
+
console.log(` Enabled: ${config.notifications?.enabled ? c.green('yes') : c.yellow('no')}`);
|
|
1762
|
+
console.log(` Sound: ${config.notifications?.sound ? c.green('enabled') : c.yellow('disabled')}`);
|
|
1763
|
+
|
|
1764
|
+
console.log(c.cyan(`\nConfig file: ${DEFAULT_CONFIG_PATH}\n`));
|
|
1765
|
+
|
|
1766
|
+
} else if (subcommand === 'get') {
|
|
1767
|
+
const key = args[1];
|
|
1768
|
+
if (!key) {
|
|
1769
|
+
console.log(c.red('❌ Error: Please specify a config key\n'));
|
|
1770
|
+
console.log(c.cyan('Example: teleportation config get relay.url\n'));
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
const value = await getConfigValue(key);
|
|
1775
|
+
if (value === null) {
|
|
1776
|
+
console.log(c.yellow(`⚠️ Config key "${key}" not found\n`));
|
|
1777
|
+
} else {
|
|
1778
|
+
console.log(c.green(JSON.stringify(value, null, 2) + '\n'));
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
} else if (subcommand === 'set') {
|
|
1782
|
+
const key = args[1];
|
|
1783
|
+
const valueStr = args[2];
|
|
1784
|
+
|
|
1785
|
+
if (!key || valueStr === undefined) {
|
|
1786
|
+
console.log(c.red('❌ Error: Please specify key and value\n'));
|
|
1787
|
+
console.log(c.cyan('Example: teleportation config set relay.url http://example.com:3030\n'));
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
// Try to parse value as JSON, number, or boolean
|
|
1792
|
+
let value = valueStr;
|
|
1793
|
+
try {
|
|
1794
|
+
value = JSON.parse(valueStr);
|
|
1795
|
+
} catch {
|
|
1796
|
+
// Not JSON, try boolean or number
|
|
1797
|
+
if (valueStr === 'true') value = true;
|
|
1798
|
+
else if (valueStr === 'false') value = false;
|
|
1799
|
+
else if (/^\d+$/.test(valueStr)) value = parseInt(valueStr, 10);
|
|
1800
|
+
else if (/^\d+\.\d+$/.test(valueStr)) value = parseFloat(valueStr);
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
await setConfigValue(key, value);
|
|
1804
|
+
console.log(c.green(`✅ Set ${key} = ${JSON.stringify(value)}\n`));
|
|
1805
|
+
|
|
1806
|
+
} else if (subcommand === 'edit') {
|
|
1807
|
+
const editor = process.env.EDITOR || 'vi';
|
|
1808
|
+
const exists = await configExists();
|
|
1809
|
+
|
|
1810
|
+
if (!exists) {
|
|
1811
|
+
// Create default config first
|
|
1812
|
+
const { saveConfig, loadConfig } = await import('file://' + configPath);
|
|
1813
|
+
const config = await loadConfig();
|
|
1814
|
+
await saveConfig(config);
|
|
1815
|
+
console.log(c.yellow('Created default config file.\n'));
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
console.log(c.cyan(`Opening config in ${editor}...\n`));
|
|
1819
|
+
try {
|
|
1820
|
+
execSync(`${editor} ${DEFAULT_CONFIG_PATH}`, { stdio: 'inherit' });
|
|
1821
|
+
console.log(c.green('✅ Config file saved\n'));
|
|
1822
|
+
} catch (e) {
|
|
1823
|
+
console.log(c.red(`❌ Error opening editor: ${e.message}\n`));
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
} else {
|
|
1827
|
+
console.log(c.red(`❌ Unknown config subcommand: ${subcommand}\n`));
|
|
1828
|
+
console.log(c.yellow('Available subcommands:'));
|
|
1829
|
+
console.log(' list - Show all settings');
|
|
1830
|
+
console.log(' get - Get specific setting');
|
|
1831
|
+
console.log(' set - Update setting');
|
|
1832
|
+
console.log(' edit - Open config in editor\n');
|
|
1833
|
+
}
|
|
1834
|
+
} catch (error) {
|
|
1835
|
+
console.log(c.red(`❌ Error: ${error.message}\n`));
|
|
1836
|
+
throw error;
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
async function commandLogout() {
|
|
1841
|
+
console.log(c.purple('Teleportation Logout\n'));
|
|
1842
|
+
|
|
1843
|
+
const manager = await loadCredentialManager();
|
|
1844
|
+
if (!manager) {
|
|
1845
|
+
console.log(c.red('❌ Failed to load credential manager'));
|
|
1846
|
+
process.exit(1);
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
const exists = await manager.exists();
|
|
1850
|
+
if (!exists) {
|
|
1851
|
+
console.log(c.yellow('⚠️ No credentials found. You are not logged in.\n'));
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
const readline = require('readline').createInterface({
|
|
1856
|
+
input: process.stdin,
|
|
1857
|
+
output: process.stdout
|
|
1858
|
+
});
|
|
1859
|
+
|
|
1860
|
+
return new Promise((resolve) => {
|
|
1861
|
+
readline.question('Are you sure you want to log out? (y/N): ', async (answer) => {
|
|
1862
|
+
readline.close();
|
|
1863
|
+
|
|
1864
|
+
if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
|
|
1865
|
+
console.log(c.yellow('Logout cancelled.\n'));
|
|
1866
|
+
resolve();
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
try {
|
|
1871
|
+
await manager.delete();
|
|
1872
|
+
console.log(c.green('✅ Successfully logged out!\n'));
|
|
1873
|
+
console.log(c.cyan('Credentials cleared from ~/.teleportation/credentials\n'));
|
|
1874
|
+
resolve();
|
|
1875
|
+
} catch (error) {
|
|
1876
|
+
console.log(c.red(`❌ Error: ${error.message}\n`));
|
|
1877
|
+
process.exit(1);
|
|
1878
|
+
}
|
|
1879
|
+
});
|
|
1880
|
+
});
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
// Worktree/Snapshot/Session command handlers
|
|
1884
|
+
async function commandWorktree(args) {
|
|
1885
|
+
const cliPath = path.join(TELEPORTATION_DIR, 'lib', 'cli', 'index.js');
|
|
1886
|
+
const { parseArgs, routeCommand, printHelp } = await import('file://' + cliPath);
|
|
1887
|
+
|
|
1888
|
+
if (args.length === 0 || args[0] === 'help' || args[0] === '--help') {
|
|
1889
|
+
printHelp();
|
|
1890
|
+
return;
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
const parsed = parseArgs(['worktree', ...args]);
|
|
1894
|
+
await routeCommand(parsed);
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
async function commandSnapshot(args) {
|
|
1898
|
+
const cliPath = path.join(TELEPORTATION_DIR, 'lib', 'cli', 'index.js');
|
|
1899
|
+
const { parseArgs, routeCommand, printHelp } = await import('file://' + cliPath);
|
|
1900
|
+
|
|
1901
|
+
if (args.length === 0 || args[0] === 'help' || args[0] === '--help') {
|
|
1902
|
+
printHelp();
|
|
1903
|
+
return;
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
const parsed = parseArgs(['snapshot', ...args]);
|
|
1907
|
+
await routeCommand(parsed);
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
async function commandSession(args) {
|
|
1911
|
+
const cliPath = path.join(TELEPORTATION_DIR, 'lib', 'cli', 'index.js');
|
|
1912
|
+
const { parseArgs, routeCommand, printHelp } = await import('file://' + cliPath);
|
|
1913
|
+
|
|
1914
|
+
if (args.length === 0 || args[0] === 'help' || args[0] === '--help') {
|
|
1915
|
+
printHelp();
|
|
1916
|
+
return;
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
const parsed = parseArgs(['session', ...args]);
|
|
1920
|
+
await routeCommand(parsed);
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
async function commandDaemon(args) {
|
|
1924
|
+
const subCommand = args[0] || 'status';
|
|
1925
|
+
|
|
1926
|
+
// Get daemon port from env with default (consistent with daemon itself)
|
|
1927
|
+
const DAEMON_PORT = process.env.TELEPORTATION_DAEMON_PORT || '3050';
|
|
1928
|
+
|
|
1929
|
+
try {
|
|
1930
|
+
// Dynamically import lifecycle module
|
|
1931
|
+
const lifecyclePath = path.join(TELEPORTATION_DIR, 'lib', 'daemon', 'lifecycle.js');
|
|
1932
|
+
const { startDaemon, stopDaemon, restartDaemon, getDaemonStatus } = await import('file://' + lifecyclePath);
|
|
1933
|
+
|
|
1934
|
+
switch (subCommand) {
|
|
1935
|
+
case 'start':
|
|
1936
|
+
console.log(c.yellow('Starting Teleportation Daemon...\n'));
|
|
1937
|
+
try {
|
|
1938
|
+
const result = await startDaemon();
|
|
1939
|
+
console.log(c.green(`✅ Daemon started successfully (PID: ${result.pid})\n`));
|
|
1940
|
+
console.log(c.cyan(`Daemon is running at http://127.0.0.1:${DAEMON_PORT}\n`));
|
|
1941
|
+
} catch (error) {
|
|
1942
|
+
console.log(c.red(`❌ Failed to start daemon: ${error.message}\n`));
|
|
1943
|
+
process.exit(1);
|
|
1944
|
+
}
|
|
1945
|
+
break;
|
|
1946
|
+
|
|
1947
|
+
case 'stop':
|
|
1948
|
+
console.log(c.yellow('Stopping Teleportation Daemon...\n'));
|
|
1949
|
+
try {
|
|
1950
|
+
const result = await stopDaemon();
|
|
1951
|
+
if (result.success) {
|
|
1952
|
+
console.log(c.green(`✅ Daemon stopped successfully${result.forced ? ' (forced)' : ''}\n`));
|
|
1953
|
+
} else {
|
|
1954
|
+
console.log(c.red('❌ Failed to stop daemon\n'));
|
|
1955
|
+
process.exit(1);
|
|
1956
|
+
}
|
|
1957
|
+
} catch (error) {
|
|
1958
|
+
console.log(c.red(`❌ Error: ${error.message}\n`));
|
|
1959
|
+
process.exit(1);
|
|
1960
|
+
}
|
|
1961
|
+
break;
|
|
1962
|
+
|
|
1963
|
+
case 'restart':
|
|
1964
|
+
console.log(c.yellow('Restarting Teleportation Daemon...\n'));
|
|
1965
|
+
try {
|
|
1966
|
+
const result = await restartDaemon();
|
|
1967
|
+
console.log(c.green(`✅ Daemon restarted successfully (PID: ${result.pid})\n`));
|
|
1968
|
+
console.log(c.cyan(`Previous daemon ${result.wasRunning ? 'was running' : 'was not running'}\n`));
|
|
1969
|
+
} catch (error) {
|
|
1970
|
+
console.log(c.red(`❌ Failed to restart daemon: ${error.message}\n`));
|
|
1971
|
+
process.exit(1);
|
|
1972
|
+
}
|
|
1973
|
+
break;
|
|
1974
|
+
|
|
1975
|
+
case 'status':
|
|
1976
|
+
const status = await getDaemonStatus();
|
|
1977
|
+
console.log(c.purple('Teleportation Daemon Status\n'));
|
|
1978
|
+
if (status.running) {
|
|
1979
|
+
console.log(c.green(`✅ Running (PID: ${status.pid})`));
|
|
1980
|
+
console.log(c.cyan(` HTTP server: http://127.0.0.1:${DAEMON_PORT}`));
|
|
1981
|
+
if (status.uptime) {
|
|
1982
|
+
console.log(c.cyan(` Uptime: ${Math.round(status.uptime / 60000)}m`));
|
|
1983
|
+
}
|
|
1984
|
+
} else {
|
|
1985
|
+
console.log(c.red('❌ Not running'));
|
|
1986
|
+
}
|
|
1987
|
+
console.log('');
|
|
1988
|
+
break;
|
|
1989
|
+
|
|
1990
|
+
case 'health':
|
|
1991
|
+
console.log(c.yellow('Checking daemon health...\n'));
|
|
1992
|
+
try {
|
|
1993
|
+
const response = await fetch(`http://127.0.0.1:${DAEMON_PORT}/health`);
|
|
1994
|
+
if (response.ok) {
|
|
1995
|
+
const data = await response.json();
|
|
1996
|
+
console.log(c.green('✅ Daemon is healthy\n'));
|
|
1997
|
+
console.log(c.cyan('Health Report:'));
|
|
1998
|
+
console.log(` Status: ${c.green(data.status)}`);
|
|
1999
|
+
console.log(` Uptime: ${c.cyan(Math.round(data.uptime) + 's')}`);
|
|
2000
|
+
console.log(` Sessions: ${c.cyan(data.sessions)}`);
|
|
2001
|
+
console.log(` Queue: ${c.cyan(data.queue)}`);
|
|
2002
|
+
console.log(` Executions: ${c.cyan(data.executions)}\n`);
|
|
2003
|
+
} else {
|
|
2004
|
+
console.log(c.red('❌ Daemon is unhealthy\n'));
|
|
2005
|
+
process.exit(1);
|
|
2006
|
+
}
|
|
2007
|
+
} catch (error) {
|
|
2008
|
+
console.log(c.red(`❌ Cannot reach daemon: ${error.message}\n`));
|
|
2009
|
+
process.exit(1);
|
|
2010
|
+
}
|
|
2011
|
+
break;
|
|
2012
|
+
|
|
2013
|
+
default:
|
|
2014
|
+
console.log(c.red(`Unknown daemon command: ${subCommand}\n`));
|
|
2015
|
+
console.log(c.cyan('Available commands:'));
|
|
2016
|
+
console.log(' teleportation daemon start - Start the daemon');
|
|
2017
|
+
console.log(' teleportation daemon stop - Stop the daemon');
|
|
2018
|
+
console.log(' teleportation daemon restart - Restart the daemon');
|
|
2019
|
+
console.log(' teleportation daemon status - Show daemon status');
|
|
2020
|
+
console.log(' teleportation daemon health - Check daemon health\n');
|
|
2021
|
+
process.exit(1);
|
|
2022
|
+
}
|
|
2023
|
+
} catch (error) {
|
|
2024
|
+
console.log(c.red(`❌ Daemon command failed: ${error.message}\n`));
|
|
2025
|
+
process.exit(1);
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
// Away Mode Commands (Task 9.0)
|
|
2030
|
+
async function commandAwayMode() {
|
|
2031
|
+
console.log(c.yellow('🚀 Marking session as away and starting daemon...\n'));
|
|
2032
|
+
|
|
2033
|
+
const sessionId = process.env.TELEPORTATION_SESSION_ID;
|
|
2034
|
+
if (!sessionId) {
|
|
2035
|
+
console.log(c.red('❌ Error: TELEPORTATION_SESSION_ID not set\n'));
|
|
2036
|
+
console.log(c.cyan('Set the environment variable: export TELEPORTATION_SESSION_ID=<session-id>\n'));
|
|
2037
|
+
process.exit(1);
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
const relayUrl = process.env.RELAY_API_URL || '';
|
|
2041
|
+
const relayKey = process.env.RELAY_API_KEY || '';
|
|
2042
|
+
|
|
2043
|
+
if (!relayUrl || !relayKey) {
|
|
2044
|
+
console.log(c.red('❌ Error: RELAY_API_URL or RELAY_API_KEY not set\n'));
|
|
2045
|
+
process.exit(1);
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
try {
|
|
2049
|
+
// Update session daemon state
|
|
2050
|
+
const res = await fetch(`${relayUrl}/api/sessions/${encodeURIComponent(sessionId)}/daemon-state`, {
|
|
2051
|
+
method: 'PATCH',
|
|
2052
|
+
headers: {
|
|
2053
|
+
'Content-Type': 'application/json',
|
|
2054
|
+
'Authorization': `Bearer ${relayKey}`,
|
|
2055
|
+
},
|
|
2056
|
+
body: JSON.stringify({
|
|
2057
|
+
is_away: true,
|
|
2058
|
+
status: 'running',
|
|
2059
|
+
started_reason: 'cli_away',
|
|
2060
|
+
}),
|
|
2061
|
+
});
|
|
2062
|
+
|
|
2063
|
+
if (!res.ok) {
|
|
2064
|
+
console.log(c.yellow('⚠️ Warning: Could not update session state via Relay API\n'));
|
|
2065
|
+
} else {
|
|
2066
|
+
console.log(c.green('✅ Session marked as away in Relay API\n'));
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
console.log(c.green('✅ Session marked as away. Daemon is ready.\n'));
|
|
2070
|
+
console.log(c.cyan('When you return, run: teleportation back\n'));
|
|
2071
|
+
} catch (error) {
|
|
2072
|
+
console.log(c.red('❌ Error: ' + error.message + '\n'));
|
|
2073
|
+
process.exit(1);
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
async function commandBackMode() {
|
|
2078
|
+
console.log(c.yellow('🔙 Marking session as back...\n'));
|
|
2079
|
+
|
|
2080
|
+
const sessionId = process.env.TELEPORTATION_SESSION_ID;
|
|
2081
|
+
if (!sessionId) {
|
|
2082
|
+
console.log(c.red('❌ Error: TELEPORTATION_SESSION_ID not set\n'));
|
|
2083
|
+
console.log(c.cyan('Set the environment variable: export TELEPORTATION_SESSION_ID=<session-id>\n'));
|
|
2084
|
+
process.exit(1);
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
const relayUrl = process.env.RELAY_API_URL || '';
|
|
2088
|
+
const relayKey = process.env.RELAY_API_KEY || '';
|
|
2089
|
+
|
|
2090
|
+
if (!relayUrl || !relayKey) {
|
|
2091
|
+
console.log(c.red('❌ Error: RELAY_API_URL or RELAY_API_KEY not set\n'));
|
|
2092
|
+
process.exit(1);
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
try {
|
|
2096
|
+
// Update session daemon state
|
|
2097
|
+
const res = await fetch(`${relayUrl}/api/sessions/${encodeURIComponent(sessionId)}/daemon-state`, {
|
|
2098
|
+
method: 'PATCH',
|
|
2099
|
+
headers: {
|
|
2100
|
+
'Content-Type': 'application/json',
|
|
2101
|
+
'Authorization': `Bearer ${relayKey}`,
|
|
2102
|
+
},
|
|
2103
|
+
body: JSON.stringify({
|
|
2104
|
+
is_away: false,
|
|
2105
|
+
status: 'stopped',
|
|
2106
|
+
started_reason: null,
|
|
2107
|
+
}),
|
|
2108
|
+
});
|
|
2109
|
+
|
|
2110
|
+
if (!res.ok) {
|
|
2111
|
+
console.log(c.yellow('⚠️ Warning: Could not update session state via Relay API\n'));
|
|
2112
|
+
} else {
|
|
2113
|
+
console.log(c.green('✅ Session marked as back in Relay API\n'));
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
console.log(c.green('✅ Session marked as back.\n'));
|
|
2117
|
+
} catch (error) {
|
|
2118
|
+
console.log(c.red('❌ Error: ' + error.message + '\n'));
|
|
2119
|
+
process.exit(1);
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
async function commandDaemonStatusDisplay() {
|
|
2124
|
+
console.log(c.cyan('\n📊 Daemon Status\n'));
|
|
2125
|
+
|
|
2126
|
+
const sessionId = process.env.TELEPORTATION_SESSION_ID;
|
|
2127
|
+
const relayUrl = process.env.RELAY_API_URL || '';
|
|
2128
|
+
const relayKey = process.env.RELAY_API_KEY || '';
|
|
2129
|
+
|
|
2130
|
+
if (sessionId && relayUrl && relayKey) {
|
|
2131
|
+
try {
|
|
2132
|
+
const res = await fetch(`${relayUrl}/api/sessions/${encodeURIComponent(sessionId)}`, {
|
|
2133
|
+
headers: { 'Authorization': `Bearer ${relayKey}` },
|
|
2134
|
+
});
|
|
2135
|
+
|
|
2136
|
+
if (res.ok) {
|
|
2137
|
+
const session = await res.json();
|
|
2138
|
+
const daemonState = session.daemon_state;
|
|
2139
|
+
|
|
2140
|
+
if (daemonState) {
|
|
2141
|
+
console.log(c.yellow('Session State:'));
|
|
2142
|
+
console.log(` Status: ${daemonState.status === 'running' ? c.green('Running') : c.red('Stopped')}`);
|
|
2143
|
+
console.log(` Away: ${daemonState.is_away ? c.yellow('Yes') : c.green('No')}`);
|
|
2144
|
+
|
|
2145
|
+
if (daemonState.started_at) {
|
|
2146
|
+
const startedDate = new Date(daemonState.started_at);
|
|
2147
|
+
console.log(` Started: ${startedDate.toLocaleString()}`);
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
if (daemonState.started_reason) {
|
|
2151
|
+
console.log(` Started Reason: ${daemonState.started_reason}`);
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
if (daemonState.last_approval_location) {
|
|
2155
|
+
console.log(` Last Approval: ${daemonState.last_approval_location}`);
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
if (daemonState.stopped_reason) {
|
|
2159
|
+
console.log(` Stopped Reason: ${daemonState.stopped_reason}`);
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
} catch (err) {
|
|
2164
|
+
console.log(c.yellow('⚠️ Could not fetch session state from Relay API\n'));
|
|
2165
|
+
}
|
|
2166
|
+
} else {
|
|
2167
|
+
console.log(c.yellow('⚠️ Session ID or Relay API not configured\n'));
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
console.log();
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
async function commandInbox() {
|
|
2174
|
+
const sessionId = process.env.TELEPORTATION_SESSION_ID;
|
|
2175
|
+
if (!sessionId) {
|
|
2176
|
+
console.log(c.red('❌ Error: TELEPORTATION_SESSION_ID not set\n'));
|
|
2177
|
+
console.log(c.cyan('Set the environment variable: export TELEPORTATION_SESSION_ID=<session-id>\n'));
|
|
2178
|
+
process.exit(1);
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
const creds = await getCredentials();
|
|
2182
|
+
const relayUrl = creds.RELAY_API_URL;
|
|
2183
|
+
const relayKey = creds.RELAY_API_KEY;
|
|
2184
|
+
|
|
2185
|
+
if (!relayUrl || !relayKey) {
|
|
2186
|
+
console.log(c.red('❌ Error: RELAY_API_URL or RELAY_API_KEY not configured\n'));
|
|
2187
|
+
process.exit(1);
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
try {
|
|
2191
|
+
const url = `${relayUrl}/api/messages/pending?session_id=${encodeURIComponent(sessionId)}&agent_id=main`;
|
|
2192
|
+
const res = await fetch(url, {
|
|
2193
|
+
headers: {
|
|
2194
|
+
'Authorization': `Bearer ${relayKey}`,
|
|
2195
|
+
},
|
|
2196
|
+
});
|
|
2197
|
+
|
|
2198
|
+
if (!res.ok) {
|
|
2199
|
+
console.log(c.red(`❌ Error: Inbox request failed with status ${res.status}\n`));
|
|
2200
|
+
process.exit(1);
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
const data = await res.json();
|
|
2204
|
+
const keys = data && typeof data === 'object' ? Object.keys(data) : [];
|
|
2205
|
+
if (!keys.length) {
|
|
2206
|
+
console.log(c.cyan('📭 No pending inbox messages for this session\n'));
|
|
2207
|
+
return;
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
console.log(c.cyan('📨 Next inbox message:\n'));
|
|
2211
|
+
console.log(' ID: ' + c.green(data.id));
|
|
2212
|
+
console.log(' Text: ' + data.text + '\n');
|
|
2213
|
+
console.log(c.cyan('Use `teleportation inbox-ack ' + data.id + '` to acknowledge this message.\n'));
|
|
2214
|
+
} catch (error) {
|
|
2215
|
+
console.log(c.red('❌ Error: ' + error.message + '\n'));
|
|
2216
|
+
process.exit(1);
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
async function commandInboxAck(id) {
|
|
2221
|
+
if (!id) {
|
|
2222
|
+
console.log(c.red('❌ Error: Message id is required\n'));
|
|
2223
|
+
console.log(c.cyan('Usage: teleportation inbox-ack <id>\n'));
|
|
2224
|
+
process.exit(1);
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
const creds = await getCredentials();
|
|
2228
|
+
const relayUrl = creds.RELAY_API_URL;
|
|
2229
|
+
const relayKey = creds.RELAY_API_KEY;
|
|
2230
|
+
|
|
2231
|
+
if (!relayUrl || !relayKey) {
|
|
2232
|
+
console.log(c.red('❌ Error: RELAY_API_URL or RELAY_API_KEY not configured\n'));
|
|
2233
|
+
process.exit(1);
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
try {
|
|
2237
|
+
const url = `${relayUrl}/api/messages/${encodeURIComponent(id)}/ack`;
|
|
2238
|
+
const res = await fetch(url, {
|
|
2239
|
+
method: 'POST',
|
|
2240
|
+
headers: {
|
|
2241
|
+
'Authorization': `Bearer ${relayKey}`,
|
|
2242
|
+
},
|
|
2243
|
+
});
|
|
2244
|
+
|
|
2245
|
+
if (!res.ok) {
|
|
2246
|
+
console.log(c.red(`❌ Error: Acknowledge failed with status ${res.status}\n`));
|
|
2247
|
+
process.exit(1);
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
console.log(c.green('✅ Message acknowledged\n'));
|
|
2251
|
+
} catch (error) {
|
|
2252
|
+
console.log(c.red('❌ Error: ' + error.message + '\n'));
|
|
2253
|
+
process.exit(1);
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
/**
|
|
2258
|
+
* Install hooks globally to ~/.claude/hooks/
|
|
2259
|
+
* This command copies hooks from the teleportation project to the global location
|
|
2260
|
+
* and merges settings into ~/.claude/settings.json
|
|
2261
|
+
*/
|
|
2262
|
+
async function commandInstallHooks() {
|
|
2263
|
+
console.log(c.purple('🔧 Installing Teleportation Hooks Globally\n'));
|
|
2264
|
+
|
|
2265
|
+
const globalHooksDir = path.join(HOME_DIR, '.claude', 'hooks');
|
|
2266
|
+
const globalSettings = path.join(HOME_DIR, '.claude', 'settings.json');
|
|
2267
|
+
const sourceHooksDir = path.join(TELEPORTATION_DIR, '.claude', 'hooks');
|
|
2268
|
+
|
|
2269
|
+
// List of hooks to install
|
|
2270
|
+
const hooks = [
|
|
2271
|
+
'pre_tool_use.mjs',
|
|
2272
|
+
'post_tool_use.mjs',
|
|
2273
|
+
'permission_request.mjs',
|
|
2274
|
+
'stop.mjs',
|
|
2275
|
+
'session_start.mjs',
|
|
2276
|
+
'session_end.mjs',
|
|
2277
|
+
'notification.mjs',
|
|
2278
|
+
'user_prompt_submit.mjs',
|
|
2279
|
+
'config-loader.mjs',
|
|
2280
|
+
'session-register.mjs',
|
|
2281
|
+
'heartbeat.mjs' // Spawned by session-register.mjs, needs to be in hooks directory
|
|
2282
|
+
];
|
|
2283
|
+
|
|
2284
|
+
// Step 1: Ensure directories exist
|
|
2285
|
+
console.log(c.yellow('Step 1: Creating directories...\n'));
|
|
2286
|
+
try {
|
|
2287
|
+
if (!fs.existsSync(path.join(HOME_DIR, '.claude'))) {
|
|
2288
|
+
fs.mkdirSync(path.join(HOME_DIR, '.claude'), { recursive: true });
|
|
2289
|
+
}
|
|
2290
|
+
if (!fs.existsSync(globalHooksDir)) {
|
|
2291
|
+
fs.mkdirSync(globalHooksDir, { recursive: true });
|
|
2292
|
+
}
|
|
2293
|
+
console.log(c.green(' ✅ ~/.claude/hooks/ directory ready\n'));
|
|
2294
|
+
} catch (e) {
|
|
2295
|
+
console.log(c.red(` ❌ Failed to create directories: ${e.message}\n`));
|
|
2296
|
+
process.exit(1);
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
// Step 2: Copy hook files
|
|
2300
|
+
console.log(c.yellow('Step 2: Copying hooks...\n'));
|
|
2301
|
+
let installed = 0;
|
|
2302
|
+
let failed = 0;
|
|
2303
|
+
|
|
2304
|
+
for (const hook of hooks) {
|
|
2305
|
+
const src = path.join(sourceHooksDir, hook);
|
|
2306
|
+
const dest = path.join(globalHooksDir, hook);
|
|
2307
|
+
|
|
2308
|
+
try {
|
|
2309
|
+
if (!fs.existsSync(src)) {
|
|
2310
|
+
console.log(c.yellow(` ⚠️ ${hook} not found in source, skipping`));
|
|
2311
|
+
continue;
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
fs.copyFileSync(src, dest);
|
|
2315
|
+
fs.chmodSync(dest, 0o755); // Make executable
|
|
2316
|
+
console.log(c.green(` ✅ ${hook}`));
|
|
2317
|
+
installed++;
|
|
2318
|
+
} catch (e) {
|
|
2319
|
+
console.log(c.red(` ❌ ${hook}: ${e.message}`));
|
|
2320
|
+
failed++;
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
console.log(`\n Installed: ${c.green(installed)}, Failed: ${failed > 0 ? c.red(failed) : '0'}\n`);
|
|
2325
|
+
|
|
2326
|
+
// Step 2.5: Install lib files that hooks depend on (metadata.js, etc.)
|
|
2327
|
+
console.log(c.yellow('Step 2.5: Installing lib files...\n'));
|
|
2328
|
+
try {
|
|
2329
|
+
const installerPath = path.join(TELEPORTATION_DIR, 'lib', 'install', 'installer.js');
|
|
2330
|
+
if (fs.existsSync(installerPath)) {
|
|
2331
|
+
const installer = await import('file://' + installerPath);
|
|
2332
|
+
const libResult = await installer.installLibFiles();
|
|
2333
|
+
if (libResult.installed.length > 0) {
|
|
2334
|
+
libResult.installed.forEach(file => {
|
|
2335
|
+
console.log(c.green(` ✅ ${file}`));
|
|
2336
|
+
});
|
|
2337
|
+
}
|
|
2338
|
+
if (libResult.failed.length > 0) {
|
|
2339
|
+
libResult.failed.forEach(({ file, error }) => {
|
|
2340
|
+
console.log(c.yellow(` ⚠️ ${file}: ${error}`));
|
|
2341
|
+
});
|
|
2342
|
+
}
|
|
2343
|
+
console.log(`\n Lib files installed: ${c.green(libResult.installed.length)}, Failed: ${libResult.failed.length > 0 ? c.yellow(libResult.failed.length) : '0'}\n`);
|
|
2344
|
+
} else {
|
|
2345
|
+
console.log(c.yellow(' ⚠️ Installer module not found, skipping lib files\n'));
|
|
2346
|
+
}
|
|
2347
|
+
} catch (e) {
|
|
2348
|
+
console.log(c.yellow(` ⚠️ Failed to install lib files: ${e.message}\n`));
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
// Step 3: Update settings.json
|
|
2352
|
+
console.log(c.yellow('Step 3: Updating Claude Code settings...\n'));
|
|
2353
|
+
|
|
2354
|
+
// Safely quote paths - JSON.stringify escapes special chars to prevent command injection
|
|
2355
|
+
const quotePath = (p) => JSON.stringify(p);
|
|
2356
|
+
|
|
2357
|
+
const hooksConfig = {
|
|
2358
|
+
PreToolUse: [{
|
|
2359
|
+
matcher: ".*",
|
|
2360
|
+
hooks: [{
|
|
2361
|
+
type: "command",
|
|
2362
|
+
command: `node ${quotePath(path.join(globalHooksDir, 'pre_tool_use.mjs'))}`
|
|
2363
|
+
}]
|
|
2364
|
+
}],
|
|
2365
|
+
PostToolUse: [{
|
|
2366
|
+
matcher: ".*",
|
|
2367
|
+
hooks: [{
|
|
2368
|
+
type: "command",
|
|
2369
|
+
command: `node ${quotePath(path.join(globalHooksDir, 'post_tool_use.mjs'))}`
|
|
2370
|
+
}]
|
|
2371
|
+
}],
|
|
2372
|
+
PermissionRequest: [{
|
|
2373
|
+
matcher: ".*",
|
|
2374
|
+
hooks: [{
|
|
2375
|
+
type: "command",
|
|
2376
|
+
command: `node ${quotePath(path.join(globalHooksDir, 'permission_request.mjs'))}`
|
|
2377
|
+
}]
|
|
2378
|
+
}],
|
|
2379
|
+
Stop: [{
|
|
2380
|
+
matcher: ".*",
|
|
2381
|
+
hooks: [{
|
|
2382
|
+
type: "command",
|
|
2383
|
+
command: `node ${quotePath(path.join(globalHooksDir, 'stop.mjs'))}`
|
|
2384
|
+
}]
|
|
2385
|
+
}],
|
|
2386
|
+
SessionStart: [{
|
|
2387
|
+
matcher: ".*",
|
|
2388
|
+
hooks: [{
|
|
2389
|
+
type: "command",
|
|
2390
|
+
command: `node ${quotePath(path.join(globalHooksDir, 'session_start.mjs'))}`
|
|
2391
|
+
}]
|
|
2392
|
+
}],
|
|
2393
|
+
SessionEnd: [{
|
|
2394
|
+
matcher: ".*",
|
|
2395
|
+
hooks: [{
|
|
2396
|
+
type: "command",
|
|
2397
|
+
command: `node ${quotePath(path.join(globalHooksDir, 'session_end.mjs'))}`
|
|
2398
|
+
}]
|
|
2399
|
+
}],
|
|
2400
|
+
Notification: [{
|
|
2401
|
+
matcher: ".*",
|
|
2402
|
+
hooks: [{
|
|
2403
|
+
type: "command",
|
|
2404
|
+
command: `node ${quotePath(path.join(globalHooksDir, 'notification.mjs'))}`
|
|
2405
|
+
}]
|
|
2406
|
+
}],
|
|
2407
|
+
UserPromptSubmit: [{
|
|
2408
|
+
matcher: ".*",
|
|
2409
|
+
hooks: [{
|
|
2410
|
+
type: "command",
|
|
2411
|
+
command: `node ${quotePath(path.join(globalHooksDir, 'user_prompt_submit.mjs'))}`
|
|
2412
|
+
}]
|
|
2413
|
+
}]
|
|
2414
|
+
};
|
|
2415
|
+
|
|
2416
|
+
try {
|
|
2417
|
+
let existingSettings = {};
|
|
2418
|
+
|
|
2419
|
+
// Load existing settings if present
|
|
2420
|
+
if (fs.existsSync(globalSettings)) {
|
|
2421
|
+
try {
|
|
2422
|
+
const content = fs.readFileSync(globalSettings, 'utf8');
|
|
2423
|
+
existingSettings = JSON.parse(content);
|
|
2424
|
+
console.log(c.cyan(' Found existing settings, merging...\n'));
|
|
2425
|
+
} catch (e) {
|
|
2426
|
+
console.log(c.yellow(` ⚠️ Could not parse existing settings, creating new...\n`));
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
// Merge hooks intelligently - preserve user hooks, avoid duplicates
|
|
2431
|
+
const mergeHookArrays = (existing, incoming) => {
|
|
2432
|
+
if (!existing || !Array.isArray(existing)) return incoming;
|
|
2433
|
+
if (!incoming || !Array.isArray(incoming)) return existing;
|
|
2434
|
+
|
|
2435
|
+
// Get commands from incoming hooks to check for duplicates
|
|
2436
|
+
const incomingCommands = new Set(
|
|
2437
|
+
incoming.flatMap(h => (h.hooks || []).map(hh => hh.command))
|
|
2438
|
+
);
|
|
2439
|
+
|
|
2440
|
+
// Filter out existing hooks that have the same command (will be replaced)
|
|
2441
|
+
const filteredExisting = existing.filter(h =>
|
|
2442
|
+
!(h.hooks || []).some(hh => incomingCommands.has(hh.command))
|
|
2443
|
+
);
|
|
2444
|
+
|
|
2445
|
+
// Combine: existing (non-duplicate) + incoming (teleportation hooks)
|
|
2446
|
+
return [...filteredExisting, ...incoming];
|
|
2447
|
+
};
|
|
2448
|
+
|
|
2449
|
+
// Merge all hook types with warnings about user hooks
|
|
2450
|
+
const mergedHooks = { ...(existingSettings.hooks || {}) };
|
|
2451
|
+
let hasUserHooks = false;
|
|
2452
|
+
|
|
2453
|
+
for (const [hookType, hookConfig] of Object.entries(hooksConfig)) {
|
|
2454
|
+
const existingHooksForType = existingSettings.hooks?.[hookType] || [];
|
|
2455
|
+
|
|
2456
|
+
// Find user-defined hooks (not from teleportation)
|
|
2457
|
+
const userHooks = existingHooksForType.filter(h => {
|
|
2458
|
+
const commands = (h.hooks || []).map(hh => hh.command || '');
|
|
2459
|
+
return !commands.some(cmd =>
|
|
2460
|
+
cmd.includes('teleportation') ||
|
|
2461
|
+
cmd.includes('.claude/hooks') ||
|
|
2462
|
+
cmd.includes('~/.claude/hooks')
|
|
2463
|
+
);
|
|
2464
|
+
});
|
|
2465
|
+
|
|
2466
|
+
if (userHooks.length > 0) {
|
|
2467
|
+
hasUserHooks = true;
|
|
2468
|
+
console.log(c.yellow(` ⚠️ Preserving ${userHooks.length} custom ${hookType} hook(s):`));
|
|
2469
|
+
userHooks.forEach(h => {
|
|
2470
|
+
const cmds = (h.hooks || []).map(hh => hh.command || 'unknown');
|
|
2471
|
+
cmds.forEach(cmd => console.log(c.dim(` • ${cmd}`)));
|
|
2472
|
+
});
|
|
2473
|
+
console.log('');
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
mergedHooks[hookType] = mergeHookArrays(existingHooksForType, hookConfig);
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
if (hasUserHooks) {
|
|
2480
|
+
console.log(c.cyan(' Your custom hooks will continue to work alongside Teleportation hooks.\n'));
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
const mergedSettings = {
|
|
2484
|
+
...existingSettings,
|
|
2485
|
+
hooks: mergedHooks
|
|
2486
|
+
};
|
|
2487
|
+
|
|
2488
|
+
// Write settings
|
|
2489
|
+
fs.writeFileSync(globalSettings, JSON.stringify(mergedSettings, null, 2));
|
|
2490
|
+
console.log(c.green(' ✅ ~/.claude/settings.json updated\n'));
|
|
2491
|
+
} catch (e) {
|
|
2492
|
+
console.log(c.red(` ❌ Failed to update settings: ${e.message}\n`));
|
|
2493
|
+
process.exit(1);
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
// Summary
|
|
2497
|
+
console.log(c.cyan('╭─────────────────────────────────────────────────────╮'));
|
|
2498
|
+
console.log(c.cyan('│ │'));
|
|
2499
|
+
console.log(c.cyan('│ 🎉 ') + c.green('Hooks Installed Successfully!') + c.cyan(' │'));
|
|
2500
|
+
console.log(c.cyan('│ │'));
|
|
2501
|
+
console.log(c.cyan('│ Hooks location: ~/.claude/hooks/ │'));
|
|
2502
|
+
console.log(c.cyan('│ Settings: ~/.claude/settings.json │'));
|
|
2503
|
+
console.log(c.cyan('│ │'));
|
|
2504
|
+
console.log(c.cyan('│ ') + c.yellow('⚠️ Restart Claude Code') + c.cyan(' to activate hooks. │'));
|
|
2505
|
+
console.log(c.cyan('│ │'));
|
|
2506
|
+
console.log(c.cyan('╰─────────────────────────────────────────────────────╯\n'));
|
|
2507
|
+
|
|
2508
|
+
console.log(c.cyan('Next steps:'));
|
|
2509
|
+
console.log(' 1. If not logged in: teleportation login');
|
|
2510
|
+
console.log(' 2. Check status: teleportation status');
|
|
2511
|
+
console.log(' 3. Restart Claude Code to apply hooks\n');
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
async function commandCommand() {
|
|
2515
|
+
const sessionId = process.env.TELEPORTATION_SESSION_ID;
|
|
2516
|
+
if (!sessionId) {
|
|
2517
|
+
console.log(c.red('❌ Error: TELEPORTATION_SESSION_ID not set\n'));
|
|
2518
|
+
console.log(c.cyan('Set the environment variable: export TELEPORTATION_SESSION_ID=<session-id>\n'));
|
|
2519
|
+
process.exit(1);
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
const text = process.argv.slice(3).join(' ');
|
|
2523
|
+
if (!text) {
|
|
2524
|
+
console.log(c.red('❌ Error: Command text is required\n'));
|
|
2525
|
+
console.log(c.cyan('Usage: teleportation command "<text>"\n'));
|
|
2526
|
+
process.exit(1);
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
const creds = await getCredentials();
|
|
2530
|
+
const relayUrl = creds.RELAY_API_URL;
|
|
2531
|
+
const relayKey = creds.RELAY_API_KEY;
|
|
2532
|
+
|
|
2533
|
+
if (!relayUrl || !relayKey) {
|
|
2534
|
+
console.log(c.red('❌ Error: RELAY_API_URL or RELAY_API_KEY not configured\n'));
|
|
2535
|
+
process.exit(1);
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
try {
|
|
2539
|
+
const res = await fetch(`${relayUrl}/api/messages`, {
|
|
2540
|
+
method: 'POST',
|
|
2541
|
+
headers: {
|
|
2542
|
+
'Content-Type': 'application/json',
|
|
2543
|
+
'Authorization': `Bearer ${relayKey}`,
|
|
2544
|
+
},
|
|
2545
|
+
body: JSON.stringify({
|
|
2546
|
+
session_id: sessionId,
|
|
2547
|
+
text,
|
|
2548
|
+
meta: {
|
|
2549
|
+
type: 'command',
|
|
2550
|
+
from: 'user',
|
|
2551
|
+
source: 'teleportation-cli',
|
|
2552
|
+
target_agent_id: 'daemon',
|
|
2553
|
+
},
|
|
2554
|
+
}),
|
|
2555
|
+
});
|
|
2556
|
+
|
|
2557
|
+
if (!res.ok) {
|
|
2558
|
+
console.log(c.red(`❌ Error: Failed to enqueue command (status ${res.status})\n`));
|
|
2559
|
+
process.exit(1);
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
const data = await res.json();
|
|
2563
|
+
console.log(c.green('✅ Command enqueued successfully\n'));
|
|
2564
|
+
if (data.id) {
|
|
2565
|
+
console.log(c.cyan('Message ID: ') + data.id + '\n');
|
|
2566
|
+
}
|
|
2567
|
+
} catch (error) {
|
|
2568
|
+
console.log(c.red('❌ Error: ' + error.message + '\n'));
|
|
2569
|
+
process.exit(1);
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
/**
|
|
2574
|
+
* Update command - pulls latest code and reinstalls hooks
|
|
2575
|
+
*/
|
|
2576
|
+
async function commandUpdate() {
|
|
2577
|
+
console.log(c.cyan('\n⚡ Teleportation Update\n'));
|
|
2578
|
+
|
|
2579
|
+
// Validate HOME_DIR exists
|
|
2580
|
+
if (!HOME_DIR || !fs.existsSync(HOME_DIR)) {
|
|
2581
|
+
console.log(c.red('❌ Error: HOME directory not found\n'));
|
|
2582
|
+
console.log(c.yellow('Set HOME environment variable and try again.\n'));
|
|
2583
|
+
return;
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
const installDir = path.join(HOME_DIR, '.teleportation-cli');
|
|
2587
|
+
|
|
2588
|
+
// Check if installed via git
|
|
2589
|
+
if (!fs.existsSync(path.join(installDir, '.git'))) {
|
|
2590
|
+
console.log(c.yellow('⚠️ Not installed via git. Reinstall with:\n'));
|
|
2591
|
+
console.log(c.green(' curl -fsSL https://get.teleportation.dev | bash\n'));
|
|
2592
|
+
return;
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
console.log(c.yellow('Step 1: Checking for local changes...\n'));
|
|
2596
|
+
|
|
2597
|
+
const { execSync, execFile } = require('child_process');
|
|
2598
|
+
let stashedChanges = false;
|
|
2599
|
+
let stashHash = null;
|
|
2600
|
+
|
|
2601
|
+
// Helper to check if command exists (POSIX compliant)
|
|
2602
|
+
// Whitelist prevents command injection if cmd parameter is compromised
|
|
2603
|
+
const commandExists = (cmd) => {
|
|
2604
|
+
// Whitelist of allowed commands to prevent command injection
|
|
2605
|
+
const validCommands = ['bun', 'npm', 'git', 'node'];
|
|
2606
|
+
if (!validCommands.includes(cmd)) {
|
|
2607
|
+
return false;
|
|
2608
|
+
}
|
|
2609
|
+
try {
|
|
2610
|
+
execSync(`command -v ${cmd}`, { stdio: 'pipe' });
|
|
2611
|
+
return true;
|
|
2612
|
+
} catch {
|
|
2613
|
+
return false;
|
|
2614
|
+
}
|
|
2615
|
+
};
|
|
2616
|
+
|
|
2617
|
+
// Helper to restore stashed changes using specific stash hash
|
|
2618
|
+
const restoreStash = () => {
|
|
2619
|
+
if (stashedChanges && stashHash !== null) {
|
|
2620
|
+
try {
|
|
2621
|
+
console.log(c.yellow(' Restoring stashed changes...\n'));
|
|
2622
|
+
// Use execFile with args array for safety
|
|
2623
|
+
execFile('git', ['stash', 'pop', `stash@{${stashHash}}`], {
|
|
2624
|
+
cwd: installDir,
|
|
2625
|
+
stdio: 'pipe'
|
|
2626
|
+
});
|
|
2627
|
+
console.log(c.green(' ✅ Stashed changes restored\n'));
|
|
2628
|
+
} catch (e) {
|
|
2629
|
+
console.log(c.yellow(` ⚠️ Could not restore stash: ${e.message}\n`));
|
|
2630
|
+
console.log(c.yellow(` Run \`git stash pop stash@{${stashHash}}\` manually to restore.\n`));
|
|
2631
|
+
}
|
|
2632
|
+
} else if (stashedChanges) {
|
|
2633
|
+
// Fallback if stash hash wasn't captured
|
|
2634
|
+
console.log(c.yellow(' ⚠️ Stash hash not available. Run `git stash pop` manually.\n'));
|
|
2635
|
+
}
|
|
2636
|
+
};
|
|
2637
|
+
|
|
2638
|
+
// Get current version before update
|
|
2639
|
+
let versionFrom = 'unknown';
|
|
2640
|
+
try {
|
|
2641
|
+
const pkgBefore = JSON.parse(fs.readFileSync(path.join(installDir, 'package.json'), 'utf8'));
|
|
2642
|
+
versionFrom = pkgBefore.version || 'unknown';
|
|
2643
|
+
} catch (e) {
|
|
2644
|
+
// Ignore - will show unknown
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
// Check for uncommitted changes (including untracked files)
|
|
2648
|
+
try {
|
|
2649
|
+
const status = execSync('git status --porcelain', { cwd: installDir, encoding: 'utf8' });
|
|
2650
|
+
if (status.trim()) {
|
|
2651
|
+
console.log(c.yellow(' ⚠️ Uncommitted changes detected. Stashing...\n'));
|
|
2652
|
+
try {
|
|
2653
|
+
// Use -u to include untracked files, capture stash hash from output
|
|
2654
|
+
const stashOutput = execSync('git stash push -u -m "teleportation-update-stash"', {
|
|
2655
|
+
cwd: installDir,
|
|
2656
|
+
encoding: 'utf8',
|
|
2657
|
+
stdio: 'pipe'
|
|
2658
|
+
});
|
|
2659
|
+
|
|
2660
|
+
// Extract stash hash from output (e.g., "Saved working directory and index state On main: stash@{0}")
|
|
2661
|
+
const stashMatch = stashOutput.match(/stash@\{(\d+)\}/);
|
|
2662
|
+
if (stashMatch) {
|
|
2663
|
+
stashHash = stashMatch[1];
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
console.log(c.green(` ✅ Changes stashed (will restore on failure)\n`));
|
|
2667
|
+
stashedChanges = true;
|
|
2668
|
+
} catch (stashErr) {
|
|
2669
|
+
console.log(c.red(` ❌ Failed to stash: ${stashErr.message}`));
|
|
2670
|
+
console.log(c.yellow(' Please commit or discard changes manually.\n'));
|
|
2671
|
+
return;
|
|
2672
|
+
}
|
|
2673
|
+
} else {
|
|
2674
|
+
console.log(c.green(' ✅ Working directory clean\n'));
|
|
2675
|
+
}
|
|
2676
|
+
} catch (e) {
|
|
2677
|
+
console.log(c.yellow(` ⚠️ Could not check git status: ${e.message}\n`));
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
console.log(c.yellow('Step 2: Pulling latest changes...\n'));
|
|
2681
|
+
|
|
2682
|
+
try {
|
|
2683
|
+
// Detect current branch dynamically
|
|
2684
|
+
const branchRaw = execSync('git rev-parse --abbrev-ref HEAD', { cwd: installDir, encoding: 'utf8' }).trim();
|
|
2685
|
+
|
|
2686
|
+
// Sanitize branch name - only allow alphanumeric, dots, underscores, slashes, hyphens
|
|
2687
|
+
// Also enforce max length for safety
|
|
2688
|
+
if (!/^[a-zA-Z0-9._\/-]{1,100}$/.test(branchRaw)) {
|
|
2689
|
+
console.log(c.red(` ❌ Invalid branch name detected: ${branchRaw}\n`));
|
|
2690
|
+
console.log(c.yellow(' Branch names must contain only: letters, numbers, dots, underscores, slashes, hyphens (max 100 chars)\n'));
|
|
2691
|
+
restoreStash();
|
|
2692
|
+
return;
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2695
|
+
const branch = branchRaw;
|
|
2696
|
+
console.log(c.dim(` Branch: ${branch}\n`));
|
|
2697
|
+
|
|
2698
|
+
// Use execFile with args array to prevent command injection
|
|
2699
|
+
execFile('git', ['pull', 'origin', branch], {
|
|
2700
|
+
cwd: installDir,
|
|
2701
|
+
stdio: 'inherit'
|
|
2702
|
+
});
|
|
2703
|
+
console.log(c.green('\n ✅ Code updated\n'));
|
|
2704
|
+
} catch (e) {
|
|
2705
|
+
console.log(c.red(` ❌ Failed to pull: ${e.message}\n`));
|
|
2706
|
+
restoreStash();
|
|
2707
|
+
return;
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
console.log(c.yellow('Step 3: Installing dependencies...\n'));
|
|
2711
|
+
|
|
2712
|
+
let depsInstalled = false;
|
|
2713
|
+
try {
|
|
2714
|
+
// Check which package manager is available
|
|
2715
|
+
if (commandExists('bun')) {
|
|
2716
|
+
// Don't use --silent so errors are visible
|
|
2717
|
+
execSync('bun install', { cwd: installDir, stdio: 'inherit' });
|
|
2718
|
+
console.log(c.green('\n ✅ Dependencies updated (bun)\n'));
|
|
2719
|
+
depsInstalled = true;
|
|
2720
|
+
} else if (commandExists('npm')) {
|
|
2721
|
+
// Don't use --silent so errors are visible
|
|
2722
|
+
execSync('npm install', { cwd: installDir, stdio: 'inherit' });
|
|
2723
|
+
console.log(c.green('\n ✅ Dependencies updated (npm)\n'));
|
|
2724
|
+
depsInstalled = true;
|
|
2725
|
+
} else {
|
|
2726
|
+
console.log(c.red(' ❌ No package manager found (bun or npm required)\n'));
|
|
2727
|
+
console.log(c.yellow(' Install Node.js or Bun and run: teleportation update\n'));
|
|
2728
|
+
}
|
|
2729
|
+
} catch (e) {
|
|
2730
|
+
console.log(c.red(`\n ❌ Dependency installation failed: ${e.message}\n`));
|
|
2731
|
+
console.log(c.yellow(' Hooks may not work correctly. Run manually:\n'));
|
|
2732
|
+
console.log(c.dim(` cd ${installDir} && npm install\n`));
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2735
|
+
console.log(c.yellow('Step 4: Updating hooks...\n'));
|
|
2736
|
+
|
|
2737
|
+
// Run install-hooks with error handling
|
|
2738
|
+
let hooksInstalled = false;
|
|
2739
|
+
try {
|
|
2740
|
+
await commandInstallHooks();
|
|
2741
|
+
hooksInstalled = true;
|
|
2742
|
+
} catch (e) {
|
|
2743
|
+
console.log(c.red(` ❌ Hook installation failed: ${e.message}\n`));
|
|
2744
|
+
console.log(c.yellow(' Run `teleportation install-hooks` manually to retry.\n'));
|
|
2745
|
+
// Don't restore stash here - code was updated successfully, hooks can be fixed separately
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2748
|
+
// Get version after update
|
|
2749
|
+
let versionTo = 'unknown';
|
|
2750
|
+
try {
|
|
2751
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(installDir, 'package.json'), 'utf8'));
|
|
2752
|
+
versionTo = pkg.version || 'unknown';
|
|
2753
|
+
} catch (e) {
|
|
2754
|
+
console.log(c.yellow(` ⚠️ Could not read version: ${e.message}\n`));
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
// Calculate dynamic padding (box width is 55 chars, need space for labels and borders)
|
|
2758
|
+
const formatVersionLine = (label, version) => {
|
|
2759
|
+
const maxVersionLen = 30; // Reasonable max for version strings
|
|
2760
|
+
const versionDisplay = version.length > maxVersionLen ? version.slice(0, maxVersionLen - 3) + '...' : version;
|
|
2761
|
+
const padding = 55 - label.length - versionDisplay.length - 4; // 4 for borders and spaces
|
|
2762
|
+
return `│ ${label}${' '.repeat(Math.max(1, padding))}${versionDisplay}│`;
|
|
2763
|
+
};
|
|
2764
|
+
|
|
2765
|
+
// Show appropriate completion message
|
|
2766
|
+
console.log('');
|
|
2767
|
+
if (hooksInstalled && depsInstalled) {
|
|
2768
|
+
console.log(c.green('╭─────────────────────────────────────────────────────╮'));
|
|
2769
|
+
console.log(c.green('│ ✅ Update complete! │'));
|
|
2770
|
+
console.log(c.green('│ │'));
|
|
2771
|
+
if (versionFrom !== 'unknown' || versionTo !== 'unknown') {
|
|
2772
|
+
console.log(c.green(formatVersionLine('From:', versionFrom)));
|
|
2773
|
+
console.log(c.green(formatVersionLine('To: ', versionTo)));
|
|
2774
|
+
} else {
|
|
2775
|
+
console.log(c.green(formatVersionLine('Version:', versionTo)));
|
|
2776
|
+
}
|
|
2777
|
+
console.log(c.green('│ │'));
|
|
2778
|
+
console.log(c.green('│ ') + c.yellow('⚠️ Restart Claude Code to apply changes.') + c.green(' │'));
|
|
2779
|
+
console.log(c.green('╰─────────────────────────────────────────────────────╯'));
|
|
2780
|
+
} else if (hooksInstalled) {
|
|
2781
|
+
console.log(c.yellow('╭─────────────────────────────────────────────────────╮'));
|
|
2782
|
+
console.log(c.yellow('│ ⚠️ Update partially complete │'));
|
|
2783
|
+
console.log(c.yellow('│ │'));
|
|
2784
|
+
if (versionFrom !== 'unknown' || versionTo !== 'unknown') {
|
|
2785
|
+
console.log(c.yellow(formatVersionLine('From:', versionFrom)));
|
|
2786
|
+
console.log(c.yellow(formatVersionLine('To: ', versionTo)));
|
|
2787
|
+
} else {
|
|
2788
|
+
console.log(c.yellow(formatVersionLine('Version:', versionTo)));
|
|
2789
|
+
}
|
|
2790
|
+
console.log(c.yellow('│ Hooks: ✅ Dependencies: ❌ │'));
|
|
2791
|
+
console.log(c.yellow('│ │'));
|
|
2792
|
+
console.log(c.yellow('│ Run: cd ~/.teleportation-cli && npm install │'));
|
|
2793
|
+
console.log(c.yellow('╰─────────────────────────────────────────────────────╯'));
|
|
2794
|
+
} else {
|
|
2795
|
+
console.log(c.red('╭─────────────────────────────────────────────────────╮'));
|
|
2796
|
+
console.log(c.red('│ ❌ Update failed │'));
|
|
2797
|
+
console.log(c.red('│ │'));
|
|
2798
|
+
console.log(c.red('│ Code was updated but hooks failed to install. │'));
|
|
2799
|
+
console.log(c.red('│ Run: teleportation install-hooks │'));
|
|
2800
|
+
console.log(c.red('╰─────────────────────────────────────────────────────╯'));
|
|
2801
|
+
}
|
|
2802
|
+
console.log('');
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
// Main
|
|
2806
|
+
const command = process.argv[2] || 'help';
|
|
2807
|
+
const args = process.argv.slice(3);
|
|
2808
|
+
|
|
2809
|
+
// Handle async commands that need to complete before exit
|
|
2810
|
+
const asyncCommands = ['login', 'logout', 'status', 'test', 'env', 'config', 'daemon', 'away', 'back', 'daemon-status', 'command', 'inbox', 'inbox-ack', 'install-hooks', 'update'];
|
|
2811
|
+
if (asyncCommands.includes(command)) {
|
|
2812
|
+
// These commands handle their own async execution
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
try {
|
|
2816
|
+
switch (command) {
|
|
2817
|
+
case 'setup':
|
|
2818
|
+
commandSetup().catch(err => {
|
|
2819
|
+
console.error(c.red('❌ Error:'), err.message);
|
|
2820
|
+
process.exit(1);
|
|
2821
|
+
});
|
|
2822
|
+
break;
|
|
2823
|
+
case 'backup':
|
|
2824
|
+
commandBackup(args).catch(err => {
|
|
2825
|
+
console.error(c.red('❌ Error:'), err.message);
|
|
2826
|
+
process.exit(1);
|
|
2827
|
+
});
|
|
2828
|
+
break;
|
|
2829
|
+
case 'on':
|
|
2830
|
+
commandOn().catch(err => {
|
|
2831
|
+
console.error(c.red('❌ Error:'), err.message);
|
|
2832
|
+
process.exit(1);
|
|
2833
|
+
});
|
|
2834
|
+
break;
|
|
2835
|
+
case 'install-hooks':
|
|
2836
|
+
commandInstallHooks().catch(err => {
|
|
2837
|
+
console.error(c.red('❌ Error:'), err.message);
|
|
2838
|
+
process.exit(1);
|
|
2839
|
+
});
|
|
2840
|
+
break;
|
|
2841
|
+
case 'off':
|
|
2842
|
+
commandOff();
|
|
2843
|
+
break;
|
|
2844
|
+
case 'status':
|
|
2845
|
+
commandStatus().catch(err => {
|
|
2846
|
+
console.error(c.red('❌ Error:'), err.message);
|
|
2847
|
+
process.exit(1);
|
|
2848
|
+
});
|
|
2849
|
+
break;
|
|
2850
|
+
case 'start':
|
|
2851
|
+
commandStart();
|
|
2852
|
+
break;
|
|
2853
|
+
case 'stop':
|
|
2854
|
+
commandStop();
|
|
2855
|
+
break;
|
|
2856
|
+
case 'restart':
|
|
2857
|
+
commandRestart();
|
|
2858
|
+
break;
|
|
2859
|
+
case 'test':
|
|
2860
|
+
commandTest().catch(err => {
|
|
2861
|
+
console.error(c.red('❌ Error:'), err.message);
|
|
2862
|
+
process.exit(1);
|
|
2863
|
+
});
|
|
2864
|
+
break;
|
|
2865
|
+
case 'doctor':
|
|
2866
|
+
commandDoctor().catch(err => {
|
|
2867
|
+
console.error(c.red('❌ Error:'), err.message);
|
|
2868
|
+
process.exit(1);
|
|
2869
|
+
});
|
|
2870
|
+
break;
|
|
2871
|
+
case 'uninstall':
|
|
2872
|
+
commandUninstall().catch(err => {
|
|
2873
|
+
console.error(c.red('❌ Error:'), err.message);
|
|
2874
|
+
process.exit(1);
|
|
2875
|
+
});
|
|
2876
|
+
break;
|
|
2877
|
+
case 'env':
|
|
2878
|
+
commandEnv(args).catch(err => {
|
|
2879
|
+
console.error(c.red('❌ Error:'), err.message);
|
|
2880
|
+
process.exit(1);
|
|
2881
|
+
});
|
|
2882
|
+
break;
|
|
2883
|
+
case 'config':
|
|
2884
|
+
commandConfig(args).catch(err => {
|
|
2885
|
+
console.error(c.red('❌ Error:'), err.message);
|
|
2886
|
+
process.exit(1);
|
|
2887
|
+
});
|
|
2888
|
+
break;
|
|
2889
|
+
case 'info':
|
|
2890
|
+
commandInfo();
|
|
2891
|
+
break;
|
|
2892
|
+
case 'logs':
|
|
2893
|
+
commandLogs(args);
|
|
2894
|
+
break;
|
|
2895
|
+
case 'login':
|
|
2896
|
+
commandLogin(args).catch(err => {
|
|
2897
|
+
console.error(c.red('❌ Error:'), err.message);
|
|
2898
|
+
process.exit(1);
|
|
2899
|
+
});
|
|
2900
|
+
break;
|
|
2901
|
+
case 'daemon':
|
|
2902
|
+
commandDaemon(args).catch(err => {
|
|
2903
|
+
console.error(c.red('❌ Error:'), err.message);
|
|
2904
|
+
process.exit(1);
|
|
2905
|
+
});
|
|
2906
|
+
break;
|
|
2907
|
+
case 'away':
|
|
2908
|
+
commandAwayMode().catch(err => {
|
|
2909
|
+
console.error(c.red('❌ Error:'), err.message);
|
|
2910
|
+
process.exit(1);
|
|
2911
|
+
});
|
|
2912
|
+
break;
|
|
2913
|
+
case 'back':
|
|
2914
|
+
commandBackMode().catch(err => {
|
|
2915
|
+
console.error(c.red('❌ Error:'), err.message);
|
|
2916
|
+
process.exit(1);
|
|
2917
|
+
});
|
|
2918
|
+
break;
|
|
2919
|
+
case 'daemon-status':
|
|
2920
|
+
commandDaemonStatusDisplay().catch(err => {
|
|
2921
|
+
console.error(c.red('❌ Error:'), err.message);
|
|
2922
|
+
process.exit(1);
|
|
2923
|
+
});
|
|
2924
|
+
break;
|
|
2925
|
+
case 'command':
|
|
2926
|
+
commandCommand().catch(err => {
|
|
2927
|
+
console.error(c.red('❌ Error:'), err.message);
|
|
2928
|
+
process.exit(1);
|
|
2929
|
+
});
|
|
2930
|
+
break;
|
|
2931
|
+
case 'inbox':
|
|
2932
|
+
commandInbox().catch(err => {
|
|
2933
|
+
console.error(c.red('❌ Error:'), err.message);
|
|
2934
|
+
process.exit(1);
|
|
2935
|
+
});
|
|
2936
|
+
break;
|
|
2937
|
+
case 'inbox-ack':
|
|
2938
|
+
commandInboxAck(args[0]).catch(err => {
|
|
2939
|
+
console.error(c.red('❌ Error:'), err.message);
|
|
2940
|
+
process.exit(1);
|
|
2941
|
+
});
|
|
2942
|
+
break;
|
|
2943
|
+
case 'logout':
|
|
2944
|
+
commandLogout().catch(err => {
|
|
2945
|
+
console.error(c.red('❌ Error:'), err.message);
|
|
2946
|
+
process.exit(1);
|
|
2947
|
+
});
|
|
2948
|
+
break;
|
|
2949
|
+
case 'worktree':
|
|
2950
|
+
commandWorktree(args).catch(err => {
|
|
2951
|
+
console.error(c.red('❌ Error:'), err.message);
|
|
2952
|
+
process.exit(1);
|
|
2953
|
+
});
|
|
2954
|
+
break;
|
|
2955
|
+
case 'snapshot':
|
|
2956
|
+
commandSnapshot(args).catch(err => {
|
|
2957
|
+
console.error(c.red('❌ Error:'), err.message);
|
|
2958
|
+
process.exit(1);
|
|
2959
|
+
});
|
|
2960
|
+
break;
|
|
2961
|
+
case 'session':
|
|
2962
|
+
commandSession(args).catch(err => {
|
|
2963
|
+
console.error(c.red('❌ Error:'), err.message);
|
|
2964
|
+
process.exit(1);
|
|
2965
|
+
});
|
|
2966
|
+
break;
|
|
2967
|
+
case 'version':
|
|
2968
|
+
case '--version':
|
|
2969
|
+
case '-v':
|
|
2970
|
+
commandVersion();
|
|
2971
|
+
break;
|
|
2972
|
+
case 'update':
|
|
2973
|
+
commandUpdate().catch(err => {
|
|
2974
|
+
console.error(c.red('❌ Error:'), err.message);
|
|
2975
|
+
process.exit(1);
|
|
2976
|
+
});
|
|
2977
|
+
break;
|
|
2978
|
+
case 'help':
|
|
2979
|
+
case '--help':
|
|
2980
|
+
case '-h':
|
|
2981
|
+
default:
|
|
2982
|
+
commandHelp();
|
|
2983
|
+
}
|
|
2984
|
+
} catch (error) {
|
|
2985
|
+
console.error(c.red('❌ Error:'), error.message);
|
|
2986
|
+
process.exit(1);
|
|
2987
|
+
}
|