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,555 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Installation module for Teleportation
|
|
4
|
+
* Handles setting up hooks and settings at the PROJECT level (not global)
|
|
5
|
+
*
|
|
6
|
+
* Project-level installation:
|
|
7
|
+
* - Hooks live in PROJECT/.claude/hooks/ (source files, not copied)
|
|
8
|
+
* - Settings live in PROJECT/.claude/settings.json (with absolute paths)
|
|
9
|
+
* - Daemon lives in ~/.teleportation/daemon/ (shared across projects)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { copyFile, mkdir, chmod, readFile, writeFile, stat, readdir } from 'fs/promises';
|
|
13
|
+
import { join, dirname, resolve } from 'path';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
15
|
+
import { homedir } from 'os';
|
|
16
|
+
import { execSync } from 'child_process';
|
|
17
|
+
|
|
18
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
+
const __dirname = dirname(__filename);
|
|
20
|
+
|
|
21
|
+
const HOME_DIR = homedir();
|
|
22
|
+
|
|
23
|
+
// Protocol/config version - increment when hooks behavior changes significantly
|
|
24
|
+
// This helps identify outdated installations that may not send all required metadata
|
|
25
|
+
export const TELEPORTATION_VERSION = '1.1.0';
|
|
26
|
+
export const TELEPORTATION_PROTOCOL_VERSION = 2;
|
|
27
|
+
|
|
28
|
+
// Runtime getters to respect environment variables set after module load
|
|
29
|
+
function getTeleportationDir() {
|
|
30
|
+
return process.env.TELEPORTATION_DIR || join(__dirname, '..', '..');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getProjectHooksDir() {
|
|
34
|
+
return process.env.TEST_HOOKS_DIR || join(getTeleportationDir(), '.claude', 'hooks');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getProjectSettings() {
|
|
38
|
+
return process.env.TEST_SETTINGS || join(getTeleportationDir(), '.claude', 'settings.json');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Claude Code reads settings from ~/.claude/settings.json (user-level)
|
|
42
|
+
function getUserSettings() {
|
|
43
|
+
return join(HOME_DIR, '.claude', 'settings.json');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Check if Node.js is installed and meets version requirements
|
|
48
|
+
*/
|
|
49
|
+
export function checkNodeVersion() {
|
|
50
|
+
const nodeVersion = process.version;
|
|
51
|
+
const major = parseInt(nodeVersion.slice(1).split('.')[0], 10);
|
|
52
|
+
|
|
53
|
+
if (major < 20) {
|
|
54
|
+
return {
|
|
55
|
+
valid: false,
|
|
56
|
+
error: `Node.js 20+ required. Found: ${nodeVersion}`
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { valid: true, version: nodeVersion };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Check if Claude Code is installed
|
|
65
|
+
*/
|
|
66
|
+
export function checkClaudeCode() {
|
|
67
|
+
try {
|
|
68
|
+
const claudePath = execSync('which claude', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
69
|
+
if (claudePath) {
|
|
70
|
+
return { valid: true, path: claudePath };
|
|
71
|
+
}
|
|
72
|
+
} catch (e) {
|
|
73
|
+
// Claude not found
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
valid: false,
|
|
78
|
+
error: 'Claude Code not found in PATH. Please install Claude Code first.'
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Ensure required directories exist
|
|
84
|
+
*/
|
|
85
|
+
export async function ensureDirectories() {
|
|
86
|
+
const projectDir = getTeleportationDir();
|
|
87
|
+
const hooksDir = getProjectHooksDir();
|
|
88
|
+
const settingsDir = dirname(getProjectSettings());
|
|
89
|
+
|
|
90
|
+
const dirs = [
|
|
91
|
+
settingsDir,
|
|
92
|
+
hooksDir,
|
|
93
|
+
join(HOME_DIR, '.teleportation'),
|
|
94
|
+
join(HOME_DIR, '.teleportation', 'daemon')
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
for (const dir of dirs) {
|
|
98
|
+
try {
|
|
99
|
+
await mkdir(dir, { recursive: true });
|
|
100
|
+
} catch (e) {
|
|
101
|
+
if (e.code !== 'EEXIST') {
|
|
102
|
+
throw new Error(`Failed to create directory ${dir}: ${e.message}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return dirs;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Verify hooks exist in project directory (no copying - they're source files)
|
|
112
|
+
*/
|
|
113
|
+
export async function verifyHooks(sourceHooksDir) {
|
|
114
|
+
const hooks = [
|
|
115
|
+
'pre_tool_use.mjs',
|
|
116
|
+
'permission_request.mjs', // Handles remote approvals when user is away
|
|
117
|
+
'post_tool_use.mjs', // Records tool executions to timeline
|
|
118
|
+
'session_start.mjs',
|
|
119
|
+
'session_end.mjs',
|
|
120
|
+
'stop.mjs',
|
|
121
|
+
'notification.mjs',
|
|
122
|
+
'user_prompt_submit.mjs', // Handles /model command detection
|
|
123
|
+
'config-loader.mjs',
|
|
124
|
+
'session-register.mjs',
|
|
125
|
+
'heartbeat.mjs' // Spawned by session-register.mjs, needs to be in hooks directory
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
const found = [];
|
|
129
|
+
const missing = [];
|
|
130
|
+
|
|
131
|
+
for (const hook of hooks) {
|
|
132
|
+
const hookPath = join(sourceHooksDir, hook);
|
|
133
|
+
try {
|
|
134
|
+
await stat(hookPath);
|
|
135
|
+
// Set executable permissions (755)
|
|
136
|
+
await chmod(hookPath, 0o755);
|
|
137
|
+
found.push(hook);
|
|
138
|
+
} catch (e) {
|
|
139
|
+
if (e.code === 'ENOENT') {
|
|
140
|
+
missing.push(hook);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { found, missing };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Install hooks (verify they exist and set permissions)
|
|
150
|
+
* Returns structure compatible with test expectations
|
|
151
|
+
*/
|
|
152
|
+
export async function installHooks(sourceHooksDir) {
|
|
153
|
+
const result = await verifyHooks(sourceHooksDir);
|
|
154
|
+
const destHooksDir = getProjectHooksDir();
|
|
155
|
+
const copyFailed = [];
|
|
156
|
+
|
|
157
|
+
// Copy hooks from source to destination if they're different
|
|
158
|
+
if (sourceHooksDir !== destHooksDir) {
|
|
159
|
+
for (const hook of result.found) {
|
|
160
|
+
const sourcePath = join(sourceHooksDir, hook);
|
|
161
|
+
const destPath = join(destHooksDir, hook);
|
|
162
|
+
try {
|
|
163
|
+
await copyFile(sourcePath, destPath);
|
|
164
|
+
await chmod(destPath, 0o755);
|
|
165
|
+
} catch (e) {
|
|
166
|
+
copyFailed.push({ file: hook, error: e.message });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
installed: result.found,
|
|
173
|
+
failed: result.missing.map(hook => ({ file: hook, error: 'File not found' })).concat(copyFailed)
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Copy daemon files to ~/.teleportation/daemon/
|
|
179
|
+
*/
|
|
180
|
+
export async function installDaemon() {
|
|
181
|
+
const sourceDaemonDir = join(getTeleportationDir(), 'lib', 'daemon');
|
|
182
|
+
const destDaemonDir = join(HOME_DIR, '.teleportation', 'daemon');
|
|
183
|
+
|
|
184
|
+
const daemonFiles = [
|
|
185
|
+
'teleportation-daemon.js',
|
|
186
|
+
'pid-manager.js',
|
|
187
|
+
'lifecycle.js'
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
const installed = [];
|
|
191
|
+
const failed = [];
|
|
192
|
+
|
|
193
|
+
for (const file of daemonFiles) {
|
|
194
|
+
const src = join(sourceDaemonDir, file);
|
|
195
|
+
const dest = join(destDaemonDir, file);
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
// Check if source exists
|
|
199
|
+
await stat(src);
|
|
200
|
+
|
|
201
|
+
// Copy file
|
|
202
|
+
await copyFile(src, dest);
|
|
203
|
+
|
|
204
|
+
// Set permissions (755 for daemon script, 644 for modules)
|
|
205
|
+
const perms = file === 'teleportation-daemon.js' ? 0o755 : 0o644;
|
|
206
|
+
await chmod(dest, perms);
|
|
207
|
+
|
|
208
|
+
installed.push(file);
|
|
209
|
+
} catch (e) {
|
|
210
|
+
if (e.code === 'ENOENT' && e.path === src) {
|
|
211
|
+
// Source file doesn't exist, skip
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
failed.push({ file, error: e.message });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return { installed, failed };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Copy daemon dependency modules to ~/.teleportation/
|
|
223
|
+
* This includes machine-coders and router
|
|
224
|
+
*/
|
|
225
|
+
export async function installDaemonModules() {
|
|
226
|
+
const sourceLibDir = join(getTeleportationDir(), 'lib');
|
|
227
|
+
const destDir = join(HOME_DIR, '.teleportation');
|
|
228
|
+
|
|
229
|
+
const modules = [
|
|
230
|
+
{
|
|
231
|
+
name: 'machine-coders',
|
|
232
|
+
files: [
|
|
233
|
+
'index.js',
|
|
234
|
+
'interface.js',
|
|
235
|
+
'claude-code-adapter.js',
|
|
236
|
+
'gemini-cli-adapter.js'
|
|
237
|
+
]
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
name: 'router',
|
|
241
|
+
files: [
|
|
242
|
+
'index.js',
|
|
243
|
+
'router.js',
|
|
244
|
+
'classifier.js',
|
|
245
|
+
'models.js',
|
|
246
|
+
'mech-llms-client.js'
|
|
247
|
+
]
|
|
248
|
+
}
|
|
249
|
+
];
|
|
250
|
+
|
|
251
|
+
const installed = [];
|
|
252
|
+
const failed = [];
|
|
253
|
+
|
|
254
|
+
for (const mod of modules) {
|
|
255
|
+
const srcDir = join(sourceLibDir, mod.name);
|
|
256
|
+
const targetDir = join(destDir, mod.name);
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
await mkdir(targetDir, { recursive: true });
|
|
260
|
+
} catch (e) {
|
|
261
|
+
if (e.code !== 'EEXIST') {
|
|
262
|
+
failed.push({ file: `${mod.name}/`, error: e.message });
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
for (const file of mod.files) {
|
|
268
|
+
const src = join(srcDir, file);
|
|
269
|
+
const dest = join(targetDir, file);
|
|
270
|
+
const displayName = `${mod.name}/${file}`;
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
await stat(src);
|
|
274
|
+
await copyFile(src, dest);
|
|
275
|
+
await chmod(dest, 0o644);
|
|
276
|
+
installed.push(displayName);
|
|
277
|
+
} catch (e) {
|
|
278
|
+
if (e.code === 'ENOENT' && e.path === src) {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
failed.push({ file: displayName, error: e.message });
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return { installed, failed };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Copy lib files that hooks depend on to ~/.teleportation/lib/
|
|
291
|
+
* These include auth, session, and config modules
|
|
292
|
+
*/
|
|
293
|
+
export async function installLibFiles() {
|
|
294
|
+
const sourceLibDir = join(getTeleportationDir(), 'lib');
|
|
295
|
+
const destLibDir = join(HOME_DIR, '.teleportation', 'lib');
|
|
296
|
+
|
|
297
|
+
// Define lib files to copy with their subdirectories
|
|
298
|
+
const libFiles = [
|
|
299
|
+
{ subdir: 'auth', files: ['credentials.js', 'api-key.js'] },
|
|
300
|
+
{ subdir: 'session', files: ['metadata.js'] },
|
|
301
|
+
{ subdir: 'config', files: ['manager.js'] }
|
|
302
|
+
];
|
|
303
|
+
|
|
304
|
+
const installed = [];
|
|
305
|
+
const failed = [];
|
|
306
|
+
|
|
307
|
+
for (const { subdir, files } of libFiles) {
|
|
308
|
+
const srcSubdir = join(sourceLibDir, subdir);
|
|
309
|
+
const destSubdir = join(destLibDir, subdir);
|
|
310
|
+
|
|
311
|
+
// Create destination subdirectory
|
|
312
|
+
try {
|
|
313
|
+
await mkdir(destSubdir, { recursive: true });
|
|
314
|
+
} catch (e) {
|
|
315
|
+
if (e.code !== 'EEXIST') {
|
|
316
|
+
failed.push({ file: `${subdir}/`, error: e.message });
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
for (const file of files) {
|
|
322
|
+
const src = join(srcSubdir, file);
|
|
323
|
+
const dest = join(destSubdir, file);
|
|
324
|
+
const displayName = `${subdir}/${file}`;
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
// Check if source exists
|
|
328
|
+
await stat(src);
|
|
329
|
+
|
|
330
|
+
// Copy file
|
|
331
|
+
await copyFile(src, dest);
|
|
332
|
+
|
|
333
|
+
// Set permissions (644 for modules)
|
|
334
|
+
await chmod(dest, 0o644);
|
|
335
|
+
|
|
336
|
+
installed.push(displayName);
|
|
337
|
+
} catch (e) {
|
|
338
|
+
if (e.code === 'ENOENT' && e.path === src) {
|
|
339
|
+
// Source file doesn't exist, skip (not critical)
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
failed.push({ file: displayName, error: e.message });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return { installed, failed };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Write version file to ~/.teleportation/version.json
|
|
352
|
+
* This file is read by hooks to include version in session metadata
|
|
353
|
+
*/
|
|
354
|
+
export async function writeVersionFile() {
|
|
355
|
+
const versionFile = join(HOME_DIR, '.teleportation', 'version.json');
|
|
356
|
+
const versionData = {
|
|
357
|
+
version: TELEPORTATION_VERSION,
|
|
358
|
+
protocol_version: TELEPORTATION_PROTOCOL_VERSION,
|
|
359
|
+
installed_at: new Date().toISOString(),
|
|
360
|
+
installed_timestamp: Date.now()
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
await writeFile(versionFile, JSON.stringify(versionData, null, 2));
|
|
364
|
+
return versionFile;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Create Claude Code settings.json at project level with absolute paths
|
|
369
|
+
*/
|
|
370
|
+
export async function createSettings() {
|
|
371
|
+
const projectHooksDir = resolve(getProjectHooksDir());
|
|
372
|
+
const projectSettings = getProjectSettings();
|
|
373
|
+
|
|
374
|
+
// Ensure parent directory exists
|
|
375
|
+
await mkdir(dirname(projectSettings), { recursive: true });
|
|
376
|
+
|
|
377
|
+
const settings = {
|
|
378
|
+
hooks: {
|
|
379
|
+
PreToolUse: [{
|
|
380
|
+
matcher: ".*",
|
|
381
|
+
hooks: [{
|
|
382
|
+
type: "command",
|
|
383
|
+
command: `node ${join(projectHooksDir, 'pre_tool_use.mjs')}`
|
|
384
|
+
}]
|
|
385
|
+
}],
|
|
386
|
+
Stop: [{
|
|
387
|
+
matcher: ".*",
|
|
388
|
+
hooks: [{
|
|
389
|
+
type: "command",
|
|
390
|
+
command: `node ${join(projectHooksDir, 'stop.mjs')}`
|
|
391
|
+
}]
|
|
392
|
+
}],
|
|
393
|
+
SessionStart: [{
|
|
394
|
+
matcher: ".*",
|
|
395
|
+
hooks: [{
|
|
396
|
+
type: "command",
|
|
397
|
+
command: `node ${join(projectHooksDir, 'session_start.mjs')}`
|
|
398
|
+
}]
|
|
399
|
+
}],
|
|
400
|
+
SessionEnd: [{
|
|
401
|
+
matcher: ".*",
|
|
402
|
+
hooks: [{
|
|
403
|
+
type: "command",
|
|
404
|
+
command: `node ${join(projectHooksDir, 'session_end.mjs')}`
|
|
405
|
+
}]
|
|
406
|
+
}],
|
|
407
|
+
Notification: [{
|
|
408
|
+
matcher: ".*",
|
|
409
|
+
hooks: [{
|
|
410
|
+
type: "command",
|
|
411
|
+
command: `node ${join(projectHooksDir, 'notification.mjs')}`
|
|
412
|
+
}]
|
|
413
|
+
}],
|
|
414
|
+
UserPromptSubmit: [{
|
|
415
|
+
matcher: ".*",
|
|
416
|
+
hooks: [{
|
|
417
|
+
type: "command",
|
|
418
|
+
command: `node ${join(projectHooksDir, 'user_prompt_submit.mjs')}`
|
|
419
|
+
}]
|
|
420
|
+
}]
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
await writeFile(projectSettings, JSON.stringify(settings, null, 2));
|
|
425
|
+
|
|
426
|
+
// IMPORTANT: Also write to user-level settings (~/.claude/settings.json)
|
|
427
|
+
// Claude Code reads from this location, not from the project directory
|
|
428
|
+
const userSettings = getUserSettings();
|
|
429
|
+
await mkdir(dirname(userSettings), { recursive: true });
|
|
430
|
+
await writeFile(userSettings, JSON.stringify(settings, null, 2));
|
|
431
|
+
|
|
432
|
+
return { projectSettings, userSettings };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Verify installation
|
|
437
|
+
*/
|
|
438
|
+
export async function verifyInstallation() {
|
|
439
|
+
const projectHooksDir = getProjectHooksDir();
|
|
440
|
+
const projectSettings = getProjectSettings();
|
|
441
|
+
|
|
442
|
+
const checks = {
|
|
443
|
+
directories: false,
|
|
444
|
+
hooks: false,
|
|
445
|
+
settings: false
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
// Check directories
|
|
449
|
+
try {
|
|
450
|
+
await stat(projectHooksDir);
|
|
451
|
+
checks.directories = true;
|
|
452
|
+
} catch (e) {
|
|
453
|
+
return { valid: false, checks, error: 'Directories not created' };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Check hooks
|
|
457
|
+
try {
|
|
458
|
+
const hooks = await readdir(projectHooksDir);
|
|
459
|
+
const requiredHooks = ['pre_tool_use.mjs', 'config-loader.mjs'];
|
|
460
|
+
const foundHooks = requiredHooks.filter(h => hooks.includes(h));
|
|
461
|
+
checks.hooks = foundHooks.length === requiredHooks.length;
|
|
462
|
+
} catch (e) {
|
|
463
|
+
return { valid: false, checks, error: 'Cannot read hooks directory' };
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Check settings
|
|
467
|
+
try {
|
|
468
|
+
await stat(projectSettings);
|
|
469
|
+
const content = await readFile(projectSettings, 'utf8');
|
|
470
|
+
const settings = JSON.parse(content);
|
|
471
|
+
checks.settings = settings.hooks && Object.keys(settings.hooks).length > 0;
|
|
472
|
+
} catch (e) {
|
|
473
|
+
return { valid: false, checks, error: 'Settings file invalid or missing' };
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const valid = Object.values(checks).every(v => v === true);
|
|
477
|
+
if (!valid) {
|
|
478
|
+
const failed = Object.entries(checks).filter(([_, v]) => !v).map(([k]) => k).join(', ');
|
|
479
|
+
return { valid, checks, error: `Failed checks: ${failed}` };
|
|
480
|
+
}
|
|
481
|
+
return { valid, checks };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Main installation function
|
|
486
|
+
*
|
|
487
|
+
* Project-level installation:
|
|
488
|
+
* - Hooks stay in PROJECT/.claude/hooks/ (source files, just verify they exist)
|
|
489
|
+
* - Settings created in PROJECT/.claude/settings.json with absolute paths
|
|
490
|
+
* - Daemon copied to ~/.teleportation/daemon/ (shared across projects)
|
|
491
|
+
*/
|
|
492
|
+
export async function install(sourceHooksDir) {
|
|
493
|
+
// Pre-flight checks
|
|
494
|
+
const nodeCheck = checkNodeVersion();
|
|
495
|
+
if (!nodeCheck.valid) {
|
|
496
|
+
throw new Error(nodeCheck.error);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const claudeCheck = checkClaudeCode();
|
|
500
|
+
if (!claudeCheck.valid) {
|
|
501
|
+
throw new Error(claudeCheck.error);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Create directories
|
|
505
|
+
await ensureDirectories();
|
|
506
|
+
|
|
507
|
+
// Install hooks (verify and copy to destination)
|
|
508
|
+
const hookResult = await installHooks(sourceHooksDir);
|
|
509
|
+
if (hookResult.failed.length > 0) {
|
|
510
|
+
throw new Error(`Failed to install hooks: ${hookResult.failed.map(f => f.file).join(', ')}`);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Install daemon (still goes to ~/.teleportation/daemon/)
|
|
514
|
+
const daemonResult = await installDaemon();
|
|
515
|
+
if (daemonResult.failed.length > 0) {
|
|
516
|
+
throw new Error(`Failed to install daemon: ${daemonResult.failed.map(f => f.file).join(', ')}`);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Install daemon modules (machine-coders, router)
|
|
520
|
+
const daemonModulesResult = await installDaemonModules();
|
|
521
|
+
if (daemonModulesResult.failed.length > 0) {
|
|
522
|
+
console.warn(`Warning: Some daemon modules failed to install: ${daemonModulesResult.failed.map(f => f.file).join(', ')}`);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Install lib files that hooks depend on (auth, session, config modules)
|
|
526
|
+
const libResult = await installLibFiles();
|
|
527
|
+
// Lib files are not critical - just log failures but don't throw
|
|
528
|
+
if (libResult.failed.length > 0) {
|
|
529
|
+
console.warn(`Warning: Some lib files failed to install: ${libResult.failed.map(f => f.file).join(', ')}`);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Create project-level settings with absolute paths
|
|
533
|
+
await createSettings();
|
|
534
|
+
|
|
535
|
+
// Write version file
|
|
536
|
+
await writeVersionFile();
|
|
537
|
+
|
|
538
|
+
// Verify
|
|
539
|
+
const verification = await verifyInstallation();
|
|
540
|
+
if (!verification.valid) {
|
|
541
|
+
throw new Error(`Installation verification failed: ${verification.error}`);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return {
|
|
545
|
+
success: true,
|
|
546
|
+
hooksInstalled: hookResult.installed.length,
|
|
547
|
+
daemonInstalled: daemonResult.installed.length,
|
|
548
|
+
libFilesInstalled: libResult.installed.length,
|
|
549
|
+
settingsFile: getProjectSettings(),
|
|
550
|
+
hooksDir: getProjectHooksDir(),
|
|
551
|
+
daemonDir: join(HOME_DIR, '.teleportation', 'daemon'),
|
|
552
|
+
libDir: join(HOME_DIR, '.teleportation', 'lib')
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|