@covibes/zeroshot 5.3.0 → 5.4.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/README.md +94 -14
- package/cli/commands/providers.js +8 -9
- package/cli/index.js +3032 -2409
- package/cli/message-formatters-normal.js +28 -6
- package/cluster-templates/base-templates/debug-workflow.json +1 -1
- package/cluster-templates/base-templates/full-workflow.json +72 -188
- package/cluster-templates/base-templates/worker-validator.json +59 -3
- package/cluster-templates/conductor-bootstrap.json +4 -4
- package/lib/docker-config.js +8 -0
- package/lib/git-remote-utils.js +165 -0
- package/lib/id-detector.js +10 -7
- package/lib/provider-defaults.js +62 -0
- package/lib/provider-names.js +2 -1
- package/lib/settings/claude-auth.js +78 -0
- package/lib/settings.js +161 -63
- package/package.json +7 -2
- package/scripts/setup-merge-queue.sh +170 -0
- package/src/agent/agent-config.js +135 -82
- package/src/agent/agent-context-builder.js +297 -188
- package/src/agent/agent-hook-executor.js +310 -113
- package/src/agent/agent-lifecycle.js +385 -325
- package/src/agent/agent-stuck-detector.js +7 -7
- package/src/agent/agent-task-executor.js +824 -565
- package/src/agent/output-extraction.js +41 -24
- package/src/agent/output-reformatter.js +1 -1
- package/src/agent/schema-utils.js +108 -73
- package/src/agent-wrapper.js +10 -1
- package/src/agents/git-pusher-template.js +285 -0
- package/src/claude-task-runner.js +85 -34
- package/src/config-validator.js +922 -657
- package/src/input-helpers.js +65 -0
- package/src/isolation-manager.js +289 -199
- package/src/issue-providers/README.md +305 -0
- package/src/issue-providers/azure-devops-provider.js +273 -0
- package/src/issue-providers/base-provider.js +232 -0
- package/src/issue-providers/github-provider.js +179 -0
- package/src/issue-providers/gitlab-provider.js +241 -0
- package/src/issue-providers/index.js +196 -0
- package/src/issue-providers/jira-provider.js +239 -0
- package/src/ledger.js +22 -5
- package/src/lib/safe-exec.js +88 -0
- package/src/orchestrator.js +1107 -811
- package/src/preflight.js +313 -159
- package/src/process-metrics.js +98 -56
- package/src/providers/anthropic/cli-builder.js +53 -25
- package/src/providers/anthropic/index.js +72 -2
- package/src/providers/anthropic/output-parser.js +32 -14
- package/src/providers/base-provider.js +70 -0
- package/src/providers/capabilities.js +9 -0
- package/src/providers/google/output-parser.js +47 -38
- package/src/providers/index.js +19 -3
- package/src/providers/openai/cli-builder.js +14 -3
- package/src/providers/openai/index.js +1 -0
- package/src/providers/openai/output-parser.js +44 -30
- package/src/providers/opencode/cli-builder.js +42 -0
- package/src/providers/opencode/index.js +103 -0
- package/src/providers/opencode/models.js +55 -0
- package/src/providers/opencode/output-parser.js +122 -0
- package/src/schemas/sub-cluster.js +68 -39
- package/src/status-footer.js +94 -48
- package/src/sub-cluster-wrapper.js +76 -35
- package/src/template-resolver.js +12 -9
- package/src/tui/data-poller.js +123 -99
- package/src/tui/formatters.js +4 -3
- package/src/tui/keybindings.js +259 -318
- package/src/tui/renderer.js +11 -21
- package/task-lib/attachable-watcher.js +118 -81
- package/task-lib/claude-recovery.js +61 -24
- package/task-lib/commands/episodes.js +105 -0
- package/task-lib/commands/list.js +2 -2
- package/task-lib/commands/logs.js +231 -189
- package/task-lib/commands/schedules.js +111 -61
- package/task-lib/config.js +0 -2
- package/task-lib/runner.js +84 -27
- package/task-lib/scheduler.js +1 -1
- package/task-lib/store.js +467 -168
- package/task-lib/tui/formatters.js +94 -45
- package/task-lib/tui/renderer.js +13 -3
- package/task-lib/tui.js +73 -32
- package/task-lib/watcher.js +128 -90
- package/src/agents/git-pusher-agent.json +0 -20
- package/src/github.js +0 -139
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base Provider - Abstract base class for issue providers
|
|
3
|
+
*
|
|
4
|
+
* This class represents a "platform" that can:
|
|
5
|
+
* 1. Fetch issues (all providers)
|
|
6
|
+
* 2. Create PRs/MRs (git-hosting providers only: GitHub, GitLab, Azure DevOps)
|
|
7
|
+
*
|
|
8
|
+
* Defines interface for fetching issues from different sources (GitHub, GitLab, Jira, etc)
|
|
9
|
+
* Each provider must implement:
|
|
10
|
+
* - Static properties: id, displayName
|
|
11
|
+
* - Static methods: detectIdentifier(), getRequiredTool()
|
|
12
|
+
* - Instance method: fetchIssue()
|
|
13
|
+
*
|
|
14
|
+
* Git-hosting providers should also implement:
|
|
15
|
+
* - Static method: supportsPR() returning true
|
|
16
|
+
* - Static method: getPRTool() for PR/MR CLI tool info
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
class IssueProvider {
|
|
20
|
+
/**
|
|
21
|
+
* Provider identifier (e.g., 'github', 'gitlab')
|
|
22
|
+
* @type {string}
|
|
23
|
+
*/
|
|
24
|
+
static id = null;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Human-readable provider name
|
|
28
|
+
* @type {string}
|
|
29
|
+
*/
|
|
30
|
+
static displayName = null;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Whether this provider supports PR/MR creation.
|
|
34
|
+
* Override in subclass to return true for git-hosting platforms.
|
|
35
|
+
* @returns {boolean}
|
|
36
|
+
*/
|
|
37
|
+
static supportsPR() {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get PR/MR CLI tool info for preflight checks.
|
|
43
|
+
* Only git-hosting providers (GitHub, GitLab, Azure DevOps) implement this.
|
|
44
|
+
* @returns {{ name: string, checkCmd: string, installHint: string, displayName: string }|null}
|
|
45
|
+
*/
|
|
46
|
+
static getPRTool() {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Detect if input matches this provider's patterns
|
|
52
|
+
* @param {string} _input - User input (URL, issue key, number)
|
|
53
|
+
* @param {Object} _settings - User settings from settings.js
|
|
54
|
+
* @param {Object|null} _gitContext - Auto-detected git remote context
|
|
55
|
+
* @returns {boolean}
|
|
56
|
+
*/
|
|
57
|
+
static detectIdentifier(_input, _settings, _gitContext) {
|
|
58
|
+
throw new Error(`${this.name}.detectIdentifier() must be implemented by subclass`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Shared detection logic for bare numbers (e.g., "123").
|
|
63
|
+
* Implements the priority cascade:
|
|
64
|
+
* 1. Git context (highest priority) - auto-detected from git remote
|
|
65
|
+
* 2. Settings (defaultIssueSource)
|
|
66
|
+
* 3. Legacy fallback (GitHub only, for backward compatibility)
|
|
67
|
+
*
|
|
68
|
+
* Call this from subclass detectIdentifier() for bare number handling.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} input - User input to check
|
|
71
|
+
* @param {Object} settings - User settings
|
|
72
|
+
* @param {Object|null} gitContext - Git context from detectGitContext()
|
|
73
|
+
* @param {string} providerId - This provider's ID (e.g., 'github', 'gitlab')
|
|
74
|
+
* @param {Object} [options] - Additional options
|
|
75
|
+
* @param {boolean} [options.requiresAdditionalSettings] - If true, check additional settings exist
|
|
76
|
+
* @param {string[]} [options.requiredSettings] - Settings keys that must exist for this provider
|
|
77
|
+
* @returns {boolean} True if this provider should handle the bare number
|
|
78
|
+
*/
|
|
79
|
+
static detectBareNumber(input, settings, gitContext, providerId, options = {}) {
|
|
80
|
+
// Only handle bare numbers
|
|
81
|
+
if (!/^\d+$/.test(input)) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 1. Git context takes highest priority
|
|
86
|
+
if (gitContext?.provider) {
|
|
87
|
+
return gitContext.provider === providerId;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 2. Check settings.defaultIssueSource
|
|
91
|
+
if (settings.defaultIssueSource) {
|
|
92
|
+
if (settings.defaultIssueSource !== providerId) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
// If provider requires additional settings, validate them
|
|
96
|
+
if (options.requiredSettings?.length > 0) {
|
|
97
|
+
return options.requiredSettings.every((key) => settings[key]);
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 3. Legacy fallback - only GitHub gets the fallback for backward compatibility
|
|
103
|
+
return providerId === 'github';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Shared detection logic for org/repo#123 format.
|
|
108
|
+
* Some providers (GitHub, GitLab) use this format.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} input - User input to check
|
|
111
|
+
* @param {Object} settings - User settings
|
|
112
|
+
* @param {Object|null} gitContext - Git context
|
|
113
|
+
* @param {string} providerId - This provider's ID
|
|
114
|
+
* @returns {boolean} True if this provider should handle the input
|
|
115
|
+
*/
|
|
116
|
+
static detectRepoIssueFormat(input, settings, gitContext, providerId) {
|
|
117
|
+
// Match org/repo#123 format
|
|
118
|
+
if (!/^[\w.-]+\/[\w.-]+#\d+$/.test(input)) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 1. Git context takes priority
|
|
123
|
+
if (gitContext?.provider) {
|
|
124
|
+
return gitContext.provider === providerId;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 2. Settings fallback
|
|
128
|
+
return settings.defaultIssueSource === providerId;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get required CLI tool info
|
|
133
|
+
* @returns {{ name: string, checkCmd: string, installHint: string }}
|
|
134
|
+
*/
|
|
135
|
+
static getRequiredTool() {
|
|
136
|
+
throw new Error(`${this.name}.getRequiredTool() must be implemented by subclass`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Check if the provider's CLI is authenticated.
|
|
141
|
+
* Override in subclass to implement provider-specific auth checks.
|
|
142
|
+
*
|
|
143
|
+
* @param {string|null} hostname - Optional hostname for providers with multi-instance support
|
|
144
|
+
* (e.g., GitLab with gitlab.com + self-hosted). When provided, only check auth for that
|
|
145
|
+
* specific instance to avoid false failures from other unconfigured instances.
|
|
146
|
+
* @returns {{ authenticated: boolean, error: string|null, recovery: string[] }}
|
|
147
|
+
* - authenticated: true if auth is valid
|
|
148
|
+
* - error: error message if not authenticated, null otherwise
|
|
149
|
+
* - recovery: array of steps to fix auth issues
|
|
150
|
+
*/
|
|
151
|
+
static checkAuth(_hostname = null) {
|
|
152
|
+
// Default: no auth check required (some CLIs like jira use config files)
|
|
153
|
+
return { authenticated: true, error: null, recovery: [] };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get settings schema for this provider.
|
|
158
|
+
* Override in subclass to define provider-specific settings.
|
|
159
|
+
*
|
|
160
|
+
* Schema format:
|
|
161
|
+
* {
|
|
162
|
+
* settingKey: {
|
|
163
|
+
* type: 'string' | 'number' | 'boolean',
|
|
164
|
+
* nullable: true | false,
|
|
165
|
+
* default: <default value>,
|
|
166
|
+
* description: 'Human-readable description',
|
|
167
|
+
* pattern?: RegExp, // Optional validation pattern
|
|
168
|
+
* patternMsg?: string // Error message if pattern fails
|
|
169
|
+
* }
|
|
170
|
+
* }
|
|
171
|
+
*
|
|
172
|
+
* @returns {Object.<string, Object>} Settings schema
|
|
173
|
+
*/
|
|
174
|
+
static getSettingsSchema() {
|
|
175
|
+
return {};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Validate a setting value for this provider.
|
|
180
|
+
* Uses schema from getSettingsSchema() by default.
|
|
181
|
+
*
|
|
182
|
+
* @param {string} key - Setting key
|
|
183
|
+
* @param {any} value - Setting value
|
|
184
|
+
* @returns {string|null|undefined} Error message if invalid, null if valid, undefined if not this provider's setting
|
|
185
|
+
*/
|
|
186
|
+
static validateSetting(key, value) {
|
|
187
|
+
const schema = this.getSettingsSchema();
|
|
188
|
+
const config = schema[key];
|
|
189
|
+
|
|
190
|
+
if (!config) {
|
|
191
|
+
return undefined; // Not this provider's setting
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Nullable check
|
|
195
|
+
if (config.nullable && value === null) {
|
|
196
|
+
return null; // null is valid
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Type validation
|
|
200
|
+
if (config.type === 'string' && typeof value !== 'string') {
|
|
201
|
+
return `${key} must be a string${config.nullable ? ' or null' : ''}`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Pattern validation
|
|
205
|
+
if (config.pattern && typeof value === 'string' && !config.pattern.test(value)) {
|
|
206
|
+
return config.patternMsg || `${key} has invalid format`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return null; // Valid
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Fetch issue from provider
|
|
214
|
+
* @param {string} _identifier - Issue identifier (URL, key, number)
|
|
215
|
+
* @param {Object} _settings - User settings from settings.js
|
|
216
|
+
* @returns {Promise<Object>} InputData object with structure:
|
|
217
|
+
* {
|
|
218
|
+
* number: Number|null,
|
|
219
|
+
* title: String,
|
|
220
|
+
* body: String,
|
|
221
|
+
* labels: Array<{name: String}>,
|
|
222
|
+
* comments: Array<{author, createdAt, body}>,
|
|
223
|
+
* url: String|null,
|
|
224
|
+
* context: String // Formatted markdown
|
|
225
|
+
* }
|
|
226
|
+
*/
|
|
227
|
+
fetchIssue(_identifier, _settings) {
|
|
228
|
+
throw new Error(`${this.constructor.name}.fetchIssue() must be implemented by subclass`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
module.exports = IssueProvider;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Provider - Fetch issues from GitHub via gh CLI
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const IssueProvider = require('./base-provider');
|
|
6
|
+
const { execSync } = require('../lib/safe-exec');
|
|
7
|
+
|
|
8
|
+
class GitHubProvider extends IssueProvider {
|
|
9
|
+
static id = 'github';
|
|
10
|
+
static displayName = 'GitHub';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* GitHub supports PR creation via gh CLI
|
|
14
|
+
*/
|
|
15
|
+
static supportsPR() {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get PR CLI tool info for preflight checks
|
|
21
|
+
*/
|
|
22
|
+
static getPRTool() {
|
|
23
|
+
return {
|
|
24
|
+
name: 'gh',
|
|
25
|
+
checkCmd: 'gh --version',
|
|
26
|
+
installHint: 'https://cli.github.com/',
|
|
27
|
+
displayName: 'GitHub',
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Detect GitHub URLs and issue references
|
|
33
|
+
* Matches:
|
|
34
|
+
* - github.com URLs
|
|
35
|
+
* - org/repo#123 format
|
|
36
|
+
* - Bare numbers when git remote is GitHub (auto-detected)
|
|
37
|
+
* - Bare numbers when defaultIssueSource=github
|
|
38
|
+
*
|
|
39
|
+
* @param {string} input - Issue identifier (URL, number, or org/repo#123)
|
|
40
|
+
* @param {Object} settings - User settings
|
|
41
|
+
* @param {Object|null} gitContext - Auto-detected git remote context
|
|
42
|
+
* @returns {boolean} True if this provider should handle the input
|
|
43
|
+
*/
|
|
44
|
+
static detectIdentifier(input, settings, gitContext = null) {
|
|
45
|
+
// GitHub URLs
|
|
46
|
+
if (input.includes('github.com') && /\/issues\/\d+/.test(input)) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// org/repo#123 format - use shared logic
|
|
51
|
+
if (IssueProvider.detectRepoIssueFormat(input, settings, gitContext, 'github')) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Bare numbers - use shared priority cascade logic
|
|
56
|
+
return IssueProvider.detectBareNumber(input, settings, gitContext, 'github');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
static getRequiredTool() {
|
|
60
|
+
return {
|
|
61
|
+
name: 'gh',
|
|
62
|
+
checkCmd: 'gh --version',
|
|
63
|
+
installHint: 'Install gh CLI: https://cli.github.com/',
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check gh CLI authentication
|
|
69
|
+
*/
|
|
70
|
+
static checkAuth() {
|
|
71
|
+
try {
|
|
72
|
+
execSync('gh auth status', { encoding: 'utf8', stdio: 'pipe' });
|
|
73
|
+
return { authenticated: true, error: null, recovery: [] };
|
|
74
|
+
} catch (err) {
|
|
75
|
+
const stderr = err.stderr || err.message || '';
|
|
76
|
+
|
|
77
|
+
if (stderr.includes('not logged in')) {
|
|
78
|
+
return {
|
|
79
|
+
authenticated: false,
|
|
80
|
+
error: 'gh CLI not authenticated',
|
|
81
|
+
recovery: [
|
|
82
|
+
'Run: gh auth login',
|
|
83
|
+
'Select GitHub.com, HTTPS, and authenticate via browser',
|
|
84
|
+
'Then verify: gh auth status',
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
authenticated: false,
|
|
91
|
+
error: stderr.trim() || 'Unknown gh auth error',
|
|
92
|
+
recovery: ['Run: gh auth login', 'Then verify: gh auth status'],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* GitHub-specific settings schema
|
|
99
|
+
* GitHub has no extra settings (cloud-only, no self-hosted config needed)
|
|
100
|
+
*/
|
|
101
|
+
static getSettingsSchema() {
|
|
102
|
+
return {};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
fetchIssue(identifier, _settings) {
|
|
106
|
+
try {
|
|
107
|
+
const issueNumber = this._extractIssueNumber(identifier);
|
|
108
|
+
|
|
109
|
+
// Fetch issue using gh CLI
|
|
110
|
+
const cmd = `gh issue view ${issueNumber} --json number,title,body,labels,assignees,comments,url`;
|
|
111
|
+
const output = execSync(cmd, { encoding: 'utf8' });
|
|
112
|
+
const issue = JSON.parse(output);
|
|
113
|
+
|
|
114
|
+
return this._parseIssue(issue);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
throw new Error(`Failed to fetch GitHub issue: ${error.message}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Extract issue number from URL or return as-is
|
|
122
|
+
* @private
|
|
123
|
+
*/
|
|
124
|
+
_extractIssueNumber(issueRef) {
|
|
125
|
+
// If it's a URL, extract the number
|
|
126
|
+
const urlMatch = issueRef.match(/\/issues\/(\d+)/);
|
|
127
|
+
if (urlMatch) {
|
|
128
|
+
return urlMatch[1];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// org/repo#123 format
|
|
132
|
+
const repoMatch = issueRef.match(/^[\w-]+\/[\w-]+#(\d+)$/);
|
|
133
|
+
if (repoMatch) {
|
|
134
|
+
return repoMatch[1];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Otherwise assume it's already a number
|
|
138
|
+
return issueRef;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Parse issue into standardized InputData format
|
|
143
|
+
* @private
|
|
144
|
+
*/
|
|
145
|
+
_parseIssue(issue) {
|
|
146
|
+
let context = `# GitHub Issue #${issue.number}\n\n`;
|
|
147
|
+
context += `## Title\n${issue.title}\n\n`;
|
|
148
|
+
|
|
149
|
+
if (issue.body) {
|
|
150
|
+
context += `## Description\n${issue.body}\n\n`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (issue.labels && issue.labels.length > 0) {
|
|
154
|
+
context += `## Labels\n`;
|
|
155
|
+
context += issue.labels.map((l) => `- ${l.name}`).join('\n');
|
|
156
|
+
context += '\n\n';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (issue.comments && issue.comments.length > 0) {
|
|
160
|
+
context += `## Comments\n\n`;
|
|
161
|
+
for (const comment of issue.comments) {
|
|
162
|
+
context += `### ${comment.author.login} (${new Date(comment.createdAt).toISOString()})\n`;
|
|
163
|
+
context += `${comment.body}\n\n`;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
number: issue.number,
|
|
169
|
+
title: issue.title,
|
|
170
|
+
body: issue.body,
|
|
171
|
+
labels: issue.labels || [],
|
|
172
|
+
comments: issue.comments || [],
|
|
173
|
+
url: issue.url || null,
|
|
174
|
+
context,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
module.exports = GitHubProvider;
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitLab Provider - Fetch issues from GitLab via glab CLI
|
|
3
|
+
* Supports both cloud (gitlab.com) and self-hosted instances
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const IssueProvider = require('./base-provider');
|
|
7
|
+
const { execSync } = require('../lib/safe-exec');
|
|
8
|
+
|
|
9
|
+
class GitLabProvider extends IssueProvider {
|
|
10
|
+
static id = 'gitlab';
|
|
11
|
+
static displayName = 'GitLab';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* GitLab supports MR creation via glab CLI
|
|
15
|
+
*/
|
|
16
|
+
static supportsPR() {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get MR CLI tool info for preflight checks
|
|
22
|
+
*/
|
|
23
|
+
static getPRTool() {
|
|
24
|
+
return {
|
|
25
|
+
name: 'glab',
|
|
26
|
+
checkCmd: 'glab --version',
|
|
27
|
+
installHint: 'brew install glab (or https://gitlab.com/gitlab-org/cli)',
|
|
28
|
+
displayName: 'GitLab',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Detect GitLab URLs and issue references
|
|
34
|
+
* Matches:
|
|
35
|
+
* - gitlab.com URLs
|
|
36
|
+
* - Self-hosted GitLab URLs (via gitlabInstance setting)
|
|
37
|
+
* - org/repo#123 when git remote is GitLab (auto-detected)
|
|
38
|
+
* - org/repo#123 when defaultIssueSource=gitlab
|
|
39
|
+
* - Bare numbers when git remote is GitLab (auto-detected)
|
|
40
|
+
* - Bare numbers when defaultIssueSource=gitlab
|
|
41
|
+
*
|
|
42
|
+
* @param {string} input - Issue identifier (URL, number, or org/repo#123)
|
|
43
|
+
* @param {Object} settings - User settings
|
|
44
|
+
* @param {Object|null} gitContext - Auto-detected git remote context
|
|
45
|
+
* @returns {boolean} True if this provider should handle the input
|
|
46
|
+
*/
|
|
47
|
+
static detectIdentifier(input, settings, gitContext = null) {
|
|
48
|
+
// GitLab cloud URLs
|
|
49
|
+
if (input.includes('gitlab.com') && /\/-\/issues\/\d+/.test(input)) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Self-hosted GitLab URLs
|
|
54
|
+
if (
|
|
55
|
+
settings.gitlabInstance &&
|
|
56
|
+
input.includes(settings.gitlabInstance) &&
|
|
57
|
+
/\/-\/issues\/\d+/.test(input)
|
|
58
|
+
) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// org/repo#123 format - use shared logic
|
|
63
|
+
if (IssueProvider.detectRepoIssueFormat(input, settings, gitContext, 'gitlab')) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Bare numbers - use shared priority cascade logic
|
|
68
|
+
return IssueProvider.detectBareNumber(input, settings, gitContext, 'gitlab');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
static getRequiredTool() {
|
|
72
|
+
return {
|
|
73
|
+
name: 'glab',
|
|
74
|
+
checkCmd: 'glab --version',
|
|
75
|
+
installHint: 'Install glab CLI: brew install glab (or https://gitlab.com/gitlab-org/cli)',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Check glab CLI authentication for a specific hostname.
|
|
81
|
+
* If hostname is provided, only checks that specific instance.
|
|
82
|
+
* Otherwise, uses glab's default context detection (git remote).
|
|
83
|
+
*
|
|
84
|
+
* @param {string|null} hostname - GitLab hostname to check (e.g., 'gitlab.com', 'gitlab.lrz.de')
|
|
85
|
+
*/
|
|
86
|
+
static checkAuth(hostname = null) {
|
|
87
|
+
// Use --hostname flag to check specific instance, avoiding false failures
|
|
88
|
+
// when other configured instances are unauthenticated
|
|
89
|
+
const cmd = hostname ? `glab auth status --hostname ${hostname}` : 'glab auth status';
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
execSync(cmd, { encoding: 'utf8', stdio: 'pipe' });
|
|
93
|
+
return { authenticated: true, error: null, recovery: [] };
|
|
94
|
+
} catch (err) {
|
|
95
|
+
const stderr = err.stderr || err.message || '';
|
|
96
|
+
|
|
97
|
+
const hostnameHint = hostname || 'your GitLab instance';
|
|
98
|
+
if (
|
|
99
|
+
stderr.includes('not logged in') ||
|
|
100
|
+
stderr.includes('no authentication') ||
|
|
101
|
+
stderr.includes('No token found')
|
|
102
|
+
) {
|
|
103
|
+
return {
|
|
104
|
+
authenticated: false,
|
|
105
|
+
error: `glab CLI not authenticated for ${hostnameHint}`,
|
|
106
|
+
recovery: [
|
|
107
|
+
hostname ? `Run: glab auth login --hostname ${hostname}` : 'Run: glab auth login',
|
|
108
|
+
'Authenticate via browser when prompted',
|
|
109
|
+
`Then verify: glab auth status${hostname ? ` --hostname ${hostname}` : ''}`,
|
|
110
|
+
],
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
authenticated: false,
|
|
116
|
+
error: stderr.trim() || 'Unknown glab auth error',
|
|
117
|
+
recovery: [
|
|
118
|
+
hostname ? `Run: glab auth login --hostname ${hostname}` : 'Run: glab auth login',
|
|
119
|
+
'Then verify: glab auth status',
|
|
120
|
+
],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* GitLab-specific settings schema
|
|
127
|
+
*/
|
|
128
|
+
static getSettingsSchema() {
|
|
129
|
+
return {
|
|
130
|
+
gitlabInstance: {
|
|
131
|
+
type: 'string',
|
|
132
|
+
nullable: true,
|
|
133
|
+
default: null,
|
|
134
|
+
description: "Self-hosted GitLab URL (e.g., 'gitlab.company.com')",
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
fetchIssue(identifier, _settings) {
|
|
140
|
+
try {
|
|
141
|
+
// glab accepts full URLs directly - use that for simplicity
|
|
142
|
+
const isUrl = /^https?:\/\//.test(identifier);
|
|
143
|
+
let cmd;
|
|
144
|
+
|
|
145
|
+
if (isUrl) {
|
|
146
|
+
// Pass URL directly to glab - handles self-hosted instances automatically
|
|
147
|
+
cmd = `glab issue view "${identifier}" --output json`;
|
|
148
|
+
} else {
|
|
149
|
+
// For org/repo#123 or bare numbers, parse and build command
|
|
150
|
+
const { issueNumber, repo } = this._parseIdentifier(identifier);
|
|
151
|
+
cmd = `glab issue view ${issueNumber} --output json`;
|
|
152
|
+
if (repo) {
|
|
153
|
+
cmd += ` --repo ${repo}`;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const output = execSync(cmd, { encoding: 'utf8' });
|
|
158
|
+
const issue = JSON.parse(output);
|
|
159
|
+
|
|
160
|
+
return this._parseIssue(issue);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
throw new Error(`Failed to fetch GitLab issue: ${error.message}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Parse identifier to extract issue number, repo, and hostname
|
|
168
|
+
* @private
|
|
169
|
+
*/
|
|
170
|
+
_parseIdentifier(identifier) {
|
|
171
|
+
// URL format: https://gitlab.example.com/org/repo/-/issues/123
|
|
172
|
+
const urlMatch = identifier.match(/https?:\/\/([^/]+)\/([^/]+\/[^/]+)\/-\/issues\/(\d+)/);
|
|
173
|
+
if (urlMatch) {
|
|
174
|
+
const hostname = urlMatch[1];
|
|
175
|
+
return {
|
|
176
|
+
hostname: hostname !== 'gitlab.com' ? hostname : null,
|
|
177
|
+
repo: urlMatch[2],
|
|
178
|
+
issueNumber: urlMatch[3],
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// org/repo#123 format
|
|
183
|
+
const repoMatch = identifier.match(/^([\w-]+\/[\w-]+)#(\d+)$/);
|
|
184
|
+
if (repoMatch) {
|
|
185
|
+
return { hostname: null, repo: repoMatch[1], issueNumber: repoMatch[2] };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Bare number (relies on glab's default repo)
|
|
189
|
+
return { hostname: null, repo: null, issueNumber: identifier };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Parse GitLab issue into standardized InputData format
|
|
194
|
+
* @private
|
|
195
|
+
*/
|
|
196
|
+
_parseIssue(issue) {
|
|
197
|
+
let context = `# GitLab Issue #${issue.iid || issue.id}\n\n`;
|
|
198
|
+
context += `## Title\n${issue.title}\n\n`;
|
|
199
|
+
|
|
200
|
+
if (issue.description) {
|
|
201
|
+
context += `## Description\n${issue.description}\n\n`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (issue.labels && issue.labels.length > 0) {
|
|
205
|
+
context += `## Labels\n`;
|
|
206
|
+
context += issue.labels.map((l) => `- ${l}`).join('\n');
|
|
207
|
+
context += '\n\n';
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (issue.notes && issue.notes.length > 0) {
|
|
211
|
+
context += `## Comments\n\n`;
|
|
212
|
+
for (const note of issue.notes) {
|
|
213
|
+
const author = note.author?.username || 'unknown';
|
|
214
|
+
context += `### ${author} (${note.created_at})\n`;
|
|
215
|
+
context += `${note.body}\n\n`;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Map GitLab labels to GitHub format
|
|
220
|
+
const labels = (issue.labels || []).map((name) => ({ name }));
|
|
221
|
+
|
|
222
|
+
// Map GitLab notes to GitHub comment format
|
|
223
|
+
const comments = (issue.notes || []).map((note) => ({
|
|
224
|
+
author: { login: note.author?.username || 'unknown' },
|
|
225
|
+
createdAt: note.created_at,
|
|
226
|
+
body: note.body,
|
|
227
|
+
}));
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
number: issue.iid || issue.id || null,
|
|
231
|
+
title: issue.title,
|
|
232
|
+
body: issue.description || '',
|
|
233
|
+
labels,
|
|
234
|
+
comments,
|
|
235
|
+
url: issue.web_url || null,
|
|
236
|
+
context,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
module.exports = GitLabProvider;
|