teleportation-cli 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/hooks/heartbeat.mjs +67 -2
- package/.claude/hooks/permission_request.mjs +55 -26
- package/.claude/hooks/pre_tool_use.mjs +29 -2
- package/.claude/hooks/session-register.mjs +64 -5
- package/.claude/hooks/user_prompt_submit.mjs +54 -0
- package/README.md +36 -12
- package/lib/auth/claude-key-extractor.js +196 -0
- package/lib/auth/credentials.js +7 -2
- package/lib/cli/remote-commands.js +649 -0
- package/lib/install/installer.js +22 -7
- package/lib/remote/code-sync.js +213 -0
- package/lib/remote/init-script-robust.js +187 -0
- package/lib/remote/liveport-client.js +417 -0
- package/lib/remote/orchestrator.js +480 -0
- package/lib/remote/pr-creator.js +382 -0
- package/lib/remote/providers/base-provider.js +407 -0
- package/lib/remote/providers/daytona-provider.js +506 -0
- package/lib/remote/providers/fly-provider.js +611 -0
- package/lib/remote/providers/provider-factory.js +228 -0
- package/lib/remote/results-delivery.js +333 -0
- package/lib/remote/session-manager.js +273 -0
- package/lib/remote/state-capture.js +324 -0
- package/lib/remote/vault-client.js +478 -0
- package/lib/session/metadata.js +80 -49
- package/lib/session/mute-checker.js +2 -1
- package/lib/utils/vault-errors.js +353 -0
- package/package.json +5 -5
- package/teleportation-cli.cjs +417 -7
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider Factory for Intelligent Provider Selection
|
|
3
|
+
*
|
|
4
|
+
* Automatically selects the best cloud provider (Fly.io or Daytona) based on:
|
|
5
|
+
* - Task description analysis
|
|
6
|
+
* - Estimated duration
|
|
7
|
+
* - Keyword detection
|
|
8
|
+
* - User preferences
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { FlyProvider } from './fly-provider.js';
|
|
12
|
+
import { DaytonaProvider } from './daytona-provider.js';
|
|
13
|
+
|
|
14
|
+
export class ProviderFactory {
|
|
15
|
+
/**
|
|
16
|
+
* Initialize ProviderFactory
|
|
17
|
+
* @param {Object} config - Factory configuration
|
|
18
|
+
* @param {VaultClient} config.vaultClient - Mech Vault client (required)
|
|
19
|
+
* @param {LivePortClient} config.livePortClient - LivePort client (required)
|
|
20
|
+
* @param {string} [config.flyApiToken] - Fly.io API token
|
|
21
|
+
* @param {string} [config.daytonaApiKey] - Daytona API key
|
|
22
|
+
* @param {string} [config.defaultProvider] - Default provider ('fly' or 'daytona')
|
|
23
|
+
*/
|
|
24
|
+
constructor(config = {}) {
|
|
25
|
+
if (!config.vaultClient) {
|
|
26
|
+
throw new Error('VaultClient is required');
|
|
27
|
+
}
|
|
28
|
+
if (!config.livePortClient) {
|
|
29
|
+
throw new Error('LivePortClient is required');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
this.config = config;
|
|
33
|
+
this.vaultClient = config.vaultClient;
|
|
34
|
+
this.livePortClient = config.livePortClient;
|
|
35
|
+
this.defaultProvider = config.defaultProvider || 'daytona';
|
|
36
|
+
|
|
37
|
+
// Keywords that suggest long-running tasks (prefer Fly)
|
|
38
|
+
this.flyKeywords = [
|
|
39
|
+
'overnight', 'multi-day', 'several days', 'long-running',
|
|
40
|
+
'migration', 'migrate', 'load test', 'stress test', 'benchmark',
|
|
41
|
+
'hours', 'days', 'extended', 'persistent',
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
// Keywords that suggest quick tasks (prefer Daytona)
|
|
45
|
+
this.daytonaKeywords = [
|
|
46
|
+
'quick', 'fast', 'review', 'pr', 'pull request',
|
|
47
|
+
'feature', 'branch', 'bug fix', 'hotfix',
|
|
48
|
+
'test', 'experiment', 'prototype', 'spike',
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Select the best provider based on task description
|
|
54
|
+
*
|
|
55
|
+
* Decision logic:
|
|
56
|
+
* 1. If provider explicitly specified in options, use it
|
|
57
|
+
* 2. Analyze task description for duration indicators
|
|
58
|
+
* 3. Match keywords to provider preferences
|
|
59
|
+
* 4. Default to Daytona for quick, ephemeral tasks
|
|
60
|
+
*
|
|
61
|
+
* @param {string} taskDescription - Description of the task to perform
|
|
62
|
+
* @param {Object} [options] - Selection options
|
|
63
|
+
* @param {string} [options.provider] - Force specific provider ('fly' or 'daytona')
|
|
64
|
+
* @returns {string} Selected provider type ('fly' or 'daytona')
|
|
65
|
+
*/
|
|
66
|
+
selectProvider(taskDescription, options = {}) {
|
|
67
|
+
// 1. Explicit override takes precedence
|
|
68
|
+
if (options.provider) {
|
|
69
|
+
return options.provider;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 2. Estimate task duration
|
|
73
|
+
const duration = this._estimateDuration(taskDescription);
|
|
74
|
+
|
|
75
|
+
// 3. Analyze keywords
|
|
76
|
+
const keywords = this._analyzeKeywords(taskDescription);
|
|
77
|
+
|
|
78
|
+
// 4. Decision logic
|
|
79
|
+
if (duration === 'long') {
|
|
80
|
+
return 'fly';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Check for Fly-specific keywords
|
|
84
|
+
const hasFlyKeyword = this.flyKeywords.some(keyword =>
|
|
85
|
+
taskDescription.toLowerCase().includes(keyword)
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
if (hasFlyKeyword) {
|
|
89
|
+
return 'fly';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Default to Daytona for quick, ephemeral tasks
|
|
93
|
+
return 'daytona';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Create a provider instance of the specified type
|
|
98
|
+
*
|
|
99
|
+
* @param {string} type - Provider type ('fly' or 'daytona')
|
|
100
|
+
* @param {Object} [providerConfig] - Provider-specific configuration
|
|
101
|
+
* @returns {FlyProvider|DaytonaProvider} Provider instance
|
|
102
|
+
* @throws {Error} If provider type is unknown
|
|
103
|
+
*/
|
|
104
|
+
createProvider(type, providerConfig = {}) {
|
|
105
|
+
const baseConfig = {
|
|
106
|
+
vaultClient: this.vaultClient,
|
|
107
|
+
livePortClient: this.livePortClient,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
switch (type) {
|
|
111
|
+
case 'fly':
|
|
112
|
+
return new FlyProvider({
|
|
113
|
+
...baseConfig,
|
|
114
|
+
flyApiToken: this.config.flyApiToken,
|
|
115
|
+
...providerConfig,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
case 'daytona':
|
|
119
|
+
return new DaytonaProvider({
|
|
120
|
+
...baseConfig,
|
|
121
|
+
daytonaApiKey: this.config.daytonaApiKey,
|
|
122
|
+
...providerConfig,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
default:
|
|
126
|
+
throw new Error(`Unknown provider type: ${type}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Select provider and create instance in one step
|
|
132
|
+
*
|
|
133
|
+
* Convenience method that combines selectProvider() and createProvider().
|
|
134
|
+
*
|
|
135
|
+
* @param {string} taskDescription - Description of the task to perform
|
|
136
|
+
* @param {Object} [options] - Selection and provider options
|
|
137
|
+
* @param {string} [options.provider] - Force specific provider ('fly' or 'daytona')
|
|
138
|
+
* @returns {FlyProvider|DaytonaProvider} Configured provider instance
|
|
139
|
+
*/
|
|
140
|
+
selectAndCreate(taskDescription, options = {}) {
|
|
141
|
+
const providerType = this.selectProvider(taskDescription, options);
|
|
142
|
+
|
|
143
|
+
// Separate provider override from provider-specific config
|
|
144
|
+
const { provider, ...providerConfig } = options;
|
|
145
|
+
|
|
146
|
+
return this.createProvider(providerType, providerConfig);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Estimate task duration from description
|
|
151
|
+
*
|
|
152
|
+
* @private
|
|
153
|
+
* @param {string} taskDescription - Task description
|
|
154
|
+
* @returns {string} Duration estimate ('short' or 'long')
|
|
155
|
+
*/
|
|
156
|
+
_estimateDuration(taskDescription) {
|
|
157
|
+
const lowerDesc = taskDescription.toLowerCase();
|
|
158
|
+
|
|
159
|
+
// Long duration indicators
|
|
160
|
+
const longIndicators = [
|
|
161
|
+
'overnight', 'multi-day', 'several days', 'multiple days',
|
|
162
|
+
'long-running', 'extended', 'hours', 'days',
|
|
163
|
+
'migration', 'refactor', 'refactoring',
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
for (const indicator of longIndicators) {
|
|
167
|
+
if (lowerDesc.includes(indicator)) {
|
|
168
|
+
return 'long';
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Short duration by default
|
|
173
|
+
return 'short';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Analyze task description for provider-specific keywords
|
|
178
|
+
*
|
|
179
|
+
* @private
|
|
180
|
+
* @param {string} taskDescription - Task description
|
|
181
|
+
* @returns {string[]} Array of matched keywords
|
|
182
|
+
*/
|
|
183
|
+
_analyzeKeywords(taskDescription) {
|
|
184
|
+
const lowerDesc = taskDescription.toLowerCase();
|
|
185
|
+
const matched = [];
|
|
186
|
+
|
|
187
|
+
// Check Fly keywords
|
|
188
|
+
for (const keyword of this.flyKeywords) {
|
|
189
|
+
if (lowerDesc.includes(keyword)) {
|
|
190
|
+
matched.push(keyword);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Check Daytona keywords
|
|
195
|
+
for (const keyword of this.daytonaKeywords) {
|
|
196
|
+
if (lowerDesc.includes(keyword)) {
|
|
197
|
+
matched.push(keyword);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return matched;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Get default configuration for a provider type
|
|
206
|
+
*
|
|
207
|
+
* @param {string} type - Provider type ('fly' or 'daytona')
|
|
208
|
+
* @returns {Object} Default configuration for the provider
|
|
209
|
+
*/
|
|
210
|
+
getDefaultProviderConfig(type) {
|
|
211
|
+
switch (type) {
|
|
212
|
+
case 'fly':
|
|
213
|
+
return {
|
|
214
|
+
region: 'iad', // Ashburn, VA
|
|
215
|
+
appName: 'teleportation-remote',
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
case 'daytona':
|
|
219
|
+
return {
|
|
220
|
+
profileId: 'default',
|
|
221
|
+
autoStopMinutes: 60,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
default:
|
|
225
|
+
return {};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Results Delivery - Remote Session Results Handler
|
|
3
|
+
*
|
|
4
|
+
* Delivers results from remote sessions back to local repository using
|
|
5
|
+
* a fallback strategy:
|
|
6
|
+
* 1. Try to create Pull Request (preferred)
|
|
7
|
+
* 2. Fallback to pushing to a local branch
|
|
8
|
+
* 3. Handles conflict detection and resolution
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { GitHandoff } from '../handoff/git-handoff.js';
|
|
12
|
+
import { PRCreator } from './pr-creator.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* ResultsDelivery class
|
|
16
|
+
*/
|
|
17
|
+
export class ResultsDelivery {
|
|
18
|
+
/**
|
|
19
|
+
* @param {Object} options
|
|
20
|
+
* @param {string} options.repoPath - Path to git repository
|
|
21
|
+
* @param {string} options.sessionId - Session ID
|
|
22
|
+
* @param {string} options.remoteBranch - Remote branch with results (e.g., teleport/wip-session-123)
|
|
23
|
+
* @param {boolean} [options.verbose] - Enable verbose logging
|
|
24
|
+
*/
|
|
25
|
+
constructor(options) {
|
|
26
|
+
if (!options || !options.repoPath) {
|
|
27
|
+
throw new Error('repoPath is required');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!options.sessionId) {
|
|
31
|
+
throw new Error('sessionId is required');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!options.remoteBranch) {
|
|
35
|
+
throw new Error('remoteBranch is required');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
this.repoPath = options.repoPath;
|
|
39
|
+
this.sessionId = options.sessionId;
|
|
40
|
+
this.remoteBranch = options.remoteBranch;
|
|
41
|
+
this.verbose = options.verbose || false;
|
|
42
|
+
|
|
43
|
+
// Create GitHandoff for git operations
|
|
44
|
+
this.gitHandoff = options.gitHandoff || new GitHandoff({
|
|
45
|
+
repoPath: this.repoPath,
|
|
46
|
+
sessionId: this.sessionId,
|
|
47
|
+
verbose: this.verbose,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Create PRCreator for GitHub PR operations
|
|
51
|
+
this.prCreator = options.prCreator || new PRCreator({
|
|
52
|
+
repoPath: this.repoPath,
|
|
53
|
+
verbose: this.verbose,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Execute git command via GitHandoff
|
|
59
|
+
* @private
|
|
60
|
+
* @param {string} command - Git command (without 'git' prefix)
|
|
61
|
+
* @returns {Promise<string>} Command output
|
|
62
|
+
*/
|
|
63
|
+
async _git(command) {
|
|
64
|
+
return this.gitHandoff.git(command);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Detect conflicts with remote branch
|
|
69
|
+
*
|
|
70
|
+
* Performs a dry-run merge to detect if merging the remote branch
|
|
71
|
+
* would cause conflicts. Uses try-finally pattern to ensure cleanup
|
|
72
|
+
* even if process is interrupted.
|
|
73
|
+
*
|
|
74
|
+
* @returns {Promise<Object>} Conflict details
|
|
75
|
+
* @returns {boolean} return.hasConflicts - Whether conflicts exist
|
|
76
|
+
* @returns {string[]} return.conflictFiles - Array of conflicting files
|
|
77
|
+
* @throws {Error} If conflict detection fails
|
|
78
|
+
*/
|
|
79
|
+
async detectConflicts() {
|
|
80
|
+
let mergeStarted = false;
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
// Fetch remote branch
|
|
84
|
+
await this._git(`fetch origin ${this.remoteBranch}`);
|
|
85
|
+
|
|
86
|
+
// Try merge with --no-commit --no-ff to simulate merge without actually committing
|
|
87
|
+
try {
|
|
88
|
+
await this._git(`merge --no-commit --no-ff origin/${this.remoteBranch}`);
|
|
89
|
+
mergeStarted = true;
|
|
90
|
+
|
|
91
|
+
// No conflicts - merge succeeded
|
|
92
|
+
return {
|
|
93
|
+
hasConflicts: false,
|
|
94
|
+
conflictFiles: [],
|
|
95
|
+
};
|
|
96
|
+
} catch (mergeError) {
|
|
97
|
+
mergeStarted = true;
|
|
98
|
+
|
|
99
|
+
// Check if this is a conflict error
|
|
100
|
+
const status = await this._git('status --porcelain');
|
|
101
|
+
|
|
102
|
+
const conflictFiles = status
|
|
103
|
+
.split('\n')
|
|
104
|
+
.filter(line => {
|
|
105
|
+
// Conflict markers: UU (both modified), AA (both added), etc.
|
|
106
|
+
return line.startsWith('UU') || line.startsWith('AA') || line.startsWith('DD') ||
|
|
107
|
+
line.startsWith('AU') || line.startsWith('UA') || line.startsWith('DU') ||
|
|
108
|
+
line.startsWith('UD');
|
|
109
|
+
})
|
|
110
|
+
.map(line => line.slice(3).trim());
|
|
111
|
+
|
|
112
|
+
if (conflictFiles.length > 0) {
|
|
113
|
+
return {
|
|
114
|
+
hasConflicts: true,
|
|
115
|
+
conflictFiles,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Some other merge error (not a conflict)
|
|
120
|
+
throw mergeError;
|
|
121
|
+
}
|
|
122
|
+
} catch (error) {
|
|
123
|
+
throw new Error(`Failed to detect conflicts: ${error?.message || error}`);
|
|
124
|
+
} finally {
|
|
125
|
+
// Always attempt to abort merge, even on success or error
|
|
126
|
+
// This ensures repo is never left in merge state
|
|
127
|
+
if (mergeStarted) {
|
|
128
|
+
try {
|
|
129
|
+
await this._git('merge --abort');
|
|
130
|
+
if (this.verbose) {
|
|
131
|
+
console.log('[results-delivery] Merge aborted, repo clean');
|
|
132
|
+
}
|
|
133
|
+
} catch (abortError) {
|
|
134
|
+
// Log warning but don't fail - repo might already be clean
|
|
135
|
+
if (this.verbose) {
|
|
136
|
+
console.warn(`[results-delivery] Merge abort warning: ${abortError?.message || abortError}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Push remote branch to a new local branch
|
|
145
|
+
*
|
|
146
|
+
* @param {Object} [options]
|
|
147
|
+
* @param {string} [options.localBranch] - Local branch name (defaults to remote-results/{sessionId})
|
|
148
|
+
* @returns {Promise<Object>} Push result
|
|
149
|
+
* @returns {string} return.branch - Local branch name
|
|
150
|
+
* @returns {string} return.commit - Commit hash
|
|
151
|
+
* @returns {string} return.strategy - Always 'branch'
|
|
152
|
+
* @throws {Error} If branch creation fails
|
|
153
|
+
*/
|
|
154
|
+
async pushToBranch(options = {}) {
|
|
155
|
+
const { localBranch = `remote-results/${this.sessionId}` } = options;
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
// Fetch remote branch
|
|
159
|
+
await this._git(`fetch origin ${this.remoteBranch}`);
|
|
160
|
+
|
|
161
|
+
// Create local branch from remote branch
|
|
162
|
+
await this._git(`branch ${localBranch} origin/${this.remoteBranch}`);
|
|
163
|
+
|
|
164
|
+
// Get commit hash
|
|
165
|
+
const commit = await this._git(`rev-parse ${localBranch}`);
|
|
166
|
+
|
|
167
|
+
if (this.verbose) {
|
|
168
|
+
console.log(`[results-delivery] Created local branch: ${localBranch}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
branch: localBranch,
|
|
173
|
+
commit: commit.trim(),
|
|
174
|
+
strategy: 'branch',
|
|
175
|
+
};
|
|
176
|
+
} catch (error) {
|
|
177
|
+
throw new Error(`Failed to push to branch: ${error?.message || error}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Create pull request with results
|
|
183
|
+
*
|
|
184
|
+
* @param {Object} options
|
|
185
|
+
* @param {string} options.task - Task description
|
|
186
|
+
* @param {string} [options.baseBranch] - Base branch (defaults to current branch)
|
|
187
|
+
* @param {string} [options.timelineUrl] - Timeline URL
|
|
188
|
+
* @param {Object} [options.conflicts] - Conflict details
|
|
189
|
+
* @param {boolean} [options.conflicts.hasConflicts] - Whether conflicts exist
|
|
190
|
+
* @param {string[]} [options.conflicts.conflictFiles] - Conflicting files
|
|
191
|
+
* @param {Object} [options.metadata] - Session metadata
|
|
192
|
+
* @returns {Promise<Object>} PR result
|
|
193
|
+
* @returns {number} return.prNumber - PR number
|
|
194
|
+
* @returns {string} return.prUrl - PR URL
|
|
195
|
+
* @returns {string} return.strategy - Always 'pr'
|
|
196
|
+
* @throws {Error} If PR creation fails
|
|
197
|
+
*/
|
|
198
|
+
async createPullRequest(options) {
|
|
199
|
+
const { task, baseBranch, timelineUrl, conflicts, metadata } = options;
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
// Fetch remote branch to ensure it exists
|
|
203
|
+
await this._git(`fetch origin ${this.remoteBranch}`);
|
|
204
|
+
|
|
205
|
+
// Get current branch as base if not specified
|
|
206
|
+
const base = baseBranch || await this.gitHandoff.getCurrentBranch();
|
|
207
|
+
|
|
208
|
+
// Generate PR title and body
|
|
209
|
+
const title = this.prCreator.generatePRTitle({
|
|
210
|
+
sessionId: this.sessionId,
|
|
211
|
+
task,
|
|
212
|
+
hasConflicts: conflicts?.hasConflicts || false,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const body = this.prCreator.generatePRBody({
|
|
216
|
+
task,
|
|
217
|
+
sessionId: this.sessionId,
|
|
218
|
+
timelineUrl,
|
|
219
|
+
conflicts,
|
|
220
|
+
metadata,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Create PR
|
|
224
|
+
const pr = await this.prCreator.createPR({
|
|
225
|
+
head: this.remoteBranch,
|
|
226
|
+
base,
|
|
227
|
+
title,
|
|
228
|
+
body,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
if (this.verbose) {
|
|
232
|
+
console.log(`[results-delivery] Created PR #${pr.number}: ${pr.url}`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
prNumber: pr.number,
|
|
237
|
+
prUrl: pr.url,
|
|
238
|
+
strategy: 'pr',
|
|
239
|
+
};
|
|
240
|
+
} catch (error) {
|
|
241
|
+
throw new Error(`Failed to create pull request: ${error?.message || error}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Deliver results using fallback strategy
|
|
247
|
+
*
|
|
248
|
+
* Strategy:
|
|
249
|
+
* 1. Detect conflicts first
|
|
250
|
+
* 2. If conflicts exist, always create PR (never auto-merge conflicts)
|
|
251
|
+
* 3. If no conflicts, try PR creation
|
|
252
|
+
* 4. If PR creation fails, fallback to branch creation
|
|
253
|
+
*
|
|
254
|
+
* @param {Object} options
|
|
255
|
+
* @param {string} options.task - Task description
|
|
256
|
+
* @param {string} [options.baseBranch] - Base branch for PR
|
|
257
|
+
* @param {string} [options.timelineUrl] - Timeline URL
|
|
258
|
+
* @param {Object} [options.metadata] - Session metadata
|
|
259
|
+
* @returns {Promise<Object>} Delivery result
|
|
260
|
+
* @returns {boolean} return.success - Whether delivery succeeded
|
|
261
|
+
* @returns {string} return.strategy - Strategy used ('pr' or 'branch')
|
|
262
|
+
* @returns {number} [return.prNumber] - PR number (if strategy='pr')
|
|
263
|
+
* @returns {string} [return.prUrl] - PR URL (if strategy='pr')
|
|
264
|
+
* @returns {string} [return.branch] - Branch name (if strategy='branch')
|
|
265
|
+
* @returns {string} [return.commit] - Commit hash (if strategy='branch')
|
|
266
|
+
* @returns {Object} [return.conflicts] - Conflict details (if conflicts detected)
|
|
267
|
+
* @returns {Object} [return.metadata] - Session metadata
|
|
268
|
+
* @throws {Error} If all strategies fail
|
|
269
|
+
*/
|
|
270
|
+
async deliverResults(options) {
|
|
271
|
+
const { task, baseBranch, timelineUrl, metadata } = options;
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
// Step 1: Detect conflicts
|
|
275
|
+
const conflicts = await this.detectConflicts();
|
|
276
|
+
|
|
277
|
+
if (this.verbose && conflicts.hasConflicts) {
|
|
278
|
+
console.log(`[results-delivery] Conflicts detected: ${conflicts.conflictFiles.join(', ')}`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Step 2: Try PR creation (preferred)
|
|
282
|
+
try {
|
|
283
|
+
const prResult = await this.createPullRequest({
|
|
284
|
+
task,
|
|
285
|
+
baseBranch,
|
|
286
|
+
timelineUrl,
|
|
287
|
+
conflicts: conflicts.hasConflicts ? conflicts : undefined,
|
|
288
|
+
metadata,
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
success: true,
|
|
293
|
+
strategy: 'pr',
|
|
294
|
+
prNumber: prResult.prNumber,
|
|
295
|
+
prUrl: prResult.prUrl,
|
|
296
|
+
conflicts: conflicts.hasConflicts ? conflicts : undefined,
|
|
297
|
+
metadata,
|
|
298
|
+
};
|
|
299
|
+
} catch (prError) {
|
|
300
|
+
if (this.verbose) {
|
|
301
|
+
console.log(`[results-delivery] PR creation failed: ${prError.message}`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Step 3: Fallback to branch creation
|
|
305
|
+
try {
|
|
306
|
+
const branchResult = await this.pushToBranch();
|
|
307
|
+
|
|
308
|
+
if (this.verbose) {
|
|
309
|
+
console.log(`[results-delivery] Created local branch: ${branchResult.branch}`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
success: true,
|
|
314
|
+
strategy: 'branch',
|
|
315
|
+
branch: branchResult.branch,
|
|
316
|
+
commit: branchResult.commit,
|
|
317
|
+
conflicts: conflicts.hasConflicts ? conflicts : undefined,
|
|
318
|
+
metadata,
|
|
319
|
+
};
|
|
320
|
+
} catch (branchError) {
|
|
321
|
+
// Both strategies failed
|
|
322
|
+
throw new Error(
|
|
323
|
+
`Failed to deliver results. PR creation: ${prError.message}. Branch creation: ${branchError.message}`
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
} catch (error) {
|
|
328
|
+
throw new Error(`Failed to deliver results: ${error?.message || error}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export default ResultsDelivery;
|