@oss-autopilot/core 0.41.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/LICENSE +21 -0
- package/README.md +85 -0
- package/dist/cli.bundle.cjs +17657 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.js +325 -0
- package/dist/commands/check-integration.d.ts +10 -0
- package/dist/commands/check-integration.js +192 -0
- package/dist/commands/comments.d.ts +24 -0
- package/dist/commands/comments.js +311 -0
- package/dist/commands/config.d.ts +11 -0
- package/dist/commands/config.js +82 -0
- package/dist/commands/daily.d.ts +29 -0
- package/dist/commands/daily.js +433 -0
- package/dist/commands/dashboard-data.d.ts +45 -0
- package/dist/commands/dashboard-data.js +132 -0
- package/dist/commands/dashboard-templates.d.ts +23 -0
- package/dist/commands/dashboard-templates.js +1627 -0
- package/dist/commands/dashboard.d.ts +18 -0
- package/dist/commands/dashboard.js +134 -0
- package/dist/commands/dismiss.d.ts +13 -0
- package/dist/commands/dismiss.js +49 -0
- package/dist/commands/init.d.ts +10 -0
- package/dist/commands/init.js +27 -0
- package/dist/commands/local-repos.d.ts +14 -0
- package/dist/commands/local-repos.js +155 -0
- package/dist/commands/parse-list.d.ts +13 -0
- package/dist/commands/parse-list.js +139 -0
- package/dist/commands/read.d.ts +12 -0
- package/dist/commands/read.js +33 -0
- package/dist/commands/search.d.ts +10 -0
- package/dist/commands/search.js +74 -0
- package/dist/commands/setup.d.ts +15 -0
- package/dist/commands/setup.js +276 -0
- package/dist/commands/shelve.d.ts +13 -0
- package/dist/commands/shelve.js +49 -0
- package/dist/commands/snooze.d.ts +18 -0
- package/dist/commands/snooze.js +83 -0
- package/dist/commands/startup.d.ts +33 -0
- package/dist/commands/startup.js +197 -0
- package/dist/commands/status.d.ts +10 -0
- package/dist/commands/status.js +43 -0
- package/dist/commands/track.d.ts +16 -0
- package/dist/commands/track.js +59 -0
- package/dist/commands/validation.d.ts +43 -0
- package/dist/commands/validation.js +112 -0
- package/dist/commands/vet.d.ts +10 -0
- package/dist/commands/vet.js +36 -0
- package/dist/core/checklist-analysis.d.ts +17 -0
- package/dist/core/checklist-analysis.js +39 -0
- package/dist/core/ci-analysis.d.ts +78 -0
- package/dist/core/ci-analysis.js +163 -0
- package/dist/core/comment-utils.d.ts +15 -0
- package/dist/core/comment-utils.js +52 -0
- package/dist/core/concurrency.d.ts +5 -0
- package/dist/core/concurrency.js +15 -0
- package/dist/core/daily-logic.d.ts +77 -0
- package/dist/core/daily-logic.js +512 -0
- package/dist/core/display-utils.d.ts +10 -0
- package/dist/core/display-utils.js +100 -0
- package/dist/core/errors.d.ts +24 -0
- package/dist/core/errors.js +34 -0
- package/dist/core/github-stats.d.ts +73 -0
- package/dist/core/github-stats.js +272 -0
- package/dist/core/github.d.ts +19 -0
- package/dist/core/github.js +60 -0
- package/dist/core/http-cache.d.ts +97 -0
- package/dist/core/http-cache.js +269 -0
- package/dist/core/index.d.ts +15 -0
- package/dist/core/index.js +15 -0
- package/dist/core/issue-conversation.d.ts +29 -0
- package/dist/core/issue-conversation.js +231 -0
- package/dist/core/issue-discovery.d.ts +85 -0
- package/dist/core/issue-discovery.js +589 -0
- package/dist/core/issue-filtering.d.ts +51 -0
- package/dist/core/issue-filtering.js +103 -0
- package/dist/core/issue-scoring.d.ts +40 -0
- package/dist/core/issue-scoring.js +92 -0
- package/dist/core/issue-vetting.d.ts +49 -0
- package/dist/core/issue-vetting.js +536 -0
- package/dist/core/logger.d.ts +21 -0
- package/dist/core/logger.js +49 -0
- package/dist/core/maintainer-analysis.d.ts +10 -0
- package/dist/core/maintainer-analysis.js +59 -0
- package/dist/core/pagination.d.ts +11 -0
- package/dist/core/pagination.js +20 -0
- package/dist/core/pr-monitor.d.ts +109 -0
- package/dist/core/pr-monitor.js +594 -0
- package/dist/core/review-analysis.d.ts +72 -0
- package/dist/core/review-analysis.js +163 -0
- package/dist/core/state.d.ts +371 -0
- package/dist/core/state.js +1089 -0
- package/dist/core/types.d.ts +507 -0
- package/dist/core/types.js +34 -0
- package/dist/core/utils.d.ts +249 -0
- package/dist/core/utils.js +422 -0
- package/dist/formatters/json.d.ts +269 -0
- package/dist/formatters/json.js +88 -0
- package/package.json +67 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard command ā thin orchestrator.
|
|
3
|
+
* Coordinates data fetching, template generation, and file output.
|
|
4
|
+
* v2: Fetches fresh data from GitHub if token available, otherwise uses cached lastDigest.
|
|
5
|
+
*/
|
|
6
|
+
interface DashboardOptions {
|
|
7
|
+
open?: boolean;
|
|
8
|
+
json?: boolean;
|
|
9
|
+
offline?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function runDashboard(options: DashboardOptions): Promise<void>;
|
|
12
|
+
/**
|
|
13
|
+
* Generate dashboard HTML from state (no GitHub fetch).
|
|
14
|
+
* Call after executeDailyCheck() which saves fresh data to state.
|
|
15
|
+
* Returns the path to the generated dashboard HTML file.
|
|
16
|
+
*/
|
|
17
|
+
export declare function writeDashboardFromState(): string;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard command ā thin orchestrator.
|
|
3
|
+
* Coordinates data fetching, template generation, and file output.
|
|
4
|
+
* v2: Fetches fresh data from GitHub if token available, otherwise uses cached lastDigest.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import { execFile } from 'child_process';
|
|
8
|
+
import { getStateManager, getDashboardPath, getGitHubToken } from '../core/index.js';
|
|
9
|
+
import { outputJson } from '../formatters/json.js';
|
|
10
|
+
import { fetchDashboardData, computePRsByRepo, computeTopRepos, getMonthlyData } from './dashboard-data.js';
|
|
11
|
+
import { buildDashboardStats, generateDashboardHtml } from './dashboard-templates.js';
|
|
12
|
+
export async function runDashboard(options) {
|
|
13
|
+
const stateManager = getStateManager();
|
|
14
|
+
const token = options.offline ? null : getGitHubToken();
|
|
15
|
+
let digest;
|
|
16
|
+
let commentedIssues = [];
|
|
17
|
+
// In offline mode, skip all GitHub API calls
|
|
18
|
+
if (options.offline) {
|
|
19
|
+
const state = stateManager.getState();
|
|
20
|
+
digest = state.lastDigest;
|
|
21
|
+
if (!digest) {
|
|
22
|
+
if (options.json) {
|
|
23
|
+
outputJson({ error: 'No cached data found. Run without --offline first.', offline: true });
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
console.error('No cached data found. Run without --offline first.');
|
|
27
|
+
}
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const lastUpdated = digest.generatedAt || state.lastDigestAt || state.lastRunAt;
|
|
31
|
+
console.error(`Offline mode: using cached data from ${lastUpdated}`);
|
|
32
|
+
}
|
|
33
|
+
else if (token) {
|
|
34
|
+
console.error('Fetching fresh data from GitHub...');
|
|
35
|
+
try {
|
|
36
|
+
const result = await fetchDashboardData(token);
|
|
37
|
+
digest = result.digest;
|
|
38
|
+
commentedIssues = result.commentedIssues;
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
console.error('Failed to fetch fresh data:', error instanceof Error ? error.message : error);
|
|
42
|
+
console.error('Falling back to cached data (issue conversations unavailable)...');
|
|
43
|
+
digest = stateManager.getState().lastDigest;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
// No token and not offline ā fall back to cached digest
|
|
48
|
+
digest = stateManager.getState().lastDigest;
|
|
49
|
+
}
|
|
50
|
+
// Check if we have a digest to display
|
|
51
|
+
if (!digest) {
|
|
52
|
+
if (options.json) {
|
|
53
|
+
outputJson({ error: 'No data available. Run daily check first with GITHUB_TOKEN.' });
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
console.error('No dashboard data available. Run the daily check first:');
|
|
57
|
+
console.error(' GITHUB_TOKEN=$(gh auth token) npm start -- daily');
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const state = stateManager.getState();
|
|
62
|
+
// Gather data for charts from digest
|
|
63
|
+
const prsByRepo = computePRsByRepo(digest, state);
|
|
64
|
+
const topRepos = computeTopRepos(prsByRepo);
|
|
65
|
+
const { monthlyMerged, monthlyClosed, monthlyOpened } = getMonthlyData(state);
|
|
66
|
+
const stats = buildDashboardStats(digest, state);
|
|
67
|
+
if (options.json) {
|
|
68
|
+
const issueResponses = commentedIssues.filter((i) => i.status === 'new_response');
|
|
69
|
+
const jsonData = {
|
|
70
|
+
stats,
|
|
71
|
+
prsByRepo,
|
|
72
|
+
topRepos: topRepos.map(([repo, data]) => ({ repo, ...data })),
|
|
73
|
+
monthlyMerged,
|
|
74
|
+
activePRs: digest.openPRs || [],
|
|
75
|
+
commentedIssues,
|
|
76
|
+
issueResponses,
|
|
77
|
+
};
|
|
78
|
+
if (options.offline) {
|
|
79
|
+
jsonData.offline = true;
|
|
80
|
+
jsonData.lastUpdated = digest.generatedAt || state.lastDigestAt || state.lastRunAt;
|
|
81
|
+
}
|
|
82
|
+
outputJson(jsonData);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const issueResponses = commentedIssues.filter((i) => i.status === 'new_response');
|
|
86
|
+
const html = generateDashboardHtml(stats, monthlyMerged, monthlyClosed, monthlyOpened, digest, state, issueResponses);
|
|
87
|
+
// Write to file in ~/.oss-autopilot/
|
|
88
|
+
const dashboardPath = getDashboardPath();
|
|
89
|
+
fs.writeFileSync(dashboardPath, html, { mode: 0o644 });
|
|
90
|
+
fs.chmodSync(dashboardPath, 0o644);
|
|
91
|
+
if (options.offline) {
|
|
92
|
+
const lastUpdated = digest.generatedAt || state.lastDigestAt || state.lastRunAt;
|
|
93
|
+
console.log(`\nš Dashboard generated (offline, cached data from ${lastUpdated}): ${dashboardPath}`);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
console.log(`\nš Dashboard generated: ${dashboardPath}`);
|
|
97
|
+
}
|
|
98
|
+
if (options.open) {
|
|
99
|
+
// Use platform-specific open command - path is hardcoded, not user input
|
|
100
|
+
const isWindows = process.platform === 'win32';
|
|
101
|
+
const openCmd = process.platform === 'darwin' ? 'open' : isWindows ? 'cmd' : 'xdg-open';
|
|
102
|
+
const args = isWindows ? ['/c', 'start', '', dashboardPath] : [dashboardPath];
|
|
103
|
+
console.log(`Dashboard: ${dashboardPath}`);
|
|
104
|
+
execFile(openCmd, args, (error) => {
|
|
105
|
+
if (error) {
|
|
106
|
+
console.error('Failed to open browser:', error.message);
|
|
107
|
+
console.error(`Open manually: ${dashboardPath}`);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
console.log('Run with --open to open in browser');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Generate dashboard HTML from state (no GitHub fetch).
|
|
117
|
+
* Call after executeDailyCheck() which saves fresh data to state.
|
|
118
|
+
* Returns the path to the generated dashboard HTML file.
|
|
119
|
+
*/
|
|
120
|
+
export function writeDashboardFromState() {
|
|
121
|
+
const stateManager = getStateManager();
|
|
122
|
+
const state = stateManager.getState();
|
|
123
|
+
const digest = state.lastDigest;
|
|
124
|
+
if (!digest) {
|
|
125
|
+
throw new Error('No digest data available. Run daily check first.');
|
|
126
|
+
}
|
|
127
|
+
const { monthlyMerged, monthlyClosed, monthlyOpened } = getMonthlyData(state);
|
|
128
|
+
const stats = buildDashboardStats(digest, state);
|
|
129
|
+
const html = generateDashboardHtml(stats, monthlyMerged, monthlyClosed, monthlyOpened, digest, state);
|
|
130
|
+
const dashboardPath = getDashboardPath();
|
|
131
|
+
fs.writeFileSync(dashboardPath, html, { mode: 0o644 });
|
|
132
|
+
fs.chmodSync(dashboardPath, 0o644);
|
|
133
|
+
return dashboardPath;
|
|
134
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dismiss/Undismiss commands
|
|
3
|
+
* Manages dismissing issue reply notifications without posting a comment.
|
|
4
|
+
* Dismissed issues resurface automatically when new responses arrive after the dismiss timestamp.
|
|
5
|
+
*/
|
|
6
|
+
import { ISSUE_URL_PATTERN } from './validation.js';
|
|
7
|
+
interface DismissCommandOptions {
|
|
8
|
+
issueUrl: string;
|
|
9
|
+
json?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export { ISSUE_URL_PATTERN };
|
|
12
|
+
export declare function runDismiss(options: DismissCommandOptions): Promise<void>;
|
|
13
|
+
export declare function runUndismiss(options: DismissCommandOptions): Promise<void>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dismiss/Undismiss commands
|
|
3
|
+
* Manages dismissing issue reply notifications without posting a comment.
|
|
4
|
+
* Dismissed issues resurface automatically when new responses arrive after the dismiss timestamp.
|
|
5
|
+
*/
|
|
6
|
+
import { getStateManager } from '../core/index.js';
|
|
7
|
+
import { outputJson } from '../formatters/json.js';
|
|
8
|
+
import { ISSUE_URL_PATTERN, validateGitHubUrl, validateUrl } from './validation.js';
|
|
9
|
+
// Re-export for backward compatibility with tests
|
|
10
|
+
export { ISSUE_URL_PATTERN };
|
|
11
|
+
export async function runDismiss(options) {
|
|
12
|
+
validateUrl(options.issueUrl);
|
|
13
|
+
validateGitHubUrl(options.issueUrl, ISSUE_URL_PATTERN, 'issue', options.json);
|
|
14
|
+
const stateManager = getStateManager();
|
|
15
|
+
const added = stateManager.dismissIssue(options.issueUrl, new Date().toISOString());
|
|
16
|
+
if (added) {
|
|
17
|
+
stateManager.save();
|
|
18
|
+
}
|
|
19
|
+
if (options.json) {
|
|
20
|
+
outputJson({ dismissed: added, url: options.issueUrl });
|
|
21
|
+
}
|
|
22
|
+
else if (added) {
|
|
23
|
+
console.log(`Dismissed: ${options.issueUrl}`);
|
|
24
|
+
console.log('Issue reply notifications are now muted.');
|
|
25
|
+
console.log('New responses after this point will resurface automatically.');
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
console.log('Issue is already dismissed.');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function runUndismiss(options) {
|
|
32
|
+
validateUrl(options.issueUrl);
|
|
33
|
+
validateGitHubUrl(options.issueUrl, ISSUE_URL_PATTERN, 'issue', options.json);
|
|
34
|
+
const stateManager = getStateManager();
|
|
35
|
+
const removed = stateManager.undismissIssue(options.issueUrl);
|
|
36
|
+
if (removed) {
|
|
37
|
+
stateManager.save();
|
|
38
|
+
}
|
|
39
|
+
if (options.json) {
|
|
40
|
+
outputJson({ undismissed: removed, url: options.issueUrl });
|
|
41
|
+
}
|
|
42
|
+
else if (removed) {
|
|
43
|
+
console.log(`Undismissed: ${options.issueUrl}`);
|
|
44
|
+
console.log('Issue reply notifications are active again.');
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
console.log('Issue was not dismissed.');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Init command
|
|
3
|
+
* Initialize with GitHub username. In v2, PRs are fetched fresh on each daily run.
|
|
4
|
+
*/
|
|
5
|
+
interface InitOptions {
|
|
6
|
+
username: string;
|
|
7
|
+
json?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function runInit(options: InitOptions): Promise<void>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Init command
|
|
3
|
+
* Initialize with GitHub username. In v2, PRs are fetched fresh on each daily run.
|
|
4
|
+
*/
|
|
5
|
+
import { getStateManager } from '../core/index.js';
|
|
6
|
+
import { outputJson } from '../formatters/json.js';
|
|
7
|
+
import { validateGitHubUsername } from './validation.js';
|
|
8
|
+
export async function runInit(options) {
|
|
9
|
+
validateGitHubUsername(options.username);
|
|
10
|
+
const stateManager = getStateManager();
|
|
11
|
+
if (!options.json) {
|
|
12
|
+
console.log(`\nš Initializing for @${options.username}...\n`);
|
|
13
|
+
}
|
|
14
|
+
// Set username in config
|
|
15
|
+
stateManager.updateConfig({ githubUsername: options.username });
|
|
16
|
+
stateManager.save();
|
|
17
|
+
if (options.json) {
|
|
18
|
+
outputJson({
|
|
19
|
+
username: options.username,
|
|
20
|
+
message: 'Username saved. Run `daily` to fetch your open PRs from GitHub.',
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
console.log(`Username set to @${options.username}.`);
|
|
25
|
+
console.log('Run `oss-autopilot daily` to fetch your open PRs from GitHub.');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local repos command (#84)
|
|
3
|
+
* Scans configurable directories for local git clones and caches results
|
|
4
|
+
*/
|
|
5
|
+
import { type LocalRepoInfo } from '../formatters/json.js';
|
|
6
|
+
interface LocalReposOptions {
|
|
7
|
+
scan?: boolean;
|
|
8
|
+
paths?: string[];
|
|
9
|
+
json?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/** Scan directories for git repos, returning a map of owner/repo ā local path */
|
|
12
|
+
export declare function scanForRepos(scanPaths: string[]): Record<string, LocalRepoInfo>;
|
|
13
|
+
export declare function runLocalRepos(options: LocalReposOptions): Promise<void>;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local repos command (#84)
|
|
3
|
+
* Scans configurable directories for local git clones and caches results
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import * as os from 'os';
|
|
8
|
+
import { execFileSync } from 'child_process';
|
|
9
|
+
import { getStateManager, debug } from '../core/index.js';
|
|
10
|
+
import { outputJson } from '../formatters/json.js';
|
|
11
|
+
/** Default directories to scan for local clones */
|
|
12
|
+
const DEFAULT_SCAN_PATHS = [
|
|
13
|
+
path.join(os.homedir(), 'Documents', 'oss'),
|
|
14
|
+
path.join(os.homedir(), 'dev'),
|
|
15
|
+
path.join(os.homedir(), 'projects'),
|
|
16
|
+
path.join(os.homedir(), 'src'),
|
|
17
|
+
path.join(os.homedir(), 'code'),
|
|
18
|
+
path.join(os.homedir(), 'repos'),
|
|
19
|
+
];
|
|
20
|
+
/** Extract the GitHub "owner/repo" remote from a git directory */
|
|
21
|
+
function getGitHubRemote(repoPath) {
|
|
22
|
+
try {
|
|
23
|
+
const remoteUrl = execFileSync('git', ['-C', repoPath, 'remote', 'get-url', 'origin'], {
|
|
24
|
+
encoding: 'utf-8',
|
|
25
|
+
timeout: 5000,
|
|
26
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
27
|
+
}).trim();
|
|
28
|
+
// Match HTTPS: https://github.com/owner/repo.git or https://github.com/owner/repo
|
|
29
|
+
const httpsMatch = remoteUrl.match(/github\.com\/([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
30
|
+
if (httpsMatch)
|
|
31
|
+
return httpsMatch[1];
|
|
32
|
+
// Match SSH: git@github.com:owner/repo.git
|
|
33
|
+
const sshMatch = remoteUrl.match(/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
34
|
+
if (sshMatch)
|
|
35
|
+
return sshMatch[1];
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
// git remote get-url failed (no remote, not a git repo, or timeout) ā skip this repo
|
|
40
|
+
debug('local-repos', `Failed to get GitHub remote for ${repoPath}`, err);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Get the current branch of a git repo */
|
|
45
|
+
function getCurrentBranch(repoPath) {
|
|
46
|
+
try {
|
|
47
|
+
return (execFileSync('git', ['-C', repoPath, 'branch', '--show-current'], {
|
|
48
|
+
encoding: 'utf-8',
|
|
49
|
+
timeout: 5000,
|
|
50
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
51
|
+
}).trim() || null);
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
// git branch --show-current failed ā repo may be in detached HEAD state or inaccessible
|
|
55
|
+
debug('local-repos', `Failed to get current branch for ${repoPath}`, err);
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Scan directories for git repos, returning a map of owner/repo ā local path */
|
|
60
|
+
export function scanForRepos(scanPaths) {
|
|
61
|
+
const repos = {};
|
|
62
|
+
for (const scanPath of scanPaths) {
|
|
63
|
+
if (!fs.existsSync(scanPath))
|
|
64
|
+
continue;
|
|
65
|
+
// Find git repos up to 3 levels deep
|
|
66
|
+
let gitDirs;
|
|
67
|
+
try {
|
|
68
|
+
const output = execFileSync('find', [scanPath, '-maxdepth', '4', '-name', '.git', '-type', 'd'], {
|
|
69
|
+
encoding: 'utf-8',
|
|
70
|
+
timeout: 30000,
|
|
71
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
72
|
+
}).trim();
|
|
73
|
+
gitDirs = output ? output.split('\n').filter(Boolean) : [];
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
// find command failed for this scan path (permission denied, path gone, etc.) ā skip it
|
|
77
|
+
debug('local-repos', `find failed for scan path ${scanPath}`, err);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
for (const gitDir of gitDirs) {
|
|
81
|
+
const repoPath = path.dirname(gitDir);
|
|
82
|
+
const remote = getGitHubRemote(repoPath);
|
|
83
|
+
if (!remote)
|
|
84
|
+
continue;
|
|
85
|
+
const currentBranch = getCurrentBranch(repoPath);
|
|
86
|
+
repos[remote] = {
|
|
87
|
+
path: repoPath,
|
|
88
|
+
exists: true,
|
|
89
|
+
currentBranch,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return repos;
|
|
94
|
+
}
|
|
95
|
+
export async function runLocalRepos(options) {
|
|
96
|
+
const stateManager = getStateManager();
|
|
97
|
+
const state = stateManager.getState();
|
|
98
|
+
const scanPaths = options.paths?.map((p) => path.resolve(p)) ??
|
|
99
|
+
state.config.localRepoScanPaths ??
|
|
100
|
+
DEFAULT_SCAN_PATHS.filter((p) => fs.existsSync(p));
|
|
101
|
+
// Use cached data unless --scan is specified
|
|
102
|
+
if (!options.scan && state.localRepoCache) {
|
|
103
|
+
const cache = state.localRepoCache;
|
|
104
|
+
const result = {
|
|
105
|
+
repos: cache.repos,
|
|
106
|
+
scanPaths: cache.scanPaths,
|
|
107
|
+
cachedAt: cache.cachedAt,
|
|
108
|
+
fromCache: true,
|
|
109
|
+
};
|
|
110
|
+
if (options.json) {
|
|
111
|
+
outputJson(result);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
console.log(`\nš Local Repos (cached ${cache.cachedAt})\n`);
|
|
115
|
+
printRepos(cache.repos);
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (!options.json) {
|
|
120
|
+
console.log(`\nš Scanning for local repos in ${scanPaths.length} directories...\n`);
|
|
121
|
+
}
|
|
122
|
+
const repos = scanForRepos(scanPaths);
|
|
123
|
+
const repoCount = Object.keys(repos).length;
|
|
124
|
+
// Cache the results in state
|
|
125
|
+
const cachedAt = new Date().toISOString();
|
|
126
|
+
try {
|
|
127
|
+
stateManager.setLocalRepoCache({ repos, scanPaths, cachedAt });
|
|
128
|
+
stateManager.save();
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
132
|
+
console.error(`Warning: Failed to cache scan results: ${msg}`);
|
|
133
|
+
}
|
|
134
|
+
const result = {
|
|
135
|
+
repos,
|
|
136
|
+
scanPaths,
|
|
137
|
+
cachedAt,
|
|
138
|
+
fromCache: false,
|
|
139
|
+
};
|
|
140
|
+
if (options.json) {
|
|
141
|
+
outputJson(result);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
console.log(`Found ${repoCount} repos:\n`);
|
|
145
|
+
printRepos(repos);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function printRepos(repos) {
|
|
149
|
+
const entries = Object.entries(repos).sort(([a], [b]) => a.localeCompare(b));
|
|
150
|
+
for (const [remote, info] of entries) {
|
|
151
|
+
const branch = info.currentBranch ? ` (${info.currentBranch})` : '';
|
|
152
|
+
console.log(` ${remote}${branch}`);
|
|
153
|
+
console.log(` ${info.path}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse issue list command (#82)
|
|
3
|
+
* Parses markdown issue lists into structured JSON with tier classification
|
|
4
|
+
*/
|
|
5
|
+
import { type ParseIssueListOutput } from '../formatters/json.js';
|
|
6
|
+
interface ParseListOptions {
|
|
7
|
+
filePath: string;
|
|
8
|
+
json?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** Parse a markdown string into structured issue items */
|
|
11
|
+
export declare function parseIssueList(content: string): ParseIssueListOutput;
|
|
12
|
+
export declare function runParseList(options: ParseListOptions): Promise<void>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse issue list command (#82)
|
|
3
|
+
* Parses markdown issue lists into structured JSON with tier classification
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import { outputJson, outputJsonError } from '../formatters/json.js';
|
|
8
|
+
/** Extract GitHub issue/PR URLs from a markdown line */
|
|
9
|
+
function extractGitHubUrl(line) {
|
|
10
|
+
const match = line.match(/https:\/\/github\.com\/([^/]+\/[^/]+)\/issues\/(\d+)/);
|
|
11
|
+
if (match) {
|
|
12
|
+
return { repo: match[1], number: parseInt(match[2], 10), url: match[0] };
|
|
13
|
+
}
|
|
14
|
+
const prMatch = line.match(/https:\/\/github\.com\/([^/]+\/[^/]+)\/pull\/(\d+)/);
|
|
15
|
+
if (prMatch) {
|
|
16
|
+
return { repo: prMatch[1], number: parseInt(prMatch[2], 10), url: prMatch[0] };
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
/** Extract issue title from a markdown line (text after URL or checkbox) */
|
|
21
|
+
function extractTitle(line) {
|
|
22
|
+
// Remove markdown link syntax: [title](url) ā title
|
|
23
|
+
let cleaned = line.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
|
|
24
|
+
// Remove bare URLs
|
|
25
|
+
cleaned = cleaned.replace(/https?:\/\/\S+/g, '');
|
|
26
|
+
// Remove list markers (-, *, +, numbered)
|
|
27
|
+
cleaned = cleaned.replace(/^\s*[-*+]\s*/, '').replace(/^\s*\d+\.\s*/, '');
|
|
28
|
+
// Remove checkboxes
|
|
29
|
+
cleaned = cleaned.replace(/\[[ xX]\]\s*/, '');
|
|
30
|
+
// Remove strikethrough markers
|
|
31
|
+
cleaned = cleaned.replace(/~~/g, '');
|
|
32
|
+
// Remove "Done" markers
|
|
33
|
+
cleaned = cleaned.replace(/\b(Done|DONE|done)\b/g, '');
|
|
34
|
+
// Remove leading/trailing punctuation and whitespace
|
|
35
|
+
cleaned = cleaned.replace(/^[\s\-āā:]+/, '').replace(/[\s\-āā:]+$/, '');
|
|
36
|
+
return cleaned.trim();
|
|
37
|
+
}
|
|
38
|
+
/** Determine if a line represents a completed item */
|
|
39
|
+
function isCompleted(line) {
|
|
40
|
+
// Strikethrough: ~~text~~
|
|
41
|
+
if (/~~.+~~/.test(line))
|
|
42
|
+
return true;
|
|
43
|
+
// Checked checkbox: [x] or [X]
|
|
44
|
+
if (/\[[xX]\]/.test(line))
|
|
45
|
+
return true;
|
|
46
|
+
// "Done" marker (standalone word, case insensitive)
|
|
47
|
+
if (/\bdone\b/i.test(line))
|
|
48
|
+
return true;
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
/** Parse a markdown string into structured issue items */
|
|
52
|
+
export function parseIssueList(content) {
|
|
53
|
+
const lines = content.split('\n');
|
|
54
|
+
const available = [];
|
|
55
|
+
const completed = [];
|
|
56
|
+
let currentTier = 'Uncategorized';
|
|
57
|
+
for (const line of lines) {
|
|
58
|
+
// Check for section headings (# or ##)
|
|
59
|
+
const headingMatch = line.match(/^#{1,3}\s+(.+)/);
|
|
60
|
+
if (headingMatch) {
|
|
61
|
+
currentTier = headingMatch[1].trim();
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
// Skip empty lines and non-list items
|
|
65
|
+
if (!line.trim() || !/^\s*[-*+]|\s*\d+\.|\s*\[[ xX]\]/.test(line)) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
// Extract GitHub URL ā skip lines without one
|
|
69
|
+
const ghUrl = extractGitHubUrl(line);
|
|
70
|
+
if (!ghUrl)
|
|
71
|
+
continue;
|
|
72
|
+
const title = extractTitle(line);
|
|
73
|
+
const item = {
|
|
74
|
+
repo: ghUrl.repo,
|
|
75
|
+
number: ghUrl.number,
|
|
76
|
+
title: title || `#${ghUrl.number}`,
|
|
77
|
+
tier: currentTier,
|
|
78
|
+
url: ghUrl.url,
|
|
79
|
+
};
|
|
80
|
+
if (isCompleted(line)) {
|
|
81
|
+
completed.push(item);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
available.push(item);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
available,
|
|
89
|
+
completed,
|
|
90
|
+
availableCount: available.length,
|
|
91
|
+
completedCount: completed.length,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export async function runParseList(options) {
|
|
95
|
+
const filePath = path.resolve(options.filePath);
|
|
96
|
+
if (!fs.existsSync(filePath)) {
|
|
97
|
+
if (options.json) {
|
|
98
|
+
outputJsonError(`File not found: ${filePath}`);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
console.error(`Error: File not found: ${filePath}`);
|
|
102
|
+
}
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
let content;
|
|
106
|
+
try {
|
|
107
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
111
|
+
if (options.json) {
|
|
112
|
+
outputJsonError(`Failed to read file: ${msg}`);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
console.error(`Error: Failed to read file: ${msg}`);
|
|
116
|
+
}
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
const result = parseIssueList(content);
|
|
120
|
+
if (options.json) {
|
|
121
|
+
outputJson(result);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
console.log(`\nš Issue List: ${filePath}\n`);
|
|
125
|
+
console.log(`Available: ${result.availableCount} | Completed: ${result.completedCount}\n`);
|
|
126
|
+
if (result.available.length > 0) {
|
|
127
|
+
console.log('--- Available ---');
|
|
128
|
+
for (const item of result.available) {
|
|
129
|
+
console.log(` [${item.tier}] ${item.repo}#${item.number}: ${item.title}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (result.completed.length > 0) {
|
|
133
|
+
console.log('\n--- Completed ---');
|
|
134
|
+
for (const item of result.completed) {
|
|
135
|
+
console.log(` [${item.tier}] ${item.repo}#${item.number}: ${item.title}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read command
|
|
3
|
+
* In v2, PR read/unread state is not tracked locally.
|
|
4
|
+
* This command is a no-op preserved for backward compatibility.
|
|
5
|
+
*/
|
|
6
|
+
interface ReadOptions {
|
|
7
|
+
prUrl?: string;
|
|
8
|
+
all?: boolean;
|
|
9
|
+
json?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function runRead(options: ReadOptions): Promise<void>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read command
|
|
3
|
+
* In v2, PR read/unread state is not tracked locally.
|
|
4
|
+
* This command is a no-op preserved for backward compatibility.
|
|
5
|
+
*/
|
|
6
|
+
import { outputJson, outputJsonError } from '../formatters/json.js';
|
|
7
|
+
import { validateUrl } from './validation.js';
|
|
8
|
+
export async function runRead(options) {
|
|
9
|
+
if (!options.all && !options.prUrl) {
|
|
10
|
+
if (options.json) {
|
|
11
|
+
outputJsonError('PR URL or --all flag required');
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
console.error('Usage: oss-autopilot read <pr-url> or oss-autopilot read --all');
|
|
15
|
+
}
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
if (options.prUrl) {
|
|
19
|
+
validateUrl(options.prUrl);
|
|
20
|
+
}
|
|
21
|
+
// In v2, unread state is not tracked locally ā PRs are fetched fresh each run.
|
|
22
|
+
if (options.json) {
|
|
23
|
+
if (options.all) {
|
|
24
|
+
outputJson({ markedAsRead: 0, all: true, message: 'In v2, PR read state is not tracked locally.' });
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
outputJson({ marked: false, url: options.prUrl, message: 'In v2, PR read state is not tracked locally.' });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
console.log('Note: In v2, PR read state is not tracked locally. PRs are fetched fresh on each daily run.');
|
|
32
|
+
}
|
|
33
|
+
}
|