comfy-pr 0.2.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.
Files changed (73) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +225 -0
  3. package/next-env.d.ts +5 -0
  4. package/package.json +115 -0
  5. package/src/CMNodes.ts +60 -0
  6. package/src/CNRepos.ts +72 -0
  7. package/src/CRNodes.ts +26 -0
  8. package/src/CRPulls.ts +4 -0
  9. package/src/FORK_OWNER.ts +5 -0
  10. package/src/FORK_PREFIX.ts +5 -0
  11. package/src/FollowRules.ts +25 -0
  12. package/src/GIT_USEREMAIL.ts +5 -0
  13. package/src/GIT_USERNAME.ts +9 -0
  14. package/src/GithubIssueComments.ts +3 -0
  15. package/src/PushedBranch.ts +3 -0
  16. package/src/Totals.ts +9 -0
  17. package/src/TrackingPRs.ts +12 -0
  18. package/src/WorkerInstances.ts +89 -0
  19. package/src/analyzePullsStatus.ts +115 -0
  20. package/src/analyzeTotals.test.ts +5 -0
  21. package/src/analyzeTotals.ts +114 -0
  22. package/src/checkComfyActivated.ts +39 -0
  23. package/src/cli.test.ts +0 -0
  24. package/src/cli.ts +40 -0
  25. package/src/clone_modify_push_Branches.ts +21 -0
  26. package/src/createComfyRegistryPRsFromCandidates.ts +55 -0
  27. package/src/createComfyRegistryPullRequests.ts +22 -0
  28. package/src/createGithubForkForRepo.ts +37 -0
  29. package/src/createGithubPullRequest.ts +94 -0
  30. package/src/createIssueComment.ts +29 -0
  31. package/src/fetchCMNodes.ts +22 -0
  32. package/src/fetchComfyRegistryNodes.ts +11 -0
  33. package/src/fetchCurrentGeoInfo.ts +6 -0
  34. package/src/fetchRelatedPullWithComments.ts +15 -0
  35. package/src/fetchRepoDescriptionMap.ts +15 -0
  36. package/src/followRuleSchema.ts +69 -0
  37. package/src/getActivatedShell.ts +18 -0
  38. package/src/getBranchWorkingDir.ts +16 -0
  39. package/src/getRepoWorkingDir.ts +6 -0
  40. package/src/ghUser.ts +6 -0
  41. package/src/index.ts +22 -0
  42. package/src/initializeFollowRules.ts +26 -0
  43. package/src/makePublishBranch.ts +58 -0
  44. package/src/makeTomlBranch.ts +53 -0
  45. package/src/matchRelatedPulls.ts +25 -0
  46. package/src/muteInstances.ts +16 -0
  47. package/src/parseIssueUrl.ts +6 -0
  48. package/src/parseOwnerRepo.ts +33 -0
  49. package/src/parsePullUrl.ts +6 -0
  50. package/src/parsePullsState.ts +6 -0
  51. package/src/parseTitleBodyOfMarkdown.ts +8 -0
  52. package/src/postSlackMessage.ts +21 -0
  53. package/src/preload.ts +30 -0
  54. package/src/pullStatusFollowSchema.ts +12 -0
  55. package/src/readTemplateTitle.ts +11 -0
  56. package/src/summaryLastPullComment.ts +8 -0
  57. package/src/tomlFillDescription.ts +17 -0
  58. package/src/updateCMNodesDuplicationWarnings.ts +56 -0
  59. package/src/updateCMRepos.ts +27 -0
  60. package/src/updateCNRepos.ts +58 -0
  61. package/src/updateCNReposCRPullsComments.ts +41 -0
  62. package/src/updateCNReposInfo.ts +57 -0
  63. package/src/updateCNReposPRCandidate.ts +85 -0
  64. package/src/updateCNReposPulls.ts +28 -0
  65. package/src/updateCNReposPullsDashboard.ts +49 -0
  66. package/src/updateCNReposRelatedPulls.ts +25 -0
  67. package/src/updateCRNodes.ts +50 -0
  68. package/src/updateCRRepos.ts +26 -0
  69. package/src/updateComfyTotals.ts +40 -0
  70. package/src/updateFollowRuleSet.ts +153 -0
  71. package/src/updateOutdatedPullsTemplates.ts +137 -0
  72. package/src/updateSlackMessages.ts +43 -0
  73. package/tailwind.config.ts +20 -0
