@oss-autopilot/core 1.17.4 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli-registry.js +417 -326
- package/dist/cli.bundle.cjs +99 -96
- package/dist/commands/daily-render.d.ts +39 -0
- package/dist/commands/daily-render.js +189 -0
- package/dist/commands/dashboard-data.js +9 -3
- package/dist/commands/index.d.ts +4 -8
- package/dist/commands/index.js +3 -5
- package/dist/commands/list-move-tier.d.ts +46 -0
- package/dist/commands/list-move-tier.js +192 -0
- package/dist/commands/pr-template.js +2 -1
- package/dist/commands/state-cmd.d.ts +10 -1
- package/dist/commands/state-cmd.js +22 -3
- package/dist/commands/track.d.ts +7 -28
- package/dist/commands/track.js +8 -30
- package/dist/core/auth.d.ts +50 -0
- package/dist/core/auth.js +160 -0
- package/dist/core/concurrency.d.ts +7 -0
- package/dist/core/concurrency.js +9 -0
- package/dist/core/daily-logic.d.ts +10 -42
- package/dist/core/daily-logic.js +14 -201
- package/dist/core/dates.d.ts +37 -0
- package/dist/core/dates.js +60 -0
- package/dist/core/errors.d.ts +14 -0
- package/dist/core/errors.js +22 -0
- package/dist/core/gist-state-store.d.ts +48 -2
- package/dist/core/gist-state-store.js +120 -24
- package/dist/core/github-stats.js +1 -1
- package/dist/core/http-cache.js +1 -1
- package/dist/core/index.d.ts +5 -1
- package/dist/core/index.js +5 -1
- package/dist/core/issue-conversation.js +3 -2
- package/dist/core/paths.d.ts +68 -0
- package/dist/core/paths.js +106 -0
- package/dist/core/pr-monitor.js +3 -1
- package/dist/core/repo-score-manager.js +1 -1
- package/dist/core/state-persistence.js +1 -1
- package/dist/core/state.d.ts +16 -2
- package/dist/core/state.js +42 -7
- package/dist/core/types.d.ts +57 -0
- package/dist/core/urls.d.ts +63 -0
- package/dist/core/urls.js +101 -0
- package/dist/formatters/json.d.ts +464 -74
- package/dist/formatters/json.js +380 -0
- package/package.json +3 -3
- package/dist/commands/read.d.ts +0 -18
- package/dist/commands/read.js +0 -20
- package/dist/core/utils.d.ts +0 -303
- package/dist/core/utils.js +0 -529
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem path helpers for the oss-autopilot user data directory.
|
|
3
|
+
*
|
|
4
|
+
* All paths root at `~/.oss-autopilot/`, and every getter that returns a
|
|
5
|
+
* directory path implicitly creates it (with mode 0o700) if missing.
|
|
6
|
+
*
|
|
7
|
+
* Extracted from utils.ts under #1116.
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import * as os from 'os';
|
|
12
|
+
/**
|
|
13
|
+
* Returns the oss-autopilot data directory path, creating it if it does not exist.
|
|
14
|
+
*
|
|
15
|
+
* The directory is located at `~/.oss-autopilot/` and serves as the root for
|
|
16
|
+
* all persisted user data (state, backups, cache).
|
|
17
|
+
*
|
|
18
|
+
* @returns Absolute path to the data directory (e.g., `/Users/you/.oss-autopilot`)
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* const dir = getDataDir();
|
|
22
|
+
* // "/Users/you/.oss-autopilot"
|
|
23
|
+
*/
|
|
24
|
+
export function getDataDir() {
|
|
25
|
+
const dir = path.join(os.homedir(), '.oss-autopilot');
|
|
26
|
+
if (!fs.existsSync(dir)) {
|
|
27
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
28
|
+
}
|
|
29
|
+
return dir;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Returns the path to the state file (`~/.oss-autopilot/state.json`).
|
|
33
|
+
*
|
|
34
|
+
* Implicitly creates the data directory via {@link getDataDir} if it does not exist.
|
|
35
|
+
*/
|
|
36
|
+
export function getStatePath() {
|
|
37
|
+
return path.join(getDataDir(), 'state.json');
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Returns the backup directory path, creating it if it does not exist.
|
|
41
|
+
*
|
|
42
|
+
* Located at `~/.oss-autopilot/backups/`. Used for automatic state backups
|
|
43
|
+
* before each write operation.
|
|
44
|
+
*/
|
|
45
|
+
export function getBackupDir() {
|
|
46
|
+
const dir = path.join(getDataDir(), 'backups');
|
|
47
|
+
if (!fs.existsSync(dir)) {
|
|
48
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
49
|
+
}
|
|
50
|
+
return dir;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Returns the HTTP cache directory path, creating it if it does not exist.
|
|
54
|
+
*
|
|
55
|
+
* Located at `~/.oss-autopilot/cache/`. Used by HttpCache to store
|
|
56
|
+
* ETag-based response caches for GitHub API endpoints.
|
|
57
|
+
*/
|
|
58
|
+
export function getCacheDir() {
|
|
59
|
+
const dir = path.join(getDataDir(), 'cache');
|
|
60
|
+
if (!fs.existsSync(dir)) {
|
|
61
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
62
|
+
}
|
|
63
|
+
return dir;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Returns the path to the local Gist ID file (`~/.oss-autopilot/gist-id`).
|
|
67
|
+
*
|
|
68
|
+
* Stores the GitHub Gist ID used by the Gist-based persistence layer,
|
|
69
|
+
* avoiding a search-by-description API call on every session.
|
|
70
|
+
*/
|
|
71
|
+
export function getGistIdPath() {
|
|
72
|
+
return path.join(getDataDir(), 'gist-id');
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Returns the path to the local state cache file (`~/.oss-autopilot/state-cache.json`).
|
|
76
|
+
*
|
|
77
|
+
* A write-through cache of the Gist-hosted state, used as a fallback when
|
|
78
|
+
* the GitHub API is unreachable (degraded mode).
|
|
79
|
+
*/
|
|
80
|
+
export function getStateCachePath() {
|
|
81
|
+
return path.join(getDataDir(), 'state-cache.json');
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Check whether the state file exists without creating the data directory.
|
|
85
|
+
*
|
|
86
|
+
* Used for first-run detection in the CLI — we don't want to create
|
|
87
|
+
* `~/.oss-autopilot/` just to check if the user has ever run the tool.
|
|
88
|
+
*/
|
|
89
|
+
export function stateFileExists() {
|
|
90
|
+
const stateFile = path.join(os.homedir(), '.oss-autopilot', 'state.json');
|
|
91
|
+
return fs.existsSync(stateFile);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Read the CLI package version from package.json relative to the running CLI bundle.
|
|
95
|
+
* Resolves `../package.json` from `process.argv[1]` (the bundle entry point).
|
|
96
|
+
* Falls back to '0.0.0' if the file is unreadable.
|
|
97
|
+
*/
|
|
98
|
+
export function getCLIVersion() {
|
|
99
|
+
try {
|
|
100
|
+
const pkgPath = path.join(path.dirname(process.argv[1]), '..', 'package.json');
|
|
101
|
+
return JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return '0.0.0';
|
|
105
|
+
}
|
|
106
|
+
}
|
package/dist/core/pr-monitor.js
CHANGED
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { getOctokit } from './github.js';
|
|
16
16
|
import { getStateManager } from './state.js';
|
|
17
|
-
import { daysBetween
|
|
17
|
+
import { daysBetween } from './dates.js';
|
|
18
|
+
import { parseGitHubUrl, extractOwnerRepo, isOwnRepo } from './urls.js';
|
|
19
|
+
import { DEFAULT_CONCURRENCY } from './concurrency.js';
|
|
18
20
|
import { determineStatus } from './status-determination.js';
|
|
19
21
|
import { runWorkerPool } from './concurrency.js';
|
|
20
22
|
import { ConfigurationError, ValidationError, errorMessage, getHttpStatusCode, isRateLimitOrAuthError, } from './errors.js';
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { isBelowMinStars } from './types.js';
|
|
11
11
|
import { debug, warn } from './logger.js';
|
|
12
|
-
import { parseGitHubUrl } from './
|
|
12
|
+
import { parseGitHubUrl } from './urls.js';
|
|
13
13
|
const MODULE = 'scoring';
|
|
14
14
|
// ── Scoring constants (#1054) ─────────────────────────────────────────
|
|
15
15
|
// Previously inlined as magic numbers in `calculateScore`. Extracted with
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import * as fs from 'fs';
|
|
7
7
|
import * as path from 'path';
|
|
8
8
|
import { AgentStateSchema } from './state-schema.js';
|
|
9
|
-
import { getStatePath, getBackupDir, getDataDir } from './
|
|
9
|
+
import { getStatePath, getBackupDir, getDataDir } from './paths.js';
|
|
10
10
|
import { errorMessage, ConcurrencyError } from './errors.js';
|
|
11
11
|
import { debug, warn } from './logger.js';
|
|
12
12
|
const MODULE = 'state';
|
package/dist/core/state.d.ts
CHANGED
|
@@ -148,18 +148,32 @@ export declare class StateManager {
|
|
|
148
148
|
getMergedPRs(): StoredMergedPR[];
|
|
149
149
|
/**
|
|
150
150
|
* Add merged PRs to storage, deduplicating by URL.
|
|
151
|
+
* Entries with URLs that fail {@link parseGitHubUrl} are dropped before
|
|
152
|
+
* persistence (read-side filters in dashboard-data already skip them, but
|
|
153
|
+
* this prevents the bad data from reaching disk in the first place).
|
|
151
154
|
* @param prs - Merged PRs to add (duplicates by URL are ignored)
|
|
155
|
+
* @returns count of entries added vs. dropped (invalid URL)
|
|
152
156
|
*/
|
|
153
|
-
addMergedPRs(prs: StoredMergedPR[]):
|
|
157
|
+
addMergedPRs(prs: StoredMergedPR[]): {
|
|
158
|
+
added: number;
|
|
159
|
+
dropped: number;
|
|
160
|
+
};
|
|
154
161
|
/** Returns the most recent merge date, used as a watermark for incremental fetching. */
|
|
155
162
|
getMergedPRWatermark(): string | undefined;
|
|
156
163
|
/** Returns all stored closed-without-merge PRs (sorted by close date descending via addClosedPRs). */
|
|
157
164
|
getClosedPRs(): StoredClosedPR[];
|
|
158
165
|
/**
|
|
159
166
|
* Add closed PRs to storage, deduplicating by URL.
|
|
167
|
+
* Entries with URLs that fail {@link parseGitHubUrl} are dropped before
|
|
168
|
+
* persistence (read-side filters in dashboard-data already skip them, but
|
|
169
|
+
* this prevents the bad data from reaching disk in the first place).
|
|
160
170
|
* @param prs - Closed PRs to add (duplicates by URL are ignored)
|
|
171
|
+
* @returns count of entries added vs. dropped (invalid URL)
|
|
161
172
|
*/
|
|
162
|
-
addClosedPRs(prs: StoredClosedPR[]):
|
|
173
|
+
addClosedPRs(prs: StoredClosedPR[]): {
|
|
174
|
+
added: number;
|
|
175
|
+
dropped: number;
|
|
176
|
+
};
|
|
163
177
|
/** Returns the most recent close date, used as a watermark for incremental fetching. */
|
|
164
178
|
getClosedPRWatermark(): string | undefined;
|
|
165
179
|
/**
|
package/dist/core/state.js
CHANGED
|
@@ -10,9 +10,28 @@ import * as repoScoring from './repo-score-manager.js';
|
|
|
10
10
|
import { debug, warn } from './logger.js';
|
|
11
11
|
import { errorMessage, ConfigurationError, ConcurrencyError } from './errors.js';
|
|
12
12
|
import { GistStateStore } from './gist-state-store.js';
|
|
13
|
-
import { getStatePath, getStateCachePath } from './
|
|
13
|
+
import { getStatePath, getStateCachePath } from './paths.js';
|
|
14
|
+
import { parseGitHubUrl } from './urls.js';
|
|
14
15
|
export { acquireLock, releaseLock, atomicWriteFileSync } from './state-persistence.js';
|
|
15
16
|
const MODULE = 'state';
|
|
17
|
+
/**
|
|
18
|
+
* Validate stored-PR URL shape before persistence (mirror of the read-side
|
|
19
|
+
* filter in dashboard-data#storedToMergedPRs/storedToClosedPRs). Bad URLs are
|
|
20
|
+
* logged at warn level so operators see the drop in the daily output.
|
|
21
|
+
*/
|
|
22
|
+
function filterValidUrlEntries(entries, kind) {
|
|
23
|
+
const valid = [];
|
|
24
|
+
let dropped = 0;
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
if (parseGitHubUrl(entry.url) === null) {
|
|
27
|
+
warn(MODULE, `Dropping ${kind} PR with invalid URL: ${entry.url}`);
|
|
28
|
+
dropped++;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
valid.push(entry);
|
|
32
|
+
}
|
|
33
|
+
return { valid, dropped };
|
|
34
|
+
}
|
|
16
35
|
/**
|
|
17
36
|
* Push state to the backing Gist when Gist mode is active. Best-effort:
|
|
18
37
|
* network/auth failures are logged via `warn()` but never propagated —
|
|
@@ -352,21 +371,29 @@ export class StateManager {
|
|
|
352
371
|
}
|
|
353
372
|
/**
|
|
354
373
|
* Add merged PRs to storage, deduplicating by URL.
|
|
374
|
+
* Entries with URLs that fail {@link parseGitHubUrl} are dropped before
|
|
375
|
+
* persistence (read-side filters in dashboard-data already skip them, but
|
|
376
|
+
* this prevents the bad data from reaching disk in the first place).
|
|
355
377
|
* @param prs - Merged PRs to add (duplicates by URL are ignored)
|
|
378
|
+
* @returns count of entries added vs. dropped (invalid URL)
|
|
356
379
|
*/
|
|
357
380
|
addMergedPRs(prs) {
|
|
358
381
|
if (prs.length === 0)
|
|
359
|
-
return;
|
|
382
|
+
return { added: 0, dropped: 0 };
|
|
383
|
+
const { valid, dropped } = filterValidUrlEntries(prs, 'merged');
|
|
384
|
+
if (valid.length === 0)
|
|
385
|
+
return { added: 0, dropped };
|
|
360
386
|
if (!this.state.mergedPRs)
|
|
361
387
|
this.state.mergedPRs = [];
|
|
362
388
|
const existingUrls = new Set(this.state.mergedPRs.map((pr) => pr.url));
|
|
363
|
-
const newPRs =
|
|
389
|
+
const newPRs = valid.filter((pr) => !existingUrls.has(pr.url));
|
|
364
390
|
if (newPRs.length === 0)
|
|
365
|
-
return;
|
|
391
|
+
return { added: 0, dropped };
|
|
366
392
|
this.state.mergedPRs.push(...newPRs);
|
|
367
393
|
this.state.mergedPRs.sort((a, b) => b.mergedAt.localeCompare(a.mergedAt));
|
|
368
394
|
debug(MODULE, `Added ${newPRs.length} merged PRs (total: ${this.state.mergedPRs.length})`);
|
|
369
395
|
this.autoSave();
|
|
396
|
+
return { added: newPRs.length, dropped };
|
|
370
397
|
}
|
|
371
398
|
/** Returns the most recent merge date, used as a watermark for incremental fetching. */
|
|
372
399
|
getMergedPRWatermark() {
|
|
@@ -379,21 +406,29 @@ export class StateManager {
|
|
|
379
406
|
}
|
|
380
407
|
/**
|
|
381
408
|
* Add closed PRs to storage, deduplicating by URL.
|
|
409
|
+
* Entries with URLs that fail {@link parseGitHubUrl} are dropped before
|
|
410
|
+
* persistence (read-side filters in dashboard-data already skip them, but
|
|
411
|
+
* this prevents the bad data from reaching disk in the first place).
|
|
382
412
|
* @param prs - Closed PRs to add (duplicates by URL are ignored)
|
|
413
|
+
* @returns count of entries added vs. dropped (invalid URL)
|
|
383
414
|
*/
|
|
384
415
|
addClosedPRs(prs) {
|
|
385
416
|
if (prs.length === 0)
|
|
386
|
-
return;
|
|
417
|
+
return { added: 0, dropped: 0 };
|
|
418
|
+
const { valid, dropped } = filterValidUrlEntries(prs, 'closed');
|
|
419
|
+
if (valid.length === 0)
|
|
420
|
+
return { added: 0, dropped };
|
|
387
421
|
if (!this.state.closedPRs)
|
|
388
422
|
this.state.closedPRs = [];
|
|
389
423
|
const existingUrls = new Set(this.state.closedPRs.map((pr) => pr.url));
|
|
390
|
-
const newPRs =
|
|
424
|
+
const newPRs = valid.filter((pr) => !existingUrls.has(pr.url));
|
|
391
425
|
if (newPRs.length === 0)
|
|
392
|
-
return;
|
|
426
|
+
return { added: 0, dropped };
|
|
393
427
|
this.state.closedPRs.push(...newPRs);
|
|
394
428
|
this.state.closedPRs.sort((a, b) => b.closedAt.localeCompare(a.closedAt));
|
|
395
429
|
debug(MODULE, `Added ${newPRs.length} closed PRs (total: ${this.state.closedPRs.length})`);
|
|
396
430
|
this.autoSave();
|
|
431
|
+
return { added: newPRs.length, dropped };
|
|
397
432
|
}
|
|
398
433
|
/** Returns the most recent close date, used as a watermark for incremental fetching. */
|
|
399
434
|
getClosedPRWatermark() {
|
package/dist/core/types.d.ts
CHANGED
|
@@ -251,3 +251,60 @@ export interface IssueCandidate {
|
|
|
251
251
|
viabilityScore: number;
|
|
252
252
|
searchPriority: SearchPriority;
|
|
253
253
|
}
|
|
254
|
+
export interface CapacityAssessment {
|
|
255
|
+
hasCapacity: boolean;
|
|
256
|
+
activePRCount: number;
|
|
257
|
+
maxActivePRs: number;
|
|
258
|
+
shelvedPRCount: number;
|
|
259
|
+
criticalIssueCount: number;
|
|
260
|
+
reason: string;
|
|
261
|
+
}
|
|
262
|
+
export type ActionableIssueType = 'ci_failing' | 'merge_conflict' | 'needs_response' | 'needs_changes' | 'incomplete_checklist';
|
|
263
|
+
export interface ActionableIssue {
|
|
264
|
+
type: ActionableIssueType;
|
|
265
|
+
pr: FetchedPR;
|
|
266
|
+
label: string;
|
|
267
|
+
/** True if the PR was created after the last daily digest (first time seen). */
|
|
268
|
+
isNewContribution: boolean;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Compact version of ActionableIssue for JSON output.
|
|
272
|
+
* References the PR by URL instead of embedding the full object,
|
|
273
|
+
* since the full PR is already available in digest.openPRs.
|
|
274
|
+
*/
|
|
275
|
+
export interface CompactActionableIssue {
|
|
276
|
+
type: ActionableIssueType;
|
|
277
|
+
prUrl: string;
|
|
278
|
+
label: string;
|
|
279
|
+
/** True if the PR was created after the last daily digest (first time seen). */
|
|
280
|
+
isNewContribution: boolean;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* A single action menu item pre-computed by the CLI.
|
|
284
|
+
* The orchestration layer can use these directly in AskUserQuestion prompts.
|
|
285
|
+
*/
|
|
286
|
+
export interface ActionMenuItem {
|
|
287
|
+
/** Stable identifier for routing (e.g., "address_all", "search", "done"). */
|
|
288
|
+
key: string;
|
|
289
|
+
/** Display text for the option (e.g., "Work through all 3 issues (Recommended)"). */
|
|
290
|
+
label: string;
|
|
291
|
+
/** Explanation shown below the label. */
|
|
292
|
+
description: string;
|
|
293
|
+
/** Present when the action would exceed the user's PR capacity limit (#765). */
|
|
294
|
+
capacityWarning?: string;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Pre-computed action menu for the orchestration layer.
|
|
298
|
+
*/
|
|
299
|
+
export interface ActionMenu {
|
|
300
|
+
/** Ordered list of menu items. */
|
|
301
|
+
items: ActionMenuItem[];
|
|
302
|
+
/** Context flags for the orchestration layer to decide on issue-list options. */
|
|
303
|
+
context: {
|
|
304
|
+
hasActionableIssues: boolean;
|
|
305
|
+
actionableCount: number;
|
|
306
|
+
hasCapacity: boolean;
|
|
307
|
+
hasIssueResponses: boolean;
|
|
308
|
+
issueResponseCount: number;
|
|
309
|
+
};
|
|
310
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub URL parsing + owner/repo helpers.
|
|
3
|
+
*
|
|
4
|
+
* All functions enforce an `https://github.com/` prefix and validate
|
|
5
|
+
* owner/repo characters. Extracted from utils.ts under #1116.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Represents a parsed GitHub pull request or issue URL.
|
|
9
|
+
*
|
|
10
|
+
* @property owner - The repository owner (e.g., `"facebook"`)
|
|
11
|
+
* @property repo - The repository name (e.g., `"react"`)
|
|
12
|
+
* @property number - The PR or issue number
|
|
13
|
+
* @property type - Whether the URL points to a pull request or an issue
|
|
14
|
+
*/
|
|
15
|
+
export interface ParsedGitHubUrl {
|
|
16
|
+
owner: string;
|
|
17
|
+
repo: string;
|
|
18
|
+
number: number;
|
|
19
|
+
type: 'pull' | 'issues';
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Parses a GitHub pull request or issue URL into its components.
|
|
23
|
+
*
|
|
24
|
+
* Only accepts HTTPS GitHub URLs (`https://github.com/...`). Returns `null` for
|
|
25
|
+
* invalid URLs, non-GitHub URLs, or URLs with invalid owner/repo characters.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* parseGitHubUrl('https://github.com/facebook/react/pull/123')
|
|
29
|
+
* // { owner: "facebook", repo: "react", number: 123, type: "pull" }
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* parseGitHubUrl('https://example.com/not-github')
|
|
33
|
+
* // null
|
|
34
|
+
*/
|
|
35
|
+
export declare function parseGitHubUrl(url: string): ParsedGitHubUrl | null;
|
|
36
|
+
/**
|
|
37
|
+
* Extracts the owner and repo from a GitHub web URL
|
|
38
|
+
* (e.g. `https://github.com/owner/repo/pull/42`, `https://github.com/owner/repo/`).
|
|
39
|
+
*
|
|
40
|
+
* Unlike {@link parseGitHubUrl}, this does **not** require a PR or issue number.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* extractOwnerRepo('https://github.com/vercel/next.js/')
|
|
44
|
+
* // { owner: "vercel", repo: "next.js" }
|
|
45
|
+
*/
|
|
46
|
+
export declare function extractOwnerRepo(url: string): {
|
|
47
|
+
owner: string;
|
|
48
|
+
repo: string;
|
|
49
|
+
} | null;
|
|
50
|
+
/**
|
|
51
|
+
* Splits an `"owner/repo"` string into its owner and repo components.
|
|
52
|
+
*
|
|
53
|
+
* @throws {Error} If the input is not in the form `"owner/repo"`.
|
|
54
|
+
*/
|
|
55
|
+
export declare function splitRepo(repoFullName: string): {
|
|
56
|
+
owner: string;
|
|
57
|
+
repo: string;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Case-insensitive check whether a repo owner matches the given GitHub username.
|
|
61
|
+
* Used to skip a user's own repos (PRs to your own repos aren't OSS contributions).
|
|
62
|
+
*/
|
|
63
|
+
export declare function isOwnRepo(owner: string, username: string): boolean;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub URL parsing + owner/repo helpers.
|
|
3
|
+
*
|
|
4
|
+
* All functions enforce an `https://github.com/` prefix and validate
|
|
5
|
+
* owner/repo characters. Extracted from utils.ts under #1116.
|
|
6
|
+
*/
|
|
7
|
+
// Validation patterns for GitHub owner and repo names
|
|
8
|
+
const OWNER_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
9
|
+
const REPO_PATTERN = /^[a-zA-Z0-9_.-]+$/;
|
|
10
|
+
function isValidOwnerRepo(owner, repo) {
|
|
11
|
+
return OWNER_PATTERN.test(owner) && REPO_PATTERN.test(repo);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Parses a GitHub pull request or issue URL into its components.
|
|
15
|
+
*
|
|
16
|
+
* Only accepts HTTPS GitHub URLs (`https://github.com/...`). Returns `null` for
|
|
17
|
+
* invalid URLs, non-GitHub URLs, or URLs with invalid owner/repo characters.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* parseGitHubUrl('https://github.com/facebook/react/pull/123')
|
|
21
|
+
* // { owner: "facebook", repo: "react", number: 123, type: "pull" }
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* parseGitHubUrl('https://example.com/not-github')
|
|
25
|
+
* // null
|
|
26
|
+
*/
|
|
27
|
+
export function parseGitHubUrl(url) {
|
|
28
|
+
if (!url.startsWith('https://github.com/')) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const prMatch = url.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
|
|
32
|
+
if (prMatch) {
|
|
33
|
+
const owner = prMatch[1];
|
|
34
|
+
const repo = prMatch[2];
|
|
35
|
+
if (!isValidOwnerRepo(owner, repo)) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
owner,
|
|
40
|
+
repo,
|
|
41
|
+
number: parseInt(prMatch[3], 10),
|
|
42
|
+
type: 'pull',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const issueMatch = url.match(/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/);
|
|
46
|
+
if (issueMatch) {
|
|
47
|
+
const owner = issueMatch[1];
|
|
48
|
+
const repo = issueMatch[2];
|
|
49
|
+
if (!isValidOwnerRepo(owner, repo)) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
owner,
|
|
54
|
+
repo,
|
|
55
|
+
number: parseInt(issueMatch[3], 10),
|
|
56
|
+
type: 'issues',
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Extracts the owner and repo from a GitHub web URL
|
|
63
|
+
* (e.g. `https://github.com/owner/repo/pull/42`, `https://github.com/owner/repo/`).
|
|
64
|
+
*
|
|
65
|
+
* Unlike {@link parseGitHubUrl}, this does **not** require a PR or issue number.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* extractOwnerRepo('https://github.com/vercel/next.js/')
|
|
69
|
+
* // { owner: "vercel", repo: "next.js" }
|
|
70
|
+
*/
|
|
71
|
+
export function extractOwnerRepo(url) {
|
|
72
|
+
if (!url.startsWith('https://github.com/'))
|
|
73
|
+
return null;
|
|
74
|
+
const match = url.match(/github\.com\/([^/]+)\/([^/]+)/);
|
|
75
|
+
if (!match)
|
|
76
|
+
return null;
|
|
77
|
+
const owner = match[1];
|
|
78
|
+
const repo = match[2];
|
|
79
|
+
if (!isValidOwnerRepo(owner, repo))
|
|
80
|
+
return null;
|
|
81
|
+
return { owner, repo };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Splits an `"owner/repo"` string into its owner and repo components.
|
|
85
|
+
*
|
|
86
|
+
* @throws {Error} If the input is not in the form `"owner/repo"`.
|
|
87
|
+
*/
|
|
88
|
+
export function splitRepo(repoFullName) {
|
|
89
|
+
const [owner, repo] = repoFullName.split('/');
|
|
90
|
+
if (!owner || !repo) {
|
|
91
|
+
throw new Error(`Invalid repo format: expected "owner/repo", got "${repoFullName}"`);
|
|
92
|
+
}
|
|
93
|
+
return { owner, repo };
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Case-insensitive check whether a repo owner matches the given GitHub username.
|
|
97
|
+
* Used to skip a user's own repos (PRs to your own repos aren't OSS contributions).
|
|
98
|
+
*/
|
|
99
|
+
export function isOwnRepo(owner, username) {
|
|
100
|
+
return owner.toLowerCase() === username.toLowerCase();
|
|
101
|
+
}
|