@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.
Files changed (147) hide show
  1. package/cli/preseed/index.js +16 -0
  2. package/cli/preseed/interactive.js +143 -0
  3. package/cli/preseed/templates.js +227 -0
  4. package/cli/seed/builders/ai-context-builder.js +85 -0
  5. package/cli/seed/builders/index.js +13 -0
  6. package/cli/seed/builders/seed-builder.js +272 -0
  7. package/cli/seed/extractors/content-extractors.js +383 -0
  8. package/cli/seed/extractors/index.js +47 -0
  9. package/cli/seed/extractors/metadata-extractors.js +167 -0
  10. package/cli/seed/extractors/section-extractor.js +54 -0
  11. package/cli/seed/extractors/stack-extractors.js +228 -0
  12. package/cli/seed/index.js +18 -0
  13. package/cli/seed/utils/folder-structure.js +84 -0
  14. package/cli/seed/utils/index.js +11 -0
  15. package/dist/cli/index.d.ts +3 -0
  16. package/dist/cli/index.js +3220 -0
  17. package/dist/cli/index.js.map +1 -0
  18. package/dist/context-McpJQa_2.d.ts +5710 -0
  19. package/dist/core/index.d.ts +635 -0
  20. package/dist/core/index.js +2593 -0
  21. package/dist/core/index.js.map +1 -0
  22. package/dist/index-QqbeEiDm.d.ts +857 -0
  23. package/dist/index-UiYCgwiH.d.ts +174 -0
  24. package/dist/index.d.ts +453 -0
  25. package/dist/index.js +44228 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/mcp/index.d.ts +1 -0
  28. package/dist/mcp/index.js +41173 -0
  29. package/dist/mcp/index.js.map +1 -0
  30. package/generators/index.ts +82 -0
  31. package/intelligence/orchestrator/config/failure-signatures.js +48 -0
  32. package/intelligence/orchestrator/config/index.js +20 -0
  33. package/intelligence/orchestrator/config/phases.js +111 -0
  34. package/intelligence/orchestrator/config/remediation.js +150 -0
  35. package/intelligence/orchestrator/config/workflows.js +168 -0
  36. package/intelligence/orchestrator/core/index.js +16 -0
  37. package/intelligence/orchestrator/core/state-manager.js +88 -0
  38. package/intelligence/orchestrator/core/telemetry.js +24 -0
  39. package/intelligence/orchestrator/index.js +17 -0
  40. package/mcp/contracts/mcp-contract.v1.json +1 -1
  41. package/package.json +16 -3
  42. package/src/cli/agent.ts +703 -0
  43. package/src/cli/analyze.ts +640 -0
  44. package/src/cli/audit.ts +707 -0
  45. package/src/cli/auth.ts +930 -0
  46. package/src/cli/billing.ts +364 -0
  47. package/src/cli/build.ts +1089 -0
  48. package/src/cli/business.ts +508 -0
  49. package/src/cli/checkpoint-utils.ts +236 -0
  50. package/src/cli/checkpoint.ts +757 -0
  51. package/src/cli/cloud-sync.ts +534 -0
  52. package/src/cli/content.ts +273 -0
  53. package/src/cli/context.ts +667 -0
  54. package/src/cli/dashboard.ts +133 -0
  55. package/src/cli/deploy.ts +704 -0
  56. package/src/cli/doctor.ts +480 -0
  57. package/src/cli/fundraise.ts +494 -0
  58. package/src/cli/generate.ts +346 -0
  59. package/src/cli/github-cmd.ts +566 -0
  60. package/src/cli/health.ts +599 -0
  61. package/src/cli/index.ts +113 -0
  62. package/src/cli/init.ts +838 -0
  63. package/src/cli/legal.ts +495 -0
  64. package/src/cli/log.ts +316 -0
  65. package/src/cli/loop.ts +1660 -0
  66. package/src/cli/manager.ts +878 -0
  67. package/src/cli/mcp.ts +275 -0
  68. package/src/cli/memory.ts +346 -0
  69. package/src/cli/metrics.ts +590 -0
  70. package/src/cli/monitor.ts +960 -0
  71. package/src/cli/mvp.ts +662 -0
  72. package/src/cli/onboard.ts +663 -0
  73. package/src/cli/orchestrator.ts +622 -0
  74. package/src/cli/plugin.ts +483 -0
  75. package/src/cli/prd.ts +671 -0
  76. package/src/cli/preseed-start.ts +1633 -0
  77. package/src/cli/preseed.ts +2434 -0
  78. package/src/cli/project.ts +526 -0
  79. package/src/cli/quality.ts +885 -0
  80. package/src/cli/security.ts +1079 -0
  81. package/src/cli/seed.ts +1224 -0
  82. package/src/cli/skill.ts +537 -0
  83. package/src/cli/suggest.ts +1225 -0
  84. package/src/cli/switch.ts +518 -0
  85. package/src/cli/task.ts +780 -0
  86. package/src/cli/telemetry.ts +172 -0
  87. package/src/cli/todo.ts +627 -0
  88. package/src/cli/types.ts +15 -0
  89. package/src/cli/update.ts +334 -0
  90. package/src/cli/visualize.ts +609 -0
  91. package/src/cli/watch.ts +895 -0
  92. package/src/cli/workspace.ts +709 -0
  93. package/src/core/action-recorder.ts +673 -0
  94. package/src/core/analyze-workflow.ts +1453 -0
  95. package/src/core/api-client.ts +1120 -0
  96. package/src/core/audit-workflow.ts +1681 -0
  97. package/src/core/auth.ts +471 -0
  98. package/src/core/build-orchestrator.ts +509 -0
  99. package/src/core/build-state.ts +621 -0
  100. package/src/core/checkpoint-engine.ts +482 -0
  101. package/src/core/config.ts +1285 -0
  102. package/src/core/context-loader.ts +694 -0
  103. package/src/core/context.ts +410 -0
  104. package/src/core/deploy-workflow.ts +1085 -0
  105. package/src/core/entitlements.ts +322 -0
  106. package/src/core/github-sync.ts +720 -0
  107. package/src/core/index.ts +981 -0
  108. package/src/core/ingest.ts +1186 -0
  109. package/src/core/metrics-engine.ts +886 -0
  110. package/src/core/mvp.ts +847 -0
  111. package/src/core/onboard-workflow.ts +1293 -0
  112. package/src/core/policies.ts +81 -0
  113. package/src/core/preseed-workflow.ts +1163 -0
  114. package/src/core/preseed.ts +1826 -0
  115. package/src/core/project-context.ts +380 -0
  116. package/src/core/project-state.ts +699 -0
  117. package/src/core/r2-sync.ts +691 -0
  118. package/src/core/scaffold.ts +1715 -0
  119. package/src/core/session.ts +286 -0
  120. package/src/core/task-extractor.ts +799 -0
  121. package/src/core/telemetry.ts +371 -0
  122. package/src/core/tier-enforcement.ts +737 -0
  123. package/src/core/utils.ts +437 -0
  124. package/src/index.ts +29 -0
  125. package/src/intelligence/agent-collab.ts +2376 -0
  126. package/src/intelligence/auto-suggest.ts +713 -0
  127. package/src/intelligence/content-gen.ts +1351 -0
  128. package/src/intelligence/cross-project.ts +1692 -0
  129. package/src/intelligence/git-memory.ts +529 -0
  130. package/src/intelligence/index.ts +318 -0
  131. package/src/intelligence/orchestrator.ts +534 -0
  132. package/src/intelligence/prd.ts +466 -0
  133. package/src/intelligence/recommendations.ts +982 -0
  134. package/src/intelligence/workflow-composer.ts +1472 -0
  135. package/src/mcp/capabilities.ts +233 -0
  136. package/src/mcp/index.ts +37 -0
  137. package/src/mcp/registry.ts +1268 -0
  138. package/src/mcp/response-formatter.ts +797 -0
  139. package/src/mcp/server.ts +240 -0
  140. package/src/types/agent.ts +69 -0
  141. package/src/types/config.ts +86 -0
  142. package/src/types/context.ts +77 -0
  143. package/src/types/index.ts +53 -0
  144. package/src/types/mcp.ts +91 -0
  145. package/src/types/skills.ts +47 -0
  146. package/src/types/workflow.ts +155 -0
  147. 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
+ }