comfy-pr 0.2.26 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +258 -157
  2. package/bot/IdleWaiter.ts +31 -0
  3. package/bot/RestartManager.spec.ts +163 -0
  4. package/bot/RestartManager.ts +190 -0
  5. package/bot/WorkingTasksManager.spec.ts +154 -0
  6. package/bot/cli.ts +1171 -0
  7. package/bot/codesearch-ai-wip.ts +351 -0
  8. package/bot/error-collector.ts +133 -0
  9. package/bot/index.ts +14 -0
  10. package/bot/restart-example.ts +99 -0
  11. package/bot/slack-bolt.ts +688 -0
  12. package/bot/slack-bot.ts +1575 -0
  13. package/bot/state.ts +19 -0
  14. package/bot/templateLoader.test.ts +84 -0
  15. package/bot/templateLoader.ts +92 -0
  16. package/next.config.ts +26 -0
  17. package/package.json +161 -41
  18. package/post-summary.ts +44 -0
  19. package/src/Authors.ts +2 -2
  20. package/src/CMNodes.ts +3 -3
  21. package/src/CNRepos.ts +30 -7
  22. package/src/CRNodes.ts +11 -9
  23. package/src/CRPulls.ts +3 -0
  24. package/src/EmailTasks.ts +27 -9
  25. package/src/FORK_OWNER.ts +3 -1
  26. package/src/FollowRules.ts +3 -3
  27. package/src/GithubIssueComments.ts +1 -1
  28. package/src/Totals.ts +9 -8
  29. package/src/WorkerInstances.ts +31 -8
  30. package/src/addCommentAction.ts +19 -11
  31. package/src/analyzePullsStatus.ts +39 -17
  32. package/src/analyzeTotals.ts +19 -11
  33. package/src/bypassRepos.ts +6 -5
  34. package/src/checkPRsFailures.ts +13 -8
  35. package/src/cli.test.ts +2 -0
  36. package/src/cli.ts +1 -1
  37. package/src/constants.ts +2 -0
  38. package/src/createComfyRegistryPRsFromCandidates.ts +20 -12
  39. package/src/createComfyRegistryPullRequests.ts +35 -10
  40. package/src/createGithubForkForRepo.ts +36 -11
  41. package/src/createGithubPullRequest.ts +83 -33
  42. package/src/createIssueComment.ts +4 -4
  43. package/src/fetchComfyRegistryNodes.ts +34 -4
  44. package/src/fetchCurrentGeoInfo.ts +3 -1
  45. package/src/fetchRelatedPullWithComments.ts +1 -1
  46. package/src/fetchRepoDescriptionMap.ts +1 -4
  47. package/src/followRuleSchema.ts +10 -4
  48. package/src/getBranchWorkingDir.ts +3 -7
  49. package/src/getRepoWorkingDir.ts +2 -3
  50. package/src/gh.spec.ts +571 -0
  51. package/src/ghData.ts +13 -0
  52. package/src/ghPageFlow.ts +33 -0
  53. package/src/ghSubscriber.ts +76 -0
  54. package/src/ghUser.ts +28 -4
  55. package/src/index.ts +13 -4
  56. package/src/initializeFollowRules.ts +11 -4
  57. package/src/logger.spec.ts +95 -0
  58. package/src/logger.ts +46 -0
  59. package/src/makeGpl3LicenseBranch.ts +66 -0
  60. package/src/makePublishBranch.ts +21 -15
  61. package/src/makeTomlBranch.ts +48 -14
  62. package/src/makeUpdateTomlLicenseBranch.ts +40 -44
  63. package/src/matchRelatedPulls.ts +9 -4
  64. package/src/normalizeGithubUrl.spec.ts +131 -0
  65. package/src/normalizeGithubUrl.ts +64 -0
  66. package/src/parseIssueUrl.spec.ts +95 -0
  67. package/src/parseIssueUrl.ts +17 -3
  68. package/src/parseOwnerRepo.spec.ts +147 -0
  69. package/src/parseOwnerRepo.ts +8 -5
  70. package/src/parsePullsState.ts +2 -2
  71. package/src/parseTitleBodyOfMarkdown.ts +1 -1
  72. package/src/pickRepoInfo.ts +37 -0
  73. package/src/postSlackMessage.ts +5 -1
  74. package/src/preload.ts +30 -27
  75. package/src/readTemplateTitle.ts +1 -3
  76. package/src/sendEmailAction.ts +14 -10
  77. package/src/sendGmail.ts +3 -3
  78. package/src/updateAuthorsForGithub.ts +53 -35
  79. package/src/updateAuthorsFromCNRepo.ts +12 -5
  80. package/src/updateCMNodesDuplicationWarnings.ts +14 -9
  81. package/src/updateCMRepos.ts +1 -1
  82. package/src/updateCNRepos.ts +14 -14
  83. package/src/updateCNReposCRPullsComments.ts +7 -5
  84. package/src/updateCNReposInfo.ts +49 -40
  85. package/src/updateCNReposPRCandidate.ts +3 -3
  86. package/src/updateCNReposPulls.ts +9 -5
  87. package/src/updateCNReposPullsDashboard.ts +11 -9
  88. package/src/updateCNReposRelatedPulls.ts +3 -3
  89. package/src/updateCRNodes.ts +24 -5
  90. package/src/updateCRRepos.ts +1 -1
  91. package/src/updateComfyTotals.ts +7 -5
  92. package/src/updateFollowRuleSet.ts +17 -9
  93. package/src/updateOutdatedPullsTemplates.ts +61 -48
  94. package/src/updateSlackMessages.ts +8 -4
  95. package/tailwind.config.ts +7 -1
  96. package/next-env.d.ts +0 -5
  97. package/src/GIT_USEREMAIL.ts +0 -5
  98. package/src/GIT_USERNAME.ts +0 -9
  99. package/src/clone_modify_push_Branches.ts +0 -21
  100. package/src/tomlFillDescription.ts +0 -17
