@girardmedia/bootspring 2.0.21 → 2.0.22
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/cli/preseed/index.js +16 -0
- package/cli/preseed/interactive.js +143 -0
- package/cli/preseed/templates.js +227 -0
- package/cli/seed/builders/ai-context-builder.js +85 -0
- package/cli/seed/builders/index.js +13 -0
- package/cli/seed/builders/seed-builder.js +272 -0
- package/cli/seed/extractors/content-extractors.js +383 -0
- package/cli/seed/extractors/index.js +47 -0
- package/cli/seed/extractors/metadata-extractors.js +167 -0
- package/cli/seed/extractors/section-extractor.js +54 -0
- package/cli/seed/extractors/stack-extractors.js +228 -0
- package/cli/seed/index.js +18 -0
- package/cli/seed/utils/folder-structure.js +84 -0
- package/cli/seed/utils/index.js +11 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +3220 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/context-McpJQa_2.d.ts +5710 -0
- package/dist/core/index.d.ts +635 -0
- package/dist/core/index.js +2593 -0
- package/dist/core/index.js.map +1 -0
- package/dist/index-QqbeEiDm.d.ts +857 -0
- package/dist/index-UiYCgwiH.d.ts +174 -0
- package/dist/index.d.ts +453 -0
- package/dist/index.js +44228 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +41173 -0
- package/dist/mcp/index.js.map +1 -0
- package/generators/index.ts +82 -0
- package/intelligence/orchestrator/config/failure-signatures.js +48 -0
- package/intelligence/orchestrator/config/index.js +20 -0
- package/intelligence/orchestrator/config/phases.js +111 -0
- package/intelligence/orchestrator/config/remediation.js +150 -0
- package/intelligence/orchestrator/config/workflows.js +168 -0
- package/intelligence/orchestrator/core/index.js +16 -0
- package/intelligence/orchestrator/core/state-manager.js +88 -0
- package/intelligence/orchestrator/core/telemetry.js +24 -0
- package/intelligence/orchestrator/index.js +17 -0
- package/mcp/contracts/mcp-contract.v1.json +1 -1
- package/package.json +16 -3
- package/src/cli/agent.ts +703 -0
- package/src/cli/analyze.ts +640 -0
- package/src/cli/audit.ts +707 -0
- package/src/cli/auth.ts +930 -0
- package/src/cli/billing.ts +364 -0
- package/src/cli/build.ts +1089 -0
- package/src/cli/business.ts +508 -0
- package/src/cli/checkpoint-utils.ts +236 -0
- package/src/cli/checkpoint.ts +757 -0
- package/src/cli/cloud-sync.ts +534 -0
- package/src/cli/content.ts +273 -0
- package/src/cli/context.ts +667 -0
- package/src/cli/dashboard.ts +133 -0
- package/src/cli/deploy.ts +704 -0
- package/src/cli/doctor.ts +480 -0
- package/src/cli/fundraise.ts +494 -0
- package/src/cli/generate.ts +346 -0
- package/src/cli/github-cmd.ts +566 -0
- package/src/cli/health.ts +599 -0
- package/src/cli/index.ts +113 -0
- package/src/cli/init.ts +838 -0
- package/src/cli/legal.ts +495 -0
- package/src/cli/log.ts +316 -0
- package/src/cli/loop.ts +1660 -0
- package/src/cli/manager.ts +878 -0
- package/src/cli/mcp.ts +275 -0
- package/src/cli/memory.ts +346 -0
- package/src/cli/metrics.ts +590 -0
- package/src/cli/monitor.ts +960 -0
- package/src/cli/mvp.ts +662 -0
- package/src/cli/onboard.ts +663 -0
- package/src/cli/orchestrator.ts +622 -0
- package/src/cli/plugin.ts +483 -0
- package/src/cli/prd.ts +671 -0
- package/src/cli/preseed-start.ts +1633 -0
- package/src/cli/preseed.ts +2434 -0
- package/src/cli/project.ts +526 -0
- package/src/cli/quality.ts +885 -0
- package/src/cli/security.ts +1079 -0
- package/src/cli/seed.ts +1224 -0
- package/src/cli/skill.ts +537 -0
- package/src/cli/suggest.ts +1225 -0
- package/src/cli/switch.ts +518 -0
- package/src/cli/task.ts +780 -0
- package/src/cli/telemetry.ts +172 -0
- package/src/cli/todo.ts +627 -0
- package/src/cli/types.ts +15 -0
- package/src/cli/update.ts +334 -0
- package/src/cli/visualize.ts +609 -0
- package/src/cli/watch.ts +895 -0
- package/src/cli/workspace.ts +709 -0
- package/src/core/action-recorder.ts +673 -0
- package/src/core/analyze-workflow.ts +1453 -0
- package/src/core/api-client.ts +1120 -0
- package/src/core/audit-workflow.ts +1681 -0
- package/src/core/auth.ts +471 -0
- package/src/core/build-orchestrator.ts +509 -0
- package/src/core/build-state.ts +621 -0
- package/src/core/checkpoint-engine.ts +482 -0
- package/src/core/config.ts +1285 -0
- package/src/core/context-loader.ts +694 -0
- package/src/core/context.ts +410 -0
- package/src/core/deploy-workflow.ts +1085 -0
- package/src/core/entitlements.ts +322 -0
- package/src/core/github-sync.ts +720 -0
- package/src/core/index.ts +981 -0
- package/src/core/ingest.ts +1186 -0
- package/src/core/metrics-engine.ts +886 -0
- package/src/core/mvp.ts +847 -0
- package/src/core/onboard-workflow.ts +1293 -0
- package/src/core/policies.ts +81 -0
- package/src/core/preseed-workflow.ts +1163 -0
- package/src/core/preseed.ts +1826 -0
- package/src/core/project-context.ts +380 -0
- package/src/core/project-state.ts +699 -0
- package/src/core/r2-sync.ts +691 -0
- package/src/core/scaffold.ts +1715 -0
- package/src/core/session.ts +286 -0
- package/src/core/task-extractor.ts +799 -0
- package/src/core/telemetry.ts +371 -0
- package/src/core/tier-enforcement.ts +737 -0
- package/src/core/utils.ts +437 -0
- package/src/index.ts +29 -0
- package/src/intelligence/agent-collab.ts +2376 -0
- package/src/intelligence/auto-suggest.ts +713 -0
- package/src/intelligence/content-gen.ts +1351 -0
- package/src/intelligence/cross-project.ts +1692 -0
- package/src/intelligence/git-memory.ts +529 -0
- package/src/intelligence/index.ts +318 -0
- package/src/intelligence/orchestrator.ts +534 -0
- package/src/intelligence/prd.ts +466 -0
- package/src/intelligence/recommendations.ts +982 -0
- package/src/intelligence/workflow-composer.ts +1472 -0
- package/src/mcp/capabilities.ts +233 -0
- package/src/mcp/index.ts +37 -0
- package/src/mcp/registry.ts +1268 -0
- package/src/mcp/response-formatter.ts +797 -0
- package/src/mcp/server.ts +240 -0
- package/src/types/agent.ts +69 -0
- package/src/types/config.ts +86 -0
- package/src/types/context.ts +77 -0
- package/src/types/index.ts +53 -0
- package/src/types/mcp.ts +91 -0
- package/src/types/skills.ts +47 -0
- package/src/types/workflow.ts +155 -0
- package/generators/index.js +0 -18
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bootspring GitHub Sync
|
|
3
|
+
* Fetch GitHub repository data using gh CLI
|
|
4
|
+
*
|
|
5
|
+
* @package bootspring
|
|
6
|
+
* @module core/github-sync
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { execSync, exec } from 'child_process';
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
import * as utils from './utils';
|
|
13
|
+
import * as projectState from './project-state';
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Types
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
export interface RepositoryInfo {
|
|
20
|
+
owner: string;
|
|
21
|
+
repo: string;
|
|
22
|
+
url: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface RepositoryMetadata {
|
|
26
|
+
name: string;
|
|
27
|
+
description?: string | undefined;
|
|
28
|
+
defaultBranchRef?: { name: string } | undefined;
|
|
29
|
+
url: string;
|
|
30
|
+
createdAt?: string | undefined;
|
|
31
|
+
updatedAt?: string | undefined;
|
|
32
|
+
stargazerCount?: number | undefined;
|
|
33
|
+
forkCount?: number | undefined;
|
|
34
|
+
isPrivate?: boolean | undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CommitStats {
|
|
38
|
+
totalCommits: number;
|
|
39
|
+
lastCommit: string | null;
|
|
40
|
+
lastCommitMessage: string | null;
|
|
41
|
+
lastCommitAuthor: string | null;
|
|
42
|
+
lastCommitSha?: string | null | undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface PrStats {
|
|
46
|
+
openPRs: number;
|
|
47
|
+
closedPRs: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ActivityItem {
|
|
51
|
+
type: 'commit' | 'pr';
|
|
52
|
+
message?: string | undefined;
|
|
53
|
+
author?: string | undefined;
|
|
54
|
+
date?: string | undefined;
|
|
55
|
+
sha?: string | undefined;
|
|
56
|
+
number?: number | undefined;
|
|
57
|
+
title?: string | undefined;
|
|
58
|
+
state?: string | undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface GhCommandOptions {
|
|
62
|
+
cwd?: string | undefined;
|
|
63
|
+
timeout?: number | undefined;
|
|
64
|
+
parseJson?: boolean | undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface SyncResult {
|
|
68
|
+
success: boolean;
|
|
69
|
+
stats?: GitHubStats | undefined;
|
|
70
|
+
error?: string | undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface GitHubStats {
|
|
74
|
+
totalCommits: number;
|
|
75
|
+
openPRs: number;
|
|
76
|
+
closedPRs: number;
|
|
77
|
+
contributors: number;
|
|
78
|
+
lastCommit: string | null;
|
|
79
|
+
lastCommitMessage: string | null;
|
|
80
|
+
lastCommitSha?: string | null | undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface ConnectResult {
|
|
84
|
+
success: boolean;
|
|
85
|
+
owner?: string | undefined;
|
|
86
|
+
repo?: string | undefined;
|
|
87
|
+
url?: string | undefined;
|
|
88
|
+
defaultBranch?: string | undefined;
|
|
89
|
+
description?: string | undefined;
|
|
90
|
+
isPrivate?: boolean | undefined;
|
|
91
|
+
stats?: GitHubStats | undefined;
|
|
92
|
+
error?: string | undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface DisconnectResult {
|
|
96
|
+
success: boolean;
|
|
97
|
+
error?: string | undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface ConnectOptions {
|
|
101
|
+
url?: string | undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface SyncOptions {
|
|
105
|
+
spinner?: { start: () => void; stop: () => void } | undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface SyncMetadata {
|
|
109
|
+
lastSync: string;
|
|
110
|
+
commitStats: CommitStats;
|
|
111
|
+
prStats: PrStats;
|
|
112
|
+
contributors: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface GitHubStatus {
|
|
116
|
+
connected: boolean;
|
|
117
|
+
ghInstalled?: boolean | undefined;
|
|
118
|
+
ghAuthenticated?: boolean | undefined;
|
|
119
|
+
detected?: RepositoryInfo | null | undefined;
|
|
120
|
+
repositoryUrl?: string | undefined;
|
|
121
|
+
owner?: string | undefined;
|
|
122
|
+
repo?: string | undefined;
|
|
123
|
+
defaultBranch?: string | undefined;
|
|
124
|
+
lastSync?: string | undefined;
|
|
125
|
+
stats?: GitHubStats | undefined;
|
|
126
|
+
syncMetadata?: SyncMetadata | null | undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ============================================================================
|
|
130
|
+
// gh CLI Utilities
|
|
131
|
+
// ============================================================================
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Check if gh CLI is installed
|
|
135
|
+
*/
|
|
136
|
+
export function isGhInstalled(): boolean {
|
|
137
|
+
try {
|
|
138
|
+
execSync('gh --version', { stdio: 'pipe' });
|
|
139
|
+
return true;
|
|
140
|
+
} catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Check if gh CLI is authenticated
|
|
147
|
+
*/
|
|
148
|
+
export function isGhAuthenticated(): boolean {
|
|
149
|
+
try {
|
|
150
|
+
const result = execSync('gh auth status', { stdio: 'pipe', encoding: 'utf-8' });
|
|
151
|
+
return !result.includes('not logged');
|
|
152
|
+
} catch {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Execute gh command and return parsed JSON
|
|
159
|
+
*/
|
|
160
|
+
export function ghCommand<T = unknown>(command: string, options: GhCommandOptions = {}): T | null {
|
|
161
|
+
try {
|
|
162
|
+
const result = execSync(`gh ${command}`, {
|
|
163
|
+
cwd: options.cwd || process.cwd(),
|
|
164
|
+
encoding: 'utf-8',
|
|
165
|
+
timeout: options.timeout || 30000,
|
|
166
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
167
|
+
});
|
|
168
|
+
return options.parseJson !== false ? JSON.parse(result) as T : result.trim() as unknown as T;
|
|
169
|
+
} catch (error) {
|
|
170
|
+
const err = error as Error;
|
|
171
|
+
utils.print.debug(`gh command failed: ${err.message}`);
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Execute gh command asynchronously
|
|
178
|
+
*/
|
|
179
|
+
export function ghCommandAsync<T = unknown>(command: string, options: GhCommandOptions = {}): Promise<T | null> {
|
|
180
|
+
return new Promise((resolve) => {
|
|
181
|
+
exec(`gh ${command}`, {
|
|
182
|
+
cwd: options.cwd || process.cwd(),
|
|
183
|
+
timeout: options.timeout || 30000
|
|
184
|
+
}, (error, stdout) => {
|
|
185
|
+
if (error) {
|
|
186
|
+
utils.print.debug(`gh command failed: ${error.message}`);
|
|
187
|
+
resolve(null);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
resolve(options.parseJson !== false ? JSON.parse(stdout) as T : stdout.trim() as unknown as T);
|
|
192
|
+
} catch {
|
|
193
|
+
resolve(null);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ============================================================================
|
|
200
|
+
// Repository Detection
|
|
201
|
+
// ============================================================================
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Detect GitHub repository from git remote
|
|
205
|
+
*/
|
|
206
|
+
export function detectRepository(projectRoot: string): RepositoryInfo | null {
|
|
207
|
+
try {
|
|
208
|
+
const result = execSync('git remote get-url origin', {
|
|
209
|
+
cwd: projectRoot,
|
|
210
|
+
encoding: 'utf-8',
|
|
211
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
212
|
+
}).trim();
|
|
213
|
+
|
|
214
|
+
// Parse GitHub URL (SSH or HTTPS)
|
|
215
|
+
// HTTPS: https://github.com/owner/repo.git
|
|
216
|
+
// SSH: git@github.com:owner/repo.git
|
|
217
|
+
let match: RegExpMatchArray | null = null;
|
|
218
|
+
|
|
219
|
+
if (result.startsWith('https://')) {
|
|
220
|
+
match = result.match(/github\.com\/([^/]+)\/([^/.]+)/);
|
|
221
|
+
} else if (result.includes('github.com')) {
|
|
222
|
+
match = result.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (match && match[1] && match[2]) {
|
|
226
|
+
const owner = match[1];
|
|
227
|
+
const repo = match[2].replace('.git', '');
|
|
228
|
+
return {
|
|
229
|
+
owner,
|
|
230
|
+
repo,
|
|
231
|
+
url: `https://github.com/${owner}/${repo}`
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
} catch {
|
|
235
|
+
// Not a git repo or no remote
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Get default branch name
|
|
243
|
+
*/
|
|
244
|
+
export function getDefaultBranch(projectRoot: string): string {
|
|
245
|
+
try {
|
|
246
|
+
// Try to get from remote
|
|
247
|
+
const result = execSync('git remote show origin | grep "HEAD branch"', {
|
|
248
|
+
cwd: projectRoot,
|
|
249
|
+
encoding: 'utf-8',
|
|
250
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
251
|
+
});
|
|
252
|
+
const match = result.match(/HEAD branch:\s*(\S+)/);
|
|
253
|
+
if (match && match[1]) {
|
|
254
|
+
return match[1];
|
|
255
|
+
}
|
|
256
|
+
} catch {
|
|
257
|
+
// Fall back to checking common branch names
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Check if main or master exists
|
|
261
|
+
try {
|
|
262
|
+
execSync('git rev-parse --verify main', { cwd: projectRoot, stdio: 'pipe' });
|
|
263
|
+
return 'main';
|
|
264
|
+
} catch {
|
|
265
|
+
try {
|
|
266
|
+
execSync('git rev-parse --verify master', { cwd: projectRoot, stdio: 'pipe' });
|
|
267
|
+
return 'master';
|
|
268
|
+
} catch {
|
|
269
|
+
return 'main';
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ============================================================================
|
|
275
|
+
// Repository Data Fetching
|
|
276
|
+
// ============================================================================
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Get repository metadata
|
|
280
|
+
*/
|
|
281
|
+
export function getRepositoryInfo(owner: string, repo: string): RepositoryMetadata | null {
|
|
282
|
+
return ghCommand<RepositoryMetadata>(
|
|
283
|
+
`repo view ${owner}/${repo} --json name,description,defaultBranchRef,url,createdAt,updatedAt,stargazerCount,forkCount,isPrivate`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Get commit statistics
|
|
289
|
+
*/
|
|
290
|
+
export function getCommitStats(owner: string, repo: string, options: { limit?: number } = {}): CommitStats {
|
|
291
|
+
const limit = options.limit || 100;
|
|
292
|
+
|
|
293
|
+
// Get recent commits
|
|
294
|
+
interface CommitData {
|
|
295
|
+
sha?: string;
|
|
296
|
+
commit?: {
|
|
297
|
+
committer?: { date?: string };
|
|
298
|
+
message?: string;
|
|
299
|
+
author?: { name?: string };
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
const commits = ghCommand<CommitData[]>(`api repos/${owner}/${repo}/commits?per_page=${limit}`, { parseJson: true });
|
|
303
|
+
|
|
304
|
+
if (!commits) {
|
|
305
|
+
return {
|
|
306
|
+
totalCommits: 0,
|
|
307
|
+
lastCommit: null,
|
|
308
|
+
lastCommitMessage: null,
|
|
309
|
+
lastCommitAuthor: null
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const latest = commits[0];
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
totalCommits: commits.length, // Note: This is limited to what we fetch
|
|
317
|
+
lastCommit: latest?.commit?.committer?.date || null,
|
|
318
|
+
lastCommitMessage: latest?.commit?.message?.split('\n')[0] || null,
|
|
319
|
+
lastCommitAuthor: latest?.commit?.author?.name || null,
|
|
320
|
+
lastCommitSha: latest?.sha?.substring(0, 7) || null
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Get pull request statistics
|
|
326
|
+
*/
|
|
327
|
+
export function getPRStats(owner: string, repo: string): PrStats {
|
|
328
|
+
// Get open PRs
|
|
329
|
+
interface PrData {
|
|
330
|
+
number: number;
|
|
331
|
+
}
|
|
332
|
+
const openPRs = ghCommand<PrData[]>(`pr list --repo ${owner}/${repo} --state open --json number`, { parseJson: true });
|
|
333
|
+
|
|
334
|
+
// Get recently closed PRs
|
|
335
|
+
const closedPRs = ghCommand<PrData[]>(`pr list --repo ${owner}/${repo} --state closed --limit 50 --json number`, { parseJson: true });
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
openPRs: openPRs?.length || 0,
|
|
339
|
+
closedPRs: closedPRs?.length || 0
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Get contributor count
|
|
345
|
+
*/
|
|
346
|
+
export function getContributorCount(owner: string, repo: string): number {
|
|
347
|
+
interface ContributorData {
|
|
348
|
+
login: string;
|
|
349
|
+
}
|
|
350
|
+
const contributors = ghCommand<ContributorData[]>(`api repos/${owner}/${repo}/contributors?per_page=100`, { parseJson: true });
|
|
351
|
+
return contributors?.length || 0;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Get recent activity (commits + PRs)
|
|
356
|
+
*/
|
|
357
|
+
export function getRecentActivity(owner: string, repo: string, limit: number = 10): ActivityItem[] {
|
|
358
|
+
const activity: ActivityItem[] = [];
|
|
359
|
+
|
|
360
|
+
// Get recent commits
|
|
361
|
+
interface CommitData {
|
|
362
|
+
sha?: string;
|
|
363
|
+
commit?: {
|
|
364
|
+
committer?: { date?: string };
|
|
365
|
+
message?: string;
|
|
366
|
+
author?: { name?: string };
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
const commits = ghCommand<CommitData[]>(`api repos/${owner}/${repo}/commits?per_page=${limit}`, { parseJson: true });
|
|
370
|
+
if (commits) {
|
|
371
|
+
for (const commit of commits.slice(0, 5)) {
|
|
372
|
+
activity.push({
|
|
373
|
+
type: 'commit',
|
|
374
|
+
message: commit.commit?.message?.split('\n')[0],
|
|
375
|
+
author: commit.commit?.author?.name,
|
|
376
|
+
date: commit.commit?.committer?.date,
|
|
377
|
+
sha: commit.sha?.substring(0, 7)
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Get recent PRs
|
|
383
|
+
interface PrData {
|
|
384
|
+
number: number;
|
|
385
|
+
title: string;
|
|
386
|
+
author?: { login: string };
|
|
387
|
+
state: string;
|
|
388
|
+
createdAt: string;
|
|
389
|
+
}
|
|
390
|
+
const prs = ghCommand<PrData[]>(`pr list --repo ${owner}/${repo} --state all --limit 5 --json number,title,author,state,createdAt`, { parseJson: true });
|
|
391
|
+
if (prs) {
|
|
392
|
+
for (const pr of prs) {
|
|
393
|
+
activity.push({
|
|
394
|
+
type: 'pr',
|
|
395
|
+
number: pr.number,
|
|
396
|
+
title: pr.title,
|
|
397
|
+
author: pr.author?.login,
|
|
398
|
+
state: pr.state,
|
|
399
|
+
date: pr.createdAt
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Sort by date
|
|
405
|
+
activity.sort((a, b) => new Date(b.date || 0).getTime() - new Date(a.date || 0).getTime());
|
|
406
|
+
|
|
407
|
+
return activity.slice(0, limit);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ============================================================================
|
|
411
|
+
// Sync Functions
|
|
412
|
+
// ============================================================================
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Full sync of GitHub data to project state
|
|
416
|
+
*/
|
|
417
|
+
export function syncGitHubData(projectRoot: string, options: SyncOptions = {}): SyncResult {
|
|
418
|
+
const state = projectState.loadState(projectRoot);
|
|
419
|
+
|
|
420
|
+
interface GitHubStateData {
|
|
421
|
+
connected?: boolean;
|
|
422
|
+
owner?: string;
|
|
423
|
+
repo?: string;
|
|
424
|
+
repositoryUrl?: string;
|
|
425
|
+
defaultBranch?: string;
|
|
426
|
+
lastSync?: string;
|
|
427
|
+
stats?: GitHubStats;
|
|
428
|
+
}
|
|
429
|
+
const githubState = (state as { github?: GitHubStateData } | null)?.github;
|
|
430
|
+
|
|
431
|
+
if (!githubState?.connected) {
|
|
432
|
+
return {
|
|
433
|
+
success: false,
|
|
434
|
+
error: 'GitHub not connected'
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const { owner, repo } = githubState;
|
|
439
|
+
|
|
440
|
+
if (!owner || !repo) {
|
|
441
|
+
return {
|
|
442
|
+
success: false,
|
|
443
|
+
error: 'GitHub owner/repo not set'
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Check gh CLI
|
|
448
|
+
if (!isGhInstalled()) {
|
|
449
|
+
return {
|
|
450
|
+
success: false,
|
|
451
|
+
error: 'gh CLI not installed'
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (!isGhAuthenticated()) {
|
|
456
|
+
return {
|
|
457
|
+
success: false,
|
|
458
|
+
error: 'gh CLI not authenticated'
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Fetch data
|
|
463
|
+
const spinner = options.spinner;
|
|
464
|
+
if (spinner) spinner.start();
|
|
465
|
+
|
|
466
|
+
try {
|
|
467
|
+
// Get commit stats
|
|
468
|
+
const commitStats = getCommitStats(owner, repo);
|
|
469
|
+
|
|
470
|
+
// Get PR stats
|
|
471
|
+
const prStats = getPRStats(owner, repo);
|
|
472
|
+
|
|
473
|
+
// Get contributor count
|
|
474
|
+
const contributors = getContributorCount(owner, repo);
|
|
475
|
+
|
|
476
|
+
// Update state
|
|
477
|
+
const updatedState = projectState.updateGitHubState(projectRoot, {
|
|
478
|
+
connected: true,
|
|
479
|
+
repositoryUrl: githubState.repositoryUrl ?? null,
|
|
480
|
+
owner,
|
|
481
|
+
repo,
|
|
482
|
+
defaultBranch: githubState.defaultBranch ?? null,
|
|
483
|
+
stats: {
|
|
484
|
+
totalCommits: commitStats.totalCommits,
|
|
485
|
+
openPRs: prStats.openPRs,
|
|
486
|
+
closedPRs: prStats.closedPRs,
|
|
487
|
+
contributors,
|
|
488
|
+
lastCommit: commitStats.lastCommit,
|
|
489
|
+
lastCommitMessage: commitStats.lastCommitMessage,
|
|
490
|
+
lastCommitSha: commitStats.lastCommitSha
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// Save sync metadata
|
|
495
|
+
saveGitHubSyncMetadata(projectRoot, {
|
|
496
|
+
lastSync: new Date().toISOString(),
|
|
497
|
+
commitStats,
|
|
498
|
+
prStats,
|
|
499
|
+
contributors
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
const updatedGithub = (updatedState as { github?: GitHubStateData } | null)?.github;
|
|
503
|
+
|
|
504
|
+
return {
|
|
505
|
+
success: true,
|
|
506
|
+
stats: updatedGithub?.stats
|
|
507
|
+
};
|
|
508
|
+
} catch (error) {
|
|
509
|
+
const err = error as Error;
|
|
510
|
+
return {
|
|
511
|
+
success: false,
|
|
512
|
+
error: err.message
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Connect to a GitHub repository
|
|
519
|
+
*/
|
|
520
|
+
export function connectRepository(projectRoot: string, options: ConnectOptions = {}): ConnectResult {
|
|
521
|
+
// Check gh CLI
|
|
522
|
+
if (!isGhInstalled()) {
|
|
523
|
+
return {
|
|
524
|
+
success: false,
|
|
525
|
+
error: 'gh CLI not installed. Install from https://cli.github.com/'
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (!isGhAuthenticated()) {
|
|
530
|
+
return {
|
|
531
|
+
success: false,
|
|
532
|
+
error: 'gh CLI not authenticated. Run: gh auth login'
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Get repository info
|
|
537
|
+
let owner: string;
|
|
538
|
+
let repo: string;
|
|
539
|
+
let url: string;
|
|
540
|
+
|
|
541
|
+
if (options.url) {
|
|
542
|
+
// Parse provided URL
|
|
543
|
+
const match = options.url.match(/github\.com\/([^/]+)\/([^/]+)/);
|
|
544
|
+
if (!match || !match[1] || !match[2]) {
|
|
545
|
+
return {
|
|
546
|
+
success: false,
|
|
547
|
+
error: 'Invalid GitHub URL'
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
owner = match[1];
|
|
551
|
+
repo = match[2].replace('.git', '').replace(/\/$/, '');
|
|
552
|
+
url = `https://github.com/${owner}/${repo}`;
|
|
553
|
+
} else {
|
|
554
|
+
// Auto-detect from git remote
|
|
555
|
+
const detected = detectRepository(projectRoot);
|
|
556
|
+
if (!detected) {
|
|
557
|
+
return {
|
|
558
|
+
success: false,
|
|
559
|
+
error: 'Could not detect GitHub repository. Use --url to specify manually.'
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
owner = detected.owner;
|
|
563
|
+
repo = detected.repo;
|
|
564
|
+
url = detected.url;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Verify repository access
|
|
568
|
+
const repoInfo = getRepositoryInfo(owner, repo);
|
|
569
|
+
if (!repoInfo) {
|
|
570
|
+
return {
|
|
571
|
+
success: false,
|
|
572
|
+
error: `Could not access repository: ${owner}/${repo}`
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Get default branch
|
|
577
|
+
const defaultBranch = repoInfo.defaultBranchRef?.name || getDefaultBranch(projectRoot);
|
|
578
|
+
|
|
579
|
+
// Update project state
|
|
580
|
+
projectState.getOrCreateState(projectRoot);
|
|
581
|
+
|
|
582
|
+
projectState.updateGitHubState(projectRoot, {
|
|
583
|
+
connected: true,
|
|
584
|
+
repositoryUrl: url,
|
|
585
|
+
owner,
|
|
586
|
+
repo,
|
|
587
|
+
defaultBranch,
|
|
588
|
+
stats: {
|
|
589
|
+
totalCommits: 0,
|
|
590
|
+
openPRs: 0,
|
|
591
|
+
closedPRs: 0,
|
|
592
|
+
contributors: 0,
|
|
593
|
+
lastCommit: null,
|
|
594
|
+
lastCommitMessage: null
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
// Initial sync
|
|
599
|
+
const syncResult = syncGitHubData(projectRoot);
|
|
600
|
+
|
|
601
|
+
return {
|
|
602
|
+
success: true,
|
|
603
|
+
owner,
|
|
604
|
+
repo,
|
|
605
|
+
url,
|
|
606
|
+
defaultBranch,
|
|
607
|
+
description: repoInfo.description,
|
|
608
|
+
isPrivate: repoInfo.isPrivate,
|
|
609
|
+
stats: syncResult.stats
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Disconnect from GitHub repository
|
|
615
|
+
*/
|
|
616
|
+
export function disconnectRepository(projectRoot: string): DisconnectResult {
|
|
617
|
+
const state = projectState.loadState(projectRoot);
|
|
618
|
+
|
|
619
|
+
interface GitHubStateData {
|
|
620
|
+
connected?: boolean;
|
|
621
|
+
}
|
|
622
|
+
const githubState = (state as { github?: GitHubStateData } | null)?.github;
|
|
623
|
+
|
|
624
|
+
if (!githubState?.connected) {
|
|
625
|
+
return {
|
|
626
|
+
success: false,
|
|
627
|
+
error: 'GitHub not connected'
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
projectState.clearGitHubState(projectRoot);
|
|
632
|
+
|
|
633
|
+
return {
|
|
634
|
+
success: true
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// ============================================================================
|
|
639
|
+
// Sync Metadata
|
|
640
|
+
// ============================================================================
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Save GitHub sync metadata
|
|
644
|
+
*/
|
|
645
|
+
export function saveGitHubSyncMetadata(projectRoot: string, metadata: SyncMetadata): void {
|
|
646
|
+
const syncFile = projectState.getGitHubSyncFilePath(projectRoot);
|
|
647
|
+
const planningDir = projectState.getPlanningDir(projectRoot);
|
|
648
|
+
|
|
649
|
+
try {
|
|
650
|
+
if (!fs.existsSync(planningDir)) {
|
|
651
|
+
fs.mkdirSync(planningDir, { recursive: true });
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
fs.writeFileSync(syncFile, JSON.stringify(metadata, null, 2), 'utf-8');
|
|
655
|
+
} catch (error) {
|
|
656
|
+
const err = error as Error;
|
|
657
|
+
utils.print.debug(`Error saving sync metadata: ${err.message}`);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Load GitHub sync metadata
|
|
663
|
+
*/
|
|
664
|
+
export function loadGitHubSyncMetadata(projectRoot: string): SyncMetadata | null {
|
|
665
|
+
const syncFile = projectState.getGitHubSyncFilePath(projectRoot);
|
|
666
|
+
|
|
667
|
+
if (!fs.existsSync(syncFile)) {
|
|
668
|
+
return null;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
try {
|
|
672
|
+
return JSON.parse(fs.readFileSync(syncFile, 'utf-8')) as SyncMetadata;
|
|
673
|
+
} catch {
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// ============================================================================
|
|
679
|
+
// Status Functions
|
|
680
|
+
// ============================================================================
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Get GitHub connection status
|
|
684
|
+
*/
|
|
685
|
+
export function getStatus(projectRoot: string): GitHubStatus {
|
|
686
|
+
const state = projectState.loadState(projectRoot);
|
|
687
|
+
|
|
688
|
+
interface GitHubStateData {
|
|
689
|
+
connected?: boolean;
|
|
690
|
+
repositoryUrl?: string;
|
|
691
|
+
owner?: string;
|
|
692
|
+
repo?: string;
|
|
693
|
+
defaultBranch?: string;
|
|
694
|
+
lastSync?: string;
|
|
695
|
+
stats?: GitHubStats;
|
|
696
|
+
}
|
|
697
|
+
const githubState = (state as { github?: GitHubStateData } | null)?.github;
|
|
698
|
+
|
|
699
|
+
if (!githubState?.connected) {
|
|
700
|
+
return {
|
|
701
|
+
connected: false,
|
|
702
|
+
ghInstalled: isGhInstalled(),
|
|
703
|
+
ghAuthenticated: isGhInstalled() && isGhAuthenticated(),
|
|
704
|
+
detected: detectRepository(projectRoot)
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const syncMetadata = loadGitHubSyncMetadata(projectRoot);
|
|
709
|
+
|
|
710
|
+
return {
|
|
711
|
+
connected: true,
|
|
712
|
+
repositoryUrl: githubState.repositoryUrl,
|
|
713
|
+
owner: githubState.owner,
|
|
714
|
+
repo: githubState.repo,
|
|
715
|
+
defaultBranch: githubState.defaultBranch,
|
|
716
|
+
lastSync: githubState.lastSync,
|
|
717
|
+
stats: githubState.stats,
|
|
718
|
+
syncMetadata
|
|
719
|
+
};
|
|
720
|
+
}
|