@gitgov/core 2.1.2 → 2.3.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.
@@ -0,0 +1,1281 @@
1
+ import picomatch from 'picomatch';
2
+
3
+ // src/file_lister/github/github_file_lister.ts
4
+
5
+ // src/file_lister/file_lister.errors.ts
6
+ var FileListerError = class extends Error {
7
+ constructor(message, code, filePath) {
8
+ super(message);
9
+ this.code = code;
10
+ this.filePath = filePath;
11
+ this.name = "FileListerError";
12
+ }
13
+ };
14
+
15
+ // src/file_lister/github/github_file_lister.ts
16
+ var GitHubFileLister = class {
17
+ owner;
18
+ repo;
19
+ ref;
20
+ basePath;
21
+ octokit;
22
+ /** Cached tree entries from the Trees API */
23
+ treeCache = null;
24
+ constructor(options, octokit) {
25
+ this.owner = options.owner;
26
+ this.repo = options.repo;
27
+ this.ref = options.ref ?? "gitgov-state";
28
+ this.basePath = options.basePath ?? "";
29
+ this.octokit = octokit;
30
+ }
31
+ // ═══════════════════════════════════════════════════════════════════════
32
+ // FileLister Interface
33
+ // ═══════════════════════════════════════════════════════════════════════
34
+ /**
35
+ * [EARS-A1] Lists files matching glob patterns.
36
+ * [EARS-B1] Uses Trees API with recursive=1 and picomatch filter.
37
+ * [EARS-B3] Applies basePath prefix for tree entries, strips from results.
38
+ * [EARS-B6] Caches tree between list() calls.
39
+ */
40
+ async list(patterns, options) {
41
+ const entries = await this.fetchTree();
42
+ const blobs = entries.filter((entry) => entry.type === "blob");
43
+ const prefix = this.basePath ? `${this.basePath}/` : "";
44
+ const relativePaths = [];
45
+ for (const blob of blobs) {
46
+ if (!blob.path) continue;
47
+ if (prefix && !blob.path.startsWith(prefix)) {
48
+ continue;
49
+ }
50
+ const relativePath = prefix ? blob.path.slice(prefix.length) : blob.path;
51
+ relativePaths.push(relativePath);
52
+ }
53
+ const isMatch = picomatch(patterns, {
54
+ ignore: options?.ignore
55
+ });
56
+ return relativePaths.filter((p) => isMatch(p)).sort();
57
+ }
58
+ /**
59
+ * [EARS-A2] Checks if a file exists via Contents API.
60
+ * [EARS-B4] Returns false for 404 responses.
61
+ */
62
+ async exists(filePath) {
63
+ const fullPath = this.buildFullPath(filePath);
64
+ try {
65
+ const { data } = await this.octokit.rest.repos.getContent({
66
+ owner: this.owner,
67
+ repo: this.repo,
68
+ path: fullPath,
69
+ ref: this.ref
70
+ });
71
+ if (Array.isArray(data) || data.type !== "file") {
72
+ return false;
73
+ }
74
+ return true;
75
+ } catch (error) {
76
+ if (isOctokitRequestError(error)) {
77
+ if (error.status === 404) return false;
78
+ if (error.status === 401 || error.status === 403) {
79
+ throw new FileListerError(
80
+ `Permission denied: ${filePath}`,
81
+ "PERMISSION_DENIED",
82
+ filePath
83
+ );
84
+ }
85
+ if (error.status >= 500) {
86
+ throw new FileListerError(
87
+ `GitHub API server error (${error.status}): ${filePath}`,
88
+ "READ_ERROR",
89
+ filePath
90
+ );
91
+ }
92
+ throw new FileListerError(
93
+ `Unexpected GitHub API response (${error.status}): ${filePath}`,
94
+ "READ_ERROR",
95
+ filePath
96
+ );
97
+ }
98
+ throw new FileListerError(
99
+ `Network error checking file: ${filePath}`,
100
+ "NETWORK_ERROR",
101
+ filePath
102
+ );
103
+ }
104
+ }
105
+ /**
106
+ * [EARS-A3] Reads file content as string.
107
+ * [EARS-B2] Decodes base64 content from Contents API.
108
+ * [EARS-B7] Falls back to Blobs API for files >1MB (null content).
109
+ */
110
+ async read(filePath) {
111
+ const fullPath = this.buildFullPath(filePath);
112
+ try {
113
+ const { data } = await this.octokit.rest.repos.getContent({
114
+ owner: this.owner,
115
+ repo: this.repo,
116
+ path: fullPath,
117
+ ref: this.ref
118
+ });
119
+ if (Array.isArray(data) || data.type !== "file") {
120
+ throw new FileListerError(
121
+ `Not a file: ${filePath}`,
122
+ "READ_ERROR",
123
+ filePath
124
+ );
125
+ }
126
+ if (data.content !== null && data.content !== void 0) {
127
+ return Buffer.from(data.content, "base64").toString("utf-8");
128
+ }
129
+ return this.readViaBlobs(data.sha, filePath);
130
+ } catch (error) {
131
+ if (error instanceof FileListerError) throw error;
132
+ if (isOctokitRequestError(error)) {
133
+ if (error.status === 404) {
134
+ throw new FileListerError(
135
+ `File not found: ${filePath}`,
136
+ "FILE_NOT_FOUND",
137
+ filePath
138
+ );
139
+ }
140
+ if (error.status === 401 || error.status === 403) {
141
+ throw new FileListerError(
142
+ `Permission denied: ${filePath}`,
143
+ "PERMISSION_DENIED",
144
+ filePath
145
+ );
146
+ }
147
+ if (error.status >= 500) {
148
+ throw new FileListerError(
149
+ `GitHub API server error (${error.status}): ${filePath}`,
150
+ "READ_ERROR",
151
+ filePath
152
+ );
153
+ }
154
+ throw new FileListerError(
155
+ `Unexpected GitHub API response (${error.status}): ${filePath}`,
156
+ "READ_ERROR",
157
+ filePath
158
+ );
159
+ }
160
+ throw new FileListerError(
161
+ `Network error reading file: ${filePath}`,
162
+ "NETWORK_ERROR",
163
+ filePath
164
+ );
165
+ }
166
+ }
167
+ /**
168
+ * [EARS-A4] Gets file statistics via Contents API.
169
+ * Returns size from API, mtime as 0 (not available via Contents API), isFile as true.
170
+ */
171
+ async stat(filePath) {
172
+ const fullPath = this.buildFullPath(filePath);
173
+ try {
174
+ const { data } = await this.octokit.rest.repos.getContent({
175
+ owner: this.owner,
176
+ repo: this.repo,
177
+ path: fullPath,
178
+ ref: this.ref
179
+ });
180
+ if (Array.isArray(data) || data.type !== "file") {
181
+ throw new FileListerError(
182
+ `Not a file: ${filePath}`,
183
+ "READ_ERROR",
184
+ filePath
185
+ );
186
+ }
187
+ return {
188
+ size: data.size,
189
+ mtime: 0,
190
+ isFile: true
191
+ };
192
+ } catch (error) {
193
+ if (error instanceof FileListerError) throw error;
194
+ if (isOctokitRequestError(error)) {
195
+ if (error.status === 404) {
196
+ throw new FileListerError(
197
+ `File not found: ${filePath}`,
198
+ "FILE_NOT_FOUND",
199
+ filePath
200
+ );
201
+ }
202
+ if (error.status === 401 || error.status === 403) {
203
+ throw new FileListerError(
204
+ `Permission denied: ${filePath}`,
205
+ "PERMISSION_DENIED",
206
+ filePath
207
+ );
208
+ }
209
+ if (error.status >= 500) {
210
+ throw new FileListerError(
211
+ `GitHub API server error (${error.status}): ${filePath}`,
212
+ "READ_ERROR",
213
+ filePath
214
+ );
215
+ }
216
+ throw new FileListerError(
217
+ `Unexpected GitHub API response (${error.status}): ${filePath}`,
218
+ "READ_ERROR",
219
+ filePath
220
+ );
221
+ }
222
+ throw new FileListerError(
223
+ `Network error getting file stats: ${filePath}`,
224
+ "NETWORK_ERROR",
225
+ filePath
226
+ );
227
+ }
228
+ }
229
+ // ═══════════════════════════════════════════════════════════════════════
230
+ // Private Helpers
231
+ // ═══════════════════════════════════════════════════════════════════════
232
+ /**
233
+ * Builds the full file path including basePath prefix.
234
+ */
235
+ buildFullPath(filePath) {
236
+ if (this.basePath) {
237
+ return `${this.basePath}/${filePath}`;
238
+ }
239
+ return filePath;
240
+ }
241
+ /**
242
+ * [EARS-B6] Fetches and caches the full repository tree.
243
+ * [EARS-C3] Throws READ_ERROR if the tree response is truncated.
244
+ */
245
+ async fetchTree() {
246
+ if (this.treeCache !== null) {
247
+ return this.treeCache;
248
+ }
249
+ try {
250
+ const { data } = await this.octokit.rest.git.getTree({
251
+ owner: this.owner,
252
+ repo: this.repo,
253
+ tree_sha: this.ref,
254
+ recursive: "1"
255
+ });
256
+ if (data.truncated) {
257
+ throw new FileListerError(
258
+ "Repository tree is truncated; too many files to list via Trees API",
259
+ "READ_ERROR"
260
+ );
261
+ }
262
+ this.treeCache = data.tree;
263
+ return this.treeCache;
264
+ } catch (error) {
265
+ if (error instanceof FileListerError) throw error;
266
+ if (isOctokitRequestError(error)) {
267
+ if (error.status === 404) {
268
+ throw new FileListerError(
269
+ "Repository or ref not found",
270
+ "FILE_NOT_FOUND"
271
+ );
272
+ }
273
+ if (error.status === 401 || error.status === 403) {
274
+ throw new FileListerError(
275
+ "Permission denied accessing repository tree",
276
+ "PERMISSION_DENIED"
277
+ );
278
+ }
279
+ if (error.status >= 500) {
280
+ throw new FileListerError(
281
+ `GitHub API server error (${error.status}) fetching tree`,
282
+ "READ_ERROR"
283
+ );
284
+ }
285
+ throw new FileListerError(
286
+ `Unexpected GitHub API response (${error.status}) fetching tree`,
287
+ "READ_ERROR"
288
+ );
289
+ }
290
+ throw new FileListerError(
291
+ "Network error fetching repository tree",
292
+ "NETWORK_ERROR"
293
+ );
294
+ }
295
+ }
296
+ /**
297
+ * [EARS-B7] Reads file content via the Blobs API (fallback for >1MB files).
298
+ */
299
+ async readViaBlobs(sha, filePath) {
300
+ try {
301
+ const { data } = await this.octokit.rest.git.getBlob({
302
+ owner: this.owner,
303
+ repo: this.repo,
304
+ file_sha: sha
305
+ });
306
+ return Buffer.from(data.content, "base64").toString("utf-8");
307
+ } catch (error) {
308
+ if (isOctokitRequestError(error)) {
309
+ if (error.status === 404) {
310
+ throw new FileListerError(
311
+ `File not found: ${filePath}`,
312
+ "FILE_NOT_FOUND",
313
+ filePath
314
+ );
315
+ }
316
+ if (error.status === 401 || error.status === 403) {
317
+ throw new FileListerError(
318
+ `Permission denied: ${filePath}`,
319
+ "PERMISSION_DENIED",
320
+ filePath
321
+ );
322
+ }
323
+ throw new FileListerError(
324
+ `GitHub API error (${error.status}): ${filePath}`,
325
+ "READ_ERROR",
326
+ filePath
327
+ );
328
+ }
329
+ throw new FileListerError(
330
+ `Network error reading blob for file: ${filePath}`,
331
+ "NETWORK_ERROR",
332
+ filePath
333
+ );
334
+ }
335
+ }
336
+ };
337
+
338
+ // src/record_store/github/github_record_store.ts
339
+ var GitHubRecordStore = class {
340
+ owner;
341
+ repo;
342
+ ref;
343
+ basePath;
344
+ extension;
345
+ idEncoder;
346
+ octokit;
347
+ /** SHA cache keyed by full file path (basePath/encoded + extension) */
348
+ shaCache = /* @__PURE__ */ new Map();
349
+ /** IGitModule dependency for putMany() atomic commits. Optional — only needed for putMany(). */
350
+ gitModule;
351
+ constructor(options, octokit, gitModule) {
352
+ this.owner = options.owner;
353
+ this.repo = options.repo;
354
+ this.ref = options.ref ?? "gitgov-state";
355
+ this.basePath = options.basePath;
356
+ this.extension = options.extension ?? ".json";
357
+ this.idEncoder = options.idEncoder;
358
+ this.octokit = octokit;
359
+ this.gitModule = gitModule;
360
+ }
361
+ async get(id) {
362
+ this.validateId(id);
363
+ const filePath = this.buildFilePath(id);
364
+ try {
365
+ const { data } = await this.octokit.rest.repos.getContent({
366
+ owner: this.owner,
367
+ repo: this.repo,
368
+ path: filePath,
369
+ ref: this.ref
370
+ });
371
+ if (Array.isArray(data) || data.type !== "file") {
372
+ throw new GitHubApiError(`Not a file: ${filePath}`, "INVALID_RESPONSE");
373
+ }
374
+ if (data.content === null || data.content === void 0) {
375
+ throw new GitHubApiError(
376
+ `File content is null (file may exceed 1MB): ${filePath}`,
377
+ "INVALID_RESPONSE"
378
+ );
379
+ }
380
+ this.shaCache.set(filePath, data.sha);
381
+ const decoded = Buffer.from(data.content, "base64").toString("utf-8");
382
+ return JSON.parse(decoded);
383
+ } catch (error) {
384
+ if (error instanceof GitHubApiError) throw error;
385
+ if (isOctokitRequestError(error) && error.status === 404) return null;
386
+ throw mapOctokitError(error, `GET ${filePath}`);
387
+ }
388
+ }
389
+ async put(id, value, opts) {
390
+ this.validateId(id);
391
+ const filePath = this.buildFilePath(id);
392
+ const content = Buffer.from(JSON.stringify(value, null, 2)).toString("base64");
393
+ const cachedSha = this.shaCache.get(filePath);
394
+ try {
395
+ const { data } = await this.octokit.rest.repos.createOrUpdateFileContents({
396
+ owner: this.owner,
397
+ repo: this.repo,
398
+ path: filePath,
399
+ message: opts?.commitMessage ?? `put ${id}`,
400
+ content,
401
+ branch: this.ref,
402
+ ...cachedSha ? { sha: cachedSha } : {}
403
+ });
404
+ if (data.content?.sha) {
405
+ this.shaCache.set(filePath, data.content.sha);
406
+ }
407
+ return { commitSha: data.commit.sha };
408
+ } catch (error) {
409
+ throw mapOctokitError(error, `PUT ${filePath}`);
410
+ }
411
+ }
412
+ /**
413
+ * [EARS-A11, EARS-A12, EARS-B8] Persists multiple records in a single atomic commit.
414
+ * Uses GitHubGitModule staging buffer: add() with contentMap, then commit().
415
+ * Empty entries array returns { commitSha: undefined } without API calls.
416
+ * Requires gitModule dependency — throws if not injected.
417
+ */
418
+ async putMany(entries, opts) {
419
+ if (entries.length === 0) {
420
+ return {};
421
+ }
422
+ if (!this.gitModule) {
423
+ throw new Error("putMany requires IGitModule dependency for atomic commits");
424
+ }
425
+ for (const { id } of entries) {
426
+ this.validateId(id);
427
+ }
428
+ const contentMap = {};
429
+ const filePaths = [];
430
+ for (const { id, value } of entries) {
431
+ const filePath = this.buildFilePath(id);
432
+ contentMap[filePath] = JSON.stringify(value, null, 2);
433
+ filePaths.push(filePath);
434
+ }
435
+ await this.gitModule.add(filePaths, { contentMap });
436
+ const message = opts?.commitMessage ?? `putMany ${entries.length} records`;
437
+ const commitSha = await this.gitModule.commit(message);
438
+ return { commitSha };
439
+ }
440
+ async delete(id, opts) {
441
+ this.validateId(id);
442
+ const filePath = this.buildFilePath(id);
443
+ let sha = this.shaCache.get(filePath);
444
+ if (sha === void 0) {
445
+ try {
446
+ const { data } = await this.octokit.rest.repos.getContent({
447
+ owner: this.owner,
448
+ repo: this.repo,
449
+ path: filePath,
450
+ ref: this.ref
451
+ });
452
+ if (Array.isArray(data) || data.type !== "file") {
453
+ return {};
454
+ }
455
+ sha = data.sha;
456
+ } catch (error) {
457
+ if (isOctokitRequestError(error) && error.status === 404) {
458
+ return {};
459
+ }
460
+ throw mapOctokitError(error, `GET ${filePath} (for delete)`);
461
+ }
462
+ }
463
+ try {
464
+ const { data } = await this.octokit.rest.repos.deleteFile({
465
+ owner: this.owner,
466
+ repo: this.repo,
467
+ path: filePath,
468
+ message: opts?.commitMessage ?? `delete ${id}`,
469
+ sha,
470
+ branch: this.ref
471
+ });
472
+ this.shaCache.delete(filePath);
473
+ return { commitSha: data.commit.sha };
474
+ } catch (error) {
475
+ if (isOctokitRequestError(error) && error.status === 404) {
476
+ this.shaCache.delete(filePath);
477
+ return {};
478
+ }
479
+ throw mapOctokitError(error, `DELETE ${filePath}`);
480
+ }
481
+ }
482
+ async list() {
483
+ try {
484
+ const { data } = await this.octokit.rest.repos.getContent({
485
+ owner: this.owner,
486
+ repo: this.repo,
487
+ path: this.basePath,
488
+ ref: this.ref
489
+ });
490
+ if (!Array.isArray(data)) {
491
+ return [];
492
+ }
493
+ const ids = data.filter((entry) => entry.name.endsWith(this.extension)).map((entry) => entry.name.slice(0, -this.extension.length));
494
+ return this.idEncoder ? ids.map((encoded) => this.idEncoder.decode(encoded)) : ids;
495
+ } catch (error) {
496
+ if (isOctokitRequestError(error) && error.status === 404) {
497
+ return [];
498
+ }
499
+ throw mapOctokitError(error, `GET ${this.basePath} (list)`);
500
+ }
501
+ }
502
+ async exists(id) {
503
+ this.validateId(id);
504
+ const filePath = this.buildFilePath(id);
505
+ try {
506
+ await this.octokit.rest.repos.getContent({
507
+ owner: this.owner,
508
+ repo: this.repo,
509
+ path: filePath,
510
+ ref: this.ref
511
+ });
512
+ return true;
513
+ } catch (error) {
514
+ if (isOctokitRequestError(error) && error.status === 404) {
515
+ return false;
516
+ }
517
+ throw mapOctokitError(error, `GET ${filePath} (exists)`);
518
+ }
519
+ }
520
+ // ─────────────────────────────────────────────────────────
521
+ // Private helpers
522
+ // ─────────────────────────────────────────────────────────
523
+ validateId(id) {
524
+ if (!id || typeof id !== "string") {
525
+ throw new GitHubApiError("ID must be a non-empty string", "INVALID_ID");
526
+ }
527
+ if (id.includes("..") || /[\/\\]/.test(id)) {
528
+ throw new GitHubApiError(
529
+ `Invalid ID: "${id}". IDs cannot contain /, \\, or ..`,
530
+ "INVALID_ID"
531
+ );
532
+ }
533
+ }
534
+ buildFilePath(id) {
535
+ const encoded = this.idEncoder ? this.idEncoder.encode(id) : id;
536
+ return `${this.basePath}/${encoded}${this.extension}`;
537
+ }
538
+ };
539
+
540
+ // src/git/errors.ts
541
+ var GitError = class _GitError extends Error {
542
+ constructor(message) {
543
+ super(message);
544
+ this.name = "GitError";
545
+ Object.setPrototypeOf(this, _GitError.prototype);
546
+ }
547
+ };
548
+ var BranchNotFoundError = class _BranchNotFoundError extends GitError {
549
+ branchName;
550
+ constructor(branchName) {
551
+ super(`Branch not found: ${branchName}`);
552
+ this.name = "BranchNotFoundError";
553
+ this.branchName = branchName;
554
+ Object.setPrototypeOf(this, _BranchNotFoundError.prototype);
555
+ }
556
+ };
557
+ var FileNotFoundError = class _FileNotFoundError extends GitError {
558
+ filePath;
559
+ commitHash;
560
+ constructor(filePath, commitHash) {
561
+ super(`File not found: ${filePath} in commit ${commitHash}`);
562
+ this.name = "FileNotFoundError";
563
+ this.filePath = filePath;
564
+ this.commitHash = commitHash;
565
+ Object.setPrototypeOf(this, _FileNotFoundError.prototype);
566
+ }
567
+ };
568
+ var BranchAlreadyExistsError = class _BranchAlreadyExistsError extends GitError {
569
+ branchName;
570
+ constructor(branchName) {
571
+ super(`Branch already exists: ${branchName}`);
572
+ this.name = "BranchAlreadyExistsError";
573
+ this.branchName = branchName;
574
+ Object.setPrototypeOf(this, _BranchAlreadyExistsError.prototype);
575
+ }
576
+ };
577
+
578
+ // src/git/github/github_git_module.ts
579
+ var GitHubGitModule = class {
580
+ owner;
581
+ repo;
582
+ defaultBranch;
583
+ octokit;
584
+ /** Staging buffer: path → content (null = delete) */
585
+ stagingBuffer = /* @__PURE__ */ new Map();
586
+ /** Active ref for operations (can be changed via checkoutBranch) */
587
+ activeRef;
588
+ constructor(options, octokit) {
589
+ this.owner = options.owner;
590
+ this.repo = options.repo;
591
+ this.defaultBranch = options.defaultBranch ?? "gitgov-state";
592
+ this.octokit = octokit;
593
+ this.activeRef = this.defaultBranch;
594
+ }
595
+ // ═══════════════════════════════════════════════════════════════
596
+ // PRIVATE HELPERS
597
+ // ═══════════════════════════════════════════════════════════════
598
+ /** Category C: Not supported via GitHub API */
599
+ notSupported(method) {
600
+ throw new GitError(
601
+ `${method} is not supported via GitHub API`
602
+ );
603
+ }
604
+ // ═══════════════════════════════════════════════════════════════
605
+ // CATEGORY A: READ OPERATIONS (EARS-A1 to A6)
606
+ // ═══════════════════════════════════════════════════════════════
607
+ /**
608
+ * [EARS-A1] Read file content via Contents API + base64 decode
609
+ * [EARS-A2] Fallback to Blobs API for files >1MB
610
+ */
611
+ async getFileContent(commitHash, filePath) {
612
+ try {
613
+ const { data } = await this.octokit.rest.repos.getContent({
614
+ owner: this.owner,
615
+ repo: this.repo,
616
+ path: filePath,
617
+ ref: commitHash
618
+ });
619
+ if (Array.isArray(data) || data.type !== "file") {
620
+ throw new GitError(`Not a file: ${filePath}`);
621
+ }
622
+ if (data.content !== null && data.content !== void 0) {
623
+ return Buffer.from(data.content, "base64").toString("utf-8");
624
+ }
625
+ const { data: blobData } = await this.octokit.rest.git.getBlob({
626
+ owner: this.owner,
627
+ repo: this.repo,
628
+ file_sha: data.sha
629
+ });
630
+ return Buffer.from(blobData.content, "base64").toString("utf-8");
631
+ } catch (error) {
632
+ if (error instanceof GitError) throw error;
633
+ if (isOctokitRequestError(error)) {
634
+ if (error.status === 404) {
635
+ throw new FileNotFoundError(filePath, commitHash);
636
+ }
637
+ if (error.status === 401 || error.status === 403) {
638
+ throw new GitError(`authentication/permission error (${error.status}): getFileContent ${filePath}`);
639
+ }
640
+ if (error.status >= 500) {
641
+ throw new GitError(`GitHub server error (${error.status}): getFileContent ${filePath}`);
642
+ }
643
+ throw new GitError(`GitHub API error (${error.status}): getFileContent ${filePath}`);
644
+ }
645
+ const msg = error instanceof Error ? error.message : String(error);
646
+ throw new GitError(`network error: ${msg}`);
647
+ }
648
+ }
649
+ /**
650
+ * [EARS-A3] Get commit SHA from branch via Refs API
651
+ * [EARS-B4] Return SHA directly if already a 40-char hex
652
+ */
653
+ async getCommitHash(ref = this.activeRef) {
654
+ if (/^[0-9a-f]{40}$/i.test(ref)) {
655
+ return ref;
656
+ }
657
+ try {
658
+ const { data } = await this.octokit.rest.git.getRef({
659
+ owner: this.owner,
660
+ repo: this.repo,
661
+ ref: `heads/${ref}`
662
+ });
663
+ return data.object.sha;
664
+ } catch (error) {
665
+ if (isOctokitRequestError(error)) {
666
+ if (error.status === 404) {
667
+ throw new BranchNotFoundError(ref);
668
+ }
669
+ if (error.status === 401 || error.status === 403) {
670
+ throw new GitError(`authentication/permission error (${error.status}): getCommitHash ${ref}`);
671
+ }
672
+ if (error.status >= 500) {
673
+ throw new GitError(`GitHub server error (${error.status}): getCommitHash ${ref}`);
674
+ }
675
+ }
676
+ const msg = error instanceof Error ? error.message : String(error);
677
+ throw new GitError(`network error: ${msg}`);
678
+ }
679
+ }
680
+ /**
681
+ * [EARS-A4] List changed files via Compare API
682
+ */
683
+ async getChangedFiles(fromCommit, toCommit, pathFilter) {
684
+ try {
685
+ const { data } = await this.octokit.rest.repos.compareCommits({
686
+ owner: this.owner,
687
+ repo: this.repo,
688
+ base: fromCommit,
689
+ head: toCommit
690
+ });
691
+ const statusMap = {
692
+ added: "A",
693
+ modified: "M",
694
+ removed: "D",
695
+ renamed: "M"
696
+ };
697
+ const files = (data.files ?? []).map((f) => ({
698
+ status: statusMap[f.status] ?? "M",
699
+ file: f.filename
700
+ })).filter((f) => !pathFilter || f.file.startsWith(pathFilter));
701
+ return files;
702
+ } catch (error) {
703
+ if (isOctokitRequestError(error)) {
704
+ if (error.status === 401 || error.status === 403) {
705
+ throw new GitError(`authentication/permission error (${error.status}): getChangedFiles ${fromCommit}...${toCommit}`);
706
+ }
707
+ if (error.status >= 500) {
708
+ throw new GitError(`GitHub server error (${error.status}): getChangedFiles`);
709
+ }
710
+ throw new GitError(`Failed to compare ${fromCommit}...${toCommit}: HTTP ${error.status}`);
711
+ }
712
+ const msg = error instanceof Error ? error.message : String(error);
713
+ throw new GitError(`network error: ${msg}`);
714
+ }
715
+ }
716
+ /**
717
+ * [EARS-A5] Get commit history via Commits API
718
+ */
719
+ async getCommitHistory(branch, options) {
720
+ try {
721
+ const { data } = await this.octokit.rest.repos.listCommits({
722
+ owner: this.owner,
723
+ repo: this.repo,
724
+ sha: branch,
725
+ ...options?.maxCount !== void 0 && { per_page: options.maxCount },
726
+ ...options?.pathFilter !== void 0 && { path: options.pathFilter }
727
+ });
728
+ return data.map((c) => ({
729
+ hash: c.sha,
730
+ message: c.commit.message,
731
+ author: `${c.commit.author?.name ?? "unknown"} <${c.commit.author?.email ?? "unknown"}>`,
732
+ date: c.commit.author?.date ?? ""
733
+ }));
734
+ } catch (error) {
735
+ if (isOctokitRequestError(error)) {
736
+ if (error.status === 401 || error.status === 403) {
737
+ throw new GitError(`authentication/permission error (${error.status}): getCommitHistory ${branch}`);
738
+ }
739
+ if (error.status >= 500) {
740
+ throw new GitError(`GitHub server error (${error.status}): getCommitHistory`);
741
+ }
742
+ throw new GitError(`Failed to get commit history: HTTP ${error.status}`);
743
+ }
744
+ const msg = error instanceof Error ? error.message : String(error);
745
+ throw new GitError(`network error: ${msg}`);
746
+ }
747
+ }
748
+ /**
749
+ * [EARS-B3] Get commit history between two commits via Compare API
750
+ */
751
+ async getCommitHistoryRange(fromHash, toHash, options) {
752
+ try {
753
+ const { data } = await this.octokit.rest.repos.compareCommits({
754
+ owner: this.owner,
755
+ repo: this.repo,
756
+ base: fromHash,
757
+ head: toHash
758
+ });
759
+ let commits = data.commits.map((c) => ({
760
+ hash: c.sha,
761
+ message: c.commit.message,
762
+ author: `${c.commit.author?.name ?? "unknown"} <${c.commit.author?.email ?? "unknown"}>`,
763
+ date: c.commit.author?.date ?? ""
764
+ }));
765
+ if (options?.pathFilter) {
766
+ const changedPaths = new Set((data.files ?? []).map((f) => f.filename));
767
+ commits = commits.filter(
768
+ () => Array.from(changedPaths).some((f) => f.startsWith(options.pathFilter))
769
+ );
770
+ }
771
+ if (options?.maxCount) {
772
+ commits = commits.slice(0, options.maxCount);
773
+ }
774
+ return commits;
775
+ } catch (error) {
776
+ if (isOctokitRequestError(error)) {
777
+ if (error.status === 401 || error.status === 403) {
778
+ throw new GitError(`authentication/permission error (${error.status}): getCommitHistoryRange ${fromHash}...${toHash}`);
779
+ }
780
+ if (error.status >= 500) {
781
+ throw new GitError(`GitHub server error (${error.status}): getCommitHistoryRange`);
782
+ }
783
+ throw new GitError(`Failed to get commit range: HTTP ${error.status}`);
784
+ }
785
+ const msg = error instanceof Error ? error.message : String(error);
786
+ throw new GitError(`network error: ${msg}`);
787
+ }
788
+ }
789
+ /**
790
+ * [EARS-A6] Get commit message via Commits API
791
+ */
792
+ async getCommitMessage(commitHash) {
793
+ try {
794
+ const { data } = await this.octokit.rest.repos.getCommit({
795
+ owner: this.owner,
796
+ repo: this.repo,
797
+ ref: commitHash
798
+ });
799
+ return data.commit.message;
800
+ } catch (error) {
801
+ if (isOctokitRequestError(error)) {
802
+ if (error.status === 404) {
803
+ throw new GitError(`Commit not found: ${commitHash}`);
804
+ }
805
+ if (error.status === 401 || error.status === 403) {
806
+ throw new GitError(`authentication/permission error (${error.status}): getCommitMessage ${commitHash}`);
807
+ }
808
+ if (error.status >= 500) {
809
+ throw new GitError(`GitHub server error (${error.status}): getCommitMessage`);
810
+ }
811
+ }
812
+ const msg = error instanceof Error ? error.message : String(error);
813
+ throw new GitError(`network error: ${msg}`);
814
+ }
815
+ }
816
+ // ═══════════════════════════════════════════════════════════════
817
+ // CATEGORY A: BRANCH OPERATIONS (EARS-B1 to B2)
818
+ // ═══════════════════════════════════════════════════════════════
819
+ /**
820
+ * [EARS-B1] Check if branch exists via Branches API
821
+ */
822
+ async branchExists(branchName) {
823
+ try {
824
+ await this.octokit.rest.repos.getBranch({
825
+ owner: this.owner,
826
+ repo: this.repo,
827
+ branch: branchName
828
+ });
829
+ return true;
830
+ } catch (error) {
831
+ if (isOctokitRequestError(error)) {
832
+ if (error.status === 404) return false;
833
+ if (error.status === 401 || error.status === 403) {
834
+ throw new GitError(`authentication/permission error (${error.status}): branchExists ${branchName}`);
835
+ }
836
+ throw new GitError(`Failed to check branch: HTTP ${error.status}`);
837
+ }
838
+ const msg = error instanceof Error ? error.message : String(error);
839
+ throw new GitError(`network error: ${msg}`);
840
+ }
841
+ }
842
+ /**
843
+ * [EARS-B2] List remote branches via Branches API
844
+ * remoteName is ignored — repo itself is the implicit remote
845
+ */
846
+ async listRemoteBranches(_remoteName) {
847
+ try {
848
+ const { data } = await this.octokit.rest.repos.listBranches({
849
+ owner: this.owner,
850
+ repo: this.repo
851
+ });
852
+ return data.map((b) => b.name);
853
+ } catch (error) {
854
+ if (isOctokitRequestError(error)) {
855
+ if (error.status === 401 || error.status === 403) {
856
+ throw new GitError(`authentication/permission error (${error.status}): listRemoteBranches`);
857
+ }
858
+ if (error.status >= 500) {
859
+ throw new GitError(`GitHub server error (${error.status}): listRemoteBranches`);
860
+ }
861
+ throw new GitError(`Failed to list branches: HTTP ${error.status}`);
862
+ }
863
+ const msg = error instanceof Error ? error.message : String(error);
864
+ throw new GitError(`network error: ${msg}`);
865
+ }
866
+ }
867
+ // ═══════════════════════════════════════════════════════════════
868
+ // CATEGORY A: WRITE OPERATIONS (EARS-C1 to C7)
869
+ // ═══════════════════════════════════════════════════════════════
870
+ /** [EARS-C1] Read file content and store in staging buffer */
871
+ async add(filePaths, options) {
872
+ for (const filePath of filePaths) {
873
+ const content = options?.contentMap?.[filePath] ?? await this.getFileContent(this.activeRef, filePath);
874
+ this.stagingBuffer.set(filePath, content);
875
+ }
876
+ }
877
+ /** [EARS-C2] Mark files as deleted in staging buffer */
878
+ async rm(filePaths) {
879
+ for (const filePath of filePaths) {
880
+ this.stagingBuffer.set(filePath, null);
881
+ }
882
+ }
883
+ /** [EARS-C7] Return staged file paths from buffer */
884
+ async getStagedFiles() {
885
+ return Array.from(this.stagingBuffer.keys());
886
+ }
887
+ /**
888
+ * [EARS-C6] Create branch via Refs API POST
889
+ */
890
+ async createBranch(branchName, startPoint) {
891
+ const sha = startPoint ? await this.getCommitHash(startPoint) : await this.getCommitHash(this.activeRef);
892
+ try {
893
+ await this.octokit.rest.git.createRef({
894
+ owner: this.owner,
895
+ repo: this.repo,
896
+ ref: `refs/heads/${branchName}`,
897
+ sha
898
+ });
899
+ } catch (error) {
900
+ if (isOctokitRequestError(error)) {
901
+ if (error.status === 422) {
902
+ throw new BranchAlreadyExistsError(branchName);
903
+ }
904
+ if (error.status === 401 || error.status === 403) {
905
+ throw new GitError(`authentication/permission error (${error.status}): createBranch ${branchName}`);
906
+ }
907
+ throw new GitError(`Failed to create branch ${branchName}: HTTP ${error.status}`);
908
+ }
909
+ const msg = error instanceof Error ? error.message : String(error);
910
+ throw new GitError(`network error: ${msg}`);
911
+ }
912
+ }
913
+ /**
914
+ * Internal commit implementation shared by commit() and commitAllowEmpty().
915
+ *
916
+ * [EARS-C3] 6-step atomic transaction
917
+ * [EARS-C4] Clears staging buffer after successful commit
918
+ * [EARS-C5] Throws if staging buffer is empty (unless allowEmpty)
919
+ */
920
+ async commitInternal(message, author, allowEmpty = false) {
921
+ if (!allowEmpty && this.stagingBuffer.size === 0) {
922
+ throw new GitError("Nothing to commit: staging buffer is empty");
923
+ }
924
+ try {
925
+ const { data: refData } = await this.octokit.rest.git.getRef({
926
+ owner: this.owner,
927
+ repo: this.repo,
928
+ ref: `heads/${this.activeRef}`
929
+ });
930
+ const currentSha = refData.object.sha;
931
+ const { data: commitData } = await this.octokit.rest.git.getCommit({
932
+ owner: this.owner,
933
+ repo: this.repo,
934
+ commit_sha: currentSha
935
+ });
936
+ const treeSha = commitData.tree.sha;
937
+ const treeEntries = [];
938
+ for (const [path, content] of this.stagingBuffer) {
939
+ if (content === null) {
940
+ treeEntries.push({
941
+ path,
942
+ mode: "100644",
943
+ type: "blob",
944
+ sha: null
945
+ });
946
+ } else {
947
+ const { data: blobData } = await this.octokit.rest.git.createBlob({
948
+ owner: this.owner,
949
+ repo: this.repo,
950
+ content: Buffer.from(content).toString("base64"),
951
+ encoding: "base64"
952
+ });
953
+ treeEntries.push({
954
+ path,
955
+ mode: "100644",
956
+ type: "blob",
957
+ sha: blobData.sha
958
+ });
959
+ }
960
+ }
961
+ const { data: treeData } = await this.octokit.rest.git.createTree({
962
+ owner: this.owner,
963
+ repo: this.repo,
964
+ base_tree: treeSha,
965
+ tree: treeEntries
966
+ });
967
+ const newTreeSha = treeData.sha;
968
+ const commitParams = {
969
+ owner: this.owner,
970
+ repo: this.repo,
971
+ message,
972
+ tree: newTreeSha,
973
+ parents: [currentSha]
974
+ };
975
+ if (author) {
976
+ commitParams.author = {
977
+ name: author.name,
978
+ email: author.email,
979
+ date: (/* @__PURE__ */ new Date()).toISOString()
980
+ };
981
+ }
982
+ const { data: newCommitData } = await this.octokit.rest.git.createCommit(commitParams);
983
+ const newCommitSha = newCommitData.sha;
984
+ try {
985
+ await this.octokit.rest.git.updateRef({
986
+ owner: this.owner,
987
+ repo: this.repo,
988
+ ref: `heads/${this.activeRef}`,
989
+ sha: newCommitSha
990
+ });
991
+ } catch (error) {
992
+ if (isOctokitRequestError(error) && error.status === 422) {
993
+ throw new GitError("non-fast-forward update rejected");
994
+ }
995
+ throw error;
996
+ }
997
+ this.stagingBuffer.clear();
998
+ return newCommitSha;
999
+ } catch (error) {
1000
+ if (error instanceof GitError) throw error;
1001
+ if (isOctokitRequestError(error)) {
1002
+ if (error.status === 401 || error.status === 403) {
1003
+ throw new GitError(`authentication/permission error (${error.status}): commit`);
1004
+ }
1005
+ if (error.status >= 500) {
1006
+ throw new GitError(`GitHub server error (${error.status}): commit`);
1007
+ }
1008
+ throw new GitError(`GitHub API error (${error.status}): commit`);
1009
+ }
1010
+ const msg = error instanceof Error ? error.message : String(error);
1011
+ throw new GitError(`network error: ${msg}`);
1012
+ }
1013
+ }
1014
+ /**
1015
+ * [EARS-C3] Commit staged changes via 6-step atomic transaction
1016
+ * [EARS-C5] Throws if staging buffer is empty
1017
+ */
1018
+ async commit(message, author) {
1019
+ return this.commitInternal(message, author, false);
1020
+ }
1021
+ // ═══════════════════════════════════════════════════════════════
1022
+ // CATEGORY B: NO-OPS (sensible defaults)
1023
+ // ═══════════════════════════════════════════════════════════════
1024
+ /** [EARS-D5] exec not supported in API mode */
1025
+ async exec(_command, _args, _options) {
1026
+ return { exitCode: 1, stdout: "", stderr: "exec() not supported in GitHub API mode" };
1027
+ }
1028
+ /** No-op: repos are created via GitHub API, not initialized locally */
1029
+ async init() {
1030
+ }
1031
+ /** [EARS-D1] Return virtual path representing the repo */
1032
+ async getRepoRoot() {
1033
+ return `github://${this.owner}/${this.repo}`;
1034
+ }
1035
+ /** [EARS-D1] Return active ref (starts as defaultBranch) */
1036
+ async getCurrentBranch() {
1037
+ return this.activeRef;
1038
+ }
1039
+ /** No-op: git config doesn't apply to GitHub API */
1040
+ async setConfig(_key, _value, _scope) {
1041
+ }
1042
+ /** [EARS-D1] Return true if staging buffer has entries */
1043
+ async hasUncommittedChanges(_pathFilter) {
1044
+ return this.stagingBuffer.size > 0;
1045
+ }
1046
+ /** No-op: GitHub API doesn't have rebase-in-progress concept */
1047
+ async isRebaseInProgress() {
1048
+ return false;
1049
+ }
1050
+ /** [EARS-D1] GitHub repos always have 'origin' conceptually */
1051
+ async isRemoteConfigured(_remoteName) {
1052
+ return true;
1053
+ }
1054
+ /** No-op: always 'origin' */
1055
+ async getBranchRemote(_branchName) {
1056
+ return "origin";
1057
+ }
1058
+ /** No-op: GitHub API handles merges atomically */
1059
+ async getConflictedFiles() {
1060
+ return [];
1061
+ }
1062
+ /** [EARS-D2] Update activeRef for subsequent operations */
1063
+ async checkoutBranch(branchName) {
1064
+ this.activeRef = branchName;
1065
+ }
1066
+ /** No-op: GitHub API doesn't have stash concept */
1067
+ async stash(_message) {
1068
+ return null;
1069
+ }
1070
+ /** No-op */
1071
+ async stashPop() {
1072
+ return false;
1073
+ }
1074
+ /** No-op */
1075
+ async stashDrop(_stashHash) {
1076
+ }
1077
+ /** No-op: API always fresh */
1078
+ async fetch(_remote) {
1079
+ }
1080
+ /** No-op: API mode */
1081
+ async pull(_remote, _branchName) {
1082
+ }
1083
+ /** No-op: API mode */
1084
+ async pullRebase(_remote, _branchName) {
1085
+ }
1086
+ /** [EARS-D4] No-op: commits via API are already remote */
1087
+ async push(_remote, _branchName) {
1088
+ }
1089
+ /** [EARS-D4] No-op: commits via API are already remote */
1090
+ async pushWithUpstream(_remote, _branchName) {
1091
+ }
1092
+ /** No-op: API mode */
1093
+ async setUpstream(_branchName, _remote, _remoteBranch) {
1094
+ }
1095
+ /** No-op */
1096
+ async rebaseAbort() {
1097
+ }
1098
+ /** [EARS-D1] Delegates to commitInternal, allowing empty staging buffer */
1099
+ async commitAllowEmpty(message, author) {
1100
+ return this.commitInternal(message, author, true);
1101
+ }
1102
+ // ═══════════════════════════════════════════════════════════════
1103
+ // CATEGORY C: NOT SUPPORTED (throw GitError)
1104
+ // ═══════════════════════════════════════════════════════════════
1105
+ /** [EARS-D3] Not supported via GitHub API */
1106
+ async rebase(_targetBranch) {
1107
+ this.notSupported("rebase");
1108
+ }
1109
+ /** [EARS-D3] Not supported via GitHub API */
1110
+ async rebaseContinue() {
1111
+ this.notSupported("rebaseContinue");
1112
+ }
1113
+ /** [EARS-D3] Not supported via GitHub API */
1114
+ async resetHard(_target) {
1115
+ this.notSupported("resetHard");
1116
+ }
1117
+ /** [EARS-D3] Not supported via GitHub API */
1118
+ async checkoutOrphanBranch(_branchName) {
1119
+ this.notSupported("checkoutOrphanBranch");
1120
+ }
1121
+ /** [EARS-D3] Not supported via GitHub API */
1122
+ async checkoutFilesFromBranch(_sourceBranch, _filePaths) {
1123
+ this.notSupported("checkoutFilesFromBranch");
1124
+ }
1125
+ /** [EARS-D3] Not supported via GitHub API */
1126
+ async getMergeBase(_branchA, _branchB) {
1127
+ this.notSupported("getMergeBase");
1128
+ }
1129
+ };
1130
+
1131
+ // src/config_store/github/github_config_store.ts
1132
+ var GitHubConfigStore = class {
1133
+ owner;
1134
+ repo;
1135
+ ref;
1136
+ basePath;
1137
+ octokit;
1138
+ /** Cached blob SHA from the last loadConfig call, used for PUT updates */
1139
+ cachedSha = null;
1140
+ constructor(options, octokit) {
1141
+ this.owner = options.owner;
1142
+ this.repo = options.repo;
1143
+ this.ref = options.ref ?? "gitgov-state";
1144
+ this.basePath = options.basePath ?? ".gitgov";
1145
+ this.octokit = octokit;
1146
+ }
1147
+ /**
1148
+ * Load project configuration from GitHub Contents API.
1149
+ *
1150
+ * [EARS-A1] Returns GitGovConfig when valid JSON is found.
1151
+ * [EARS-A2] Returns null on 404 (fail-safe).
1152
+ * [EARS-A3] Returns null on invalid JSON (fail-safe).
1153
+ * [EARS-B1] Fetches via Contents API with base64 decode.
1154
+ * [EARS-B2] Caches SHA from response for subsequent saveConfig.
1155
+ */
1156
+ async loadConfig() {
1157
+ const path = `${this.basePath}/config.json`;
1158
+ try {
1159
+ const { data } = await this.octokit.rest.repos.getContent({
1160
+ owner: this.owner,
1161
+ repo: this.repo,
1162
+ path,
1163
+ ref: this.ref
1164
+ });
1165
+ if (Array.isArray(data) || data.type !== "file") {
1166
+ return null;
1167
+ }
1168
+ if (!data.content) {
1169
+ return null;
1170
+ }
1171
+ this.cachedSha = data.sha;
1172
+ try {
1173
+ const decoded = Buffer.from(data.content, "base64").toString("utf-8");
1174
+ return JSON.parse(decoded);
1175
+ } catch {
1176
+ return null;
1177
+ }
1178
+ } catch (error) {
1179
+ if (isOctokitRequestError(error) && error.status === 404) {
1180
+ return null;
1181
+ }
1182
+ throw mapOctokitError(error, `loadConfig ${this.owner}/${this.repo}/${path}`);
1183
+ }
1184
+ }
1185
+ /**
1186
+ * Save project configuration to GitHub via Contents API PUT.
1187
+ *
1188
+ * [EARS-A4] Writes config via PUT to Contents API.
1189
+ * [EARS-B3] Includes cached SHA for updates (optimistic concurrency).
1190
+ * [EARS-B4] Omits SHA for initial creation.
1191
+ * [EARS-C1] Throws PERMISSION_DENIED on 401/403.
1192
+ * [EARS-C2] Throws CONFLICT on 409.
1193
+ * [EARS-C3] Throws SERVER_ERROR on 5xx.
1194
+ */
1195
+ async saveConfig(config) {
1196
+ const path = `${this.basePath}/config.json`;
1197
+ const content = Buffer.from(JSON.stringify(config, null, 2)).toString("base64");
1198
+ try {
1199
+ const { data } = await this.octokit.rest.repos.createOrUpdateFileContents({
1200
+ owner: this.owner,
1201
+ repo: this.repo,
1202
+ path,
1203
+ message: "chore(config): update gitgov config.json",
1204
+ content,
1205
+ branch: this.ref,
1206
+ ...this.cachedSha ? { sha: this.cachedSha } : {}
1207
+ });
1208
+ if (data.content?.sha) {
1209
+ this.cachedSha = data.content.sha;
1210
+ }
1211
+ return { commitSha: data.commit.sha };
1212
+ } catch (error) {
1213
+ throw mapOctokitError(error, `saveConfig ${this.owner}/${this.repo}/${path}`);
1214
+ }
1215
+ }
1216
+ };
1217
+
1218
+ // src/github.ts
1219
+ var GitHubApiError = class _GitHubApiError extends Error {
1220
+ constructor(message, code, statusCode) {
1221
+ super(message);
1222
+ this.code = code;
1223
+ this.statusCode = statusCode;
1224
+ this.name = "GitHubApiError";
1225
+ Object.setPrototypeOf(this, _GitHubApiError.prototype);
1226
+ }
1227
+ };
1228
+ function isOctokitRequestError(error) {
1229
+ return error instanceof Error && typeof error["status"] === "number";
1230
+ }
1231
+ function mapOctokitError(error, context) {
1232
+ if (isOctokitRequestError(error)) {
1233
+ const status = error.status;
1234
+ if (status === 401 || status === 403) {
1235
+ return new GitHubApiError(
1236
+ `Permission denied: ${context}`,
1237
+ "PERMISSION_DENIED",
1238
+ status
1239
+ );
1240
+ }
1241
+ if (status === 404) {
1242
+ return new GitHubApiError(
1243
+ `Not found: ${context}`,
1244
+ "NOT_FOUND",
1245
+ status
1246
+ );
1247
+ }
1248
+ if (status === 409) {
1249
+ return new GitHubApiError(
1250
+ `Conflict: ${context}`,
1251
+ "CONFLICT",
1252
+ status
1253
+ );
1254
+ }
1255
+ if (status === 422) {
1256
+ return new GitHubApiError(
1257
+ `Validation failed: ${context}`,
1258
+ "CONFLICT",
1259
+ status
1260
+ );
1261
+ }
1262
+ if (status >= 500) {
1263
+ return new GitHubApiError(
1264
+ `Server error (${status}): ${context}`,
1265
+ "SERVER_ERROR",
1266
+ status
1267
+ );
1268
+ }
1269
+ return new GitHubApiError(
1270
+ `GitHub API error (${status}): ${context}`,
1271
+ "SERVER_ERROR",
1272
+ status
1273
+ );
1274
+ }
1275
+ const message = error instanceof Error ? error.message : String(error);
1276
+ return new GitHubApiError(`Network error: ${message}`, "NETWORK_ERROR");
1277
+ }
1278
+
1279
+ export { GitHubApiError, GitHubConfigStore, GitHubFileLister, GitHubGitModule, GitHubRecordStore, isOctokitRequestError, mapOctokitError };
1280
+ //# sourceMappingURL=github.js.map
1281
+ //# sourceMappingURL=github.js.map