@@ -0,0 +1,22 @@
1
+ import { fetchJson } from "./utils/fetchJson";
2
+
3
+ if (import.meta.main) {
4
+ console.log(await fetchCMNodes());
5
+ }
6
+ export async function fetchCMNodes() {
7
+ const customNodeListSource =
8
+ process.env.CUSTOM_LIST_SOURCE ||
9
+ "https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/custom-node-list.json";
10
+ const nodeList = (await fetchJson(customNodeListSource)) as {
11
+ custom_nodes: {
12
+ author: "Dr.Lt.Data" | string;
13
+ title: "ComfyUI-Manager" | string;
14
+ id: "manager" | string;
15
+ reference: "https://github.com/ltdrdata/ComfyUI-Manager" | string;
16
+ files: ["https://github.com/ltdrdata/ComfyUI-Manager"] | string[];
17
+ install_type: "git-clone" | string;
18
+ description: "ComfyUI-Manager itself is also a custom node." | string;
19
+ }[];
20
+ };
21
+ return nodeList.custom_nodes;
22
+ }
@@ -0,0 +1,11 @@
1
+ import type { mockPublishedNodes } from "../mocks/mockPublishedNodes";
2
+ import DIE from "@snomiao/die";
3
+ import { fetchJson } from "./utils/fetchJson";
4
+
5
+ export async function fetchCRNodes() {
6
+ const r = (await fetchJson<typeof mockPublishedNodes>(
7
+ "https://api.comfy.org/nodes?page=1&limit=99999999"
8
+ )) as typeof mockPublishedNodes;
9
+ r.totalPages === 1 || DIE("FAIL TO FETCH ALL NODES");
10
+ return r.nodes;
11
+ }
@@ -0,0 +1,6 @@
1
+ import { fetchJson } from "./utils/fetchJson";
2
+ export async function fetchCurrentGeoInfo() {
3
+ const { query, city, iat, lon, countryCode, region, regionName } = (await fetchJson("http://ip-api.com/json")) as any;
4
+ const geo = { ip: query, city, iat, lon, countryCode, region, regionName };
5
+ return geo;
6
+ }
@@ -0,0 +1,15 @@
1
+ import pMap from "p-map";
2
+ import { fetchIssueComments } from "./gh/fetchIssueComments";
3
+ import { matchRelatedPulls } from "./matchRelatedPulls";
4
+ import type { GithubPullParsed } from "./parsePullsState";
5
+ import { summaryLastPullComment } from "./summaryLastPullComment";
6
+ /** @deprecated */
7
+ export async function fetchRelatedPullWithComments(repository: string, pulls: GithubPullParsed[]) {
8
+ const relatedPulls = await matchRelatedPulls(pulls);
9
+ const relatedPullsWithComment = await pMap(relatedPulls, async (data) => {
10
+ const comments = await fetchIssueComments(repository, data.pull);
11
+ const lastText = summaryLastPullComment(comments);
12
+ return { ...data, comments, lastText };
13
+ });
14
+ return relatedPullsWithComment;
15
+ }
@@ -0,0 +1,15 @@
1
+ import { fromPairs } from "rambda";
2
+ import { fetchCMNodes } from "./fetchCMNodes";
3
+
4
+
5
+ export async function fetchRepoDescriptionMap() {
6
+ const nodeList = await fetchCMNodes();
7
+ const repoDescriptionMap = fromPairs(
8
+ nodeList.map((e) => [e.reference, e.description])
9
+ );
10
+ repoDescriptionMap["https://github.com/snomiao/ComfyNode-Registry-test"] =
11
+ "ComfyNode-Registry-test-description";
12
+
13
+ console.log("Fetched " + nodeList.length + " CustomNode descriptions");
14
+ return repoDescriptionMap;
15
+ }
@@ -0,0 +1,69 @@
1
+ import { $before, $fresh, $stale } from "@/packages/mongodb-pipeline-ts/$fresh";
2
+ import { tryCatch } from "rambda";
3
+ import { z } from "zod";
4
+
5
+ const znot_include = z.object({ "not-include": z.string() }).transform((x) => ({ $nin: x["not-include"] }));
6
+ const mString = z.string().transform((x) =>
7
+ tryCatch<string, string | RegExp>(
8
+ (x) => new RegExp(x) as RegExp,
9
+ (x) => x as string | RegExp,
10
+ )(x),
11
+ );
12
+ const zbefore = z
13
+ .object({ $before: z.coerce.date().or(z.string()).or(z.number()) })
14
+ .transform((x) => $before(x.$before));
15
+ const zfresh = z.object({ $fresh: z.coerce.date().or(z.string()).or(z.number()) }).transform((x) => $fresh(x.$fresh));
16
+ const mstale = z.object({ $stale: z.coerce.date().or(z.string()).or(z.number()) }).transform((x) => $stale(x.$stale));
17
+ const mDate = z.date().or(zbefore).or(zfresh).or(mstale);
18
+ const mAny = z.number().or(znot_include).or(mString).or(zbefore).or(zfresh);
19
+ const mNumber = z
20
+ .number()
21
+ .or(z.object({ $gt: z.number() }))
22
+ .or(z.object({ $lt: z.number() }))
23
+ .or(z.object({ $eq: z.number() }))
24
+ .or(z.object({ $ne: z.number() }))
25
+ .or(z.object({ $gte: z.number() }))
26
+ .or(z.object({ $lte: z.number() }));
27
+ export const zAddCommentAction = z
28
+ .object({
29
+ by: z.string(),
30
+ body: z.string(),
31
+ })
32
+ .strict();
33
+ const zFollowUpRule = z.object({
34
+ name: z.string(),
35
+ $match: z
36
+ .object({
37
+ state: z.enum(["OPEN", "CLOSED", "MERGED"]),
38
+ on_registry: z.boolean(),
39
+ updated_at: mDate,
40
+ lastwords: mString,
41
+ head: mString,
42
+ comments: mNumber,
43
+ })
44
+ .strict()
45
+ .partial(),
46
+ action: z
47
+ .object({
48
+ "add-comment": zAddCommentAction,
49
+ "send-email": z
50
+ .object({
51
+ provider: z.string(),
52
+ from: z.string(),
53
+ to: z.string(),
54
+ subject: z.string(),
55
+ body: z.string(),
56
+ })
57
+ .strict(), // WARN: not implementd
58
+ "update-issue": z
59
+ .object({
60
+ tags: mAny,
61
+ })
62
+ .strict(),
63
+ // close: z.any(),
64
+ })
65
+ .partial()
66
+ .strict(),
67
+ });
68
+ // zPullsStatus
69
+ export const zFollowUpRules = zFollowUpRule.array();
@@ -0,0 +1,18 @@
1
+ import { $ as zx } from "zx";
2
+ import { getActivateCMD } from "./cli/getActivateCMD";
3
+
4
+ if (import.meta.main) {
5
+ // await checkComfyActivated();
6
+ zx.verbose = true;
7
+ const $ = getActivatedShell();
8
+ const p = await $`comfy-cli --version`;
9
+ console.log(p.stdout);
10
+ // zx({ prefix: })`comfy-cli --help`;
11
+ }
12
+
13
+ export function getActivatedShell() {
14
+ const activate = getActivateCMD();
15
+ return zx({
16
+ prefix: `echo Comfy CLI version: $(comfy-cli --version) || (apt-get install -y python3-venv && python -m venv .venv && ${activate} && pip install comfy-cli); `,
17
+ });
18
+ }
@@ -0,0 +1,16 @@
1
+ import { rm } from "fs/promises";
2
+ import { getRepoWorkingDir } from "./getRepoWorkingDir";
3
+ import { parseUrlRepoOwner } from "./parseOwnerRepo";
4
+
5
+ export async function getBranchWorkingDir(
6
+ upstreamUrl: string,
7
+ forkUrl: string,
8
+ branch: string
9
+ ) {
10
+ const src = parseUrlRepoOwner(upstreamUrl);
11
+ const dir = getRepoWorkingDir(forkUrl);
12
+ const packageName = src.repo;
13
+ const cwd = `${dir}/${branch}/${packageName}`;
14
+ await rm(cwd, { recursive: true }).catch(() => null);
15
+ return cwd;
16
+ }
@@ -0,0 +1,6 @@
1
+ import { parseUrlRepoOwner } from "./parseOwnerRepo";
2
+
3
+ export function getRepoWorkingDir(forkUrl: string) {
4
+ return `prs/${parseUrlRepoOwner(forkUrl).repo}`;
5
+ }
6
+
package/src/ghUser.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { gh } from "./gh";
2
+
3
+ export const ghUser = (await gh.users.getAuthenticated()).data;
4
+
5
+ console.log("Fetch Current Github User...");
6
+ console.log(`Current Github User: ${ghUser.login} <${ghUser.email}>`);
package/src/index.ts ADDED
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env bun
2
+ import "dotenv/config";
3
+ import { $ as zx } from "zx";
4
+ import { checkComfyActivated } from "./checkComfyActivated";
5
+ import { initializeFollowRules } from "./initializeFollowRules";
6
+ import { updateCNRepos } from "./updateCNRepos";
7
+ import { runFollowRuleSet } from "./updateFollowRuleSet";
8
+ import { updateSlackMessages } from "./updateSlackMessages";
9
+
10
+ if (import.meta.main) {
11
+ zx.verbose = true;
12
+ await Promise.all([
13
+ // try send msgs that didn't send in last run
14
+ updateSlackMessages(),
15
+ checkComfyActivated(), // needed if make pr
16
+ initializeFollowRules(),
17
+ runFollowRuleSet(),
18
+ updateCNRepos(),
19
+ ]);
20
+ console.log("All done");
21
+ process.exit(0);
22
+ }
@@ -0,0 +1,26 @@
1
+ import DIE from "@snomiao/die";
2
+ import { readFile } from "fs/promises";
3
+ import { FollowRuleSets } from "./FollowRules";
4
+
5
+ if (import.meta.main) {
6
+ const followRules = await initializeFollowRules();
7
+ const ruleset = (await FollowRuleSets.findOne({ name: "default" })) ?? DIE("default ruleset not found");
8
+ if (!ruleset?.enabled) DIE("default ruleset not enabled");
9
+
10
+ console.log("initialized follow rules");
11
+ }
12
+ /**
13
+ *
14
+ * @author: snomiao <snomiao@gmail.com>
15
+ */
16
+ export async function initializeFollowRules() {
17
+ return await updateFollowRules("default", await readFile("./templates/follow-rules.yaml", "utf8"));
18
+ }
19
+
20
+ export async function updateFollowRules(name: string, rawRuleYaml: string) {
21
+ return (await FollowRuleSets.findOneAndUpdate(
22
+ { name },
23
+ { $setOnInsert: { yaml: rawRuleYaml, enabled: false } },
24
+ { upsert: true, returnDocument: "after" },
25
+ ))!;
26
+ }
@@ -0,0 +1,58 @@
1
+ import { readFile } from "fs/promises";
2
+ import { dirname } from "path";
3
+ import { GIT_USEREMAIL } from "./GIT_USEREMAIL";
4
+ import { GIT_USERNAME } from "./GIT_USERNAME";
5
+ import { $ } from "./cli/echoBunShell";
6
+ import { getBranchWorkingDir } from "./getBranchWorkingDir";
7
+ import { gh } from "./gh";
8
+ import { parseUrlRepoOwner, stringifyGithubOrigin, stringifyGithubRepoUrl } from "./parseOwnerRepo";
9
+ import { parseTitleBodyOfMarkdown } from "./parseTitleBodyOfMarkdown";
10
+
11
+ /**
12
+ * Clone from upstream
13
+ * push to fork url
14
+ * @param dir
15
+ * @param upstreamUrl
16
+ * @param origin
17
+ * @returns
18
+ */
19
+ export async function makePublishcrBranch(upstreamUrl: string, forkUrl: Readonly<string>) {
20
+ const type = "publishcr" as const;
21
+
22
+ const origin = await stringifyGithubOrigin(parseUrlRepoOwner(forkUrl));
23
+ const branch = "publish";
24
+ const tmpl = await readFile("./templates/add-action.md", "utf8");
25
+ const { title, body } = parseTitleBodyOfMarkdown(tmpl);
26
+ const repo = parseUrlRepoOwner(origin);
27
+
28
+ if (await gh.repos.getBranch({ ...repo, branch }).catch(() => null)) {
29
+ console.log("Skip changes as branch existed: " + branch);
30
+ return { type, title, body, branch };
31
+ }
32
+
33
+ const cwd = await getBranchWorkingDir(upstreamUrl, forkUrl, branch);
34
+
35
+ const file = `${cwd}/.github/workflows/publish.yml`;
36
+ const publishYmlPath = "./templates/publish.yaml";
37
+
38
+ // commit & push changes
39
+ await $`
40
+ git clone ${upstreamUrl} ${cwd}
41
+
42
+ mkdir -p ${dirname(file)}
43
+ cat ${publishYmlPath} > ${file}
44
+
45
+ cd ${cwd}
46
+
47
+ git config user.name ${GIT_USERNAME} && \
48
+ git config user.email ${GIT_USEREMAIL} && \
49
+ git checkout -b ${branch} && \
50
+ git add . && \
51
+ git commit -am "chore(${branch}): ${title}" && \
52
+ git push "${origin}" ${branch}:${branch}
53
+ `;
54
+
55
+ const branchUrl = `${stringifyGithubRepoUrl(repo)}/tree/${branch}`;
56
+ console.log(`Branch Push OK: ${branchUrl}`);
57
+ return { type, title, body, branch };
58
+ }
@@ -0,0 +1,53 @@
1
+ import { readFile } from "fs/promises";
2
+ import { GIT_USEREMAIL } from "./GIT_USEREMAIL";
3
+ import { GIT_USERNAME } from "./GIT_USERNAME";
4
+ import { $ } from "./cli/echoBunShell";
5
+ import { getBranchWorkingDir } from "./getBranchWorkingDir";
6
+ import { gh } from "./gh";
7
+ import { parseUrlRepoOwner, stringifyGithubOrigin } from "./parseOwnerRepo";
8
+ import { parseTitleBodyOfMarkdown } from "./parseTitleBodyOfMarkdown";
9
+ import { tomlFillDescription } from "./tomlFillDescription";
10
+
11
+ export async function makePyprojectBranch(upstreamUrl: string, forkUrl: string) {
12
+ const type = "pyproject" as const;
13
+ const origin = await stringifyGithubOrigin(parseUrlRepoOwner(forkUrl));
14
+ const branch = "pyproject";
15
+ const tmpl = await readFile("./templates/add-toml.md", "utf8");
16
+ const { title, body } = parseTitleBodyOfMarkdown(tmpl);
17
+ const repo = parseUrlRepoOwner(forkUrl);
18
+
19
+ if (await gh.repos.getBranch({ ...repo, branch }).catch(() => null)) {
20
+ console.log("Skip changes as branch existed: " + branch);
21
+ return { type, title, body, branch };
22
+ }
23
+ const src = parseUrlRepoOwner(upstreamUrl);
24
+ const cwd = await getBranchWorkingDir(upstreamUrl, forkUrl, branch);
25
+
26
+ // commit changes
27
+ await $`
28
+ git clone ${upstreamUrl} ${cwd}
29
+
30
+ cd ${cwd}
31
+ echo N | comfy node init
32
+ `;
33
+
34
+ // Try fill description from ComfyUI-manager
35
+ const referenceUrl = `https://github.com/${src.owner}/${src.repo}`;
36
+ const pyprojectToml = cwd + "/pyproject.toml";
37
+ await tomlFillDescription(referenceUrl, pyprojectToml).catch((e) => {
38
+ console.error(e);
39
+ });
40
+
41
+ await $`
42
+ cd ${cwd}
43
+ git config user.name ${GIT_USERNAME} && \
44
+ git config user.email ${GIT_USEREMAIL} && \
45
+ git checkout -b ${branch} && \
46
+ git add . && \
47
+ git commit -am ${`chore(${branch}): ${title}`} && \
48
+ git push "${origin}" ${branch}:${branch}
49
+ `;
50
+ const branchUrl = `https://github.com/${repo.owner}/${repo.repo}/tree/${branch}`;
51
+ console.log(`Branch Push OK: ${branchUrl}`);
52
+ return { type, title, body, branch };
53
+ }
@@ -0,0 +1,25 @@
1
+ import pMap from "p-map";
2
+ import { match } from "ts-pattern";
3
+ import { fetchRelatedPullWithComments } from "./fetchRelatedPullWithComments";
4
+ import type { GithubPullParsed } from "./parsePullsState";
5
+ import { readTemplateTitle } from "./readTemplateTitle";
6
+
7
+ export type RelatedPullsWithComments = Awaited<ReturnType<typeof fetchRelatedPullWithComments>>;
8
+ export type RelatedPull = Awaited<ReturnType<typeof matchRelatedPulls>>[number];
9
+ export async function matchRelatedPulls(pulls: GithubPullParsed[]) {
10
+ const pyproject = await readTemplateTitle("add-toml.md");
11
+ const publishcr = await readTemplateTitle("add-action.md");
12
+ const relatedPulls = await pMap(pulls, async (pull) =>
13
+ match(pull)
14
+ .with({ title: pyproject }, (pull) => ({
15
+ type: "pyproject" as const,
16
+ pull,
17
+ }))
18
+ .with({ title: publishcr }, (pull) => ({
19
+ type: "publishcr" as const,
20
+ pull,
21
+ }))
22
+ .otherwise(() => null),
23
+ );
24
+ return relatedPulls.flatMap((e) => (e ? [e] : []));
25
+ }
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env bun
2
+ import { readFile, writeFile } from "fs/promises";
3
+ import { globby } from "globby";
4
+ import pMap from "p-map";
5
+ if (import.meta.main) {
6
+ const files = await globby("node_modules/**/*instance*.js");
7
+ await pMap(files, async (file) => {
8
+ const x = await readFile(file, "utf8");
9
+ const y = x.replace(/console.warn/, ";");
10
+ if (x !== y) {
11
+ await writeFile(file, y);
12
+ console.log("muted: " + file);
13
+ }
14
+ });
15
+ console.log("done");
16
+ }
@@ -0,0 +1,6 @@
1
+ export function parseIssueUrl(issueUrl: string) {
2
+ const [owner, repo, strNumber] = issueUrl
3
+ .match(/^https:\/\/github\.com\/([\w-]+)\/([\w-]+)\/(?:pull|issues)\/(\d+)$/)!
4
+ .slice(1);
5
+ return { owner, repo, issue_number: Number(strNumber) };
6
+ }
@@ -0,0 +1,33 @@
1
+ import { basename, dirname } from "path";
2
+
3
+ /**
4
+ * Parse owner and repo obj
5
+ * @param gitUrl git@github.ocm:owner/repo or https://github.ocm/owner/repo
6
+ */
7
+ export function parseUrlRepoOwner(gitUrl: string) {
8
+ return {
9
+ owner: basename(dirname(gitUrl.replace(/:/, "/"))),
10
+ repo: basename(gitUrl.replace(/:/, "/")).replace(/\.git$/, ""),
11
+ };
12
+ }
13
+ export function stringifyOwnerRepo({ owner, repo }: ReturnType<typeof parseUrlRepoOwner>) {
14
+ return owner + "/" + repo;
15
+ }
16
+ export function stringifyGithubRepoUrl({ owner, repo }: ReturnType<typeof parseUrlRepoOwner>) {
17
+ return "https://github.com/" + owner + "/" + repo;
18
+ }
19
+ export async function stringifyGithubOrigin({ owner, repo }: ReturnType<typeof parseUrlRepoOwner>) {
20
+ const PR_TOKEN = process.env.GH_TOKEN_COMFY_PR;
21
+ if (PR_TOKEN) {
22
+ // fails: maybe permission issue
23
+ // const USERNAME = (
24
+ // await new Octokit({
25
+ // auth: PR_TOKEN,
26
+ // }).rest.users.getAuthenticated()
27
+ // ).data.login;
28
+ // return `https://${USERNAME}:${PR_TOKEN}@github.com/${owner}/${repo}`;
29
+
30
+ return `git@github.com:${owner}/${repo}`;
31
+ }
32
+ return `git@github.com:${owner}/${repo}`;
33
+ }
@@ -0,0 +1,6 @@
1
+ export function parsePullUrl(issueUrl: string) {
2
+ const [owner, repo, strNumber] = issueUrl
3
+ .match(/^https:\/\/github\.com\/([\w-]+)\/([\w-]+)\/(?:pull)\/(\d+)$/)!
4
+ .slice(1);
5
+ return { owner, repo, pull_number: Number(strNumber) };
6
+ }
@@ -0,0 +1,6 @@
1
+ import type { GithubPull } from "./gh/GithubPull";
2
+ import { parsePull } from "./gh/parsePull";
3
+ export type GithubPullParsed = ReturnType<typeof parsePulls>[number];
4
+ export function parsePulls(data: GithubPull[]) {
5
+ return data.map((e) => parsePull(e));
6
+ }
@@ -0,0 +1,8 @@
1
+ import DIE from "@snomiao/die";
2
+
3
+ export function parseTitleBodyOfMarkdown(tmpl: string) {
4
+ tmpl.startsWith("# ") || DIE("Unrecognized template format:" + tmpl);
5
+ const title = tmpl.split("\n")[0].slice(1).trim();
6
+ const body = tmpl.split("\n").slice(1).join("\n").trim();
7
+ return { title, body };
8
+ }
@@ -0,0 +1,21 @@
1
+ import DIE from "@snomiao/die";
2
+ import { slack } from "./slack";
3
+
4
+ export async function postSlackMessage(text: string) {
5
+ const channel = process.env.SLACK_BOT_CHANNEL || DIE(new Error("missing env.SLACK_BOT_CHANNEL"));
6
+ // this api will auto retry if failed
7
+ const response = await slack.chat.postMessage({
8
+ channel,
9
+ text,
10
+ blocks: [
11
+ {
12
+ type: "section",
13
+ text: {
14
+ type: "mrkdwn",
15
+ text: text,
16
+ },
17
+ },
18
+ ],
19
+ });
20
+ return { channel, ts: response.ts, text };
21
+ }
package/src/preload.ts ADDED
@@ -0,0 +1,30 @@
1
+ await Bun.plugin({
2
+ name: "YAML",
3
+ async setup(build) {
4
+ const { load } = await import("js-yaml");
5
+
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>;
11
+
12
+ // and returns it as a module
13
+ return {
14
+ exports,
15
+ loader: "object", // special loader for JS objects
16
+ };
17
+ });
18
+ },
19
+ });
20
+
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
+ });
@@ -0,0 +1,12 @@
1
+ import { z } from "zod";
2
+
3
+ export const zPullStatusFollow = z.array(
4
+ z.object({
5
+ updated: z.string(),
6
+ state: z.string(),
7
+ url: z.string(),
8
+ head: z.string(),
9
+ comments: z.number(),
10
+ lastwords: z.string(),
11
+ }),
12
+ );
@@ -0,0 +1,11 @@
1
+ import { readFile } from "fs/promises";
2
+ import { parseTitleBodyOfMarkdown } from "./parseTitleBodyOfMarkdown";
3
+
4
+ export async function readTemplateTitle(filename: string) {
5
+ return await readTemplate(filename).then((e) => e.title);
6
+ }
7
+ export async function readTemplate(filename: string) {
8
+ return readFile("./templates/" + filename, "utf8").then(
9
+ parseTitleBodyOfMarkdown,
10
+ );
11
+ }
@@ -0,0 +1,8 @@
1
+ import { type GithubIssueComment } from "./GithubIssueComments";
2
+
3
+ export function summaryLastPullComment(comments: GithubIssueComment[]) {
4
+ // assume ascending order
5
+ const last = comments.toReversed()[0];
6
+ const lastText = (last ?? "") && "@" + last.user!.name + ":" + last.body!.replace(/\s+/gim, " ");
7
+ return lastText;
8
+ }
@@ -0,0 +1,17 @@
1
+ import { writeFile, readFile } from "fs/promises";
2
+ import DIE from "@snomiao/die";
3
+ import toml from "toml";
4
+ import { fetchRepoDescriptionMap } from "./fetchRepoDescriptionMap";
5
+
6
+ export async function tomlFillDescription(referenceUrl: string, pyprojectToml: string) {
7
+ const repoDescriptionMap = await fetchRepoDescriptionMap();
8
+ const matchedDescription = repoDescriptionMap[referenceUrl]?.toString() ||
9
+ DIE("Warn: missing description for " + referenceUrl);
10
+ const replaced = (await readFile(pyprojectToml, "utf8")).replace(
11
+ `description = ""`,
12
+ `description = ${JSON.stringify(matchedDescription)}`
13
+ );
14
+ // check validity
15
+ toml.parse(replaced);
16
+ await writeFile(pyprojectToml, replaced);
17
+ }
@@ -0,0 +1,56 @@
1
+ import { unary } from "lodash-es";
2
+ import pMap from "p-map";
3
+ import { dissoc, filter, groupBy, map, prop, toPairs } from "rambda";
4
+ import YAML from "yaml";
5
+ import { CMNodes, type CMNode } from "./CMNodes";
6
+ import { notifySlack } from "./slack/notifySlack";
7
+
8
+ export async function updateCMNodesDuplicationWarnings(nodes: CMNode[]) {
9
+ console.log("CMNodes checking duplicates");
10
+ // prettier-ignore
11
+ const dups = {
12
+ ID: filter((e: typeof nodes) => e.length > 1, groupBy((e) => e.id, nodes)),
13
+ TITLE: filter((e: typeof nodes) => e.length > 1, groupBy((e) => e.title, nodes)),
14
+ REFERENCE: filter((e: typeof nodes) => e.length > 1, groupBy((e) => e.reference, nodes)),
15
+ };
16
+ const dupsSummary = JSON.stringify(map((x) => map((x) => x.length, x), dups));
17
+ await notifySlack(
18
+ `[WARN] CMNodes duplicates: ${dupsSummary}\nSolve them in https://github.com/ltdrdata/ComfyUI-Manager/blob/main/custom-node-list.json`,
19
+ );
20
+
21
+ await pMap(
22
+ toPairs(dups),
23
+ async ([topic, nodes]) =>
24
+ await pMap(
25
+ toPairs(nodes),
26
+ async ([key, nodesRaw]) => {
27
+ const nodes = nodesRaw.map(unary(dissoc("hash")));
28
+ const hashes = nodesRaw.map(prop("hash"));
29
+ // check sent
30
+ const someDuplicateSent = await CMNodes.findOne({
31
+ hash: { $in: hashes },
32
+ [`duplicated.${topic}`]: { $exists: true },
33
+ });
34
+ if (someDuplicateSent) return;
35
+ // send slack notification
36
+ const slackNotification = await notifySlack(
37
+ `[ACTION NEEDED WARNING]: please resolve duplicated node in ${topic}: ${key}\n` +
38
+ "```\n" +
39
+ YAML.stringify(nodes) +
40
+ "```" +
41
+ "\n\nSolve them in https://github.com/ltdrdata/ComfyUI-Manager/blob/main/custom-node-list.json",
42
+ { unique: true },
43
+ );
44
+ // mark duplicates
45
+ await CMNodes.updateMany(
46
+ { hash: { $in: hashes } },
47
+ {
48
+ $set: { [`duplicated.${topic}`]: { hashes, slackNotification } },
49
+ },
50
+ );
51
+ },
52
+ { concurrency: 2 },
53
+ ),
54
+ { concurrency: 2 },
55
+ );
56
+ }
@@ -0,0 +1,27 @@
1
+ import pMap from "p-map";
2
+ import { CMNodes } from "./CMNodes";
3
+ import { CNRepos } from "./CNRepos";
4
+ import { tLog } from "./utils/tLog";
5
+ if (import.meta.main) {
6
+ await tLog("Update Repos from ComfyUI Manager", updateCMRepos);
7
+ console.log(await CMNodes.estimatedDocumentCount());
8
+ }
9
+ export async function updateCMRepos() {
10
+ await CMNodes.createIndex({ repo_id: 1 });
11
+ return await pMap(CMNodes.find({ repo_id: { $exists: false } }), async (cm, i) => {
12
+ const { reference: repository, _id } = cm;
13
+ return await CNRepos.updateOne(
14
+ { repository },
15
+ {
16
+ $set: {
17
+ cm: (await CMNodes.findOneAndUpdate(
18
+ { _id },
19
+ { $set: { repo_id: cm._id } },
20
+ { upsert: true, returnDocument: "after" },
21
+ ))!,
22
+ },
23
+ },
24
+ { upsert: true },
25
+ );
26
+ });
27
+ }