@oss-autopilot/core 0.58.0 → 0.59.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/dist/cli-registry.js +54 -0
- package/dist/cli.bundle.cjs +49 -45
- package/dist/commands/comments.d.ts +28 -0
- package/dist/commands/comments.js +28 -0
- package/dist/commands/config.d.ts +11 -0
- package/dist/commands/config.js +11 -0
- package/dist/commands/daily.d.ts +26 -2
- package/dist/commands/daily.js +26 -2
- package/dist/commands/detect-formatters.d.ts +11 -0
- package/dist/commands/detect-formatters.js +24 -0
- package/dist/commands/dismiss.d.ts +17 -0
- package/dist/commands/dismiss.js +17 -0
- package/dist/commands/index.d.ts +3 -1
- package/dist/commands/index.js +2 -0
- package/dist/commands/init.d.ts +8 -0
- package/dist/commands/init.js +8 -0
- package/dist/commands/move.d.ts +10 -0
- package/dist/commands/move.js +10 -0
- package/dist/commands/search.d.ts +18 -0
- package/dist/commands/search.js +18 -0
- package/dist/commands/setup.d.ts +17 -0
- package/dist/commands/setup.js +17 -0
- package/dist/commands/shelve.d.ts +16 -0
- package/dist/commands/shelve.js +16 -0
- package/dist/commands/startup.d.ts +16 -7
- package/dist/commands/startup.js +16 -7
- package/dist/commands/status.d.ts +8 -0
- package/dist/commands/status.js +8 -0
- package/dist/commands/track.d.ts +16 -0
- package/dist/commands/track.js +16 -0
- package/dist/commands/vet.d.ts +8 -0
- package/dist/commands/vet.js +8 -0
- package/dist/core/daily-logic.d.ts +60 -7
- package/dist/core/daily-logic.js +52 -7
- package/dist/core/formatter-detection.d.ts +61 -0
- package/dist/core/formatter-detection.js +360 -0
- package/dist/core/github.d.ts +25 -2
- package/dist/core/github.js +25 -2
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/issue-discovery.d.ts +46 -6
- package/dist/core/issue-discovery.js +46 -6
- package/dist/core/logger.d.ts +13 -0
- package/dist/core/logger.js +13 -0
- package/dist/core/pr-monitor.d.ts +43 -8
- package/dist/core/pr-monitor.js +43 -8
- package/dist/core/state.d.ts +167 -0
- package/dist/core/state.js +167 -0
- package/dist/core/types.d.ts +2 -8
- package/dist/formatters/json.d.ts +5 -0
- package/package.json +6 -3
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formatter Detection Module (#703)
|
|
3
|
+
*
|
|
4
|
+
* Programmatically detects formatters/linters configured in a local repo directory
|
|
5
|
+
* and diagnoses CI formatting failures from log output.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import { debug } from './logger.js';
|
|
10
|
+
const MODULE = 'formatter-detection';
|
|
11
|
+
// ── Prettier config file patterns ──────────────────────────────────────────
|
|
12
|
+
const PRETTIER_CONFIG_PATTERNS = [
|
|
13
|
+
'.prettierrc',
|
|
14
|
+
'.prettierrc.json',
|
|
15
|
+
'.prettierrc.yml',
|
|
16
|
+
'.prettierrc.yaml',
|
|
17
|
+
'.prettierrc.json5',
|
|
18
|
+
'.prettierrc.js',
|
|
19
|
+
'.prettierrc.cjs',
|
|
20
|
+
'.prettierrc.mjs',
|
|
21
|
+
'.prettierrc.toml',
|
|
22
|
+
'prettier.config.js',
|
|
23
|
+
'prettier.config.cjs',
|
|
24
|
+
'prettier.config.mjs',
|
|
25
|
+
];
|
|
26
|
+
// ── ESLint config file patterns ────────────────────────────────────────────
|
|
27
|
+
const ESLINT_CONFIG_PATTERNS = [
|
|
28
|
+
'.eslintrc',
|
|
29
|
+
'.eslintrc.js',
|
|
30
|
+
'.eslintrc.cjs',
|
|
31
|
+
'.eslintrc.yml',
|
|
32
|
+
'.eslintrc.yaml',
|
|
33
|
+
'.eslintrc.json',
|
|
34
|
+
'eslint.config.js',
|
|
35
|
+
'eslint.config.cjs',
|
|
36
|
+
'eslint.config.mjs',
|
|
37
|
+
'eslint.config.ts',
|
|
38
|
+
];
|
|
39
|
+
// ── package.json script names that indicate formatting ─────────────────────
|
|
40
|
+
const FORMAT_SCRIPT_NAMES = ['lint:fix', 'format', 'fmt', 'lint', 'format:check', 'format:fix'];
|
|
41
|
+
// ── CI log patterns for each formatter ────────────────────────────────────
|
|
42
|
+
const CI_PATTERNS = [
|
|
43
|
+
{
|
|
44
|
+
formatter: 'prettier',
|
|
45
|
+
patterns: [/Code style issues found/i, /Forgot to run Prettier/i, /prettier --check/i],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
formatter: 'ruff',
|
|
49
|
+
patterns: [/ruff format.*--check/i, /ruff format.*would reformat/i],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
formatter: 'black',
|
|
53
|
+
patterns: [/Oh no! .* files? would be reformatted/i, /black --check/i],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
formatter: 'rustfmt',
|
|
57
|
+
patterns: [/Diff in .*\.rs/i, /rustfmt --check/i, /cargo fmt.*--check/i],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
formatter: 'biome',
|
|
61
|
+
patterns: [/biome check/i, /biome ci/i, /Found \d+ fixable diagnostics?/i],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
formatter: 'eslint',
|
|
65
|
+
patterns: [/eslint.*--fix/i, /eslint.*\d+ problems?/i],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
formatter: 'gofmt',
|
|
69
|
+
patterns: [/gofmt -d/i, /goimports/i],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
formatter: 'clang-format',
|
|
73
|
+
patterns: [/clang-format/i],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
formatter: 'rubocop',
|
|
77
|
+
patterns: [/rubocop.*offense/i, /rubocop -a/i],
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
/**
|
|
81
|
+
* Safely read and parse a JSON file. Returns undefined on failure.
|
|
82
|
+
*/
|
|
83
|
+
function readJsonFile(filePath) {
|
|
84
|
+
try {
|
|
85
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
86
|
+
return JSON.parse(content);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
debug(MODULE, `Failed to parse ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Safely read a text file. Returns undefined on failure.
|
|
95
|
+
*/
|
|
96
|
+
function readTextFile(filePath) {
|
|
97
|
+
try {
|
|
98
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
debug(MODULE, `Failed to read ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Find the first existing file from a list of candidates in a directory.
|
|
107
|
+
*/
|
|
108
|
+
function findFirstExisting(dir, candidates) {
|
|
109
|
+
for (const candidate of candidates) {
|
|
110
|
+
if (fs.existsSync(path.join(dir, candidate))) {
|
|
111
|
+
return candidate;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Read and parse package.json once. Returns undefined if not found or invalid.
|
|
118
|
+
*/
|
|
119
|
+
function readPackageJson(repoPath) {
|
|
120
|
+
return readJsonFile(path.join(repoPath, 'package.json'));
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Extract formatting-related scripts from a parsed package.json.
|
|
124
|
+
*/
|
|
125
|
+
function extractPackageJsonScripts(pkg) {
|
|
126
|
+
if (!pkg?.scripts)
|
|
127
|
+
return [];
|
|
128
|
+
const results = [];
|
|
129
|
+
for (const scriptName of FORMAT_SCRIPT_NAMES) {
|
|
130
|
+
const command = pkg.scripts[scriptName];
|
|
131
|
+
if (command) {
|
|
132
|
+
results.push({ name: scriptName, command });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return results;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Check if prettier is listed in devDependencies or dependencies.
|
|
139
|
+
*/
|
|
140
|
+
function hasPrettierDependency(pkg) {
|
|
141
|
+
if (!pkg)
|
|
142
|
+
return false;
|
|
143
|
+
return !!(pkg.devDependencies?.['prettier'] || pkg.dependencies?.['prettier']);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Check if a TOML file contains a specific section header.
|
|
147
|
+
*/
|
|
148
|
+
function tomlHasSection(repoPath, fileName, sectionPattern) {
|
|
149
|
+
const content = readTextFile(path.join(repoPath, fileName));
|
|
150
|
+
if (!content)
|
|
151
|
+
return false;
|
|
152
|
+
return content.includes(sectionPattern);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Detect formatters and linters configured in a repository.
|
|
156
|
+
*
|
|
157
|
+
* Checks config files in priority order using fs.existsSync() / fs.readFileSync().
|
|
158
|
+
* Returns all detected formatters, plus any formatting-related package.json scripts.
|
|
159
|
+
*
|
|
160
|
+
* @param repoPath - Absolute path to the repository root directory
|
|
161
|
+
* @returns Detection result with formatters ordered by priority and extracted package.json scripts
|
|
162
|
+
* @throws {Error} If repoPath does not exist or is not a directory
|
|
163
|
+
*/
|
|
164
|
+
export function detectFormatters(repoPath) {
|
|
165
|
+
if (!fs.existsSync(repoPath) || !fs.statSync(repoPath).isDirectory()) {
|
|
166
|
+
throw new Error(`Repository path does not exist or is not a directory: ${repoPath}`);
|
|
167
|
+
}
|
|
168
|
+
const formatters = [];
|
|
169
|
+
const pkg = readPackageJson(repoPath);
|
|
170
|
+
// 1. Biome (highest priority for JS/TS — replaces prettier + eslint)
|
|
171
|
+
const biomeConfig = findFirstExisting(repoPath, ['biome.json', 'biome.jsonc']);
|
|
172
|
+
if (biomeConfig) {
|
|
173
|
+
formatters.push({
|
|
174
|
+
name: 'biome',
|
|
175
|
+
configPath: biomeConfig,
|
|
176
|
+
fixCommand: 'npx @biomejs/biome check --write',
|
|
177
|
+
checkCommand: 'npx @biomejs/biome check',
|
|
178
|
+
supportsFileArgs: true,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
// 2. Prettier
|
|
182
|
+
const prettierConfig = findFirstExisting(repoPath, PRETTIER_CONFIG_PATTERNS);
|
|
183
|
+
if (prettierConfig) {
|
|
184
|
+
formatters.push({
|
|
185
|
+
name: 'prettier',
|
|
186
|
+
configPath: prettierConfig,
|
|
187
|
+
fixCommand: 'npx prettier --write .',
|
|
188
|
+
checkCommand: 'npx prettier --check .',
|
|
189
|
+
supportsFileArgs: true,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
else if (hasPrettierDependency(pkg)) {
|
|
193
|
+
formatters.push({
|
|
194
|
+
name: 'prettier',
|
|
195
|
+
configPath: 'package.json',
|
|
196
|
+
fixCommand: 'npx prettier --write .',
|
|
197
|
+
checkCommand: 'npx prettier --check .',
|
|
198
|
+
supportsFileArgs: true,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
// 3. ESLint
|
|
202
|
+
const eslintConfig = findFirstExisting(repoPath, ESLINT_CONFIG_PATTERNS);
|
|
203
|
+
if (eslintConfig) {
|
|
204
|
+
formatters.push({
|
|
205
|
+
name: 'eslint',
|
|
206
|
+
configPath: eslintConfig,
|
|
207
|
+
fixCommand: 'npx eslint --fix .',
|
|
208
|
+
checkCommand: 'npx eslint .',
|
|
209
|
+
supportsFileArgs: true,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
// 4. Rust (Cargo.toml → rustfmt)
|
|
213
|
+
if (fs.existsSync(path.join(repoPath, 'Cargo.toml'))) {
|
|
214
|
+
formatters.push({
|
|
215
|
+
name: 'rustfmt',
|
|
216
|
+
configPath: 'Cargo.toml',
|
|
217
|
+
fixCommand: 'cargo fmt',
|
|
218
|
+
checkCommand: 'cargo fmt --check',
|
|
219
|
+
supportsFileArgs: false,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
// 5. Python — ruff takes priority over black
|
|
223
|
+
const hasPyproject = fs.existsSync(path.join(repoPath, 'pyproject.toml'));
|
|
224
|
+
if (hasPyproject && tomlHasSection(repoPath, 'pyproject.toml', '[tool.ruff]')) {
|
|
225
|
+
formatters.push({
|
|
226
|
+
name: 'ruff',
|
|
227
|
+
configPath: 'pyproject.toml',
|
|
228
|
+
fixCommand: 'ruff format .',
|
|
229
|
+
checkCommand: 'ruff format --check .',
|
|
230
|
+
supportsFileArgs: true,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
else if (hasPyproject && tomlHasSection(repoPath, 'pyproject.toml', '[tool.black]')) {
|
|
234
|
+
formatters.push({
|
|
235
|
+
name: 'black',
|
|
236
|
+
configPath: 'pyproject.toml',
|
|
237
|
+
fixCommand: 'black .',
|
|
238
|
+
checkCommand: 'black --check .',
|
|
239
|
+
supportsFileArgs: true,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
else if (fs.existsSync(path.join(repoPath, 'ruff.toml'))) {
|
|
243
|
+
formatters.push({
|
|
244
|
+
name: 'ruff',
|
|
245
|
+
configPath: 'ruff.toml',
|
|
246
|
+
fixCommand: 'ruff format .',
|
|
247
|
+
checkCommand: 'ruff format --check .',
|
|
248
|
+
supportsFileArgs: true,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
// 6. Go
|
|
252
|
+
if (fs.existsSync(path.join(repoPath, 'go.mod'))) {
|
|
253
|
+
formatters.push({
|
|
254
|
+
name: 'gofmt',
|
|
255
|
+
configPath: 'go.mod',
|
|
256
|
+
fixCommand: 'gofmt -w .',
|
|
257
|
+
checkCommand: 'gofmt -d .',
|
|
258
|
+
supportsFileArgs: true,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
// 7. Clang-format
|
|
262
|
+
if (fs.existsSync(path.join(repoPath, '.clang-format'))) {
|
|
263
|
+
formatters.push({
|
|
264
|
+
name: 'clang-format',
|
|
265
|
+
configPath: '.clang-format',
|
|
266
|
+
fixCommand: 'clang-format -i',
|
|
267
|
+
checkCommand: 'clang-format --dry-run --Werror',
|
|
268
|
+
supportsFileArgs: true,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
// 8. RuboCop
|
|
272
|
+
if (fs.existsSync(path.join(repoPath, '.rubocop.yml'))) {
|
|
273
|
+
formatters.push({
|
|
274
|
+
name: 'rubocop',
|
|
275
|
+
configPath: '.rubocop.yml',
|
|
276
|
+
fixCommand: 'rubocop -a',
|
|
277
|
+
checkCommand: 'rubocop',
|
|
278
|
+
supportsFileArgs: true,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
// Extract package.json scripts
|
|
282
|
+
const packageJsonScripts = extractPackageJsonScripts(pkg);
|
|
283
|
+
debug(MODULE, `Detected ${formatters.length} formatters in ${repoPath}`);
|
|
284
|
+
return { formatters, packageJsonScripts, repoPath };
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Diagnose whether CI log output indicates a formatting failure.
|
|
288
|
+
*
|
|
289
|
+
* Pattern-matches known formatter error strings. When repoPath is provided,
|
|
290
|
+
* cross-references with {@link detectFormatters} to provide a targeted fix command.
|
|
291
|
+
*
|
|
292
|
+
* @param logOutput - Raw CI log output to analyze
|
|
293
|
+
* @param repoPath - Optional repo path for cross-referencing with local formatter config
|
|
294
|
+
* @returns Diagnosis with matched formatter, fix command, and evidence strings
|
|
295
|
+
*/
|
|
296
|
+
export function diagnoseCIFormatterFailure(logOutput, repoPath) {
|
|
297
|
+
if (!logOutput.trim()) {
|
|
298
|
+
return { isFormattingFailure: false, evidence: [] };
|
|
299
|
+
}
|
|
300
|
+
const evidence = [];
|
|
301
|
+
let matchedFormatter;
|
|
302
|
+
for (const { formatter, patterns } of CI_PATTERNS) {
|
|
303
|
+
for (const pattern of patterns) {
|
|
304
|
+
const match = logOutput.match(pattern);
|
|
305
|
+
if (match) {
|
|
306
|
+
evidence.push(match[0]);
|
|
307
|
+
if (!matchedFormatter) {
|
|
308
|
+
matchedFormatter = formatter;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (!matchedFormatter) {
|
|
314
|
+
return { isFormattingFailure: false, evidence: [] };
|
|
315
|
+
}
|
|
316
|
+
// Cross-reference with local detection to get the fix command
|
|
317
|
+
let fixCommand;
|
|
318
|
+
if (repoPath) {
|
|
319
|
+
try {
|
|
320
|
+
const detected = detectFormatters(repoPath);
|
|
321
|
+
const localMatch = detected.formatters.find((f) => f.name === matchedFormatter);
|
|
322
|
+
if (localMatch) {
|
|
323
|
+
fixCommand = localMatch.fixCommand;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
catch (err) {
|
|
327
|
+
debug(MODULE, `Cross-reference failed for ${repoPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// Fallback fix commands when CI-matched formatter wasn't found locally or no repoPath provided
|
|
331
|
+
if (!fixCommand) {
|
|
332
|
+
const fallbackCommands = {
|
|
333
|
+
prettier: 'npx prettier --write .',
|
|
334
|
+
eslint: 'npx eslint --fix .',
|
|
335
|
+
biome: 'npx @biomejs/biome check --write',
|
|
336
|
+
black: 'black .',
|
|
337
|
+
ruff: 'ruff format .',
|
|
338
|
+
rustfmt: 'cargo fmt',
|
|
339
|
+
gofmt: 'gofmt -w .',
|
|
340
|
+
'clang-format': 'clang-format -i',
|
|
341
|
+
rubocop: 'rubocop -a',
|
|
342
|
+
};
|
|
343
|
+
fixCommand = fallbackCommands[matchedFormatter];
|
|
344
|
+
}
|
|
345
|
+
return {
|
|
346
|
+
isFormattingFailure: true,
|
|
347
|
+
formatter: matchedFormatter,
|
|
348
|
+
fixCommand,
|
|
349
|
+
evidence,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Return the first (highest-priority) detected formatter, or undefined if none found.
|
|
354
|
+
*
|
|
355
|
+
* @param result - Detection result from {@link detectFormatters}
|
|
356
|
+
* @returns The highest-priority formatter, or undefined if none detected
|
|
357
|
+
*/
|
|
358
|
+
export function getPreferredFormatter(result) {
|
|
359
|
+
return result.formatters[0];
|
|
360
|
+
}
|
package/dist/core/github.d.ts
CHANGED
|
@@ -11,14 +11,37 @@ export interface RateLimitInfo {
|
|
|
11
11
|
/** ISO timestamp when the rate limit window resets. */
|
|
12
12
|
resetAt: string;
|
|
13
13
|
}
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Throttle callbacks used by the Octokit client. Exported for testability.
|
|
16
|
+
* @returns Rate limit and secondary rate limit handler callbacks
|
|
17
|
+
*/
|
|
15
18
|
export declare function getRateLimitCallbacks(): {
|
|
16
19
|
onRateLimit: (retryAfter: number, options: unknown, _octokit: unknown, retryCount: number) => boolean;
|
|
17
20
|
onSecondaryRateLimit: (retryAfter: number, options: unknown, _octokit: unknown, retryCount: number) => boolean;
|
|
18
21
|
};
|
|
22
|
+
/**
|
|
23
|
+
* Get a shared, throttled Octokit instance for the given token.
|
|
24
|
+
* Returns a cached instance if the token matches, otherwise creates a new one.
|
|
25
|
+
*
|
|
26
|
+
* The client retries on primary rate limits (up to 2 retries) and
|
|
27
|
+
* secondary rate limits (1 retry).
|
|
28
|
+
*
|
|
29
|
+
* @param token - GitHub personal access token
|
|
30
|
+
* @returns Authenticated Octokit instance with rate limit throttling
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* import { getOctokit, requireGitHubToken } from '@oss-autopilot/core';
|
|
35
|
+
*
|
|
36
|
+
* const octokit = getOctokit(requireGitHubToken());
|
|
37
|
+
* const { data } = await octokit.repos.get({ owner: 'facebook', repo: 'react' });
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
19
40
|
export declare function getOctokit(token: string): Octokit;
|
|
20
41
|
/**
|
|
21
42
|
* Check the GitHub Search API rate limit quota.
|
|
22
|
-
*
|
|
43
|
+
*
|
|
44
|
+
* @param token - GitHub personal access token
|
|
45
|
+
* @returns Remaining requests, total limit, and reset time for the search endpoint
|
|
23
46
|
*/
|
|
24
47
|
export declare function checkRateLimit(token: string): Promise<RateLimitInfo>;
|
package/dist/core/github.js
CHANGED
|
@@ -12,7 +12,10 @@ let _currentToken = null;
|
|
|
12
12
|
function formatResetTime(date) {
|
|
13
13
|
return date.toLocaleTimeString('en-US', { hour12: false });
|
|
14
14
|
}
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Throttle callbacks used by the Octokit client. Exported for testability.
|
|
17
|
+
* @returns Rate limit and secondary rate limit handler callbacks
|
|
18
|
+
*/
|
|
16
19
|
export function getRateLimitCallbacks() {
|
|
17
20
|
return {
|
|
18
21
|
onRateLimit: (retryAfter, options, _octokit, retryCount) => {
|
|
@@ -37,6 +40,24 @@ export function getRateLimitCallbacks() {
|
|
|
37
40
|
},
|
|
38
41
|
};
|
|
39
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Get a shared, throttled Octokit instance for the given token.
|
|
45
|
+
* Returns a cached instance if the token matches, otherwise creates a new one.
|
|
46
|
+
*
|
|
47
|
+
* The client retries on primary rate limits (up to 2 retries) and
|
|
48
|
+
* secondary rate limits (1 retry).
|
|
49
|
+
*
|
|
50
|
+
* @param token - GitHub personal access token
|
|
51
|
+
* @returns Authenticated Octokit instance with rate limit throttling
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```typescript
|
|
55
|
+
* import { getOctokit, requireGitHubToken } from '@oss-autopilot/core';
|
|
56
|
+
*
|
|
57
|
+
* const octokit = getOctokit(requireGitHubToken());
|
|
58
|
+
* const { data } = await octokit.repos.get({ owner: 'facebook', repo: 'react' });
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
40
61
|
export function getOctokit(token) {
|
|
41
62
|
// Return cached instance only if token matches
|
|
42
63
|
if (_octokit && _currentToken === token)
|
|
@@ -51,7 +72,9 @@ export function getOctokit(token) {
|
|
|
51
72
|
}
|
|
52
73
|
/**
|
|
53
74
|
* Check the GitHub Search API rate limit quota.
|
|
54
|
-
*
|
|
75
|
+
*
|
|
76
|
+
* @param token - GitHub personal access token
|
|
77
|
+
* @returns Remaining requests, total limit, and reset time for the search endpoint
|
|
55
78
|
*/
|
|
56
79
|
export async function checkRateLimit(token) {
|
|
57
80
|
const octokit = getOctokit(token);
|
package/dist/core/index.d.ts
CHANGED
|
@@ -16,4 +16,5 @@ export { HttpCache, getHttpCache, cachedRequest, type CacheEntry } from './http-
|
|
|
16
16
|
export { CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
|
|
17
17
|
export { computeContributionStats, type ContributionStats, type ComputeStatsInput } from './stats.js';
|
|
18
18
|
export { fetchPRTemplate, type PRTemplateResult } from './pr-template.js';
|
|
19
|
+
export { detectFormatters, diagnoseCIFormatterFailure, getPreferredFormatter, type DetectedFormatter, type FormatterDetectionResult, type CIFormatterDiagnosis, type FormatterName, } from './formatter-detection.js';
|
|
19
20
|
export * from './types.js';
|
package/dist/core/index.js
CHANGED
|
@@ -16,4 +16,5 @@ export { HttpCache, getHttpCache, cachedRequest } from './http-cache.js';
|
|
|
16
16
|
export { CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
|
|
17
17
|
export { computeContributionStats } from './stats.js';
|
|
18
18
|
export { fetchPRTemplate } from './pr-template.js';
|
|
19
|
+
export { detectFormatters, diagnoseCIFormatterFailure, getPreferredFormatter, } from './formatter-detection.js';
|
|
19
20
|
export * from './types.js';
|
|
@@ -10,6 +10,18 @@
|
|
|
10
10
|
* - search-phases.ts — search helpers, caching, batched repo search
|
|
11
11
|
*/
|
|
12
12
|
import { type IssueCandidate } from './types.js';
|
|
13
|
+
/**
|
|
14
|
+
* Multi-phase issue discovery engine that searches GitHub for contributable issues.
|
|
15
|
+
*
|
|
16
|
+
* Search phases (in priority order):
|
|
17
|
+
* 1. Repos where user has merged PRs (highest merge probability)
|
|
18
|
+
* 2. Preferred organizations
|
|
19
|
+
* 3. Starred repos
|
|
20
|
+
* 4. General label-filtered search
|
|
21
|
+
* 5. Actively maintained repos
|
|
22
|
+
*
|
|
23
|
+
* Each candidate is vetted for claimability and scored 0-100 for viability.
|
|
24
|
+
*/
|
|
13
25
|
export declare class IssueDiscovery {
|
|
14
26
|
private octokit;
|
|
15
27
|
private stateManager;
|
|
@@ -17,21 +29,42 @@ export declare class IssueDiscovery {
|
|
|
17
29
|
private vetter;
|
|
18
30
|
/** Set after searchIssues() runs if rate limits affected the search (low pre-flight quota or mid-search rate limit hits). */
|
|
19
31
|
rateLimitWarning: string | null;
|
|
32
|
+
/** @param githubToken - GitHub personal access token or token from `gh auth token` */
|
|
20
33
|
constructor(githubToken: string);
|
|
21
34
|
/**
|
|
22
35
|
* Fetch the authenticated user's starred repositories from GitHub.
|
|
23
36
|
* Updates the state manager with the list and timestamp.
|
|
37
|
+
* @returns Array of starred repo names in "owner/repo" format
|
|
24
38
|
*/
|
|
25
39
|
fetchStarredRepos(): Promise<string[]>;
|
|
26
40
|
/**
|
|
27
|
-
* Get starred repos, fetching from GitHub if cache is stale
|
|
41
|
+
* Get starred repos, fetching from GitHub if cache is stale.
|
|
42
|
+
* @returns Array of starred repo names in "owner/repo" format
|
|
28
43
|
*/
|
|
29
44
|
getStarredReposWithRefresh(): Promise<string[]>;
|
|
30
45
|
/**
|
|
31
46
|
* Search for issues matching our criteria.
|
|
32
47
|
* Searches in priority order: merged-PR repos first (no label filter), then starred repos,
|
|
33
|
-
* then general search, then actively maintained repos
|
|
48
|
+
* then general search, then actively maintained repos.
|
|
34
49
|
* Filters out issues from low-scoring and excluded repos.
|
|
50
|
+
*
|
|
51
|
+
* @param options - Search configuration
|
|
52
|
+
* @param options.languages - Programming languages to filter by
|
|
53
|
+
* @param options.labels - Issue labels to search for
|
|
54
|
+
* @param options.maxResults - Maximum candidates to return (default: 10)
|
|
55
|
+
* @returns Scored and sorted issue candidates
|
|
56
|
+
* @throws {ValidationError} If no candidates found and no rate limits prevented the search
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```typescript
|
|
60
|
+
* import { IssueDiscovery, requireGitHubToken } from '@oss-autopilot/core';
|
|
61
|
+
*
|
|
62
|
+
* const discovery = new IssueDiscovery(requireGitHubToken());
|
|
63
|
+
* const candidates = await discovery.searchIssues({ maxResults: 5 });
|
|
64
|
+
* for (const c of candidates) {
|
|
65
|
+
* console.log(`${c.issue.repo}#${c.issue.number}: ${c.viabilityScore}/100`);
|
|
66
|
+
* }
|
|
67
|
+
* ```
|
|
35
68
|
*/
|
|
36
69
|
searchIssues(options?: {
|
|
37
70
|
languages?: string[];
|
|
@@ -39,16 +72,23 @@ export declare class IssueDiscovery {
|
|
|
39
72
|
maxResults?: number;
|
|
40
73
|
}): Promise<IssueCandidate[]>;
|
|
41
74
|
/**
|
|
42
|
-
* Vet a specific issue
|
|
75
|
+
* Vet a specific issue for claimability and project health.
|
|
76
|
+
* @param issueUrl - Full GitHub issue URL
|
|
77
|
+
* @returns The vetted issue candidate with recommendation and scores
|
|
78
|
+
* @throws {ValidationError} If the URL is invalid or the issue cannot be fetched
|
|
43
79
|
*/
|
|
44
80
|
vetIssue(issueUrl: string): Promise<IssueCandidate>;
|
|
45
81
|
/**
|
|
46
|
-
* Save search results to ~/.oss-autopilot/found-issues.md
|
|
47
|
-
* Results are sorted by viability score (highest first)
|
|
82
|
+
* Save search results to ~/.oss-autopilot/found-issues.md.
|
|
83
|
+
* Results are sorted by viability score (highest first).
|
|
84
|
+
* @param candidates - Issue candidates to save
|
|
85
|
+
* @returns Absolute path to the written file
|
|
48
86
|
*/
|
|
49
87
|
saveSearchResults(candidates: IssueCandidate[]): string;
|
|
50
88
|
/**
|
|
51
|
-
* Format issue candidate
|
|
89
|
+
* Format issue candidate as a markdown display string.
|
|
90
|
+
* @param candidate - The issue candidate to format
|
|
91
|
+
* @returns Multi-line markdown string with vetting details
|
|
52
92
|
*/
|
|
53
93
|
formatCandidate(candidate: IssueCandidate): string;
|
|
54
94
|
}
|
|
@@ -22,6 +22,18 @@ import { IssueVetter } from './issue-vetting.js';
|
|
|
22
22
|
import { getTopicsForCategories } from './category-mapping.js';
|
|
23
23
|
import { buildLabelQuery, buildEffectiveLabels, interleaveArrays, cachedSearchIssues, filterVetAndScore, searchInRepos, } from './search-phases.js';
|
|
24
24
|
const MODULE = 'issue-discovery';
|
|
25
|
+
/**
|
|
26
|
+
* Multi-phase issue discovery engine that searches GitHub for contributable issues.
|
|
27
|
+
*
|
|
28
|
+
* Search phases (in priority order):
|
|
29
|
+
* 1. Repos where user has merged PRs (highest merge probability)
|
|
30
|
+
* 2. Preferred organizations
|
|
31
|
+
* 3. Starred repos
|
|
32
|
+
* 4. General label-filtered search
|
|
33
|
+
* 5. Actively maintained repos
|
|
34
|
+
*
|
|
35
|
+
* Each candidate is vetted for claimability and scored 0-100 for viability.
|
|
36
|
+
*/
|
|
25
37
|
export class IssueDiscovery {
|
|
26
38
|
octokit;
|
|
27
39
|
stateManager;
|
|
@@ -29,6 +41,7 @@ export class IssueDiscovery {
|
|
|
29
41
|
vetter;
|
|
30
42
|
/** Set after searchIssues() runs if rate limits affected the search (low pre-flight quota or mid-search rate limit hits). */
|
|
31
43
|
rateLimitWarning = null;
|
|
44
|
+
/** @param githubToken - GitHub personal access token or token from `gh auth token` */
|
|
32
45
|
constructor(githubToken) {
|
|
33
46
|
this.githubToken = githubToken;
|
|
34
47
|
this.octokit = getOctokit(githubToken);
|
|
@@ -38,6 +51,7 @@ export class IssueDiscovery {
|
|
|
38
51
|
/**
|
|
39
52
|
* Fetch the authenticated user's starred repositories from GitHub.
|
|
40
53
|
* Updates the state manager with the list and timestamp.
|
|
54
|
+
* @returns Array of starred repo names in "owner/repo" format
|
|
41
55
|
*/
|
|
42
56
|
async fetchStarredRepos() {
|
|
43
57
|
info(MODULE, 'Fetching starred repositories...');
|
|
@@ -93,7 +107,8 @@ export class IssueDiscovery {
|
|
|
93
107
|
}
|
|
94
108
|
}
|
|
95
109
|
/**
|
|
96
|
-
* Get starred repos, fetching from GitHub if cache is stale
|
|
110
|
+
* Get starred repos, fetching from GitHub if cache is stale.
|
|
111
|
+
* @returns Array of starred repo names in "owner/repo" format
|
|
97
112
|
*/
|
|
98
113
|
async getStarredReposWithRefresh() {
|
|
99
114
|
if (this.stateManager.isStarredReposStale()) {
|
|
@@ -104,8 +119,26 @@ export class IssueDiscovery {
|
|
|
104
119
|
/**
|
|
105
120
|
* Search for issues matching our criteria.
|
|
106
121
|
* Searches in priority order: merged-PR repos first (no label filter), then starred repos,
|
|
107
|
-
* then general search, then actively maintained repos
|
|
122
|
+
* then general search, then actively maintained repos.
|
|
108
123
|
* Filters out issues from low-scoring and excluded repos.
|
|
124
|
+
*
|
|
125
|
+
* @param options - Search configuration
|
|
126
|
+
* @param options.languages - Programming languages to filter by
|
|
127
|
+
* @param options.labels - Issue labels to search for
|
|
128
|
+
* @param options.maxResults - Maximum candidates to return (default: 10)
|
|
129
|
+
* @returns Scored and sorted issue candidates
|
|
130
|
+
* @throws {ValidationError} If no candidates found and no rate limits prevented the search
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```typescript
|
|
134
|
+
* import { IssueDiscovery, requireGitHubToken } from '@oss-autopilot/core';
|
|
135
|
+
*
|
|
136
|
+
* const discovery = new IssueDiscovery(requireGitHubToken());
|
|
137
|
+
* const candidates = await discovery.searchIssues({ maxResults: 5 });
|
|
138
|
+
* for (const c of candidates) {
|
|
139
|
+
* console.log(`${c.issue.repo}#${c.issue.number}: ${c.viabilityScore}/100`);
|
|
140
|
+
* }
|
|
141
|
+
* ```
|
|
109
142
|
*/
|
|
110
143
|
async searchIssues(options = {}) {
|
|
111
144
|
const config = this.stateManager.getState().config;
|
|
@@ -453,14 +486,19 @@ export class IssueDiscovery {
|
|
|
453
486
|
return capped.slice(0, maxResults);
|
|
454
487
|
}
|
|
455
488
|
/**
|
|
456
|
-
* Vet a specific issue
|
|
489
|
+
* Vet a specific issue for claimability and project health.
|
|
490
|
+
* @param issueUrl - Full GitHub issue URL
|
|
491
|
+
* @returns The vetted issue candidate with recommendation and scores
|
|
492
|
+
* @throws {ValidationError} If the URL is invalid or the issue cannot be fetched
|
|
457
493
|
*/
|
|
458
494
|
async vetIssue(issueUrl) {
|
|
459
495
|
return this.vetter.vetIssue(issueUrl);
|
|
460
496
|
}
|
|
461
497
|
/**
|
|
462
|
-
* Save search results to ~/.oss-autopilot/found-issues.md
|
|
463
|
-
* Results are sorted by viability score (highest first)
|
|
498
|
+
* Save search results to ~/.oss-autopilot/found-issues.md.
|
|
499
|
+
* Results are sorted by viability score (highest first).
|
|
500
|
+
* @param candidates - Issue candidates to save
|
|
501
|
+
* @returns Absolute path to the written file
|
|
464
502
|
*/
|
|
465
503
|
saveSearchResults(candidates) {
|
|
466
504
|
// Sort by viability score descending
|
|
@@ -489,7 +527,9 @@ export class IssueDiscovery {
|
|
|
489
527
|
return outputFile;
|
|
490
528
|
}
|
|
491
529
|
/**
|
|
492
|
-
* Format issue candidate
|
|
530
|
+
* Format issue candidate as a markdown display string.
|
|
531
|
+
* @param candidate - The issue candidate to format
|
|
532
|
+
* @returns Multi-line markdown string with vetting details
|
|
493
533
|
*/
|
|
494
534
|
formatCandidate(candidate) {
|
|
495
535
|
const { issue, vettingResult, projectHealth, recommendation, reasonsToApprove, reasonsToSkip } = candidate;
|