teleportation-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/.claude/hooks/config-loader.mjs +93 -0
  2. package/.claude/hooks/heartbeat.mjs +331 -0
  3. package/.claude/hooks/notification.mjs +35 -0
  4. package/.claude/hooks/permission_request.mjs +307 -0
  5. package/.claude/hooks/post_tool_use.mjs +137 -0
  6. package/.claude/hooks/pre_tool_use.mjs +451 -0
  7. package/.claude/hooks/session-register.mjs +274 -0
  8. package/.claude/hooks/session_end.mjs +256 -0
  9. package/.claude/hooks/session_start.mjs +308 -0
  10. package/.claude/hooks/stop.mjs +277 -0
  11. package/.claude/hooks/user_prompt_submit.mjs +91 -0
  12. package/LICENSE +21 -0
  13. package/README.md +243 -0
  14. package/lib/auth/api-key.js +110 -0
  15. package/lib/auth/credentials.js +341 -0
  16. package/lib/backup/manager.js +461 -0
  17. package/lib/cli/daemon-commands.js +299 -0
  18. package/lib/cli/index.js +303 -0
  19. package/lib/cli/session-commands.js +294 -0
  20. package/lib/cli/snapshot-commands.js +223 -0
  21. package/lib/cli/worktree-commands.js +291 -0
  22. package/lib/config/manager.js +306 -0
  23. package/lib/daemon/lifecycle.js +336 -0
  24. package/lib/daemon/pid-manager.js +160 -0
  25. package/lib/daemon/teleportation-daemon.js +2009 -0
  26. package/lib/handoff/config.js +102 -0
  27. package/lib/handoff/example.js +152 -0
  28. package/lib/handoff/git-handoff.js +351 -0
  29. package/lib/handoff/handoff.js +277 -0
  30. package/lib/handoff/index.js +25 -0
  31. package/lib/handoff/session-state.js +238 -0
  32. package/lib/install/installer.js +555 -0
  33. package/lib/machine-coders/claude-code-adapter.js +329 -0
  34. package/lib/machine-coders/example.js +239 -0
  35. package/lib/machine-coders/gemini-cli-adapter.js +406 -0
  36. package/lib/machine-coders/index.js +103 -0
  37. package/lib/machine-coders/interface.js +168 -0
  38. package/lib/router/classifier.js +251 -0
  39. package/lib/router/example.js +92 -0
  40. package/lib/router/index.js +69 -0
  41. package/lib/router/mech-llms-client.js +277 -0
  42. package/lib/router/models.js +188 -0
  43. package/lib/router/router.js +382 -0
  44. package/lib/session/cleanup.js +100 -0
  45. package/lib/session/metadata.js +258 -0
  46. package/lib/session/mute-checker.js +114 -0
  47. package/lib/session-registry/manager.js +302 -0
  48. package/lib/snapshot/manager.js +390 -0
  49. package/lib/utils/errors.js +166 -0
  50. package/lib/utils/logger.js +148 -0
  51. package/lib/utils/retry.js +155 -0
  52. package/lib/worktree/manager.js +301 -0
  53. package/package.json +66 -0
  54. package/teleportation-cli.cjs +2987 -0
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Handoff Configuration
3
+ *
4
+ * Default settings and config loading for cloud handoff.
5
+ */
6
+
7
+ import { readFileSync, existsSync } from 'fs';
8
+ import { join } from 'path';
9
+ import { homedir } from 'os';
10
+
11
+ /**
12
+ * Default handoff configuration
13
+ */
14
+ export const DEFAULT_HANDOFF_CONFIG = {
15
+ // Whether auto-handoff is enabled
16
+ enabled: true,
17
+
18
+ // What triggers auto-handoff: "away" | "idle" | "manual"
19
+ // - "away": When approval times out (is_away: true)
20
+ // - "idle": After idle_timeout_minutes of no activity
21
+ // - "manual": Only via explicit `teleport handoff` command
22
+ trigger: 'away',
23
+
24
+ // Minutes of inactivity before auto-handoff (for "idle" trigger)
25
+ idle_timeout_minutes: 5,
26
+
27
+ // Auto-commit uncommitted changes before handoff
28
+ commit_uncommitted: true,
29
+
30
+ // Send push notification on handoff
31
+ notify_on_handoff: true,
32
+
33
+ // Cloud provider: "daytona" | "gitpod" | "codespaces" | "devpod"
34
+ cloud_provider: 'daytona',
35
+
36
+ // Branch prefix for WIP commits
37
+ wip_branch_prefix: 'teleport/wip-',
38
+
39
+ // Auto-sync when returning to local
40
+ auto_sync_on_return: false, // Prompt by default
41
+
42
+ // How often cloud should auto-commit (minutes, 0 = disabled)
43
+ cloud_auto_commit_interval: 5,
44
+ };
45
+
46
+ /**
47
+ * Load handoff config from various sources
48
+ * Priority: env vars > project config > user config > defaults
49
+ *
50
+ * @param {string} [projectPath] - Project directory path
51
+ * @returns {Object} Merged configuration
52
+ */
53
+ export function getHandoffConfig(projectPath = process.cwd()) {
54
+ let config = { ...DEFAULT_HANDOFF_CONFIG };
55
+
56
+ // 1. Load user-level config (~/.teleportation/config.json)
57
+ const userConfigPath = join(homedir(), '.teleportation', 'config.json');
58
+ if (existsSync(userConfigPath)) {
59
+ try {
60
+ const userConfig = JSON.parse(readFileSync(userConfigPath, 'utf-8'));
61
+ if (userConfig.auto_handoff) {
62
+ config = { ...config, ...userConfig.auto_handoff };
63
+ }
64
+ } catch (e) {
65
+ // Ignore parse errors
66
+ }
67
+ }
68
+
69
+ // 2. Load project-level config (.teleportation/config.json)
70
+ const projectConfigPath = join(projectPath, '.teleportation', 'config.json');
71
+ if (existsSync(projectConfigPath)) {
72
+ try {
73
+ const projectConfig = JSON.parse(readFileSync(projectConfigPath, 'utf-8'));
74
+ if (projectConfig.auto_handoff) {
75
+ config = { ...config, ...projectConfig.auto_handoff };
76
+ }
77
+ } catch (e) {
78
+ // Ignore parse errors
79
+ }
80
+ }
81
+
82
+ // 3. Override with environment variables
83
+ if (process.env.TELEPORT_HANDOFF_ENABLED !== undefined) {
84
+ config.enabled = process.env.TELEPORT_HANDOFF_ENABLED === 'true';
85
+ }
86
+ if (process.env.TELEPORT_HANDOFF_TRIGGER) {
87
+ config.trigger = process.env.TELEPORT_HANDOFF_TRIGGER;
88
+ }
89
+ if (process.env.TELEPORT_CLOUD_PROVIDER) {
90
+ config.cloud_provider = process.env.TELEPORT_CLOUD_PROVIDER;
91
+ }
92
+ if (process.env.TELEPORT_IDLE_TIMEOUT) {
93
+ config.idle_timeout_minutes = parseInt(process.env.TELEPORT_IDLE_TIMEOUT, 10);
94
+ }
95
+
96
+ return config;
97
+ }
98
+
99
+ export default {
100
+ DEFAULT_HANDOFF_CONFIG,
101
+ getHandoffConfig,
102
+ };
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Handoff Module Example / Test Script
4
+ *
5
+ * Demonstrates the handoff and sync operations.
6
+ *
7
+ * Usage:
8
+ * node lib/handoff/example.js status
9
+ * node lib/handoff/example.js handoff [message]
10
+ * node lib/handoff/example.js sync
11
+ */
12
+
13
+ import { handoff, sync, getHandoffStatus } from './handoff.js';
14
+ import { getHandoffConfig } from './config.js';
15
+
16
+ const projectPath = process.cwd();
17
+ const sessionId = process.env.TELEPORT_SESSION_ID || `test-${Date.now()}`;
18
+
19
+ async function showStatus() {
20
+ console.log('=== Handoff Status ===\n');
21
+
22
+ const config = getHandoffConfig(projectPath);
23
+ console.log('Configuration:');
24
+ console.log(` Enabled: ${config.enabled}`);
25
+ console.log(` Trigger: ${config.trigger}`);
26
+ console.log(` Cloud Provider: ${config.cloud_provider}`);
27
+ console.log(` WIP Branch Prefix: ${config.wip_branch_prefix}`);
28
+ console.log('');
29
+
30
+ try {
31
+ const status = await getHandoffStatus({ projectPath, sessionId });
32
+
33
+ console.log('Git Status:');
34
+ console.log(` Current Branch: ${status.currentBranch}`);
35
+ console.log(` WIP Branch: ${status.wipBranch}`);
36
+ console.log(` Local Commit: ${status.localCommit?.slice(0, 7)}`);
37
+ console.log(` Remote Commit: ${status.remoteCommit?.slice(0, 7) || '(none)'}`);
38
+ console.log(` Uncommitted Changes: ${status.hasUncommittedChanges ? 'Yes' : 'No'}`);
39
+ console.log(` Remote Changes: ${status.hasRemoteChanges ? 'Yes' : 'No'}`);
40
+ console.log('');
41
+
42
+ console.log('Session State:');
43
+ console.log(` Location: ${status.location}`);
44
+ console.log(` Session ID: ${status.sessionId}`);
45
+ if (status.lastHandoff) {
46
+ console.log(` Last Handoff: ${status.lastHandoff.type} at ${status.lastHandoff.timestamp}`);
47
+ }
48
+ if (status.cloudContainer) {
49
+ console.log(` Cloud Container: ${status.cloudContainer.id}`);
50
+ }
51
+ console.log('');
52
+
53
+ if (status.diff) {
54
+ console.log('Diff Summary:');
55
+ console.log(` Ahead: ${status.diff.ahead} commits`);
56
+ console.log(` Behind: ${status.diff.behind} commits`);
57
+ if (status.diff.files.length > 0) {
58
+ console.log(` Files: ${status.diff.files.slice(0, 5).join(', ')}${status.diff.files.length > 5 ? '...' : ''}`);
59
+ }
60
+ }
61
+ } catch (error) {
62
+ console.error(`Error: ${error.message}`);
63
+ }
64
+ }
65
+
66
+ async function doHandoff(message) {
67
+ console.log('=== Performing Handoff ===\n');
68
+ console.log(`Project: ${projectPath}`);
69
+ console.log(`Session: ${sessionId}`);
70
+ console.log(`Message: ${message || '(none)'}`);
71
+ console.log('');
72
+
73
+ const result = await handoff({
74
+ projectPath,
75
+ sessionId,
76
+ message,
77
+ onProgress: ({ step, message }) => {
78
+ console.log(`[${step}] ${message}`);
79
+ },
80
+ });
81
+
82
+ console.log('');
83
+ if (result.success) {
84
+ console.log('✓ Handoff successful!');
85
+ console.log(` Branch: ${result.branch}`);
86
+ console.log(` Commit: ${result.commit?.slice(0, 7)}`);
87
+ if (result.files?.length > 0) {
88
+ console.log(` Files: ${result.files.length}`);
89
+ }
90
+ } else {
91
+ console.log(`✗ Handoff failed: ${result.error}`);
92
+ }
93
+ }
94
+
95
+ async function doSync() {
96
+ console.log('=== Syncing from Cloud ===\n');
97
+ console.log(`Project: ${projectPath}`);
98
+ console.log(`Session: ${sessionId}`);
99
+ console.log('');
100
+
101
+ const result = await sync({
102
+ projectPath,
103
+ sessionId,
104
+ strategy: 'merge',
105
+ onProgress: ({ step, message }) => {
106
+ console.log(`[${step}] ${message}`);
107
+ },
108
+ });
109
+
110
+ console.log('');
111
+ if (result.success) {
112
+ console.log('✓ Sync successful!');
113
+ console.log(` Commit: ${result.commit?.slice(0, 7)}`);
114
+ if (result.files?.length > 0) {
115
+ console.log(` Files synced: ${result.files.length}`);
116
+ result.files.slice(0, 5).forEach(f => console.log(` - ${f}`));
117
+ if (result.files.length > 5) {
118
+ console.log(` ... and ${result.files.length - 5} more`);
119
+ }
120
+ }
121
+ if (result.message) {
122
+ console.log(` Note: ${result.message}`);
123
+ }
124
+ } else {
125
+ console.log(`✗ Sync failed: ${result.error}`);
126
+ if (result.conflicts?.length > 0) {
127
+ console.log(' Conflicts:');
128
+ result.conflicts.forEach(f => console.log(` - ${f}`));
129
+ }
130
+ }
131
+ }
132
+
133
+ // Main
134
+ const command = process.argv[2] || 'status';
135
+ const arg = process.argv[3];
136
+
137
+ switch (command) {
138
+ case 'status':
139
+ await showStatus();
140
+ break;
141
+ case 'handoff':
142
+ await doHandoff(arg);
143
+ break;
144
+ case 'sync':
145
+ await doSync();
146
+ break;
147
+ default:
148
+ console.log('Usage:');
149
+ console.log(' node lib/handoff/example.js status');
150
+ console.log(' node lib/handoff/example.js handoff [message]');
151
+ console.log(' node lib/handoff/example.js sync');
152
+ }
@@ -0,0 +1,351 @@
1
+ /**
2
+ * Git Handoff Operations
3
+ *
4
+ * Handles git operations for seamless local ↔ cloud handoff:
5
+ * - Auto-commit WIP changes
6
+ * - Push to teleport/wip-<session> branch
7
+ * - Pull/sync from cloud
8
+ * - Conflict detection
9
+ */
10
+
11
+ import { execSync, exec } from 'child_process';
12
+ import { promisify } from 'util';
13
+ import { writeFileSync, unlinkSync } from 'fs';
14
+ import { tmpdir } from 'os';
15
+ import { join } from 'path';
16
+
17
+ const execAsync = promisify(exec);
18
+
19
+ /**
20
+ * GitHandoff class for managing git operations during handoff
21
+ */
22
+ export class GitHandoff {
23
+ /**
24
+ * @param {Object} options
25
+ * @param {string} options.repoPath - Path to the git repository
26
+ * @param {string} options.sessionId - Teleportation session ID
27
+ * @param {string} [options.branchPrefix] - Prefix for WIP branches
28
+ * @param {boolean} [options.verbose] - Enable verbose logging
29
+ */
30
+ constructor(options) {
31
+ this.repoPath = options.repoPath;
32
+ this.sessionId = options.sessionId;
33
+ this.branchPrefix = options.branchPrefix || 'teleport/wip-';
34
+ this.verbose = options.verbose || false;
35
+
36
+ this.wipBranch = `${this.branchPrefix}${this.sessionId}`;
37
+ }
38
+
39
+ /**
40
+ * Execute a git command in the repo
41
+ * @private
42
+ */
43
+ async git(command, options = {}) {
44
+ const fullCommand = `git ${command}`;
45
+ if (this.verbose) {
46
+ console.log(`[git-handoff] ${fullCommand}`);
47
+ }
48
+
49
+ try {
50
+ const { stdout, stderr } = await execAsync(fullCommand, {
51
+ cwd: this.repoPath,
52
+ ...options,
53
+ });
54
+ return stdout.trim();
55
+ } catch (error) {
56
+ if (this.verbose) {
57
+ console.error(`[git-handoff] Error: ${error.message}`);
58
+ }
59
+ throw error;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Check if this is a valid git repository
65
+ * @returns {Promise<boolean>}
66
+ */
67
+ async isGitRepo() {
68
+ try {
69
+ await this.git('rev-parse --is-inside-work-tree');
70
+ return true;
71
+ } catch {
72
+ return false;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Check if the repo has uncommitted changes
78
+ * @returns {Promise<boolean>}
79
+ */
80
+ async hasUncommittedChanges() {
81
+ const status = await this.git('status --porcelain');
82
+ return status.length > 0;
83
+ }
84
+
85
+ /**
86
+ * Alias for hasUncommittedChanges
87
+ * @returns {Promise<boolean>}
88
+ */
89
+ async hasChanges() {
90
+ return this.hasUncommittedChanges();
91
+ }
92
+
93
+ /**
94
+ * Get list of changed files
95
+ * @returns {Promise<string[]>}
96
+ */
97
+ async getChangedFiles() {
98
+ const status = await this.git('status --porcelain');
99
+ if (!status) return [];
100
+
101
+ return status.split('\n').map(line => {
102
+ // Format: "XY filename" or "XY filename -> newname"
103
+ const match = line.match(/^..\s+(.+?)(?:\s+->\s+.+)?$/);
104
+ return match ? match[1] : line.slice(3);
105
+ });
106
+ }
107
+
108
+ /**
109
+ * Get the current branch name
110
+ * @returns {Promise<string>}
111
+ */
112
+ async getCurrentBranch() {
113
+ return this.git('rev-parse --abbrev-ref HEAD');
114
+ }
115
+
116
+ /**
117
+ * Get the current commit hash
118
+ * @returns {Promise<string>}
119
+ */
120
+ async getCurrentCommit() {
121
+ return this.git('rev-parse HEAD');
122
+ }
123
+
124
+ /**
125
+ * Get the remote URL for origin
126
+ * @returns {Promise<string|null>}
127
+ */
128
+ async getRemoteUrl() {
129
+ try {
130
+ return await this.git('remote get-url origin');
131
+ } catch {
132
+ return null;
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Check if a remote branch exists
138
+ * @param {string} branch - Branch name
139
+ * @returns {Promise<boolean>}
140
+ */
141
+ async remoteBranchExists(branch) {
142
+ try {
143
+ await this.git(`ls-remote --exit-code origin ${branch}`);
144
+ return true;
145
+ } catch {
146
+ return false;
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Commit all changes with a WIP message
152
+ * @param {string} [message] - Optional message to include
153
+ * @returns {Promise<{commit: string, files: string[]}>}
154
+ */
155
+ async commitWip(message = '') {
156
+ const files = await this.getChangedFiles();
157
+ if (files.length === 0) {
158
+ return { commit: await this.getCurrentCommit(), files: [] };
159
+ }
160
+
161
+ // Stage all changes
162
+ await this.git('add -A');
163
+
164
+ // Build commit message
165
+ const timestamp = new Date().toISOString();
166
+ const commitMessage = [
167
+ `[teleport] WIP handoff${message ? `: ${message}` : ''}`,
168
+ '',
169
+ `Session: ${this.sessionId}`,
170
+ `Timestamp: ${timestamp}`,
171
+ `Source: local`,
172
+ '',
173
+ 'Files changed:',
174
+ ...files.map(f => `- ${f}`),
175
+ ].join('\n');
176
+
177
+ // Commit using temp file to avoid shell injection
178
+ // Using -F flag prevents any shell metacharacter interpretation
179
+ const tempFile = join(tmpdir(), `teleport-commit-${Date.now()}-${Math.random().toString(36).slice(2)}.txt`);
180
+ try {
181
+ writeFileSync(tempFile, commitMessage, { mode: 0o600 });
182
+ await this.git(`commit -F "${tempFile}"`);
183
+ } finally {
184
+ try { unlinkSync(tempFile); } catch { /* ignore cleanup errors */ }
185
+ }
186
+
187
+ const commit = await this.getCurrentCommit();
188
+ return { commit, files };
189
+ }
190
+
191
+ /**
192
+ * Push current branch to the WIP branch on remote
193
+ * @param {boolean} [force=true] - Force push (default true for WIP branches)
194
+ * @returns {Promise<{branch: string, commit: string}>}
195
+ */
196
+ async pushToWipBranch(force = true) {
197
+ const commit = await this.getCurrentCommit();
198
+ const forceFlag = force ? '--force' : '';
199
+
200
+ await this.git(`push origin HEAD:${this.wipBranch} ${forceFlag}`);
201
+
202
+ return { branch: this.wipBranch, commit };
203
+ }
204
+
205
+ /**
206
+ * Fetch the WIP branch from remote
207
+ * @returns {Promise<boolean>} - True if branch exists and was fetched
208
+ */
209
+ async fetchWipBranch() {
210
+ try {
211
+ await this.git(`fetch origin ${this.wipBranch}`);
212
+ return true;
213
+ } catch {
214
+ return false;
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Check if there are changes on the remote WIP branch
220
+ * @returns {Promise<{hasChanges: boolean, localCommit: string, remoteCommit: string|null}>}
221
+ */
222
+ async checkRemoteChanges() {
223
+ const localCommit = await this.getCurrentCommit();
224
+
225
+ const fetched = await this.fetchWipBranch();
226
+ if (!fetched) {
227
+ return { hasChanges: false, localCommit, remoteCommit: null };
228
+ }
229
+
230
+ try {
231
+ const remoteCommit = await this.git(`rev-parse origin/${this.wipBranch}`);
232
+ return {
233
+ hasChanges: localCommit !== remoteCommit,
234
+ localCommit,
235
+ remoteCommit,
236
+ };
237
+ } catch {
238
+ return { hasChanges: false, localCommit, remoteCommit: null };
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Sync changes from the remote WIP branch
244
+ * @param {Object} options
245
+ * @param {string} [options.strategy='merge'] - 'merge' or 'rebase'
246
+ * @param {boolean} [options.stashLocal=true] - Stash local changes first
247
+ * @returns {Promise<{success: boolean, commit: string, conflicts: string[]}>}
248
+ */
249
+ async syncFromRemote(options = {}) {
250
+ const { strategy = 'merge', stashLocal = true } = options;
251
+
252
+ // Stash local changes if any
253
+ let stashed = false;
254
+ if (stashLocal && await this.hasUncommittedChanges()) {
255
+ await this.git('stash push -m "teleport-sync-stash"');
256
+ stashed = true;
257
+ }
258
+
259
+ try {
260
+ // Fetch latest
261
+ await this.fetchWipBranch();
262
+
263
+ // Merge or rebase
264
+ if (strategy === 'rebase') {
265
+ await this.git(`rebase origin/${this.wipBranch}`);
266
+ } else {
267
+ await this.git(`merge origin/${this.wipBranch} --no-edit`);
268
+ }
269
+
270
+ // Pop stash if we stashed
271
+ if (stashed) {
272
+ try {
273
+ await this.git('stash pop');
274
+ } catch (e) {
275
+ // Stash pop conflict - leave in stash
276
+ console.warn('[git-handoff] Stash pop had conflicts, changes left in stash');
277
+ }
278
+ }
279
+
280
+ return {
281
+ success: true,
282
+ commit: await this.getCurrentCommit(),
283
+ conflicts: [],
284
+ };
285
+ } catch (error) {
286
+ // Check for merge conflicts
287
+ const status = await this.git('status --porcelain');
288
+ const conflicts = status
289
+ .split('\n')
290
+ .filter(line => line.startsWith('UU') || line.startsWith('AA'))
291
+ .map(line => line.slice(3));
292
+
293
+ return {
294
+ success: false,
295
+ commit: await this.getCurrentCommit(),
296
+ conflicts,
297
+ };
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Get a summary of the diff between local and remote
303
+ * @returns {Promise<{ahead: number, behind: number, files: string[]}>}
304
+ */
305
+ async getDiffSummary() {
306
+ await this.fetchWipBranch();
307
+
308
+ try {
309
+ const ahead = await this.git(`rev-list --count origin/${this.wipBranch}..HEAD`);
310
+ const behind = await this.git(`rev-list --count HEAD..origin/${this.wipBranch}`);
311
+
312
+ let files = [];
313
+ if (parseInt(behind) > 0) {
314
+ const diff = await this.git(`diff --name-only HEAD..origin/${this.wipBranch}`);
315
+ files = diff ? diff.split('\n') : [];
316
+ }
317
+
318
+ return {
319
+ ahead: parseInt(ahead),
320
+ behind: parseInt(behind),
321
+ files,
322
+ };
323
+ } catch {
324
+ return { ahead: 0, behind: 0, files: [] };
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Clean up the WIP branch (delete from remote)
330
+ * @returns {Promise<boolean>}
331
+ */
332
+ async cleanupWipBranch() {
333
+ try {
334
+ await this.git(`push origin --delete ${this.wipBranch}`);
335
+ return true;
336
+ } catch {
337
+ return false;
338
+ }
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Create a GitHandoff instance
344
+ * @param {Object} options - Same as GitHandoff constructor
345
+ * @returns {GitHandoff}
346
+ */
347
+ export function createGitHandoff(options) {
348
+ return new GitHandoff(options);
349
+ }
350
+
351
+ export default GitHandoff;