@@ -8,7 +8,7 @@ export type RelatedPullsWithComments = Awaited<ReturnType<typeof fetchRelatedPul
8
8
  export type RelatedPull = Awaited<ReturnType<typeof matchRelatedPulls>>[number];
9
9
  export async function matchRelatedPulls(pulls: GithubPullParsed[]): Promise<
10
10
  {
11
- type: "pyproject" | "publishcr" | "licence-update";
11
+ type: "pyproject" | "publishcr" | "licence-update" | "gpl3-license";
12
12
  pull: {
13
13
  title: string;
14
14
  number: number;
@@ -24,9 +24,10 @@ export async function matchRelatedPulls(pulls: GithubPullParsed[]): Promise<
24
24
  };
25
25
  }[]
26
26
  > {
27
- const pyproject = await readTemplateTitle("add-toml.md");
28
- const publishcr = await readTemplateTitle("add-action.md");
29
- const licenseUpdate = await readTemplateTitle("update-toml-license.md");
27
+ const pyproject = await readTemplateTitle("./templates/add-toml.md");
28
+ const publishcr = await readTemplateTitle("./templates/add-action.md");
29
+ const licenseUpdate = await readTemplateTitle("./templates/update-toml-license.md");
30
+ const gpl3License = await readTemplateTitle("./templates/add-gpl3-license.md");
30
31
  const relatedPulls = await pMap(pulls, async (pull) =>
31
32
  match(pull)
32
33
  .with({ title: pyproject }, (pull) => ({
@@ -41,6 +42,10 @@ export async function matchRelatedPulls(pulls: GithubPullParsed[]): Promise<
41
42
  type: "publishcr" as const,
42
43
  pull,
43
44
  }))
45
+ .with({ title: gpl3License }, (pull) => ({
46
+ type: "gpl3-license" as const,
47
+ pull,
48
+ }))
44
49
  .otherwise(() => null),
45
50
  );
46
51
  return relatedPulls.flatMap((e) => (e ? [e] : []));
@@ -0,0 +1,131 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ normalizeGithubUrl,
4
+ normalizeGithubUrls,
5
+ normalizeGithubUrlsInObject,
6
+ } from "./normalizeGithubUrl";
7
+
8
+ describe("normalizeGithubUrl", () => {
9
+ test("should normalize comfyanonymous to Comfy-Org", () => {
10
+ const url = "https://github.com/comfyanonymous/ComfyUI/issues/123";
11
+ const expected = "https://github.com/Comfy-Org/ComfyUI/issues/123";
12
+ expect(normalizeGithubUrl(url)).toBe(expected);
13
+ });
14
+
15
+ test("should handle already normalized URLs", () => {
16
+ const url = "https://github.com/Comfy-Org/ComfyUI/issues/123";
17
+ expect(normalizeGithubUrl(url)).toBe(url);
18
+ });
19
+
20
+ test("should be case insensitive", () => {
21
+ const url = "https://github.com/ComfyAnonymous/ComfyUI/issues/123";
22
+ const expected = "https://github.com/Comfy-Org/ComfyUI/issues/123";
23
+ expect(normalizeGithubUrl(url)).toBe(expected);
24
+ });
25
+
26
+ test("should handle pull request URLs", () => {
27
+ const url = "https://github.com/comfyanonymous/ComfyUI/pull/456";
28
+ const expected = "https://github.com/Comfy-Org/ComfyUI/pull/456";
29
+ expect(normalizeGithubUrl(url)).toBe(expected);
30
+ });
31
+
32
+ test("should handle comment URLs", () => {
33
+ const url = "https://github.com/comfyanonymous/ComfyUI/issues/123#issuecomment-456789";
34
+ const expected = "https://github.com/Comfy-Org/ComfyUI/issues/123#issuecomment-456789";
35
+ expect(normalizeGithubUrl(url)).toBe(expected);
36
+ });
37
+
38
+ test("should handle release URLs", () => {
39
+ const url = "https://github.com/comfyanonymous/ComfyUI/releases/tag/v1.0.0";
40
+ const expected = "https://github.com/Comfy-Org/ComfyUI/releases/tag/v1.0.0";
41
+ expect(normalizeGithubUrl(url)).toBe(expected);
42
+ });
43
+
44
+ test("should not affect other GitHub repos (Comfy-Org)", () => {
45
+ const url = "https://github.com/Comfy-Org/ComfyUI_frontend/issues/123";
46
+ expect(normalizeGithubUrl(url)).toBe(url);
47
+ });
48
+
49
+ test("should not affect other comfyanonymous repos", () => {
50
+ // Only ComfyUI was migrated — other hypothetical repos under comfyanonymous should be untouched
51
+ const url = "https://github.com/comfyanonymous/some-other-repo/issues/1";
52
+ expect(normalizeGithubUrl(url)).toBe(url);
53
+ });
54
+
55
+ test("should handle repo URL without trailing path", () => {
56
+ const url = "https://github.com/comfyanonymous/ComfyUI";
57
+ const expected = "https://github.com/Comfy-Org/ComfyUI";
58
+ expect(normalizeGithubUrl(url)).toBe(expected);
59
+ });
60
+
61
+ test("should handle empty string", () => {
62
+ expect(normalizeGithubUrl("")).toBe("");
63
+ });
64
+
65
+ test("should handle non-GitHub URLs", () => {
66
+ const url = "https://example.com/path";
67
+ expect(normalizeGithubUrl(url)).toBe(url);
68
+ });
69
+ });
70
+
71
+ describe("normalizeGithubUrls", () => {
72
+ test("should normalize multiple URLs", () => {
73
+ const urls = [
74
+ "https://github.com/comfyanonymous/ComfyUI/issues/123",
75
+ "https://github.com/Comfy-Org/ComfyUI/issues/456",
76
+ "https://github.com/comfyanonymous/ComfyUI/pull/789",
77
+ ];
78
+ const expected = [
79
+ "https://github.com/Comfy-Org/ComfyUI/issues/123",
80
+ "https://github.com/Comfy-Org/ComfyUI/issues/456",
81
+ "https://github.com/Comfy-Org/ComfyUI/pull/789",
82
+ ];
83
+ expect(normalizeGithubUrls(urls)).toEqual(expected);
84
+ });
85
+
86
+ test("should handle empty array", () => {
87
+ expect(normalizeGithubUrls([])).toEqual([]);
88
+ });
89
+ });
90
+
91
+ describe("normalizeGithubUrlsInObject", () => {
92
+ test("should normalize URL fields in object", () => {
93
+ const obj = {
94
+ sourceIssueUrl: "https://github.com/comfyanonymous/ComfyUI/issues/123",
95
+ targetIssueUrl: "https://github.com/Comfy-Org/ComfyUI_frontend/issues/456",
96
+ otherField: "not a url",
97
+ };
98
+ const result = normalizeGithubUrlsInObject(obj, ["sourceIssueUrl", "targetIssueUrl"]);
99
+ expect(result.sourceIssueUrl).toBe("https://github.com/Comfy-Org/ComfyUI/issues/123");
100
+ expect(result.targetIssueUrl).toBe("https://github.com/Comfy-Org/ComfyUI_frontend/issues/456");
101
+ expect(result.otherField).toBe("not a url");
102
+ });
103
+
104
+ test("should not modify non-URL fields", () => {
105
+ const obj = {
106
+ url: "https://github.com/comfyanonymous/ComfyUI/issues/123",
107
+ count: 42,
108
+ flag: true,
109
+ };
110
+ const result = normalizeGithubUrlsInObject(obj, ["url"]);
111
+ expect(result.url).toBe("https://github.com/Comfy-Org/ComfyUI/issues/123");
112
+ expect(result.count).toBe(42);
113
+ expect(result.flag).toBe(true);
114
+ });
115
+
116
+ test("should handle objects with no URL fields", () => {
117
+ const obj = { name: "test", value: 123 };
118
+ const result = normalizeGithubUrlsInObject(obj, []);
119
+ expect(result).toEqual(obj);
120
+ });
121
+
122
+ test("should create a new object (not mutate)", () => {
123
+ const obj = {
124
+ url: "https://github.com/comfyanonymous/ComfyUI/issues/123",
125
+ };
126
+ const result = normalizeGithubUrlsInObject(obj, ["url"]);
127
+ expect(result).not.toBe(obj);
128
+ expect(obj.url).toBe("https://github.com/comfyanonymous/ComfyUI/issues/123");
129
+ expect(result.url).toBe("https://github.com/Comfy-Org/ComfyUI/issues/123");
130
+ });
131
+ });
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Normalize GitHub URLs to use the canonical Comfy-Org organization name
3
+ *
4
+ * This ensures that URLs pointing to the same resource but with different
5
+ * organization names (comfyanonymous vs Comfy-Org) are treated as identical.
6
+ *
7
+ * @param url - GitHub URL to normalize
8
+ * @returns Normalized URL with comfyanonymous replaced by Comfy-Org
9
+ *
10
+ * @example
11
+ * normalizeGithubUrl("https://github.com/comfyanonymous/ComfyUI/issues/123")
12
+ * // => "https://github.com/Comfy-Org/ComfyUI/issues/123"
13
+ *
14
+ * normalizeGithubUrl("https://github.com/Comfy-Org/ComfyUI/issues/123")
15
+ * // => "https://github.com/Comfy-Org/ComfyUI/issues/123"
16
+ */
17
+ export function normalizeGithubUrl(url: string): string {
18
+ if (!url) return url;
19
+
20
+ // Only ComfyUI was migrated from comfyanonymous to Comfy-Org
21
+ return url.replace(
22
+ /github\.com\/comfyanonymous\/ComfyUI([/?#]|$)/gi,
23
+ "github.com/Comfy-Org/ComfyUI$1",
24
+ );
25
+ }
26
+
27
+ /**
28
+ * Normalize multiple GitHub URLs at once
29
+ *
30
+ * @param urls - Array of GitHub URLs to normalize
31
+ * @returns Array of normalized URLs
32
+ */
33
+ export function normalizeGithubUrls(urls: string[]): string[] {
34
+ return urls.map(normalizeGithubUrl);
35
+ }
36
+
37
+ /**
38
+ * Normalize GitHub URLs in an object's properties
39
+ *
40
+ * @param obj - Object containing URL properties
41
+ * @param urlFields - Array of field names that contain URLs
42
+ * @returns New object with normalized URLs
43
+ *
44
+ * @example
45
+ * normalizeGithubUrlsInObject(
46
+ * { sourceIssueUrl: "https://github.com/comfyanonymous/ComfyUI/issues/123" },
47
+ * ["sourceIssueUrl"]
48
+ * )
49
+ * // => { sourceIssueUrl: "https://github.com/Comfy-Org/ComfyUI/issues/123" }
50
+ */
51
+ export function normalizeGithubUrlsInObject<T extends Record<string, unknown>>(
52
+ obj: T,
53
+ urlFields: (keyof T)[],
54
+ ): T {
55
+ const result = { ...obj };
56
+
57
+ for (const field of urlFields) {
58
+ if (typeof result[field] === "string") {
59
+ result[field] = normalizeGithubUrl(result[field] as string) as T[keyof T];
60
+ }
61
+ }
62
+
63
+ return result;
64
+ }
@@ -0,0 +1,95 @@
1
+ import { parseIssueUrl } from "./parseIssueUrl";
2
+
3
+ describe("parseIssueUrl", () => {
4
+ it("should parse a valid pull request URL", () => {
5
+ const url = "https://github.com/owner/repo/pull/123";
6
+ const result = parseIssueUrl(url);
7
+
8
+ expect(result).toEqual({
9
+ owner: "owner",
10
+ repo: "repo",
11
+ issue_number: 123,
12
+ });
13
+ });
14
+
15
+ it("should parse a valid issue URL", () => {
16
+ const url = "https://github.com/owner/repo/issues/456";
17
+ const result = parseIssueUrl(url);
18
+
19
+ expect(result).toEqual({
20
+ owner: "owner",
21
+ repo: "repo",
22
+ issue_number: 456,
23
+ });
24
+ });
25
+
26
+ it("should handle owner and repo names with hyphens", () => {
27
+ const url = "https://github.com/my-org/my-repo/pull/789";
28
+ const result = parseIssueUrl(url);
29
+
30
+ expect(result).toEqual({
31
+ owner: "my-org",
32
+ repo: "my-repo",
33
+ issue_number: 789,
34
+ });
35
+ });
36
+
37
+ it("should handle numeric issue numbers correctly", () => {
38
+ const url = "https://github.com/test/test/issues/1";
39
+ const result = parseIssueUrl(url);
40
+
41
+ expect(result).toEqual({
42
+ owner: "test",
43
+ repo: "test",
44
+ issue_number: 1,
45
+ });
46
+ });
47
+
48
+ it("should handle large issue numbers", () => {
49
+ const url = "https://github.com/test/test/pull/999999";
50
+ const result = parseIssueUrl(url);
51
+
52
+ expect(result).toEqual({
53
+ owner: "test",
54
+ repo: "test",
55
+ issue_number: 999999,
56
+ });
57
+ });
58
+
59
+ it("should throw an error for invalid URL format", () => {
60
+ const invalidUrl = "https://github.com/owner/repo/invalid/123";
61
+
62
+ expect(() => parseIssueUrl(invalidUrl)).toThrow();
63
+ });
64
+
65
+ it("should throw an error for non-GitHub URLs", () => {
66
+ const invalidUrl = "https://gitlab.com/owner/repo/pull/123";
67
+
68
+ expect(() => parseIssueUrl(invalidUrl)).toThrow();
69
+ });
70
+
71
+ it("should throw an error for URLs without issue/pull number", () => {
72
+ const invalidUrl = "https://github.com/owner/repo/pull/";
73
+
74
+ expect(() => parseIssueUrl(invalidUrl)).toThrow();
75
+ });
76
+
77
+ it("should throw an error for URLs with non-numeric issue numbers", () => {
78
+ const invalidUrl = "https://github.com/owner/repo/pull/abc";
79
+
80
+ expect(() => parseIssueUrl(invalidUrl)).toThrow();
81
+ });
82
+
83
+ it("should throw an error for incomplete URLs", () => {
84
+ const invalidUrl = "https://github.com/owner";
85
+
86
+ expect(() => parseIssueUrl(invalidUrl)).toThrow();
87
+ });
88
+
89
+ it("should handle URLs with trailing slashes", () => {
90
+ // Note: The current regex doesn't handle trailing slashes, so this should fail
91
+ const url = "https://github.com/owner/repo/pull/123/";
92
+
93
+ expect(() => parseIssueUrl(url)).toThrow();
94
+ });
95
+ });
@@ -1,6 +1,20 @@
1
+ import DIE from "@snomiao/die";
2
+
3
+ /**
4
+ * Parse pull or issue url, get issue_number
5
+ */
1
6
  export function parseIssueUrl(issueUrl: string) {
2
- const [owner, repo, strNumber] = issueUrl
3
- .match(/^https:\/\/github\.com\/([\w-]+)\/([\w-]+)\/(?:pull|issues)\/(\d+)$/)!
4
- .slice(1);
7
+ const m =
8
+ issueUrl.match(
9
+ /^https:\/\/github\.com\/([\w-]+)\/([\w-]+)\/(?:pull|issues)\/(\d+)(?:\/files)?(?:#.*)?$/,
10
+ ) || DIE(`Invalid issue URL: ${issueUrl}`);
11
+ const [owner, repo, strNumber] = m.slice(1);
12
+ if (!owner || !repo || !strNumber) {
13
+ throw new Error(`Invalid issue URL: ${issueUrl}`);
14
+ }
5
15
  return { owner, repo, issue_number: Number(strNumber) };
6
16
  }
17
+
18
+ export function stringifyIssueUrl({ owner, repo, issue_number }: ReturnType<typeof parseIssueUrl>) {
19
+ return `https://github.com/${owner}/${repo}/issues/${issue_number}`;
20
+ }
@@ -0,0 +1,147 @@
1
+ import {
2
+ parseGithubRepoUrl,
3
+ stringifyGithubOrigin,
4
+ stringifyGithubRepoUrl,
5
+ stringifyOwnerRepo,
6
+ } from "./parseOwnerRepo";
7
+
8
+ describe("parseOwnerRepo", () => {
9
+ describe("parseGithubRepoUrl", () => {
10
+ it("should parse SSH GitHub URL", () => {
11
+ const result = parseGithubRepoUrl("git@github.com:owner/repo");
12
+ expect(result).toEqual({
13
+ owner: "owner",
14
+ repo: "repo",
15
+ });
16
+ });
17
+
18
+ it("should parse SSH GitHub URL with .git extension", () => {
19
+ const result = parseGithubRepoUrl("git@github.com:owner/repo.git");
20
+ expect(result).toEqual({
21
+ owner: "owner",
22
+ repo: "repo",
23
+ });
24
+ });
25
+
26
+ it("should parse HTTPS GitHub URL", () => {
27
+ const result = parseGithubRepoUrl("https://github.com/owner/repo");
28
+ expect(result).toEqual({
29
+ owner: "owner",
30
+ repo: "repo",
31
+ });
32
+ });
33
+
34
+ it("should parse HTTPS GitHub URL with .git extension", () => {
35
+ const result = parseGithubRepoUrl("https://github.com/owner/repo.git");
36
+ expect(result).toEqual({
37
+ owner: "owner",
38
+ repo: "repo",
39
+ });
40
+ });
41
+
42
+ it("should handle owner/repo with hyphens and underscores", () => {
43
+ const result = parseGithubRepoUrl("git@github.com:my-org_name/my-repo_name");
44
+ expect(result).toEqual({
45
+ owner: "my-org_name",
46
+ repo: "my-repo_name",
47
+ });
48
+ });
49
+
50
+ it("should handle owner/repo with numbers", () => {
51
+ const result = parseGithubRepoUrl("https://github.com/user123/repo456");
52
+ expect(result).toEqual({
53
+ owner: "user123",
54
+ repo: "repo456",
55
+ });
56
+ });
57
+
58
+ it("should handle real-world example URLs", () => {
59
+ const result1 = parseGithubRepoUrl("git@github.com:Comfy-Org/ComfyUI-Registry");
60
+ expect(result1).toEqual({
61
+ owner: "Comfy-Org",
62
+ repo: "ComfyUI-Registry",
63
+ });
64
+
65
+ const result2 = parseGithubRepoUrl("https://github.com/snomiao/ComfyUI-FD-Tagger.git");
66
+ expect(result2).toEqual({
67
+ owner: "snomiao",
68
+ repo: "ComfyUI-FD-Tagger",
69
+ });
70
+ });
71
+ });
72
+
73
+ describe("stringifyOwnerRepo", () => {
74
+ it("should stringify owner and repo", () => {
75
+ const result = stringifyOwnerRepo({ owner: "owner", repo: "repo" });
76
+ expect(result).toBe("owner/repo");
77
+ });
78
+
79
+ it("should handle special characters", () => {
80
+ const result = stringifyOwnerRepo({ owner: "my-org_name", repo: "my-repo_name" });
81
+ expect(result).toBe("my-org_name/my-repo_name");
82
+ });
83
+
84
+ it("should handle numbers", () => {
85
+ const result = stringifyOwnerRepo({ owner: "user123", repo: "repo456" });
86
+ expect(result).toBe("user123/repo456");
87
+ });
88
+ });
89
+
90
+ describe("stringifyGithubRepoUrl", () => {
91
+ it("should create HTTPS GitHub URL", () => {
92
+ const result = stringifyGithubRepoUrl({ owner: "owner", repo: "repo" });
93
+ expect(result).toBe("https://github.com/owner/repo");
94
+ });
95
+
96
+ it("should handle special characters", () => {
97
+ const result = stringifyGithubRepoUrl({ owner: "my-org_name", repo: "my-repo_name" });
98
+ expect(result).toBe("https://github.com/my-org_name/my-repo_name");
99
+ });
100
+
101
+ it("should handle real-world examples", () => {
102
+ const result = stringifyGithubRepoUrl({ owner: "Comfy-Org", repo: "ComfyUI-Registry" });
103
+ expect(result).toBe("https://github.com/Comfy-Org/ComfyUI-Registry");
104
+ });
105
+ });
106
+
107
+ describe("stringifyGithubOrigin", () => {
108
+ it("should return SSH URL regardless of token presence", async () => {
109
+ // The function always returns SSH URL according to the current implementation
110
+ const result = await stringifyGithubOrigin({ owner: "owner", repo: "repo" });
111
+ expect(result).toBe("git@github.com:owner/repo");
112
+ });
113
+
114
+ it("should handle special characters in owner/repo", async () => {
115
+ const result = await stringifyGithubOrigin({ owner: "my-org_name", repo: "my-repo_name" });
116
+ expect(result).toBe("git@github.com:my-org_name/my-repo_name");
117
+ });
118
+
119
+ it("should handle real-world examples", async () => {
120
+ const result = await stringifyGithubOrigin({ owner: "Comfy-Org", repo: "ComfyUI-Registry" });
121
+ expect(result).toBe("git@github.com:Comfy-Org/ComfyUI-Registry");
122
+ });
123
+ });
124
+
125
+ describe("integration tests", () => {
126
+ it("should work end-to-end with parseGithubRepoUrl and stringifyOwnerRepo", () => {
127
+ const originalUrl = "git@github.com:Comfy-Org/ComfyUI-Registry.git";
128
+ const parsed = parseGithubRepoUrl(originalUrl);
129
+ const stringified = stringifyOwnerRepo(parsed);
130
+ expect(stringified).toBe("Comfy-Org/ComfyUI-Registry");
131
+ });
132
+
133
+ it("should work end-to-end with parseGithubRepoUrl and stringifyGithubRepoUrl", () => {
134
+ const originalUrl = "git@github.com:snomiao/ComfyUI-FD-Tagger";
135
+ const parsed = parseGithubRepoUrl(originalUrl);
136
+ const httpsUrl = stringifyGithubRepoUrl(parsed);
137
+ expect(httpsUrl).toBe("https://github.com/snomiao/ComfyUI-FD-Tagger");
138
+ });
139
+
140
+ it("should work end-to-end with parseGithubRepoUrl and stringifyGithubOrigin", async () => {
141
+ const originalUrl = "https://github.com/owner/repo.git";
142
+ const parsed = parseGithubRepoUrl(originalUrl);
143
+ const sshUrl = await stringifyGithubOrigin(parsed);
144
+ expect(sshUrl).toBe("git@github.com:owner/repo");
145
+ });
146
+ });
147
+ });
@@ -2,21 +2,24 @@ import { basename, dirname } from "path";
2
2
 
3
3
  /**
4
4
  * Parse owner and repo obj
5
- * @param gitUrl git@github.ocm:owner/repo or https://github.ocm/owner/repo
5
+ * @param gitUrl git@github.com:owner/repo or https://github.com/owner/repo
6
6
  */
7
- export function parseUrlRepoOwner(gitUrl: string) {
7
+ export function parseGithubRepoUrl(gitUrl: string) {
8
8
  return {
9
9
  owner: basename(dirname(gitUrl.replace(/:/, "/"))),
10
10
  repo: basename(gitUrl.replace(/:/, "/")).replace(/\.git$/, ""),
11
11
  };
12
12
  }
13
- export function stringifyOwnerRepo({ owner, repo }: ReturnType<typeof parseUrlRepoOwner>) {
13
+ export function stringifyOwnerRepo({ owner, repo }: ReturnType<typeof parseGithubRepoUrl>) {
14
14
  return owner + "/" + repo;
15
15
  }
16
- export function stringifyGithubRepoUrl({ owner, repo }: ReturnType<typeof parseUrlRepoOwner>) {
16
+ export function stringifyGithubRepoUrl({ owner, repo }: ReturnType<typeof parseGithubRepoUrl>) {
17
17
  return "https://github.com/" + owner + "/" + repo;
18
18
  }
19
- export async function stringifyGithubOrigin({ owner, repo }: ReturnType<typeof parseUrlRepoOwner>) {
19
+ export async function stringifyGithubOrigin({
20
+ owner,
21
+ repo,
22
+ }: ReturnType<typeof parseGithubRepoUrl>) {
20
23
  const PR_TOKEN = process.env.GH_TOKEN_COMFY_PR;
21
24
  if (PR_TOKEN) {
22
25
  // fails: maybe permission issue
@@ -1,5 +1,5 @@
1
- import type { GithubPull } from "./gh/GithubPull";
2
- import { parsePull } from "./gh/parsePull";
1
+ import type { GithubPull } from "@/lib/github/GithubPull";
2
+ import { parsePull } from "@/lib/github/parsePull";
3
3
  export type GithubPullParsed = ReturnType<typeof parsePulls>[number];
4
4
  export function parsePulls(data: GithubPull[]) {
5
5
  return data.map((e) => parsePull(e));
@@ -1,7 +1,7 @@
1
1
  import DIE from "@snomiao/die";
2
2
 
3
3
  export function parseTitleBodyOfMarkdown(tmpl: string) {
4
- tmpl.startsWith("# ") || DIE("Unrecognized template format:" + tmpl);
4
+ if (!tmpl.startsWith("# ")) DIE("Unrecognized template format:" + tmpl);
5
5
  const title = tmpl.split("\n")[0].slice(1).trim();
6
6
  const body = tmpl.split("\n").slice(1).join("\n").trim();
7
7
  return { title, body };
@@ -0,0 +1,37 @@
1
+ import { gh } from "@/lib/github";
2
+
3
+ type GithubRepo = Awaited<ReturnType<typeof gh.repos.get>>["data"];
4
+
5
+ /**
6
+ * Pick only the fields we need from a GitHub repository response.
7
+ * This reduces document size from ~5KB to ~300 bytes.
8
+ *
9
+ * Fields kept based on actual usage in codebase:
10
+ * - html_url: repository links
11
+ * - archived: statistics and filtering
12
+ * - default_branch: branch operations
13
+ * - private: access control
14
+ * - owner.login: author tracking
15
+ * - license: license checks
16
+ * - updated_at: activity tracking
17
+ */
18
+ export function pickRepoInfo(repo: GithubRepo) {
19
+ return {
20
+ html_url: repo.html_url,
21
+ archived: repo.archived,
22
+ default_branch: repo.default_branch,
23
+ private: repo.private,
24
+ updated_at: repo.updated_at,
25
+ owner: {
26
+ login: repo.owner.login,
27
+ },
28
+ license: repo.license
29
+ ? {
30
+ spdx_id: repo.license.spdx_id,
31
+ name: repo.license.name,
32
+ }
33
+ : null,
34
+ };
35
+ }
36
+
37
+ export type PickedRepoInfo = ReturnType<typeof pickRepoInfo>;
@@ -1,8 +1,12 @@
1
1
  import DIE from "@snomiao/die";
2
- import { slack } from "./slack";
2
+ import { getSlack } from "@/lib/slack";
3
3
 
4
+ /**
5
+ * @deprecated use upsertSlackMessage from gh-desktop-release-notification/upsertSlackMessage.ts
6
+ */
4
7
  export async function postSlackMessage(text: string) {
5
8
  const channel = process.env.SLACK_BOT_CHANNEL || DIE(new Error("missing env.SLACK_BOT_CHANNEL"));
9
+ const slack = getSlack();
6
10
  // this api will auto retry if failed
7
11
  const response = await slack.chat.postMessage({
8
12
  channel,
package/src/preload.ts CHANGED
@@ -1,30 +1,33 @@
1
- await Bun.plugin({
2
- name: "YAML",
3
- async setup(build) {
4
- const { load } = await import("js-yaml");
1
+ void (async () => {
2
+ await Bun.plugin({
3
+ name: "YAML",
4
+ async setup(build) {
5
+ const { load } = await import("js-yaml");
5
6
 
6
- // when a .yaml file is imported...
7
- build.onLoad({ filter: /\.(yaml|yml)$/ }, async (args) => {
8
- // read and parse the file
9
- const text = await Bun.file(args.path).text();
10
- const exports = load(text) as Record<string, any>;
7
+ // when a .yaml file is imported...
8
+ build.onLoad({ filter: /\.(yaml|yml)$/ }, async (args) => {
9
+ // read and parse the file
10
+ const text = await Bun.file(args.path).text();
11
+ const exports = load(text) as Record<string, unknown>;
11
12
 
12
- // and returns it as a module
13
- return {
14
- exports,
15
- loader: "object", // special loader for JS objects
16
- };
17
- });
18
- },
19
- });
13
+ // and returns it as a module
14
+ return {
15
+ exports,
16
+ loader: "object", // special loader for JS objects
17
+ };
18
+ });
19
+ },
20
+ });
21
+ })();
20
22
 
21
- Bun.plugin({
22
- name: "preload-plugin",
23
- setup(builder) {
24
- builder.onLoad({ filter: /\.ts$/ }, async (args) => {
25
- const text = await Bun.file(args.path).text();
26
- // console.log("text", text);
27
- return { contents: text, loader: args.loader };
28
- });
29
- },
30
- });
23
+ // 2025-08-15 seems unnecessary
24
+ // Bun.plugin({
25
+ // name: "preload-plugin",
26
+ // setup(builder) {
27
+ // builder.onLoad({ filter: /\.ts$/ }, async (args) => {
28
+ // const text = await Bun.file(args.path).text();
29
+ // // console.log("text", text);
30
+ // return { contents: text, loader: args.loader };
31
+ // });
32
+ // },
33
+ // });
@@ -5,7 +5,5 @@ export async function readTemplateTitle(filename: string) {
5
5
  return await readTemplate(filename).then((e) => e.title);
6
6
  }
7
7
  export async function readTemplate(filename: string) {
8
- return readFile("./templates/" + filename, "utf8").then(
9
- parseTitleBodyOfMarkdown,
10
- );
8
+ return readFile(filename, "utf8").then(parseTitleBodyOfMarkdown);
11
9
  }