@aigne/afs-github 1.11.0-beta.6 → 1.11.0-beta.7
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/dist/index.d.mts +110 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1330 -112
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -4
package/dist/index.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { throttling } from "@octokit/plugin-throttling";
|
|
|
3
3
|
import { Octokit } from "@octokit/rest";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { AFSNotFoundError, AFSReadonlyError } from "@aigne/afs";
|
|
6
7
|
import { MappingCompiler } from "@aigne/afs-mapping";
|
|
7
8
|
import { WorldMappingCore } from "@aigne/afs-world-mapping";
|
|
8
9
|
import { z } from "zod";
|
|
@@ -380,7 +381,8 @@ var AFSGitHub = class {
|
|
|
380
381
|
path: "/{owner}/{repo}/issues/{$.number}",
|
|
381
382
|
summary: "$.title",
|
|
382
383
|
content: "$.body",
|
|
383
|
-
|
|
384
|
+
meta: {
|
|
385
|
+
kind: "github:issue",
|
|
384
386
|
type: "issue",
|
|
385
387
|
state: "$.state",
|
|
386
388
|
author: "$.user.login"
|
|
@@ -401,7 +403,7 @@ var AFSGitHub = class {
|
|
|
401
403
|
path: "/{owner}/{repo}/issues/{$.number}",
|
|
402
404
|
summary: "$.title",
|
|
403
405
|
content: "$.body",
|
|
404
|
-
|
|
406
|
+
meta: {
|
|
405
407
|
type: "issue",
|
|
406
408
|
state: "$.state",
|
|
407
409
|
author: "$.user.login"
|
|
@@ -424,7 +426,8 @@ var AFSGitHub = class {
|
|
|
424
426
|
path: "/{owner}/{repo}/pulls/{$.number}",
|
|
425
427
|
summary: "$.title",
|
|
426
428
|
content: "$.body",
|
|
427
|
-
|
|
429
|
+
meta: {
|
|
430
|
+
kind: "github:pull-request",
|
|
428
431
|
type: "pull_request",
|
|
429
432
|
state: "$.state",
|
|
430
433
|
author: "$.user.login"
|
|
@@ -445,7 +448,7 @@ var AFSGitHub = class {
|
|
|
445
448
|
path: "/{owner}/{repo}/pulls/{$.number}",
|
|
446
449
|
summary: "$.title",
|
|
447
450
|
content: "$.body",
|
|
448
|
-
|
|
451
|
+
meta: {
|
|
449
452
|
type: "pull_request",
|
|
450
453
|
state: "$.state",
|
|
451
454
|
author: "$.user.login"
|
|
@@ -567,7 +570,8 @@ var AFSGitHub = class {
|
|
|
567
570
|
id: "issues",
|
|
568
571
|
path: "/issues",
|
|
569
572
|
summary: "Repository Issues",
|
|
570
|
-
|
|
573
|
+
meta: {
|
|
574
|
+
kind: "github:directory",
|
|
571
575
|
childrenCount: -1,
|
|
572
576
|
description: "List and read repository issues"
|
|
573
577
|
}
|
|
@@ -576,7 +580,8 @@ var AFSGitHub = class {
|
|
|
576
580
|
id: "pulls",
|
|
577
581
|
path: "/pulls",
|
|
578
582
|
summary: "Pull Requests",
|
|
579
|
-
|
|
583
|
+
meta: {
|
|
584
|
+
kind: "github:directory",
|
|
580
585
|
childrenCount: -1,
|
|
581
586
|
description: "List and read pull requests"
|
|
582
587
|
}
|
|
@@ -585,7 +590,8 @@ var AFSGitHub = class {
|
|
|
585
590
|
id: "repo",
|
|
586
591
|
path: "/repo",
|
|
587
592
|
summary: "Repository Code",
|
|
588
|
-
|
|
593
|
+
meta: {
|
|
594
|
+
kind: "github:directory",
|
|
589
595
|
childrenCount: -1,
|
|
590
596
|
description: "Repository source code (via Contents API)"
|
|
591
597
|
}
|
|
@@ -593,6 +599,19 @@ var AFSGitHub = class {
|
|
|
593
599
|
];
|
|
594
600
|
}
|
|
595
601
|
/**
|
|
602
|
+
* Check if path points to a file (leaf node) like /issues/{n} or /pulls/{n}
|
|
603
|
+
* These are individual issues/PRs that don't have children.
|
|
604
|
+
*/
|
|
605
|
+
isFilePath(path) {
|
|
606
|
+
const segments = path.replace(/^\/+|\/+$/g, "").split("/").filter(Boolean);
|
|
607
|
+
if (this.options.mode === "single-repo") {
|
|
608
|
+
if (segments.length === 2 && (segments[0] === "issues" || segments[0] === "pulls")) return /^\d+$/.test(segments[1]);
|
|
609
|
+
} else if (this.options.mode === "org") {
|
|
610
|
+
if (segments.length === 3 && (segments[1] === "issues" || segments[1] === "pulls")) return /^\d+$/.test(segments[2]);
|
|
611
|
+
}
|
|
612
|
+
return false;
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
596
615
|
* Check if path is root or empty
|
|
597
616
|
*/
|
|
598
617
|
isRootPath(path) {
|
|
@@ -612,13 +631,13 @@ var AFSGitHub = class {
|
|
|
612
631
|
childrenCount = 3;
|
|
613
632
|
} else {
|
|
614
633
|
summary = this.description ?? "GitHub";
|
|
615
|
-
childrenCount =
|
|
634
|
+
childrenCount = void 0;
|
|
616
635
|
}
|
|
617
636
|
return {
|
|
618
637
|
id: this.name,
|
|
619
638
|
path: "/",
|
|
620
639
|
summary,
|
|
621
|
-
|
|
640
|
+
meta: {
|
|
622
641
|
childrenCount,
|
|
623
642
|
description: summary
|
|
624
643
|
}
|
|
@@ -633,7 +652,7 @@ var AFSGitHub = class {
|
|
|
633
652
|
id: `${repoPath}/issues`,
|
|
634
653
|
path: `${repoPath}/issues`,
|
|
635
654
|
summary: "Repository Issues",
|
|
636
|
-
|
|
655
|
+
meta: {
|
|
637
656
|
childrenCount: -1,
|
|
638
657
|
description: "List and read repository issues"
|
|
639
658
|
}
|
|
@@ -642,7 +661,7 @@ var AFSGitHub = class {
|
|
|
642
661
|
id: `${repoPath}/pulls`,
|
|
643
662
|
path: `${repoPath}/pulls`,
|
|
644
663
|
summary: "Pull Requests",
|
|
645
|
-
|
|
664
|
+
meta: {
|
|
646
665
|
childrenCount: -1,
|
|
647
666
|
description: "List and read pull requests"
|
|
648
667
|
}
|
|
@@ -651,7 +670,7 @@ var AFSGitHub = class {
|
|
|
651
670
|
id: `${repoPath}/repo`,
|
|
652
671
|
path: `${repoPath}/repo`,
|
|
653
672
|
summary: "Repository Code",
|
|
654
|
-
|
|
673
|
+
meta: {
|
|
655
674
|
childrenCount: -1,
|
|
656
675
|
description: "Repository source code (via Contents API)"
|
|
657
676
|
}
|
|
@@ -742,6 +761,8 @@ var AFSGitHub = class {
|
|
|
742
761
|
* - /repo -> list all branches
|
|
743
762
|
* - /repo/{branch} -> list branch root
|
|
744
763
|
* - /repo/{branch}/path -> list contents at path
|
|
764
|
+
*
|
|
765
|
+
* @throws AFSNotFoundError if path does not exist
|
|
745
766
|
*/
|
|
746
767
|
async listViaContentsAPI(path, _options) {
|
|
747
768
|
const { owner, repo, branch, filePath } = this.parseRepoPath(path);
|
|
@@ -753,7 +774,8 @@ var AFSGitHub = class {
|
|
|
753
774
|
id: `${repoPrefix}/${b.name}`,
|
|
754
775
|
path: `${repoPrefix}/${b.name}`,
|
|
755
776
|
summary: b.name + (b.name === defaultBranch ? " (default)" : ""),
|
|
756
|
-
|
|
777
|
+
meta: {
|
|
778
|
+
kind: "github:branch",
|
|
757
779
|
type: "branch",
|
|
758
780
|
sha: b.commit.sha,
|
|
759
781
|
protected: b.protected,
|
|
@@ -762,24 +784,23 @@ var AFSGitHub = class {
|
|
|
762
784
|
}
|
|
763
785
|
})) };
|
|
764
786
|
} catch (error) {
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
message: this.formatErrorMessage(error, `branches for ${owner}/${repo}`)
|
|
768
|
-
};
|
|
787
|
+
if (error?.status === 404) throw new AFSNotFoundError(path);
|
|
788
|
+
throw error;
|
|
769
789
|
}
|
|
770
790
|
try {
|
|
771
791
|
const contents = await this.client.getContents(owner, repo, filePath, branch);
|
|
772
792
|
if (!Array.isArray(contents)) {
|
|
793
|
+
if (contents.type === "file") return { data: [] };
|
|
773
794
|
const entryPath = filePath ? `${repoPrefix}/${branch}/${contents.path}` : `${repoPrefix}/${branch}/${contents.name}`;
|
|
774
795
|
return { data: [{
|
|
775
796
|
id: entryPath,
|
|
776
797
|
path: entryPath,
|
|
777
798
|
summary: contents.name,
|
|
778
|
-
|
|
799
|
+
meta: {
|
|
800
|
+
kind: contents.type === "dir" ? "github:directory" : "github:file",
|
|
779
801
|
type: contents.type,
|
|
780
802
|
size: contents.size,
|
|
781
|
-
sha: contents.sha
|
|
782
|
-
childrenCount: contents.type === "dir" ? -1 : void 0
|
|
803
|
+
sha: contents.sha
|
|
783
804
|
}
|
|
784
805
|
}] };
|
|
785
806
|
}
|
|
@@ -789,50 +810,94 @@ var AFSGitHub = class {
|
|
|
789
810
|
id: entryPath,
|
|
790
811
|
path: entryPath,
|
|
791
812
|
summary: item.name,
|
|
792
|
-
|
|
813
|
+
meta: {
|
|
814
|
+
kind: item.type === "dir" ? "github:directory" : "github:file",
|
|
793
815
|
type: item.type,
|
|
794
816
|
size: item.size,
|
|
795
|
-
sha: item.sha
|
|
796
|
-
childrenCount: item.type === "dir" ? -1 : void 0
|
|
817
|
+
sha: item.sha
|
|
797
818
|
}
|
|
798
819
|
};
|
|
799
820
|
}) };
|
|
800
821
|
} catch (error) {
|
|
801
|
-
if (error?.status === 404)
|
|
802
|
-
|
|
803
|
-
message: `Path not found: ${branch}/${filePath || ""}`
|
|
804
|
-
};
|
|
805
|
-
return {
|
|
806
|
-
data: [],
|
|
807
|
-
message: this.formatErrorMessage(error, `repository contents for ${owner}/${repo}`)
|
|
808
|
-
};
|
|
822
|
+
if (error?.status === 404) throw new AFSNotFoundError(path, `Path not found: ${branch}/${filePath || ""}`);
|
|
823
|
+
throw error;
|
|
809
824
|
}
|
|
810
825
|
}
|
|
811
826
|
/**
|
|
812
|
-
* Read
|
|
813
|
-
*
|
|
827
|
+
* Read via GitHub Contents API
|
|
828
|
+
* Supports:
|
|
829
|
+
* - /repo - return repo virtual directory
|
|
830
|
+
* - /repo/{branch} - return branch directory entry
|
|
831
|
+
* - /repo/{branch}/{path} - return file/directory entry
|
|
832
|
+
*
|
|
833
|
+
* @throws AFSNotFoundError if path does not exist
|
|
814
834
|
*/
|
|
815
835
|
async readViaContentsAPI(path, _options) {
|
|
816
836
|
const { owner, repo, branch, filePath } = this.parseRepoPath(path);
|
|
817
837
|
const repoPrefix = this.getRepoPathPrefix(repo);
|
|
818
|
-
if (!branch) return {
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
838
|
+
if (!branch) return { data: {
|
|
839
|
+
id: `${repoPrefix}`,
|
|
840
|
+
path: repoPrefix,
|
|
841
|
+
summary: "Repository Code",
|
|
842
|
+
meta: { description: "Repository source code (via Contents API)" }
|
|
843
|
+
} };
|
|
844
|
+
if (!filePath) try {
|
|
845
|
+
const branches = await this.client.getBranches(owner, repo);
|
|
846
|
+
const defaultBranch = await this.client.getDefaultBranch(owner, repo);
|
|
847
|
+
const branchInfo = branches.find((b) => b.name === branch);
|
|
848
|
+
if (!branchInfo) throw new AFSNotFoundError(path, `Branch not found: ${branch}`);
|
|
849
|
+
let childrenCount;
|
|
850
|
+
try {
|
|
851
|
+
const contents = await this.client.getContents(owner, repo, "", branch);
|
|
852
|
+
if (Array.isArray(contents)) childrenCount = contents.length;
|
|
853
|
+
} catch {}
|
|
854
|
+
const entryPath = `${repoPrefix}/${branch}`;
|
|
855
|
+
return { data: {
|
|
856
|
+
id: entryPath,
|
|
857
|
+
path: entryPath,
|
|
858
|
+
summary: branch + (branch === defaultBranch ? " (default)" : ""),
|
|
859
|
+
meta: {
|
|
860
|
+
type: "branch",
|
|
861
|
+
sha: branchInfo.commit.sha,
|
|
862
|
+
protected: branchInfo.protected,
|
|
863
|
+
isDefault: branch === defaultBranch,
|
|
864
|
+
childrenCount
|
|
865
|
+
}
|
|
866
|
+
} };
|
|
867
|
+
} catch (error) {
|
|
868
|
+
if (error instanceof AFSNotFoundError) throw error;
|
|
869
|
+
if (error?.status === 404) throw new AFSNotFoundError(path);
|
|
870
|
+
throw error;
|
|
871
|
+
}
|
|
826
872
|
try {
|
|
827
873
|
const contents = await this.client.getContents(owner, repo, filePath, branch);
|
|
828
|
-
if (Array.isArray(contents))
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
874
|
+
if (Array.isArray(contents)) {
|
|
875
|
+
const entryPath$1 = `${repoPrefix}/${branch}/${filePath}`;
|
|
876
|
+
return { data: {
|
|
877
|
+
id: entryPath$1,
|
|
878
|
+
path: entryPath$1,
|
|
879
|
+
summary: filePath.split("/").pop() || filePath,
|
|
880
|
+
meta: {
|
|
881
|
+
type: "dir",
|
|
882
|
+
childrenCount: contents.length,
|
|
883
|
+
branch
|
|
884
|
+
}
|
|
885
|
+
} };
|
|
886
|
+
}
|
|
887
|
+
if (contents.type === "dir") {
|
|
888
|
+
const entryPath$1 = `${repoPrefix}/${branch}/${filePath}`;
|
|
889
|
+
return { data: {
|
|
890
|
+
id: entryPath$1,
|
|
891
|
+
path: entryPath$1,
|
|
892
|
+
summary: contents.name,
|
|
893
|
+
meta: {
|
|
894
|
+
type: "dir",
|
|
895
|
+
sha: contents.sha,
|
|
896
|
+
branch
|
|
897
|
+
}
|
|
898
|
+
} };
|
|
899
|
+
}
|
|
900
|
+
if (contents.type !== "file") throw new AFSNotFoundError(path, `Unsupported type: ${contents.type}`);
|
|
836
901
|
let content;
|
|
837
902
|
if (contents.content) content = Buffer.from(contents.content, "base64").toString("utf-8");
|
|
838
903
|
else content = await this.client.getBlob(owner, repo, contents.sha);
|
|
@@ -842,7 +907,7 @@ var AFSGitHub = class {
|
|
|
842
907
|
path: entryPath,
|
|
843
908
|
summary: contents.name,
|
|
844
909
|
content,
|
|
845
|
-
|
|
910
|
+
meta: {
|
|
846
911
|
type: "file",
|
|
847
912
|
size: contents.size,
|
|
848
913
|
sha: contents.sha,
|
|
@@ -850,14 +915,9 @@ var AFSGitHub = class {
|
|
|
850
915
|
}
|
|
851
916
|
} };
|
|
852
917
|
} catch (error) {
|
|
853
|
-
if (error
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
};
|
|
857
|
-
return {
|
|
858
|
-
data: void 0,
|
|
859
|
-
message: this.formatErrorMessage(error, `file ${filePath} in ${owner}/${repo}@${branch}`)
|
|
860
|
-
};
|
|
918
|
+
if (error instanceof AFSNotFoundError) throw error;
|
|
919
|
+
if (error?.status === 404) throw new AFSNotFoundError(path, `Path not found: ${branch}/${filePath}`);
|
|
920
|
+
throw error;
|
|
861
921
|
}
|
|
862
922
|
}
|
|
863
923
|
/**
|
|
@@ -901,7 +961,7 @@ var AFSGitHub = class {
|
|
|
901
961
|
id: repo.name,
|
|
902
962
|
path: `/${repo.name}`,
|
|
903
963
|
summary: repo.description || repo.name,
|
|
904
|
-
|
|
964
|
+
meta: {
|
|
905
965
|
childrenCount: 3,
|
|
906
966
|
description: repo.description,
|
|
907
967
|
private: repo.private
|
|
@@ -937,41 +997,34 @@ var AFSGitHub = class {
|
|
|
937
997
|
return allRepos;
|
|
938
998
|
}
|
|
939
999
|
/**
|
|
940
|
-
*
|
|
1000
|
+
* Apply limit to entries array
|
|
941
1001
|
*/
|
|
942
|
-
|
|
943
|
-
|
|
1002
|
+
applyLimit(entries, limit) {
|
|
1003
|
+
if (limit !== void 0 && limit > 0 && entries.length > limit) return entries.slice(0, limit);
|
|
1004
|
+
return entries;
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* List immediate children at a path (maxDepth=1 only)
|
|
1008
|
+
*/
|
|
1009
|
+
async listImmediate(path, options) {
|
|
1010
|
+
const actionsResult = this.listActions(path);
|
|
1011
|
+
if (actionsResult) return actionsResult;
|
|
1012
|
+
if (this.isFilePath(path)) return { data: [] };
|
|
944
1013
|
if (this.isRepoPath(path)) return this.listViaContentsAPI(path, options);
|
|
945
1014
|
if (this.isRootPath(path)) {
|
|
946
|
-
if (maxDepth === 0) return { data: [this.getRootEntry()] };
|
|
947
1015
|
if (this.options.mode === "org") return this.listOrgRepos(!!this.options.auth?.token);
|
|
948
1016
|
return { data: this.getVirtualDirectories() };
|
|
949
1017
|
}
|
|
950
1018
|
if (this.options.mode === "org" && this.isRepoRootPath(path)) {
|
|
951
1019
|
const repoName = path.replace(/^\/+|\/+$/g, "");
|
|
952
|
-
if (maxDepth === 0) return { data: [{
|
|
953
|
-
id: repoName,
|
|
954
|
-
path: `/${repoName}`,
|
|
955
|
-
summary: `Repository: ${this.options.owner}/${repoName}`,
|
|
956
|
-
metadata: { childrenCount: 2 }
|
|
957
|
-
}] };
|
|
958
1020
|
return { data: this.getRepoVirtualDirectories(`/${repoName}`) };
|
|
959
1021
|
}
|
|
960
1022
|
const fullPath = this.resolvePath(path);
|
|
961
|
-
if (!this.compiled)
|
|
962
|
-
data: [],
|
|
963
|
-
message: "Mapping not loaded"
|
|
964
|
-
};
|
|
1023
|
+
if (!this.compiled) throw new AFSNotFoundError(path, "Mapping not loaded");
|
|
965
1024
|
const resolved = this.compiled.resolve(fullPath);
|
|
966
|
-
if (!resolved || !resolved.operations.list)
|
|
967
|
-
data: [],
|
|
968
|
-
message: `No list operation for path: ${fullPath}`
|
|
969
|
-
};
|
|
1025
|
+
if (!resolved || !resolved.operations.list) throw new AFSNotFoundError(path, `No list operation for path: ${fullPath}`);
|
|
970
1026
|
const request = this.compiled.buildRequest(fullPath, "list", { query: options?.filter });
|
|
971
|
-
if (!request)
|
|
972
|
-
data: [],
|
|
973
|
-
message: "Failed to build request"
|
|
974
|
-
};
|
|
1027
|
+
if (!request) throw new AFSNotFoundError(path, "Failed to build request");
|
|
975
1028
|
try {
|
|
976
1029
|
let responseData = (await this.client.request(`${request.method} ${request.path}`, request.params)).data;
|
|
977
1030
|
if (Array.isArray(responseData) && fullPath.endsWith("/issues")) responseData = responseData.filter((item) => !item.pull_request);
|
|
@@ -985,16 +1038,132 @@ var AFSGitHub = class {
|
|
|
985
1038
|
}
|
|
986
1039
|
return { data: entries };
|
|
987
1040
|
} catch (error) {
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1041
|
+
if (error?.status === 404) throw new AFSNotFoundError(path);
|
|
1042
|
+
throw error;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Check if a path represents a directory (has children)
|
|
1047
|
+
*/
|
|
1048
|
+
isDirectoryPath(path) {
|
|
1049
|
+
if (this.isRootPath(path)) return true;
|
|
1050
|
+
if (this.isFilePath(path)) return false;
|
|
1051
|
+
if (this.getVirtualDirectoryEntry(path)) return true;
|
|
1052
|
+
if (this.isRepoPath(path)) return true;
|
|
1053
|
+
const fullPath = this.resolvePath(path);
|
|
1054
|
+
if (this.compiled) {
|
|
1055
|
+
if (this.compiled.resolve(fullPath)?.operations.list) return true;
|
|
1056
|
+
}
|
|
1057
|
+
return false;
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* List entries at a path
|
|
1061
|
+
*
|
|
1062
|
+
* Per AFS semantics:
|
|
1063
|
+
* - maxDepth=0: return empty array (no children)
|
|
1064
|
+
* - maxDepth=1: return immediate children
|
|
1065
|
+
* - maxDepth>1: recursively include deeper levels
|
|
1066
|
+
* - limit: maximum number of entries to return
|
|
1067
|
+
*
|
|
1068
|
+
* @throws AFSNotFoundError if path does not exist
|
|
1069
|
+
*/
|
|
1070
|
+
async list(path, options) {
|
|
1071
|
+
const maxDepth = options?.maxDepth ?? 1;
|
|
1072
|
+
const limit = options?.limit;
|
|
1073
|
+
if (maxDepth === 0) return { data: [] };
|
|
1074
|
+
const result = await this.listImmediate(path, options);
|
|
1075
|
+
let entries = result.data;
|
|
1076
|
+
if (maxDepth > 1) {
|
|
1077
|
+
const childEntries = [];
|
|
1078
|
+
for (const entry of entries) if (this.isDirectoryPath(entry.path)) try {
|
|
1079
|
+
const childResult = await this.listImmediate(entry.path, options);
|
|
1080
|
+
childEntries.push(...childResult.data);
|
|
1081
|
+
} catch {}
|
|
1082
|
+
entries = [...entries, ...childEntries];
|
|
1083
|
+
}
|
|
1084
|
+
return {
|
|
1085
|
+
data: this.applyLimit(entries, limit),
|
|
1086
|
+
message: result.message
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* Check if path is a .meta path and extract the target path
|
|
1091
|
+
*/
|
|
1092
|
+
isMetaPath(path) {
|
|
1093
|
+
const normalized = path.replace(/\/+$/, "");
|
|
1094
|
+
if (normalized.endsWith("/.meta")) return {
|
|
1095
|
+
isMeta: true,
|
|
1096
|
+
targetPath: normalized.slice(0, -6) || "/"
|
|
1097
|
+
};
|
|
1098
|
+
return {
|
|
1099
|
+
isMeta: false,
|
|
1100
|
+
targetPath: path
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Get entry info for a virtual directory path
|
|
1105
|
+
*/
|
|
1106
|
+
getVirtualDirectoryEntry(path) {
|
|
1107
|
+
const segments = path.replace(/^\/+|\/+$/g, "").split("/").filter(Boolean);
|
|
1108
|
+
if (segments.length === 0) return this.getRootEntry();
|
|
1109
|
+
if (this.options.mode === "single-repo") {
|
|
1110
|
+
if (segments.length === 1) {
|
|
1111
|
+
const dir = this.getVirtualDirectories().find((d) => d.path === `/${segments[0]}`);
|
|
1112
|
+
if (dir) return dir;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
if (this.options.mode === "org") {
|
|
1116
|
+
if (segments.length === 1) return {
|
|
1117
|
+
id: segments[0],
|
|
1118
|
+
path: `/${segments[0]}`,
|
|
1119
|
+
summary: `Repository: ${this.options.owner}/${segments[0]}`,
|
|
1120
|
+
meta: { childrenCount: 3 }
|
|
991
1121
|
};
|
|
1122
|
+
if (segments.length === 2) {
|
|
1123
|
+
const repoName = segments[0];
|
|
1124
|
+
const dir = this.getRepoVirtualDirectories(`/${repoName}`).find((d) => d.path === `/${repoName}/${segments[1]}`);
|
|
1125
|
+
if (dir) return dir;
|
|
1126
|
+
}
|
|
992
1127
|
}
|
|
993
1128
|
}
|
|
994
1129
|
/**
|
|
995
1130
|
* Read a single entry
|
|
1131
|
+
*
|
|
1132
|
+
* Supports:
|
|
1133
|
+
* - Directory paths (returns entry info)
|
|
1134
|
+
* - .meta paths (returns metadata)
|
|
1135
|
+
* - Issue/PR paths (returns content via API)
|
|
1136
|
+
* - Repo file paths (returns content via Contents API)
|
|
1137
|
+
*
|
|
1138
|
+
* @throws AFSNotFoundError if path does not exist
|
|
996
1139
|
*/
|
|
997
1140
|
async read(path, options) {
|
|
1141
|
+
if (path === "/.meta/.capabilities") return this.readCapabilities();
|
|
1142
|
+
const actionsMatch = path.match(/^(.*)\/\.actions\/([^/]+)$/);
|
|
1143
|
+
if (actionsMatch) {
|
|
1144
|
+
const actionsListPath = `${actionsMatch[1] || ""}/.actions`;
|
|
1145
|
+
const actionName = actionsMatch[2];
|
|
1146
|
+
const actionsResult = this.listActions(actionsListPath);
|
|
1147
|
+
if (actionsResult) {
|
|
1148
|
+
const actionEntry = actionsResult.data.find((entry) => entry.id === actionName);
|
|
1149
|
+
if (actionEntry) return { data: actionEntry };
|
|
1150
|
+
}
|
|
1151
|
+
throw new AFSNotFoundError(path);
|
|
1152
|
+
}
|
|
1153
|
+
const { isMeta, targetPath } = this.isMetaPath(path);
|
|
1154
|
+
if (isMeta) return this.readMeta(targetPath);
|
|
1155
|
+
const virtualEntry = this.getVirtualDirectoryEntry(path);
|
|
1156
|
+
if (virtualEntry) {
|
|
1157
|
+
const cc = virtualEntry.meta?.childrenCount;
|
|
1158
|
+
if ((cc === void 0 || cc === -1) && this.isDirectoryPath(path)) try {
|
|
1159
|
+
const children = await this.listImmediate(path);
|
|
1160
|
+
virtualEntry.meta = {
|
|
1161
|
+
...virtualEntry.meta,
|
|
1162
|
+
childrenCount: children.data.length
|
|
1163
|
+
};
|
|
1164
|
+
} catch {}
|
|
1165
|
+
return { data: virtualEntry };
|
|
1166
|
+
}
|
|
998
1167
|
if (this.isRepoPath(path)) return this.readViaContentsAPI(path, options);
|
|
999
1168
|
if (this.options.mode === "org" || this.options.mode === "single-repo") {
|
|
1000
1169
|
const segments = path.replace(/^\/+|\/+$/g, "").split("/");
|
|
@@ -1002,41 +1171,23 @@ var AFSGitHub = class {
|
|
|
1002
1171
|
const pullsIdx = segments.indexOf("pulls");
|
|
1003
1172
|
if (issuesIdx !== -1 && segments[issuesIdx + 1]) {
|
|
1004
1173
|
const issueNum = segments[issuesIdx + 1];
|
|
1005
|
-
if (!/^\d+$/.test(issueNum))
|
|
1006
|
-
data: void 0,
|
|
1007
|
-
message: "Invalid issue number"
|
|
1008
|
-
};
|
|
1174
|
+
if (!/^\d+$/.test(issueNum)) throw new AFSNotFoundError(path, "Invalid issue number");
|
|
1009
1175
|
}
|
|
1010
1176
|
if (pullsIdx !== -1 && segments[pullsIdx + 1]) {
|
|
1011
1177
|
const prNum = segments[pullsIdx + 1];
|
|
1012
|
-
if (!/^\d+$/.test(prNum))
|
|
1013
|
-
data: void 0,
|
|
1014
|
-
message: "Invalid pull request number"
|
|
1015
|
-
};
|
|
1178
|
+
if (!/^\d+$/.test(prNum)) throw new AFSNotFoundError(path, "Invalid pull request number");
|
|
1016
1179
|
}
|
|
1017
1180
|
}
|
|
1018
1181
|
const fullPath = this.resolvePath(path);
|
|
1019
|
-
if (!this.compiled)
|
|
1020
|
-
data: void 0,
|
|
1021
|
-
message: "Mapping not loaded"
|
|
1022
|
-
};
|
|
1182
|
+
if (!this.compiled) throw new AFSNotFoundError(path, "Mapping not loaded");
|
|
1023
1183
|
const resolved = this.compiled.resolve(fullPath);
|
|
1024
|
-
if (!resolved || !resolved.operations.read)
|
|
1025
|
-
data: void 0,
|
|
1026
|
-
message: `No read operation for path: ${fullPath}`
|
|
1027
|
-
};
|
|
1184
|
+
if (!resolved || !resolved.operations.read) throw new AFSNotFoundError(path, `No read operation for path: ${fullPath}`);
|
|
1028
1185
|
const request = this.compiled.buildRequest(fullPath, "read", {});
|
|
1029
|
-
if (!request)
|
|
1030
|
-
data: void 0,
|
|
1031
|
-
message: "Failed to build request"
|
|
1032
|
-
};
|
|
1186
|
+
if (!request) throw new AFSNotFoundError(path, "Failed to build request");
|
|
1033
1187
|
try {
|
|
1034
1188
|
const response = await this.client.request(`${request.method} ${request.path}`, request.params);
|
|
1035
1189
|
const entries = this.compiled.projectResponse(fullPath, "read", response.data);
|
|
1036
|
-
if (entries.length === 0)
|
|
1037
|
-
data: void 0,
|
|
1038
|
-
message: "No data returned"
|
|
1039
|
-
};
|
|
1190
|
+
if (entries.length === 0) throw new AFSNotFoundError(path, "No data returned");
|
|
1040
1191
|
const entry = entries[0];
|
|
1041
1192
|
if (this.options.mode === "single-repo") {
|
|
1042
1193
|
const prefix = `/${this.options.owner}/${this.options.repo}`;
|
|
@@ -1047,12 +1198,41 @@ var AFSGitHub = class {
|
|
|
1047
1198
|
}
|
|
1048
1199
|
return { data: entry };
|
|
1049
1200
|
} catch (error) {
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
};
|
|
1201
|
+
if (error instanceof AFSNotFoundError) throw error;
|
|
1202
|
+
if (error?.status === 404) throw new AFSNotFoundError(path);
|
|
1203
|
+
throw error;
|
|
1054
1204
|
}
|
|
1055
1205
|
}
|
|
1206
|
+
/**
|
|
1207
|
+
* Read metadata for a path
|
|
1208
|
+
* Returns an entry with meta field containing the metadata
|
|
1209
|
+
*
|
|
1210
|
+
* @throws AFSNotFoundError if path does not exist
|
|
1211
|
+
*/
|
|
1212
|
+
async readMeta(targetPath) {
|
|
1213
|
+
const metaPath = targetPath === "/" ? "/.meta" : `${targetPath}/.meta`;
|
|
1214
|
+
const virtualEntry = this.getVirtualDirectoryEntry(targetPath);
|
|
1215
|
+
if (virtualEntry) return { data: {
|
|
1216
|
+
id: `${virtualEntry.id}/.meta`,
|
|
1217
|
+
path: metaPath,
|
|
1218
|
+
summary: `Metadata for ${targetPath}`,
|
|
1219
|
+
meta: virtualEntry.meta ?? {}
|
|
1220
|
+
} };
|
|
1221
|
+
const enrichedMeta = await this.getEnrichedMeta(targetPath);
|
|
1222
|
+
if (enrichedMeta) return { data: {
|
|
1223
|
+
id: `${targetPath}/.meta`,
|
|
1224
|
+
path: metaPath,
|
|
1225
|
+
summary: `Metadata for ${targetPath}`,
|
|
1226
|
+
meta: enrichedMeta
|
|
1227
|
+
} };
|
|
1228
|
+
const readResult = await this.read(targetPath);
|
|
1229
|
+
return { data: {
|
|
1230
|
+
id: `${readResult.data.id}/.meta`,
|
|
1231
|
+
path: metaPath,
|
|
1232
|
+
summary: `Metadata for ${targetPath}`,
|
|
1233
|
+
meta: readResult.data.meta ?? {}
|
|
1234
|
+
} };
|
|
1235
|
+
}
|
|
1056
1236
|
async loadMapping(mappingPath) {
|
|
1057
1237
|
const compiler = new MappingCompiler();
|
|
1058
1238
|
try {
|
|
@@ -1118,6 +1298,1044 @@ var AFSGitHub = class {
|
|
|
1118
1298
|
error: "Write operations not yet implemented"
|
|
1119
1299
|
};
|
|
1120
1300
|
}
|
|
1301
|
+
/**
|
|
1302
|
+
* Get metadata for a path without content.
|
|
1303
|
+
* Supports issues, PRs, and virtual directories.
|
|
1304
|
+
*/
|
|
1305
|
+
async stat(path, _options) {
|
|
1306
|
+
const parsed = this.parseIssuePRPath(path);
|
|
1307
|
+
if (parsed?.type === "issue") return this.statIssue(parsed.number);
|
|
1308
|
+
if (parsed?.type === "pull") return this.statPR(parsed.number);
|
|
1309
|
+
const virtualEntry = this.getVirtualDirectoryEntry(path);
|
|
1310
|
+
if (virtualEntry) return { data: virtualEntry };
|
|
1311
|
+
throw new AFSNotFoundError(path);
|
|
1312
|
+
}
|
|
1313
|
+
async statIssue(issueNumber) {
|
|
1314
|
+
const owner = this.options.owner;
|
|
1315
|
+
const repo = this.options.repo;
|
|
1316
|
+
try {
|
|
1317
|
+
const issue = (await this.client.request("GET /repos/{owner}/{repo}/issues/{issue_number}", {
|
|
1318
|
+
owner,
|
|
1319
|
+
repo,
|
|
1320
|
+
issue_number: issueNumber
|
|
1321
|
+
})).data;
|
|
1322
|
+
return { data: {
|
|
1323
|
+
id: String(issue.number),
|
|
1324
|
+
path: `/issues/${issue.number}`,
|
|
1325
|
+
summary: issue.title,
|
|
1326
|
+
meta: {
|
|
1327
|
+
type: "issue",
|
|
1328
|
+
state: issue.state,
|
|
1329
|
+
title: issue.title,
|
|
1330
|
+
author: issue.user?.login,
|
|
1331
|
+
commentCount: issue.comments,
|
|
1332
|
+
labels: (issue.labels ?? []).map((l) => l.name),
|
|
1333
|
+
created_at: issue.created_at,
|
|
1334
|
+
updated_at: issue.updated_at
|
|
1335
|
+
}
|
|
1336
|
+
} };
|
|
1337
|
+
} catch (error) {
|
|
1338
|
+
if (error?.status === 404) throw new AFSNotFoundError(`/issues/${issueNumber}`);
|
|
1339
|
+
throw error;
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
async statPR(prNumber) {
|
|
1343
|
+
const owner = this.options.owner;
|
|
1344
|
+
const repo = this.options.repo;
|
|
1345
|
+
try {
|
|
1346
|
+
const pr = (await this.client.request("GET /repos/{owner}/{repo}/pulls/{pull_number}", {
|
|
1347
|
+
owner,
|
|
1348
|
+
repo,
|
|
1349
|
+
pull_number: prNumber
|
|
1350
|
+
})).data;
|
|
1351
|
+
return { data: {
|
|
1352
|
+
id: String(pr.number),
|
|
1353
|
+
path: `/pulls/${pr.number}`,
|
|
1354
|
+
summary: pr.title,
|
|
1355
|
+
meta: {
|
|
1356
|
+
type: "pull_request",
|
|
1357
|
+
state: pr.state,
|
|
1358
|
+
title: pr.title,
|
|
1359
|
+
author: pr.user?.login,
|
|
1360
|
+
draft: pr.draft,
|
|
1361
|
+
reviewers: (pr.requested_reviewers ?? []).map((r) => r.login),
|
|
1362
|
+
labels: (pr.labels ?? []).map((l) => l.name),
|
|
1363
|
+
created_at: pr.created_at,
|
|
1364
|
+
updated_at: pr.updated_at,
|
|
1365
|
+
merged_at: pr.merged_at
|
|
1366
|
+
}
|
|
1367
|
+
} };
|
|
1368
|
+
} catch (error) {
|
|
1369
|
+
if (error?.status === 404) throw new AFSNotFoundError(`/pulls/${prNumber}`);
|
|
1370
|
+
throw error;
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1374
|
+
* Search issues and PRs using the GitHub Search API.
|
|
1375
|
+
* Path determines scope: /issues searches issues, /pulls searches PRs, / searches both.
|
|
1376
|
+
*/
|
|
1377
|
+
async search(path, query, options) {
|
|
1378
|
+
const owner = this.options.owner;
|
|
1379
|
+
const repo = this.options.repo;
|
|
1380
|
+
const sanitizedQuery = query.replace(/[\r\n]+/g, " ").replace(/ghp_[a-zA-Z0-9]+/g, "");
|
|
1381
|
+
const normalized = path.replace(/^\/+|\/+$/g, "");
|
|
1382
|
+
const isPulls = normalized === "pulls" || normalized.endsWith("/pulls");
|
|
1383
|
+
const isIssues = normalized === "issues" || normalized.endsWith("/issues");
|
|
1384
|
+
let q = sanitizedQuery.trim();
|
|
1385
|
+
q += ` repo:${owner}/${repo}`;
|
|
1386
|
+
if (isPulls) q += " is:pr";
|
|
1387
|
+
else if (isIssues) q += " is:issue";
|
|
1388
|
+
try {
|
|
1389
|
+
return { data: (await this.client.request("GET /search/issues", {
|
|
1390
|
+
q: q.trim(),
|
|
1391
|
+
per_page: options?.limit ?? 30
|
|
1392
|
+
})).data.items.map((item) => {
|
|
1393
|
+
const hasPR = "pull_request" in item;
|
|
1394
|
+
const type = hasPR ? "pulls" : "issues";
|
|
1395
|
+
return {
|
|
1396
|
+
id: String(item.number),
|
|
1397
|
+
path: `/${type}/${item.number}`,
|
|
1398
|
+
summary: item.title,
|
|
1399
|
+
meta: {
|
|
1400
|
+
type: hasPR ? "pull_request" : "issue",
|
|
1401
|
+
state: item.state,
|
|
1402
|
+
author: item.user?.login
|
|
1403
|
+
}
|
|
1404
|
+
};
|
|
1405
|
+
}) };
|
|
1406
|
+
} catch (error) {
|
|
1407
|
+
if (error?.status === 422) return {
|
|
1408
|
+
data: [],
|
|
1409
|
+
message: "Invalid search query"
|
|
1410
|
+
};
|
|
1411
|
+
throw error;
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
/**
|
|
1415
|
+
* Provide human/LLM-readable explanation of a path.
|
|
1416
|
+
*/
|
|
1417
|
+
async explain(path, _options) {
|
|
1418
|
+
const normalized = path.replace(/^\/+|\/+$/g, "");
|
|
1419
|
+
if (normalized === "") return this.explainRoot();
|
|
1420
|
+
if (normalized === "issues") return this.explainIssuesDir();
|
|
1421
|
+
if (normalized === "pulls") return this.explainPullsDir();
|
|
1422
|
+
const parsed = this.parseIssuePRPath(path);
|
|
1423
|
+
if (parsed?.type === "issue") return this.explainIssue(parsed.number);
|
|
1424
|
+
if (parsed?.type === "pull") return this.explainPR(parsed.number);
|
|
1425
|
+
throw new AFSNotFoundError(path);
|
|
1426
|
+
}
|
|
1427
|
+
async explainRoot() {
|
|
1428
|
+
const owner = this.options.owner;
|
|
1429
|
+
const repo = this.options.repo;
|
|
1430
|
+
const data = (await this.client.request("GET /repos/{owner}/{repo}", {
|
|
1431
|
+
owner,
|
|
1432
|
+
repo
|
|
1433
|
+
})).data;
|
|
1434
|
+
const lines = [
|
|
1435
|
+
`# ${data.full_name}`,
|
|
1436
|
+
"",
|
|
1437
|
+
data.description ? `${data.description}` : "",
|
|
1438
|
+
"",
|
|
1439
|
+
`- **Language**: ${data.language ?? "N/A"}`,
|
|
1440
|
+
`- **Stars**: ${data.stargazers_count ?? 0} ⭐`,
|
|
1441
|
+
`- **Forks**: ${data.forks_count ?? 0}`,
|
|
1442
|
+
`- **Open Issues**: ${data.open_issues_count ?? 0}`,
|
|
1443
|
+
`- **Default Branch**: ${data.default_branch ?? "main"}`,
|
|
1444
|
+
`- **License**: ${data.license?.spdx_id ?? "N/A"}`,
|
|
1445
|
+
`- **Visibility**: ${data.private ? "Private" : "Public"}`,
|
|
1446
|
+
"",
|
|
1447
|
+
"## Structure",
|
|
1448
|
+
"",
|
|
1449
|
+
"- `/issues` — Repository issues",
|
|
1450
|
+
"- `/pulls` — Pull requests",
|
|
1451
|
+
"- `/repo` — Repository source code"
|
|
1452
|
+
];
|
|
1453
|
+
const topics = data.topics;
|
|
1454
|
+
if (topics && topics.length > 0) lines.push("", `**Topics**: ${topics.join(", ")}`);
|
|
1455
|
+
return {
|
|
1456
|
+
format: "markdown",
|
|
1457
|
+
content: lines.filter((l) => l !== void 0).join("\n")
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
async explainIssuesDir() {
|
|
1461
|
+
const owner = this.options.owner;
|
|
1462
|
+
const repo = this.options.repo;
|
|
1463
|
+
const [openResult, closedResult] = await Promise.all([this.client.request("GET /search/issues", {
|
|
1464
|
+
q: `repo:${owner}/${repo} is:issue is:open`,
|
|
1465
|
+
per_page: 1
|
|
1466
|
+
}), this.client.request("GET /search/issues", {
|
|
1467
|
+
q: `repo:${owner}/${repo} is:issue is:closed`,
|
|
1468
|
+
per_page: 1
|
|
1469
|
+
})]);
|
|
1470
|
+
const openCount = openResult.data.total_count;
|
|
1471
|
+
const closedCount = closedResult.data.total_count;
|
|
1472
|
+
return {
|
|
1473
|
+
format: "markdown",
|
|
1474
|
+
content: [
|
|
1475
|
+
`# Issues — ${owner}/${repo}`,
|
|
1476
|
+
"",
|
|
1477
|
+
`- **Open**: ${openCount}`,
|
|
1478
|
+
`- **Closed**: ${closedCount}`,
|
|
1479
|
+
`- **Total**: ${openCount + closedCount}`,
|
|
1480
|
+
"",
|
|
1481
|
+
"Use `list /issues` to browse issues.",
|
|
1482
|
+
"Use `search /issues <query>` to find specific issues."
|
|
1483
|
+
].join("\n")
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
async explainPullsDir() {
|
|
1487
|
+
const owner = this.options.owner;
|
|
1488
|
+
const repo = this.options.repo;
|
|
1489
|
+
const [openResult, closedResult, mergedResult] = await Promise.all([
|
|
1490
|
+
this.client.request("GET /search/issues", {
|
|
1491
|
+
q: `repo:${owner}/${repo} is:pr is:open`,
|
|
1492
|
+
per_page: 1
|
|
1493
|
+
}),
|
|
1494
|
+
this.client.request("GET /search/issues", {
|
|
1495
|
+
q: `repo:${owner}/${repo} is:pr is:closed`,
|
|
1496
|
+
per_page: 1
|
|
1497
|
+
}),
|
|
1498
|
+
this.client.request("GET /search/issues", {
|
|
1499
|
+
q: `repo:${owner}/${repo} is:pr is:merged`,
|
|
1500
|
+
per_page: 1
|
|
1501
|
+
})
|
|
1502
|
+
]);
|
|
1503
|
+
const openCount = openResult.data.total_count;
|
|
1504
|
+
const closedCount = closedResult.data.total_count;
|
|
1505
|
+
const mergedCount = mergedResult.data.total_count;
|
|
1506
|
+
return {
|
|
1507
|
+
format: "markdown",
|
|
1508
|
+
content: [
|
|
1509
|
+
`# Pull Requests — ${owner}/${repo}`,
|
|
1510
|
+
"",
|
|
1511
|
+
`- **Open**: ${openCount}`,
|
|
1512
|
+
`- **Closed**: ${closedCount}`,
|
|
1513
|
+
`- **Merged**: ${mergedCount}`,
|
|
1514
|
+
"",
|
|
1515
|
+
"Use `list /pulls` to browse pull requests.",
|
|
1516
|
+
"Use `search /pulls <query>` to find specific PRs."
|
|
1517
|
+
].join("\n")
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
async explainIssue(issueNumber) {
|
|
1521
|
+
const owner = this.options.owner;
|
|
1522
|
+
const repo = this.options.repo;
|
|
1523
|
+
try {
|
|
1524
|
+
const issue = (await this.client.request("GET /repos/{owner}/{repo}/issues/{issue_number}", {
|
|
1525
|
+
owner,
|
|
1526
|
+
repo,
|
|
1527
|
+
issue_number: issueNumber
|
|
1528
|
+
})).data;
|
|
1529
|
+
const labels = (issue.labels ?? []).map((l) => l.name);
|
|
1530
|
+
const assignees = (issue.assignees ?? []).map((a) => a.login);
|
|
1531
|
+
return {
|
|
1532
|
+
format: "markdown",
|
|
1533
|
+
content: [
|
|
1534
|
+
`# Issue #${issue.number}: ${issue.title}`,
|
|
1535
|
+
"",
|
|
1536
|
+
`- **State**: ${issue.state}`,
|
|
1537
|
+
`- **Author**: ${issue.user?.login}`,
|
|
1538
|
+
`- **Created**: ${issue.created_at}`,
|
|
1539
|
+
`- **Updated**: ${issue.updated_at}`,
|
|
1540
|
+
labels.length > 0 ? `- **Labels**: ${labels.join(", ")}` : null,
|
|
1541
|
+
assignees.length > 0 ? `- **Assignees**: ${assignees.join(", ")}` : null,
|
|
1542
|
+
`- **Comments**: ${issue.comments}`
|
|
1543
|
+
].filter((l) => l !== null).join("\n")
|
|
1544
|
+
};
|
|
1545
|
+
} catch (error) {
|
|
1546
|
+
if (error?.status === 404) throw new AFSNotFoundError(`/issues/${issueNumber}`);
|
|
1547
|
+
throw error;
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
async explainPR(prNumber) {
|
|
1551
|
+
const owner = this.options.owner;
|
|
1552
|
+
const repo = this.options.repo;
|
|
1553
|
+
try {
|
|
1554
|
+
const pr = (await this.client.request("GET /repos/{owner}/{repo}/pulls/{pull_number}", {
|
|
1555
|
+
owner,
|
|
1556
|
+
repo,
|
|
1557
|
+
pull_number: prNumber
|
|
1558
|
+
})).data;
|
|
1559
|
+
const labels = (pr.labels ?? []).map((l) => l.name);
|
|
1560
|
+
const reviewers = (pr.requested_reviewers ?? []).map((r) => r.login);
|
|
1561
|
+
return {
|
|
1562
|
+
format: "markdown",
|
|
1563
|
+
content: [
|
|
1564
|
+
`# PR #${pr.number}: ${pr.title}`,
|
|
1565
|
+
"",
|
|
1566
|
+
`- **State**: ${pr.state}${pr.draft ? " (draft)" : ""}`,
|
|
1567
|
+
`- **Author**: ${pr.user?.login}`,
|
|
1568
|
+
`- **Created**: ${pr.created_at}`,
|
|
1569
|
+
`- **Updated**: ${pr.updated_at}`,
|
|
1570
|
+
pr.merged_at ? `- **Merged**: ${pr.merged_at}` : null,
|
|
1571
|
+
labels.length > 0 ? `- **Labels**: ${labels.join(", ")}` : null,
|
|
1572
|
+
reviewers.length > 0 ? `- **Reviewers**: ${reviewers.join(", ")}` : null,
|
|
1573
|
+
`- **Head**: ${pr.head?.ref} → **Base**: ${pr.base?.ref}`
|
|
1574
|
+
].filter((l) => l !== null).join("\n")
|
|
1575
|
+
};
|
|
1576
|
+
} catch (error) {
|
|
1577
|
+
if (error?.status === 404) throw new AFSNotFoundError(`/pulls/${prNumber}`);
|
|
1578
|
+
throw error;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
async readCapabilities() {
|
|
1582
|
+
return { data: {
|
|
1583
|
+
id: "/.meta/.capabilities",
|
|
1584
|
+
path: "/.meta/.capabilities",
|
|
1585
|
+
summary: "Provider capabilities manifest",
|
|
1586
|
+
content: {
|
|
1587
|
+
schemaVersion: 1,
|
|
1588
|
+
provider: this.name,
|
|
1589
|
+
description: this.description ?? `GitHub: ${this.options.owner}/${this.options.repo}`,
|
|
1590
|
+
tools: [],
|
|
1591
|
+
operations: {
|
|
1592
|
+
read: true,
|
|
1593
|
+
list: true,
|
|
1594
|
+
write: false,
|
|
1595
|
+
delete: false,
|
|
1596
|
+
search: true,
|
|
1597
|
+
exec: true,
|
|
1598
|
+
stat: true,
|
|
1599
|
+
explain: true
|
|
1600
|
+
},
|
|
1601
|
+
actions: [
|
|
1602
|
+
{
|
|
1603
|
+
kind: "github:issue",
|
|
1604
|
+
description: "Issue lifecycle actions",
|
|
1605
|
+
catalog: [
|
|
1606
|
+
{
|
|
1607
|
+
name: "close-issue",
|
|
1608
|
+
description: "Close an issue"
|
|
1609
|
+
},
|
|
1610
|
+
{
|
|
1611
|
+
name: "reopen-issue",
|
|
1612
|
+
description: "Reopen a closed issue"
|
|
1613
|
+
},
|
|
1614
|
+
{
|
|
1615
|
+
name: "add-comment",
|
|
1616
|
+
description: "Add a comment to an issue"
|
|
1617
|
+
},
|
|
1618
|
+
{
|
|
1619
|
+
name: "add-label",
|
|
1620
|
+
description: "Add a label to an issue"
|
|
1621
|
+
},
|
|
1622
|
+
{
|
|
1623
|
+
name: "remove-label",
|
|
1624
|
+
description: "Remove a label from an issue"
|
|
1625
|
+
}
|
|
1626
|
+
],
|
|
1627
|
+
discovery: { pathTemplate: "/issues/:number/.actions" }
|
|
1628
|
+
},
|
|
1629
|
+
{
|
|
1630
|
+
kind: "github:pull_request",
|
|
1631
|
+
description: "Pull request lifecycle actions",
|
|
1632
|
+
catalog: [
|
|
1633
|
+
{
|
|
1634
|
+
name: "merge-pr",
|
|
1635
|
+
description: "Merge a pull request"
|
|
1636
|
+
},
|
|
1637
|
+
{
|
|
1638
|
+
name: "approve-pr",
|
|
1639
|
+
description: "Approve a pull request"
|
|
1640
|
+
},
|
|
1641
|
+
{
|
|
1642
|
+
name: "request-changes",
|
|
1643
|
+
description: "Request changes on a pull request"
|
|
1644
|
+
},
|
|
1645
|
+
{
|
|
1646
|
+
name: "add-comment",
|
|
1647
|
+
description: "Add a comment to a pull request"
|
|
1648
|
+
},
|
|
1649
|
+
{
|
|
1650
|
+
name: "add-label",
|
|
1651
|
+
description: "Add a label to a pull request"
|
|
1652
|
+
},
|
|
1653
|
+
{
|
|
1654
|
+
name: "remove-label",
|
|
1655
|
+
description: "Remove a label from a pull request"
|
|
1656
|
+
}
|
|
1657
|
+
],
|
|
1658
|
+
discovery: { pathTemplate: "/pulls/:number/.actions" }
|
|
1659
|
+
},
|
|
1660
|
+
{
|
|
1661
|
+
kind: "github:repo",
|
|
1662
|
+
description: "Repository file operations",
|
|
1663
|
+
catalog: [
|
|
1664
|
+
{
|
|
1665
|
+
name: "read-file",
|
|
1666
|
+
description: "Read a file from the repository"
|
|
1667
|
+
},
|
|
1668
|
+
{
|
|
1669
|
+
name: "write-file",
|
|
1670
|
+
description: "Write a file to the repository"
|
|
1671
|
+
},
|
|
1672
|
+
{
|
|
1673
|
+
name: "create-branch",
|
|
1674
|
+
description: "Create a new branch"
|
|
1675
|
+
}
|
|
1676
|
+
],
|
|
1677
|
+
discovery: { pathTemplate: "/repo/:branch/.actions" }
|
|
1678
|
+
}
|
|
1679
|
+
]
|
|
1680
|
+
}
|
|
1681
|
+
} };
|
|
1682
|
+
}
|
|
1683
|
+
/**
|
|
1684
|
+
* Get enriched metadata for an issue or PR path.
|
|
1685
|
+
* Returns labels, assignees, milestone, reactions from GitHub API.
|
|
1686
|
+
*/
|
|
1687
|
+
async getEnrichedMeta(path) {
|
|
1688
|
+
const parsed = this.parseIssuePRPath(path);
|
|
1689
|
+
if (!parsed) return null;
|
|
1690
|
+
const owner = this.options.owner;
|
|
1691
|
+
const repo = this.options.repo;
|
|
1692
|
+
if (parsed.type === "issue") try {
|
|
1693
|
+
const issue = (await this.client.request("GET /repos/{owner}/{repo}/issues/{issue_number}", {
|
|
1694
|
+
owner,
|
|
1695
|
+
repo,
|
|
1696
|
+
issue_number: parsed.number
|
|
1697
|
+
})).data;
|
|
1698
|
+
return {
|
|
1699
|
+
type: "issue",
|
|
1700
|
+
state: issue.state,
|
|
1701
|
+
author: issue.user?.login,
|
|
1702
|
+
labels: (issue.labels ?? []).map((l) => l.name),
|
|
1703
|
+
assignees: (issue.assignees ?? []).map((a) => a.login),
|
|
1704
|
+
milestone: issue.milestone?.title ?? null,
|
|
1705
|
+
reactions: issue.reactions ? this.extractReactions(issue.reactions) : this.zeroReactions(),
|
|
1706
|
+
commentCount: issue.comments,
|
|
1707
|
+
created_at: issue.created_at,
|
|
1708
|
+
updated_at: issue.updated_at
|
|
1709
|
+
};
|
|
1710
|
+
} catch (error) {
|
|
1711
|
+
if (error?.status === 404) throw new AFSNotFoundError(path);
|
|
1712
|
+
throw error;
|
|
1713
|
+
}
|
|
1714
|
+
if (parsed.type === "pull") try {
|
|
1715
|
+
const pr = (await this.client.request("GET /repos/{owner}/{repo}/pulls/{pull_number}", {
|
|
1716
|
+
owner,
|
|
1717
|
+
repo,
|
|
1718
|
+
pull_number: parsed.number
|
|
1719
|
+
})).data;
|
|
1720
|
+
return {
|
|
1721
|
+
type: "pull_request",
|
|
1722
|
+
state: pr.state,
|
|
1723
|
+
author: pr.user?.login,
|
|
1724
|
+
draft: pr.draft,
|
|
1725
|
+
labels: (pr.labels ?? []).map((l) => l.name),
|
|
1726
|
+
assignees: (pr.assignees ?? []).map((a) => a.login),
|
|
1727
|
+
milestone: pr.milestone?.title ?? null,
|
|
1728
|
+
reactions: pr.reactions ? this.extractReactions(pr.reactions) : this.zeroReactions(),
|
|
1729
|
+
reviewers: (pr.requested_reviewers ?? []).map((r) => r.login),
|
|
1730
|
+
commentCount: pr.comments,
|
|
1731
|
+
merged_at: pr.merged_at,
|
|
1732
|
+
created_at: pr.created_at,
|
|
1733
|
+
updated_at: pr.updated_at
|
|
1734
|
+
};
|
|
1735
|
+
} catch (error) {
|
|
1736
|
+
if (error?.status === 404) throw new AFSNotFoundError(path);
|
|
1737
|
+
throw error;
|
|
1738
|
+
}
|
|
1739
|
+
return null;
|
|
1740
|
+
}
|
|
1741
|
+
extractReactions(reactions) {
|
|
1742
|
+
return {
|
|
1743
|
+
"+1": reactions["+1"] ?? 0,
|
|
1744
|
+
"-1": reactions["-1"] ?? 0,
|
|
1745
|
+
laugh: reactions.laugh ?? 0,
|
|
1746
|
+
hooray: reactions.hooray ?? 0,
|
|
1747
|
+
confused: reactions.confused ?? 0,
|
|
1748
|
+
heart: reactions.heart ?? 0,
|
|
1749
|
+
rocket: reactions.rocket ?? 0,
|
|
1750
|
+
eyes: reactions.eyes ?? 0
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
zeroReactions() {
|
|
1754
|
+
return {
|
|
1755
|
+
"+1": 0,
|
|
1756
|
+
"-1": 0,
|
|
1757
|
+
laugh: 0,
|
|
1758
|
+
hooray: 0,
|
|
1759
|
+
confused: 0,
|
|
1760
|
+
heart: 0,
|
|
1761
|
+
rocket: 0,
|
|
1762
|
+
eyes: 0
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
/**
|
|
1766
|
+
* Parse a path to determine if it's an issue or PR path.
|
|
1767
|
+
* Returns { type, number } or null.
|
|
1768
|
+
*/
|
|
1769
|
+
parseIssuePRPath(path) {
|
|
1770
|
+
const segments = path.replace(/^\/+|\/+$/g, "").split("/").filter(Boolean);
|
|
1771
|
+
if (this.options.mode === "single-repo") {
|
|
1772
|
+
if (segments.length === 2 && segments[0] === "issues" && /^\d+$/.test(segments[1])) return {
|
|
1773
|
+
type: "issue",
|
|
1774
|
+
number: Number.parseInt(segments[1], 10)
|
|
1775
|
+
};
|
|
1776
|
+
if (segments.length === 2 && segments[0] === "pulls" && /^\d+$/.test(segments[1])) return {
|
|
1777
|
+
type: "pull",
|
|
1778
|
+
number: Number.parseInt(segments[1], 10)
|
|
1779
|
+
};
|
|
1780
|
+
} else if (this.options.mode === "org") {
|
|
1781
|
+
if (segments.length === 3 && segments[1] === "issues" && /^\d+$/.test(segments[2])) return {
|
|
1782
|
+
type: "issue",
|
|
1783
|
+
number: Number.parseInt(segments[2], 10)
|
|
1784
|
+
};
|
|
1785
|
+
if (segments.length === 3 && segments[1] === "pulls" && /^\d+$/.test(segments[2])) return {
|
|
1786
|
+
type: "pull",
|
|
1787
|
+
number: Number.parseInt(segments[2], 10)
|
|
1788
|
+
};
|
|
1789
|
+
}
|
|
1790
|
+
return null;
|
|
1791
|
+
}
|
|
1792
|
+
/**
|
|
1793
|
+
* List available actions at a path.
|
|
1794
|
+
* Returns null if the path is not an .actions path.
|
|
1795
|
+
*/
|
|
1796
|
+
listActions(path) {
|
|
1797
|
+
const normalized = path.replace(/^\/+|\/+$/g, "");
|
|
1798
|
+
if (normalized === ".actions") return { data: [
|
|
1799
|
+
{
|
|
1800
|
+
id: "create-issue",
|
|
1801
|
+
path: "/.actions/create-issue",
|
|
1802
|
+
summary: "Create a new issue",
|
|
1803
|
+
meta: {
|
|
1804
|
+
kind: "afs:executable",
|
|
1805
|
+
name: "create-issue"
|
|
1806
|
+
}
|
|
1807
|
+
},
|
|
1808
|
+
{
|
|
1809
|
+
id: "create-pr",
|
|
1810
|
+
path: "/.actions/create-pr",
|
|
1811
|
+
summary: "Create a new pull request",
|
|
1812
|
+
meta: {
|
|
1813
|
+
kind: "afs:executable",
|
|
1814
|
+
name: "create-pr"
|
|
1815
|
+
}
|
|
1816
|
+
},
|
|
1817
|
+
{
|
|
1818
|
+
id: "create-file",
|
|
1819
|
+
path: "/.actions/create-file",
|
|
1820
|
+
summary: "Create a file in the repository",
|
|
1821
|
+
meta: {
|
|
1822
|
+
kind: "afs:executable",
|
|
1823
|
+
name: "create-file"
|
|
1824
|
+
}
|
|
1825
|
+
},
|
|
1826
|
+
{
|
|
1827
|
+
id: "update-file",
|
|
1828
|
+
path: "/.actions/update-file",
|
|
1829
|
+
summary: "Update a file in the repository",
|
|
1830
|
+
meta: {
|
|
1831
|
+
kind: "afs:executable",
|
|
1832
|
+
name: "update-file"
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
] };
|
|
1836
|
+
const issueActionsMatch = normalized.match(/^issues\/(\d+)\/\.actions$/);
|
|
1837
|
+
if (issueActionsMatch) {
|
|
1838
|
+
const num = issueActionsMatch[1];
|
|
1839
|
+
return { data: [
|
|
1840
|
+
{
|
|
1841
|
+
id: "close-issue",
|
|
1842
|
+
path: `/issues/${num}/.actions/close-issue`,
|
|
1843
|
+
summary: "Close this issue",
|
|
1844
|
+
meta: {
|
|
1845
|
+
kind: "afs:executable",
|
|
1846
|
+
name: "close-issue"
|
|
1847
|
+
}
|
|
1848
|
+
},
|
|
1849
|
+
{
|
|
1850
|
+
id: "reopen-issue",
|
|
1851
|
+
path: `/issues/${num}/.actions/reopen-issue`,
|
|
1852
|
+
summary: "Reopen this issue",
|
|
1853
|
+
meta: {
|
|
1854
|
+
kind: "afs:executable",
|
|
1855
|
+
name: "reopen-issue"
|
|
1856
|
+
}
|
|
1857
|
+
},
|
|
1858
|
+
{
|
|
1859
|
+
id: "add-comment",
|
|
1860
|
+
path: `/issues/${num}/.actions/add-comment`,
|
|
1861
|
+
summary: "Add a comment",
|
|
1862
|
+
meta: {
|
|
1863
|
+
kind: "afs:executable",
|
|
1864
|
+
name: "add-comment"
|
|
1865
|
+
}
|
|
1866
|
+
},
|
|
1867
|
+
{
|
|
1868
|
+
id: "add-label",
|
|
1869
|
+
path: `/issues/${num}/.actions/add-label`,
|
|
1870
|
+
summary: "Add a label",
|
|
1871
|
+
meta: {
|
|
1872
|
+
kind: "afs:executable",
|
|
1873
|
+
name: "add-label"
|
|
1874
|
+
}
|
|
1875
|
+
},
|
|
1876
|
+
{
|
|
1877
|
+
id: "remove-label",
|
|
1878
|
+
path: `/issues/${num}/.actions/remove-label`,
|
|
1879
|
+
summary: "Remove a label",
|
|
1880
|
+
meta: {
|
|
1881
|
+
kind: "afs:executable",
|
|
1882
|
+
name: "remove-label"
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
] };
|
|
1886
|
+
}
|
|
1887
|
+
const prActionsMatch = normalized.match(/^pulls\/(\d+)\/\.actions$/);
|
|
1888
|
+
if (prActionsMatch) {
|
|
1889
|
+
const num = prActionsMatch[1];
|
|
1890
|
+
return { data: [
|
|
1891
|
+
{
|
|
1892
|
+
id: "merge-pr",
|
|
1893
|
+
path: `/pulls/${num}/.actions/merge-pr`,
|
|
1894
|
+
summary: "Merge this pull request",
|
|
1895
|
+
meta: {
|
|
1896
|
+
kind: "afs:executable",
|
|
1897
|
+
name: "merge-pr"
|
|
1898
|
+
}
|
|
1899
|
+
},
|
|
1900
|
+
{
|
|
1901
|
+
id: "request-review",
|
|
1902
|
+
path: `/pulls/${num}/.actions/request-review`,
|
|
1903
|
+
summary: "Request a review",
|
|
1904
|
+
meta: {
|
|
1905
|
+
kind: "afs:executable",
|
|
1906
|
+
name: "request-review"
|
|
1907
|
+
}
|
|
1908
|
+
},
|
|
1909
|
+
{
|
|
1910
|
+
id: "add-comment",
|
|
1911
|
+
path: `/pulls/${num}/.actions/add-comment`,
|
|
1912
|
+
summary: "Add a comment",
|
|
1913
|
+
meta: {
|
|
1914
|
+
kind: "afs:executable",
|
|
1915
|
+
name: "add-comment"
|
|
1916
|
+
}
|
|
1917
|
+
},
|
|
1918
|
+
{
|
|
1919
|
+
id: "add-label",
|
|
1920
|
+
path: `/pulls/${num}/.actions/add-label`,
|
|
1921
|
+
summary: "Add a label",
|
|
1922
|
+
meta: {
|
|
1923
|
+
kind: "afs:executable",
|
|
1924
|
+
name: "add-label"
|
|
1925
|
+
}
|
|
1926
|
+
},
|
|
1927
|
+
{
|
|
1928
|
+
id: "remove-label",
|
|
1929
|
+
path: `/pulls/${num}/.actions/remove-label`,
|
|
1930
|
+
summary: "Remove a label",
|
|
1931
|
+
meta: {
|
|
1932
|
+
kind: "afs:executable",
|
|
1933
|
+
name: "remove-label"
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
] };
|
|
1937
|
+
}
|
|
1938
|
+
return null;
|
|
1939
|
+
}
|
|
1940
|
+
/**
|
|
1941
|
+
* Execute an action at a path.
|
|
1942
|
+
* Path format: /{resource}/.actions/{action-name}
|
|
1943
|
+
*/
|
|
1944
|
+
async exec(path, args, _options) {
|
|
1945
|
+
if (this.accessMode === "readonly") throw new AFSReadonlyError("Cannot exec on a readonly provider");
|
|
1946
|
+
const actionMatch = path.replace(/^\/+|\/+$/g, "").match(/^(?:(.+)\/)?\.actions\/([^/]+)$/);
|
|
1947
|
+
if (!actionMatch) return {
|
|
1948
|
+
success: false,
|
|
1949
|
+
error: {
|
|
1950
|
+
code: "INVALID_PATH",
|
|
1951
|
+
message: `Invalid action path: ${path}`
|
|
1952
|
+
}
|
|
1953
|
+
};
|
|
1954
|
+
const resourcePath = actionMatch[1] || "";
|
|
1955
|
+
const actionName = actionMatch[2];
|
|
1956
|
+
try {
|
|
1957
|
+
if (resourcePath === "") return await this.execRootAction(actionName, args);
|
|
1958
|
+
const issueMatch = resourcePath.match(/^issues\/(\d+)$/);
|
|
1959
|
+
if (issueMatch) {
|
|
1960
|
+
const issueNumber = Number.parseInt(issueMatch[1], 10);
|
|
1961
|
+
return await this.execIssueAction(issueNumber, actionName, args);
|
|
1962
|
+
}
|
|
1963
|
+
const prMatch = resourcePath.match(/^pulls\/(\d+)$/);
|
|
1964
|
+
if (prMatch) {
|
|
1965
|
+
const prNumber = Number.parseInt(prMatch[1], 10);
|
|
1966
|
+
return await this.execPRAction(prNumber, actionName, args);
|
|
1967
|
+
}
|
|
1968
|
+
return {
|
|
1969
|
+
success: false,
|
|
1970
|
+
error: {
|
|
1971
|
+
code: "NOT_FOUND",
|
|
1972
|
+
message: `No actions for path: /${resourcePath}`
|
|
1973
|
+
}
|
|
1974
|
+
};
|
|
1975
|
+
} catch (error) {
|
|
1976
|
+
return this.handleExecError(error);
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
async execRootAction(actionName, args) {
|
|
1980
|
+
const owner = this.options.owner;
|
|
1981
|
+
const repo = this.options.repo;
|
|
1982
|
+
switch (actionName) {
|
|
1983
|
+
case "create-issue": {
|
|
1984
|
+
const title = args.title;
|
|
1985
|
+
if (!title) return {
|
|
1986
|
+
success: false,
|
|
1987
|
+
error: {
|
|
1988
|
+
code: "VALIDATION_ERROR",
|
|
1989
|
+
message: "title is required"
|
|
1990
|
+
}
|
|
1991
|
+
};
|
|
1992
|
+
const issue = (await this.client.request("POST /repos/{owner}/{repo}/issues", {
|
|
1993
|
+
owner,
|
|
1994
|
+
repo,
|
|
1995
|
+
title,
|
|
1996
|
+
body: args.body ?? void 0,
|
|
1997
|
+
labels: args.labels ?? void 0,
|
|
1998
|
+
assignees: args.assignees ?? void 0
|
|
1999
|
+
})).data;
|
|
2000
|
+
return {
|
|
2001
|
+
success: true,
|
|
2002
|
+
data: {
|
|
2003
|
+
number: issue.number,
|
|
2004
|
+
url: issue.html_url
|
|
2005
|
+
}
|
|
2006
|
+
};
|
|
2007
|
+
}
|
|
2008
|
+
case "create-pr": {
|
|
2009
|
+
const title = args.title;
|
|
2010
|
+
const head = args.head;
|
|
2011
|
+
const base = args.base;
|
|
2012
|
+
if (!title || !head || !base) return {
|
|
2013
|
+
success: false,
|
|
2014
|
+
error: {
|
|
2015
|
+
code: "VALIDATION_ERROR",
|
|
2016
|
+
message: "title, head, and base are required"
|
|
2017
|
+
}
|
|
2018
|
+
};
|
|
2019
|
+
if (head === base) return {
|
|
2020
|
+
success: false,
|
|
2021
|
+
error: {
|
|
2022
|
+
code: "VALIDATION_ERROR",
|
|
2023
|
+
message: "head and base must be different branches"
|
|
2024
|
+
}
|
|
2025
|
+
};
|
|
2026
|
+
const pr = (await this.client.request("POST /repos/{owner}/{repo}/pulls", {
|
|
2027
|
+
owner,
|
|
2028
|
+
repo,
|
|
2029
|
+
title,
|
|
2030
|
+
body: args.body ?? void 0,
|
|
2031
|
+
head,
|
|
2032
|
+
base,
|
|
2033
|
+
draft: args.draft ?? false
|
|
2034
|
+
})).data;
|
|
2035
|
+
return {
|
|
2036
|
+
success: true,
|
|
2037
|
+
data: {
|
|
2038
|
+
number: pr.number,
|
|
2039
|
+
url: pr.html_url
|
|
2040
|
+
}
|
|
2041
|
+
};
|
|
2042
|
+
}
|
|
2043
|
+
case "create-file": {
|
|
2044
|
+
const filePath = args.path;
|
|
2045
|
+
if (!filePath) return {
|
|
2046
|
+
success: false,
|
|
2047
|
+
error: {
|
|
2048
|
+
code: "VALIDATION_ERROR",
|
|
2049
|
+
message: "path is required"
|
|
2050
|
+
}
|
|
2051
|
+
};
|
|
2052
|
+
if (filePath.includes("..") || filePath.startsWith("/")) return {
|
|
2053
|
+
success: false,
|
|
2054
|
+
error: {
|
|
2055
|
+
code: "VALIDATION_ERROR",
|
|
2056
|
+
message: "path must not contain '..' or start with '/'"
|
|
2057
|
+
}
|
|
2058
|
+
};
|
|
2059
|
+
const content = args.content ?? "";
|
|
2060
|
+
const message = args.message ?? `Create ${filePath}`;
|
|
2061
|
+
const result = (await this.client.request("PUT /repos/{owner}/{repo}/contents/{path}", {
|
|
2062
|
+
owner,
|
|
2063
|
+
repo,
|
|
2064
|
+
path: filePath,
|
|
2065
|
+
message,
|
|
2066
|
+
content: Buffer.from(content).toString("base64"),
|
|
2067
|
+
branch: args.branch ?? void 0
|
|
2068
|
+
})).data;
|
|
2069
|
+
const fileInfo = result.content;
|
|
2070
|
+
const commitInfo = result.commit;
|
|
2071
|
+
return {
|
|
2072
|
+
success: true,
|
|
2073
|
+
data: {
|
|
2074
|
+
sha: fileInfo?.sha,
|
|
2075
|
+
commitSha: commitInfo?.sha
|
|
2076
|
+
}
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2079
|
+
case "update-file": {
|
|
2080
|
+
const filePath = args.path;
|
|
2081
|
+
const sha = args.sha;
|
|
2082
|
+
if (!filePath) return {
|
|
2083
|
+
success: false,
|
|
2084
|
+
error: {
|
|
2085
|
+
code: "VALIDATION_ERROR",
|
|
2086
|
+
message: "path is required"
|
|
2087
|
+
}
|
|
2088
|
+
};
|
|
2089
|
+
if (!sha) return {
|
|
2090
|
+
success: false,
|
|
2091
|
+
error: {
|
|
2092
|
+
code: "VALIDATION_ERROR",
|
|
2093
|
+
message: "sha is required (optimistic lock)"
|
|
2094
|
+
}
|
|
2095
|
+
};
|
|
2096
|
+
if (filePath.includes("..") || filePath.startsWith("/")) return {
|
|
2097
|
+
success: false,
|
|
2098
|
+
error: {
|
|
2099
|
+
code: "VALIDATION_ERROR",
|
|
2100
|
+
message: "path must not contain '..' or start with '/'"
|
|
2101
|
+
}
|
|
2102
|
+
};
|
|
2103
|
+
const content = args.content ?? "";
|
|
2104
|
+
const message = args.message ?? `Update ${filePath}`;
|
|
2105
|
+
const result = (await this.client.request("PUT /repos/{owner}/{repo}/contents/{path}", {
|
|
2106
|
+
owner,
|
|
2107
|
+
repo,
|
|
2108
|
+
path: filePath,
|
|
2109
|
+
message,
|
|
2110
|
+
content: Buffer.from(content).toString("base64"),
|
|
2111
|
+
sha,
|
|
2112
|
+
branch: args.branch ?? void 0
|
|
2113
|
+
})).data;
|
|
2114
|
+
const fileInfo = result.content;
|
|
2115
|
+
const commitInfo = result.commit;
|
|
2116
|
+
return {
|
|
2117
|
+
success: true,
|
|
2118
|
+
data: {
|
|
2119
|
+
sha: fileInfo?.sha,
|
|
2120
|
+
commitSha: commitInfo?.sha
|
|
2121
|
+
}
|
|
2122
|
+
};
|
|
2123
|
+
}
|
|
2124
|
+
default: return {
|
|
2125
|
+
success: false,
|
|
2126
|
+
error: {
|
|
2127
|
+
code: "NOT_FOUND",
|
|
2128
|
+
message: `Unknown action: ${actionName}`
|
|
2129
|
+
}
|
|
2130
|
+
};
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
async execIssueAction(issueNumber, actionName, args) {
|
|
2134
|
+
const owner = this.options.owner;
|
|
2135
|
+
const repo = this.options.repo;
|
|
2136
|
+
switch (actionName) {
|
|
2137
|
+
case "close-issue": return {
|
|
2138
|
+
success: true,
|
|
2139
|
+
data: { state: (await this.client.request("PATCH /repos/{owner}/{repo}/issues/{issue_number}", {
|
|
2140
|
+
owner,
|
|
2141
|
+
repo,
|
|
2142
|
+
issue_number: issueNumber,
|
|
2143
|
+
state: "closed",
|
|
2144
|
+
state_reason: args.reason ?? void 0
|
|
2145
|
+
})).data.state }
|
|
2146
|
+
};
|
|
2147
|
+
case "reopen-issue": return {
|
|
2148
|
+
success: true,
|
|
2149
|
+
data: { state: (await this.client.request("PATCH /repos/{owner}/{repo}/issues/{issue_number}", {
|
|
2150
|
+
owner,
|
|
2151
|
+
repo,
|
|
2152
|
+
issue_number: issueNumber,
|
|
2153
|
+
state: "open"
|
|
2154
|
+
})).data.state }
|
|
2155
|
+
};
|
|
2156
|
+
case "add-comment": {
|
|
2157
|
+
const body = args.body;
|
|
2158
|
+
if (!body) return {
|
|
2159
|
+
success: false,
|
|
2160
|
+
error: {
|
|
2161
|
+
code: "VALIDATION_ERROR",
|
|
2162
|
+
message: "body is required"
|
|
2163
|
+
}
|
|
2164
|
+
};
|
|
2165
|
+
return {
|
|
2166
|
+
success: true,
|
|
2167
|
+
data: { id: (await this.client.request("POST /repos/{owner}/{repo}/issues/{issue_number}/comments", {
|
|
2168
|
+
owner,
|
|
2169
|
+
repo,
|
|
2170
|
+
issue_number: issueNumber,
|
|
2171
|
+
body
|
|
2172
|
+
})).data.id }
|
|
2173
|
+
};
|
|
2174
|
+
}
|
|
2175
|
+
case "add-label": {
|
|
2176
|
+
const label = args.label;
|
|
2177
|
+
if (!label) return {
|
|
2178
|
+
success: false,
|
|
2179
|
+
error: {
|
|
2180
|
+
code: "VALIDATION_ERROR",
|
|
2181
|
+
message: "label is required"
|
|
2182
|
+
}
|
|
2183
|
+
};
|
|
2184
|
+
if (label.length > 200) return {
|
|
2185
|
+
success: false,
|
|
2186
|
+
error: {
|
|
2187
|
+
code: "VALIDATION_ERROR",
|
|
2188
|
+
message: "label name exceeds maximum length (200)"
|
|
2189
|
+
}
|
|
2190
|
+
};
|
|
2191
|
+
await this.client.request("POST /repos/{owner}/{repo}/issues/{issue_number}/labels", {
|
|
2192
|
+
owner,
|
|
2193
|
+
repo,
|
|
2194
|
+
issue_number: issueNumber,
|
|
2195
|
+
labels: [label]
|
|
2196
|
+
});
|
|
2197
|
+
return {
|
|
2198
|
+
success: true,
|
|
2199
|
+
data: { label }
|
|
2200
|
+
};
|
|
2201
|
+
}
|
|
2202
|
+
case "remove-label": {
|
|
2203
|
+
const label = args.label;
|
|
2204
|
+
if (!label) return {
|
|
2205
|
+
success: false,
|
|
2206
|
+
error: {
|
|
2207
|
+
code: "VALIDATION_ERROR",
|
|
2208
|
+
message: "label is required"
|
|
2209
|
+
}
|
|
2210
|
+
};
|
|
2211
|
+
try {
|
|
2212
|
+
await this.client.request("DELETE /repos/{owner}/{repo}/issues/{issue_number}/labels/{name}", {
|
|
2213
|
+
owner,
|
|
2214
|
+
repo,
|
|
2215
|
+
issue_number: issueNumber,
|
|
2216
|
+
name: label
|
|
2217
|
+
});
|
|
2218
|
+
return {
|
|
2219
|
+
success: true,
|
|
2220
|
+
data: { label }
|
|
2221
|
+
};
|
|
2222
|
+
} catch (error) {
|
|
2223
|
+
if (error?.status === 404) return {
|
|
2224
|
+
success: false,
|
|
2225
|
+
error: {
|
|
2226
|
+
code: "NOT_FOUND",
|
|
2227
|
+
message: `Label "${label}" not found`
|
|
2228
|
+
}
|
|
2229
|
+
};
|
|
2230
|
+
throw error;
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
default: return {
|
|
2234
|
+
success: false,
|
|
2235
|
+
error: {
|
|
2236
|
+
code: "NOT_FOUND",
|
|
2237
|
+
message: `Unknown issue action: ${actionName}`
|
|
2238
|
+
}
|
|
2239
|
+
};
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
async execPRAction(prNumber, actionName, args) {
|
|
2243
|
+
const owner = this.options.owner;
|
|
2244
|
+
const repo = this.options.repo;
|
|
2245
|
+
switch (actionName) {
|
|
2246
|
+
case "merge-pr": {
|
|
2247
|
+
const method = args.method ?? "merge";
|
|
2248
|
+
const result = (await this.client.request("PUT /repos/{owner}/{repo}/pulls/{pull_number}/merge", {
|
|
2249
|
+
owner,
|
|
2250
|
+
repo,
|
|
2251
|
+
pull_number: prNumber,
|
|
2252
|
+
merge_method: method,
|
|
2253
|
+
commit_title: args.commitTitle ?? void 0,
|
|
2254
|
+
commit_message: args.commitMessage ?? void 0
|
|
2255
|
+
})).data;
|
|
2256
|
+
return {
|
|
2257
|
+
success: true,
|
|
2258
|
+
data: {
|
|
2259
|
+
merged: result.merged,
|
|
2260
|
+
sha: result.sha
|
|
2261
|
+
}
|
|
2262
|
+
};
|
|
2263
|
+
}
|
|
2264
|
+
case "request-review": {
|
|
2265
|
+
const reviewers = args.reviewers;
|
|
2266
|
+
if (!reviewers || reviewers.length === 0) return {
|
|
2267
|
+
success: false,
|
|
2268
|
+
error: {
|
|
2269
|
+
code: "VALIDATION_ERROR",
|
|
2270
|
+
message: "reviewers is required"
|
|
2271
|
+
}
|
|
2272
|
+
};
|
|
2273
|
+
await this.client.request("POST /repos/{owner}/{repo}/pulls/{pull_number}/requested_reviewers", {
|
|
2274
|
+
owner,
|
|
2275
|
+
repo,
|
|
2276
|
+
pull_number: prNumber,
|
|
2277
|
+
reviewers
|
|
2278
|
+
});
|
|
2279
|
+
return {
|
|
2280
|
+
success: true,
|
|
2281
|
+
data: { reviewers }
|
|
2282
|
+
};
|
|
2283
|
+
}
|
|
2284
|
+
case "add-comment": return this.execIssueAction(prNumber, "add-comment", args);
|
|
2285
|
+
case "add-label": return this.execIssueAction(prNumber, "add-label", args);
|
|
2286
|
+
case "remove-label": return this.execIssueAction(prNumber, "remove-label", args);
|
|
2287
|
+
default: return {
|
|
2288
|
+
success: false,
|
|
2289
|
+
error: {
|
|
2290
|
+
code: "NOT_FOUND",
|
|
2291
|
+
message: `Unknown PR action: ${actionName}`
|
|
2292
|
+
}
|
|
2293
|
+
};
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
/**
|
|
2297
|
+
* Handle errors from action execution.
|
|
2298
|
+
* Sanitizes error messages to prevent token leaks.
|
|
2299
|
+
*/
|
|
2300
|
+
handleExecError(error) {
|
|
2301
|
+
const status = error?.status;
|
|
2302
|
+
const sanitized = (error instanceof Error ? error.message : String(error)).replace(/ghp_[a-zA-Z0-9]+/g, "[REDACTED]");
|
|
2303
|
+
if (status === 403) return {
|
|
2304
|
+
success: false,
|
|
2305
|
+
error: {
|
|
2306
|
+
code: "FORBIDDEN",
|
|
2307
|
+
message: `Insufficient permissions. Check that your token has the required access.`
|
|
2308
|
+
}
|
|
2309
|
+
};
|
|
2310
|
+
if (status === 404) return {
|
|
2311
|
+
success: false,
|
|
2312
|
+
error: {
|
|
2313
|
+
code: "NOT_FOUND",
|
|
2314
|
+
message: `Resource not found: ${sanitized}`
|
|
2315
|
+
}
|
|
2316
|
+
};
|
|
2317
|
+
if (status === 405 || status === 409) return {
|
|
2318
|
+
success: false,
|
|
2319
|
+
error: {
|
|
2320
|
+
code: "CONFLICT",
|
|
2321
|
+
message: sanitized
|
|
2322
|
+
}
|
|
2323
|
+
};
|
|
2324
|
+
if (status === 422) return {
|
|
2325
|
+
success: false,
|
|
2326
|
+
error: {
|
|
2327
|
+
code: "VALIDATION_ERROR",
|
|
2328
|
+
message: sanitized
|
|
2329
|
+
}
|
|
2330
|
+
};
|
|
2331
|
+
return {
|
|
2332
|
+
success: false,
|
|
2333
|
+
error: {
|
|
2334
|
+
code: "INTERNAL_ERROR",
|
|
2335
|
+
message: sanitized
|
|
2336
|
+
}
|
|
2337
|
+
};
|
|
2338
|
+
}
|
|
1121
2339
|
};
|
|
1122
2340
|
|
|
1123
2341
|
//#endregion
|