comfy-pr 0.2.27 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +1219 -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 +1499 -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 +164 -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
@@ -0,0 +1,163 @@
1
+ import { describe, test, expect, beforeEach, afterEach, mock } from "bun:test";
2
+ import { RestartManager } from "./RestartManager";
3
+ import { writeFile, mkdir, rm } from "fs/promises";
4
+ import { join } from "path";
5
+
6
+ describe("RestartManager", () => {
7
+ let testDir: string;
8
+ let restartManager: RestartManager;
9
+ let isIdleMock: ReturnType<typeof mock>;
10
+ let onRestartMock: ReturnType<typeof mock>;
11
+
12
+ beforeEach(async () => {
13
+ // Create a temporary test directory
14
+ testDir = join(process.cwd(), ".test-restart-manager");
15
+ await mkdir(testDir, { recursive: true });
16
+
17
+ // Create mocks
18
+ isIdleMock = mock(() => true);
19
+ onRestartMock = mock(() => {});
20
+ });
21
+
22
+ afterEach(async () => {
23
+ // Clean up
24
+ if (restartManager) {
25
+ restartManager.stop();
26
+ }
27
+ await rm(testDir, { recursive: true, force: true });
28
+ });
29
+
30
+ test("should start watching directories", () => {
31
+ restartManager = new RestartManager({
32
+ watchPaths: [testDir],
33
+ isIdle: isIdleMock,
34
+ onRestart: onRestartMock,
35
+ debounceDelay: 100,
36
+ idleCheckInterval: 100,
37
+ });
38
+
39
+ restartManager.start();
40
+ // If no error is thrown, the test passes
41
+ expect(true).toBe(true);
42
+ });
43
+
44
+ test("should queue restart when file changes and bot is idle", async () => {
45
+ restartManager = new RestartManager({
46
+ watchPaths: [testDir],
47
+ isIdle: isIdleMock,
48
+ onRestart: onRestartMock,
49
+ debounceDelay: 100,
50
+ idleCheckInterval: 100,
51
+ });
52
+
53
+ restartManager.start();
54
+
55
+ // Manually trigger file change (simulating what fs.watch would do)
56
+ // @ts-ignore - accessing private method for testing
57
+ restartManager.onFileChange("test.ts");
58
+
59
+ // Wait for debounce + idle check
60
+ await new Promise((resolve) => setTimeout(resolve, 300));
61
+
62
+ // Should have called onRestart since bot is idle
63
+ expect(onRestartMock).toHaveBeenCalled();
64
+ });
65
+
66
+ test("should wait for idle state before restarting", async () => {
67
+ let isIdle = false;
68
+ isIdleMock = mock(() => isIdle);
69
+
70
+ restartManager = new RestartManager({
71
+ watchPaths: [testDir],
72
+ isIdle: isIdleMock,
73
+ onRestart: onRestartMock,
74
+ debounceDelay: 100,
75
+ idleCheckInterval: 100,
76
+ });
77
+
78
+ restartManager.start();
79
+
80
+ // Manually trigger file change
81
+ // @ts-ignore - accessing private method for testing
82
+ restartManager.onFileChange("test.ts");
83
+
84
+ // Wait for debounce
85
+ await new Promise((resolve) => setTimeout(resolve, 200));
86
+
87
+ // Should not have restarted yet (bot is busy)
88
+ expect(onRestartMock).not.toHaveBeenCalled();
89
+
90
+ // Make bot idle
91
+ isIdle = true;
92
+
93
+ // Wait for idle check
94
+ await new Promise((resolve) => setTimeout(resolve, 200));
95
+
96
+ // Should have restarted now
97
+ expect(onRestartMock).toHaveBeenCalled();
98
+ });
99
+
100
+ test("should ignore markdown files", async () => {
101
+ restartManager = new RestartManager({
102
+ watchPaths: [testDir],
103
+ isIdle: isIdleMock,
104
+ onRestart: onRestartMock,
105
+ debounceDelay: 100,
106
+ idleCheckInterval: 100,
107
+ });
108
+
109
+ restartManager.start();
110
+
111
+ // Create a markdown file (should be ignored)
112
+ const mdFile = join(testDir, "README.md");
113
+ await writeFile(mdFile, "# Test");
114
+
115
+ // Wait for debounce + idle check
116
+ await new Promise((resolve) => setTimeout(resolve, 300));
117
+
118
+ // Should NOT have called onRestart (markdown files are ignored)
119
+ expect(onRestartMock).not.toHaveBeenCalled();
120
+ });
121
+
122
+ test("should debounce multiple file changes", async () => {
123
+ restartManager = new RestartManager({
124
+ watchPaths: [testDir],
125
+ isIdle: isIdleMock,
126
+ onRestart: onRestartMock,
127
+ debounceDelay: 200,
128
+ idleCheckInterval: 100,
129
+ });
130
+
131
+ restartManager.start();
132
+
133
+ // Trigger multiple file changes quickly
134
+ // @ts-ignore - accessing private method for testing
135
+ restartManager.onFileChange("test1.ts");
136
+ await new Promise((resolve) => setTimeout(resolve, 50));
137
+ // @ts-ignore
138
+ restartManager.onFileChange("test2.ts");
139
+ await new Promise((resolve) => setTimeout(resolve, 50));
140
+ // @ts-ignore
141
+ restartManager.onFileChange("test3.ts");
142
+
143
+ // Wait for debounce + idle check
144
+ await new Promise((resolve) => setTimeout(resolve, 400));
145
+
146
+ // Should only restart once despite multiple changes
147
+ expect(onRestartMock).toHaveBeenCalledTimes(1);
148
+ });
149
+
150
+ test("should stop watching when stop() is called", () => {
151
+ restartManager = new RestartManager({
152
+ watchPaths: [testDir],
153
+ isIdle: isIdleMock,
154
+ onRestart: onRestartMock,
155
+ });
156
+
157
+ restartManager.start();
158
+ restartManager.stop();
159
+
160
+ // If no error is thrown, the test passes
161
+ expect(true).toBe(true);
162
+ });
163
+ });
@@ -0,0 +1,190 @@
1
+ import { watch, type FSWatcher } from "fs";
2
+ import { resolve } from "path";
3
+
4
+ /**
5
+ * RestartManager watches for file changes and queues a restart
6
+ * that only executes when the bot is idle (no active tasks).
7
+ *
8
+ * @example
9
+ * const restartManager = new RestartManager({
10
+ * watchPaths: ['bot/**\/*.ts', 'src/**\/*.ts'],
11
+ * isIdle: () => TaskInputFlows.size === 0,
12
+ * onRestart: () => process.exit(0),
13
+ * idleCheckInterval: 5000,
14
+ * debounceDelay: 1000
15
+ * });
16
+ * restartManager.start();
17
+ */
18
+ export class RestartManager {
19
+ private restartPending = false;
20
+ private watchers: FSWatcher[] = [];
21
+ private debounceTimer: Timer | null = null;
22
+ private idleCheckTimer: Timer | null = null;
23
+
24
+ constructor(
25
+ private options: {
26
+ /** Paths to watch for changes (can use glob patterns with Bun) */
27
+ watchPaths: string[];
28
+ /** Function that returns true when the bot is idle */
29
+ isIdle: () => boolean;
30
+ /** Callback to execute the restart (typically process.exit(0)) */
31
+ onRestart: () => void;
32
+ /** How often to check if bot is idle (ms) */
33
+ idleCheckInterval?: number;
34
+ /** Debounce delay for file changes (ms) */
35
+ debounceDelay?: number;
36
+ /** Logger function */
37
+ logger?: {
38
+ info: (msg: string, meta?: unknown) => void;
39
+ warn: (msg: string, meta?: unknown) => void;
40
+ };
41
+ },
42
+ ) {
43
+ this.options.idleCheckInterval ??= 5000;
44
+ this.options.debounceDelay ??= 1000;
45
+ this.options.logger ??= {
46
+ info: (msg) => console.log(`[RestartManager] ${msg}`),
47
+ warn: (msg) => console.warn(`[RestartManager] ${msg}`),
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Start watching for file changes
53
+ */
54
+ start() {
55
+ const { watchPaths, logger } = this.options;
56
+
57
+ logger!.info(`Starting file watcher for ${watchPaths.length} paths`);
58
+
59
+ for (const watchPath of watchPaths) {
60
+ try {
61
+ const absolutePath = resolve(watchPath);
62
+ const watcher = watch(absolutePath, { recursive: true }, (eventType, filename) => {
63
+ logger!.info(`File event: ${eventType} on ${filename}`);
64
+ if (filename) {
65
+ this.onFileChange(filename);
66
+ }
67
+ });
68
+
69
+ this.watchers.push(watcher);
70
+ logger!.info(`Watching: ${absolutePath}`);
71
+ } catch (error) {
72
+ logger!.warn(`Failed to watch ${watchPath}: ${error}`);
73
+ }
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Stop watching and clear pending restart
79
+ */
80
+ stop() {
81
+ const { logger } = this.options;
82
+
83
+ for (const watcher of this.watchers) {
84
+ watcher.close();
85
+ }
86
+ this.watchers = [];
87
+
88
+ if (this.debounceTimer) {
89
+ clearTimeout(this.debounceTimer);
90
+ this.debounceTimer = null;
91
+ }
92
+
93
+ if (this.idleCheckTimer) {
94
+ clearInterval(this.idleCheckTimer);
95
+ this.idleCheckTimer = null;
96
+ }
97
+
98
+ this.restartPending = false;
99
+ logger!.info("File watcher stopped");
100
+ }
101
+
102
+ /**
103
+ * Handle file change event
104
+ */
105
+ private onFileChange(filename: string) {
106
+ const { debounceDelay, logger } = this.options;
107
+
108
+ // Ignore certain files
109
+ if (this.shouldIgnoreFile(filename)) {
110
+ return;
111
+ }
112
+
113
+ logger!.info(`File changed: ${filename}`);
114
+
115
+ // Clear existing debounce timer
116
+ if (this.debounceTimer) {
117
+ clearTimeout(this.debounceTimer);
118
+ }
119
+
120
+ // Debounce file changes
121
+ this.debounceTimer = setTimeout(() => {
122
+ this.queueRestart();
123
+ }, debounceDelay);
124
+ }
125
+
126
+ /**
127
+ * Queue a restart to execute when idle
128
+ */
129
+ private queueRestart() {
130
+ const { logger, isIdle, idleCheckInterval } = this.options;
131
+
132
+ if (this.restartPending) {
133
+ logger!.info("Restart already pending, skipping duplicate queue");
134
+ return;
135
+ }
136
+
137
+ this.restartPending = true;
138
+ logger!.warn("⏳ Restart queued - waiting for bot to become idle...");
139
+
140
+ // Check if already idle
141
+ if (isIdle()) {
142
+ this.executeRestart();
143
+ return;
144
+ }
145
+
146
+ // Start checking for idle state
147
+ this.idleCheckTimer = setInterval(() => {
148
+ if (isIdle()) {
149
+ this.executeRestart();
150
+ } else {
151
+ logger!.info("Bot is busy, waiting for idle state...");
152
+ }
153
+ }, idleCheckInterval);
154
+ }
155
+
156
+ /**
157
+ * Execute the restart
158
+ */
159
+ private executeRestart() {
160
+ const { logger, onRestart } = this.options;
161
+
162
+ if (this.idleCheckTimer) {
163
+ clearInterval(this.idleCheckTimer);
164
+ this.idleCheckTimer = null;
165
+ }
166
+
167
+ logger!.warn("🔄 Bot is idle - restarting now!");
168
+ this.stop();
169
+ onRestart();
170
+ }
171
+
172
+ /**
173
+ * Check if file should be ignored
174
+ */
175
+ private shouldIgnoreFile(filename: string): boolean {
176
+ const ignorePatterns = [
177
+ /node_modules/,
178
+ /\.cache/,
179
+ /\.logs/,
180
+ /\.git/,
181
+ /\.nedb/,
182
+ /\.sqlite/,
183
+ /\.log$/,
184
+ /\.md$/, // Ignore markdown files
185
+ /~$/, // Ignore temp files
186
+ ];
187
+
188
+ return ignorePatterns.some((pattern) => pattern.test(filename));
189
+ }
190
+ }
@@ -0,0 +1,154 @@
1
+ import { describe, test, expect, beforeEach } from "bun:test";
2
+ import Keyv from "keyv";
3
+
4
+ describe("Working Tasks Manager", () => {
5
+ let State: Keyv;
6
+
7
+ beforeEach(() => {
8
+ // Use in-memory store for testing
9
+ State = new Keyv();
10
+ });
11
+
12
+ // Helper functions (same as in bot/index.ts)
13
+ async function addWorkingTask(event: unknown) {
14
+ const workingTasks = ((await State.get("current-working-tasks")) as
15
+ | { workingMessageEvents: unknown[] }
16
+ | undefined) || { workingMessageEvents: [] };
17
+ const events = workingTasks.workingMessageEvents || [];
18
+
19
+ const exists = events.some(
20
+ (e: unknown) =>
21
+ (e as Record<string, unknown>).ts === (event as Record<string, unknown>).ts &&
22
+ (e as Record<string, unknown>).channel === (event as Record<string, unknown>).channel,
23
+ );
24
+ if (!exists) {
25
+ events.push(event);
26
+ await State.set("current-working-tasks", { workingMessageEvents: events });
27
+ }
28
+ }
29
+
30
+ async function removeWorkingTask(event: unknown) {
31
+ const workingTasks = ((await State.get("current-working-tasks")) as
32
+ | { workingMessageEvents: unknown[] }
33
+ | undefined) || { workingMessageEvents: [] };
34
+ const events = workingTasks.workingMessageEvents || [];
35
+
36
+ const filtered = events.filter(
37
+ (e: unknown) =>
38
+ !(
39
+ (e as Record<string, unknown>).ts === (event as Record<string, unknown>).ts &&
40
+ (e as Record<string, unknown>).channel === (event as Record<string, unknown>).channel
41
+ ),
42
+ );
43
+ await State.set("current-working-tasks", { workingMessageEvents: filtered });
44
+ }
45
+
46
+ test("should add task to working list", async () => {
47
+ const event = {
48
+ type: "app_mention" as const,
49
+ user: "U123",
50
+ ts: "1234567890.123456",
51
+ text: "test message",
52
+ team: "T123",
53
+ blocks: [],
54
+ channel: "C123",
55
+ event_ts: "1234567890.123456",
56
+ };
57
+
58
+ await addWorkingTask(event);
59
+
60
+ const workingTasks = (await State.get("current-working-tasks")) as
61
+ | { workingMessageEvents: Array<Record<string, unknown>> }
62
+ | undefined;
63
+ expect(workingTasks).toBeDefined();
64
+ expect(workingTasks?.workingMessageEvents).toHaveLength(1);
65
+ expect(workingTasks?.workingMessageEvents[0].ts).toBe(event.ts);
66
+ });
67
+
68
+ test("should not add duplicate tasks", async () => {
69
+ const event = {
70
+ type: "app_mention" as const,
71
+ user: "U123",
72
+ ts: "1234567890.123456",
73
+ text: "test message",
74
+ team: "T123",
75
+ blocks: [],
76
+ channel: "C123",
77
+ event_ts: "1234567890.123456",
78
+ };
79
+
80
+ await addWorkingTask(event);
81
+ await addWorkingTask(event); // Add same event again
82
+
83
+ const workingTasks = (await State.get("current-working-tasks")) as
84
+ | { workingMessageEvents: unknown[] }
85
+ | undefined;
86
+ expect(workingTasks?.workingMessageEvents).toHaveLength(1);
87
+ });
88
+
89
+ test("should remove task from working list", async () => {
90
+ const event = {
91
+ type: "app_mention" as const,
92
+ user: "U123",
93
+ ts: "1234567890.123456",
94
+ text: "test message",
95
+ team: "T123",
96
+ blocks: [],
97
+ channel: "C123",
98
+ event_ts: "1234567890.123456",
99
+ };
100
+
101
+ await addWorkingTask(event);
102
+ await removeWorkingTask(event);
103
+
104
+ const workingTasks = (await State.get("current-working-tasks")) as
105
+ | { workingMessageEvents: unknown[] }
106
+ | undefined;
107
+ expect(workingTasks?.workingMessageEvents).toHaveLength(0);
108
+ });
109
+
110
+ test("should handle multiple tasks", async () => {
111
+ const event1 = {
112
+ type: "app_mention" as const,
113
+ user: "U123",
114
+ ts: "1234567890.123456",
115
+ text: "test message 1",
116
+ team: "T123",
117
+ blocks: [],
118
+ channel: "C123",
119
+ event_ts: "1234567890.123456",
120
+ };
121
+
122
+ const event2 = {
123
+ type: "app_mention" as const,
124
+ user: "U456",
125
+ ts: "1234567890.654321",
126
+ text: "test message 2",
127
+ team: "T123",
128
+ blocks: [],
129
+ channel: "C456",
130
+ event_ts: "1234567890.654321",
131
+ };
132
+
133
+ await addWorkingTask(event1);
134
+ await addWorkingTask(event2);
135
+
136
+ let workingTasks = (await State.get("current-working-tasks")) as
137
+ | { workingMessageEvents: Array<Record<string, unknown>> }
138
+ | undefined;
139
+ expect(workingTasks?.workingMessageEvents).toHaveLength(2);
140
+
141
+ await removeWorkingTask(event1);
142
+
143
+ workingTasks = (await State.get("current-working-tasks")) as
144
+ | { workingMessageEvents: Array<Record<string, unknown>> }
145
+ | undefined;
146
+ expect(workingTasks?.workingMessageEvents).toHaveLength(1);
147
+ expect(workingTasks?.workingMessageEvents[0].ts).toBe(event2.ts);
148
+ });
149
+
150
+ test("should handle empty state on first access", async () => {
151
+ const workingTasks = await State.get("current-working-tasks");
152
+ expect(workingTasks).toBeUndefined();
153
+ });
154
+ });