@shepai/cli 1.56.0 → 1.57.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.
- package/apis/json-schema/NotificationEventConfig.yaml +20 -0
- package/apis/json-schema/NotificationEventType.yaml +4 -0
- package/dist/packages/core/src/application/ports/output/services/git-pr-service.interface.d.ts +22 -0
- package/dist/packages/core/src/application/ports/output/services/git-pr-service.interface.d.ts.map +1 -1
- package/dist/packages/core/src/domain/factories/settings-defaults.factory.d.ts.map +1 -1
- package/dist/packages/core/src/domain/factories/settings-defaults.factory.js +4 -0
- package/dist/packages/core/src/domain/generated/output.d.ts +21 -1
- package/dist/packages/core/src/domain/generated/output.d.ts.map +1 -1
- package/dist/packages/core/src/domain/generated/output.js +4 -0
- package/dist/packages/core/src/infrastructure/persistence/sqlite/mappers/settings.mapper.d.ts +4 -0
- package/dist/packages/core/src/infrastructure/persistence/sqlite/mappers/settings.mapper.d.ts.map +1 -1
- package/dist/packages/core/src/infrastructure/persistence/sqlite/mappers/settings.mapper.js +8 -0
- package/dist/packages/core/src/infrastructure/persistence/sqlite/migrations.d.ts.map +1 -1
- package/dist/packages/core/src/infrastructure/persistence/sqlite/migrations.js +18 -0
- package/dist/packages/core/src/infrastructure/services/git/git-pr.service.d.ts +3 -1
- package/dist/packages/core/src/infrastructure/services/git/git-pr.service.d.ts.map +1 -1
- package/dist/packages/core/src/infrastructure/services/git/git-pr.service.js +28 -0
- package/dist/packages/core/src/infrastructure/services/notifications/notification.service.d.ts.map +1 -1
- package/dist/packages/core/src/infrastructure/services/notifications/notification.service.js +4 -0
- package/dist/packages/core/src/infrastructure/services/pr-sync/pr-sync-watcher.service.d.ts +61 -0
- package/dist/packages/core/src/infrastructure/services/pr-sync/pr-sync-watcher.service.d.ts.map +1 -0
- package/dist/packages/core/src/infrastructure/services/pr-sync/pr-sync-watcher.service.js +286 -0
- package/dist/src/presentation/cli/commands/ui.command.d.ts.map +1 -1
- package/dist/src/presentation/cli/commands/ui.command.js +6 -0
- package/dist/src/presentation/web/components/common/feature-node/derive-feature-state.d.ts.map +1 -1
- package/dist/src/presentation/web/components/common/feature-node/derive-feature-state.js +6 -0
- package/dist/src/presentation/web/dev-server.js +11 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/web/.next/BUILD_ID +1 -1
- package/web/.next/build-manifest.json +2 -2
- package/web/.next/cache/.previewinfo +1 -1
- package/web/.next/cache/.rscinfo +1 -1
- package/web/.next/cache/.tsbuildinfo +1 -1
- package/web/.next/cache/config.json +3 -3
- package/web/.next/fallback-build-manifest.json +2 -2
- package/web/.next/prerender-manifest.json +3 -3
- package/web/.next/required-server-files.js +1 -1
- package/web/.next/required-server-files.json +1 -1
- package/web/.next/server/app/_global-error.html +2 -2
- package/web/.next/server/app/_global-error.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/page/server-reference-manifest.json +14 -14
- package/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/tools/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/version/page_client-reference-manifest.js +1 -1
- package/web/.next/server/chunks/403f9_next_dist_esm_build_templates_app-route_4d623b8e.js +2 -2
- package/web/.next/server/chunks/403f9_next_dist_esm_build_templates_app-route_4d623b8e.js.map +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__2d1a8e37._.js +2 -2
- package/web/.next/server/chunks/ssr/[root-of-the-server]__48c68a96._.js +2 -2
- package/web/.next/server/chunks/ssr/[root-of-the-server]__48c68a96._.js.map +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__fbc89707._.js +1 -1
- package/web/.next/server/chunks/ssr/_0e2c2b4f._.js +1 -1
- package/web/.next/server/chunks/ssr/_0e2c2b4f._.js.map +1 -1
- package/web/.next/server/chunks/ssr/src_presentation_web_ed0934e5._.js +1 -1
- package/web/.next/server/chunks/ssr/src_presentation_web_ed0934e5._.js.map +1 -1
- package/web/.next/server/pages/500.html +2 -2
- package/web/.next/server/server-reference-manifest.js +1 -1
- package/web/.next/server/server-reference-manifest.json +15 -15
- package/web/.next/standalone/src/presentation/web/.next/BUILD_ID +1 -1
- package/web/.next/standalone/src/presentation/web/.next/build-manifest.json +2 -2
- package/web/.next/standalone/src/presentation/web/.next/prerender-manifest.json +3 -3
- package/web/.next/standalone/src/presentation/web/.next/required-server-files.json +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.html +2 -2
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/page/server-reference-manifest.json +14 -14
- package/web/.next/standalone/src/presentation/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/tools/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/version/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/chunks/403f9_next_dist_esm_build_templates_app-route_4d623b8e.js +2 -2
- package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__2d1a8e37._.js +2 -2
- package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__48c68a96._.js +2 -2
- package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__fbc89707._.js +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/_0e2c2b4f._.js +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/src_presentation_web_ed0934e5._.js +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/pages/500.html +2 -2
- package/web/.next/standalone/src/presentation/web/.next/server/server-reference-manifest.js +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/server-reference-manifest.json +15 -15
- package/web/.next/standalone/src/presentation/web/components/common/feature-node/derive-feature-state.ts +6 -0
- package/web/.next/standalone/src/presentation/web/dev-server.ts +15 -0
- package/web/.next/standalone/src/presentation/web/server.js +1 -1
- package/web/.next/static/chunks/{62eba5d9a5e28761.js → 225de19e886a62ab.js} +2 -2
- package/web/.next/static/chunks/{ca4bb930b4a3f3a9.js → a1f2befd62b321c0.js} +1 -1
- package/web/.next/trace +1 -1
- package/web/.next/trace-build +1 -1
- /package/web/.next/static/{TNUev4cyuW_BghWxcCTOD → V2579MPjpJ_hqKjPsmMlR}/_buildManifest.js +0 -0
- /package/web/.next/static/{TNUev4cyuW_BghWxcCTOD → V2579MPjpJ_hqKjPsmMlR}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/static/{TNUev4cyuW_BghWxcCTOD → V2579MPjpJ_hqKjPsmMlR}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PR Sync Watcher Service
|
|
3
|
+
*
|
|
4
|
+
* Polls GitHub PR status and CI status for features in the Review lifecycle
|
|
5
|
+
* stage, updating feature records and emitting notifications when transitions
|
|
6
|
+
* are detected. Follows the NotificationWatcherService polling pattern.
|
|
7
|
+
*
|
|
8
|
+
* Maintains in-memory tracking of last-known PR and CI status per feature
|
|
9
|
+
* to avoid duplicate updates and notifications. Features are grouped by
|
|
10
|
+
* repositoryPath for batch `gh pr list` queries.
|
|
11
|
+
*/
|
|
12
|
+
import { SdlcLifecycle, PrStatus, CiStatus, AgentRunStatus, NotificationEventType, NotificationSeverity, } from '../../../domain/generated/output.js';
|
|
13
|
+
const DEFAULT_POLL_INTERVAL_MS = 3000;
|
|
14
|
+
const TAG = '[PrSyncWatcher]';
|
|
15
|
+
const CI_STATUS_MAP = {
|
|
16
|
+
success: CiStatus.Success,
|
|
17
|
+
failure: CiStatus.Failure,
|
|
18
|
+
pending: CiStatus.Pending,
|
|
19
|
+
};
|
|
20
|
+
export class PrSyncWatcherService {
|
|
21
|
+
featureRepo;
|
|
22
|
+
agentRunRepo;
|
|
23
|
+
gitPrService;
|
|
24
|
+
notificationService;
|
|
25
|
+
pollIntervalMs;
|
|
26
|
+
trackedFeatures = new Map();
|
|
27
|
+
skippedRepos = new Set();
|
|
28
|
+
intervalId = null;
|
|
29
|
+
constructor(featureRepo, agentRunRepo, gitPrService, notificationService, pollIntervalMs = DEFAULT_POLL_INTERVAL_MS) {
|
|
30
|
+
this.featureRepo = featureRepo;
|
|
31
|
+
this.agentRunRepo = agentRunRepo;
|
|
32
|
+
this.gitPrService = gitPrService;
|
|
33
|
+
this.notificationService = notificationService;
|
|
34
|
+
this.pollIntervalMs = pollIntervalMs;
|
|
35
|
+
}
|
|
36
|
+
isRunning() {
|
|
37
|
+
return this.intervalId !== null;
|
|
38
|
+
}
|
|
39
|
+
start() {
|
|
40
|
+
if (this.intervalId !== null)
|
|
41
|
+
return;
|
|
42
|
+
// eslint-disable-next-line no-console
|
|
43
|
+
console.log(`${TAG} Starting (poll every ${this.pollIntervalMs}ms)`);
|
|
44
|
+
// Run first poll immediately
|
|
45
|
+
void this.poll();
|
|
46
|
+
this.intervalId = setInterval(() => {
|
|
47
|
+
void this.poll();
|
|
48
|
+
}, this.pollIntervalMs);
|
|
49
|
+
}
|
|
50
|
+
stop() {
|
|
51
|
+
if (this.intervalId !== null) {
|
|
52
|
+
clearInterval(this.intervalId);
|
|
53
|
+
this.intervalId = null;
|
|
54
|
+
// eslint-disable-next-line no-console
|
|
55
|
+
console.log(`${TAG} Stopped`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async poll() {
|
|
59
|
+
try {
|
|
60
|
+
const allFeatures = await this.featureRepo.list({ lifecycle: SdlcLifecycle.Review });
|
|
61
|
+
// Include features with a valid repositoryPath (with or without PR data)
|
|
62
|
+
const features = allFeatures.filter((f) => f.repositoryPath);
|
|
63
|
+
if (features.length === 0) {
|
|
64
|
+
this.trackedFeatures.clear();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
// Group features by repositoryPath for batch queries
|
|
68
|
+
const byRepo = new Map();
|
|
69
|
+
for (const feature of features) {
|
|
70
|
+
const group = byRepo.get(feature.repositoryPath) ?? [];
|
|
71
|
+
group.push(feature);
|
|
72
|
+
byRepo.set(feature.repositoryPath, group);
|
|
73
|
+
}
|
|
74
|
+
// Process each repository
|
|
75
|
+
for (const [repoPath, repoFeatures] of byRepo) {
|
|
76
|
+
await this.processRepository(repoPath, repoFeatures);
|
|
77
|
+
}
|
|
78
|
+
// Prune features no longer in Review
|
|
79
|
+
const currentFeatureIds = new Set(features.map((f) => f.id));
|
|
80
|
+
for (const trackedId of this.trackedFeatures.keys()) {
|
|
81
|
+
if (!currentFeatureIds.has(trackedId)) {
|
|
82
|
+
this.trackedFeatures.delete(trackedId);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
88
|
+
// eslint-disable-next-line no-console
|
|
89
|
+
console.warn(`${TAG} Poll failed: ${msg}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async processRepository(repoPath, features) {
|
|
93
|
+
if (this.skippedRepos.has(repoPath))
|
|
94
|
+
return;
|
|
95
|
+
// Skip repos without a git remote — gh pr list will always fail
|
|
96
|
+
try {
|
|
97
|
+
if (!(await this.gitPrService.hasRemote(repoPath))) {
|
|
98
|
+
// eslint-disable-next-line no-console
|
|
99
|
+
console.log(`${TAG} Skipping ${repoPath} (no git remote)`);
|
|
100
|
+
this.skippedRepos.add(repoPath);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
let prStatuses;
|
|
108
|
+
try {
|
|
109
|
+
prStatuses = await this.gitPrService.listPrStatuses(repoPath);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
113
|
+
// eslint-disable-next-line no-console
|
|
114
|
+
console.warn(`${TAG} listPrStatuses failed for ${repoPath}: ${msg}`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
// Build lookups by PR number and by head branch name.
|
|
118
|
+
// Multiple PRs can share the same headRefName (e.g. old merged PR + new open PR).
|
|
119
|
+
// Prefer Open PRs so we don't match stale merged/closed ones.
|
|
120
|
+
const statusByNumber = new Map();
|
|
121
|
+
const statusByBranch = new Map();
|
|
122
|
+
for (const pr of prStatuses) {
|
|
123
|
+
statusByNumber.set(pr.number, pr);
|
|
124
|
+
if (pr.headRefName) {
|
|
125
|
+
const existing = statusByBranch.get(pr.headRefName);
|
|
126
|
+
if (!existing || (existing.state !== PrStatus.Open && pr.state === PrStatus.Open)) {
|
|
127
|
+
statusByBranch.set(pr.headRefName, pr);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
for (const feature of features) {
|
|
132
|
+
await this.processFeature(feature, statusByNumber, statusByBranch);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async processFeature(feature, statusByNumber, statusByBranch) {
|
|
136
|
+
// If feature has no PR data, try to discover an Open PR by matching branch name.
|
|
137
|
+
// Only link Open PRs — merged/closed PRs with the same branch are likely stale
|
|
138
|
+
// from a previous iteration. The watcher will catch Open→Merged transitions
|
|
139
|
+
// on subsequent polls once the PR is linked.
|
|
140
|
+
if (!feature.pr) {
|
|
141
|
+
const matchedPr = statusByBranch.get(feature.branch);
|
|
142
|
+
if (matchedPr?.state !== PrStatus.Open)
|
|
143
|
+
return;
|
|
144
|
+
// eslint-disable-next-line no-console
|
|
145
|
+
console.log(`${TAG} Discovered PR #${matchedPr.number} for "${feature.name}" via branch "${feature.branch}"`);
|
|
146
|
+
feature.pr = {
|
|
147
|
+
url: matchedPr.url,
|
|
148
|
+
number: matchedPr.number,
|
|
149
|
+
status: PrStatus.Open,
|
|
150
|
+
};
|
|
151
|
+
this.trackedFeatures.set(feature.id, {
|
|
152
|
+
prStatus: PrStatus.Open,
|
|
153
|
+
ciStatus: undefined,
|
|
154
|
+
featureName: feature.name,
|
|
155
|
+
});
|
|
156
|
+
await this.featureRepo.update(feature);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const pr = feature.pr;
|
|
160
|
+
const prStatusInfo = statusByNumber.get(pr.number);
|
|
161
|
+
let needsUpdate = false;
|
|
162
|
+
// Initialize or retrieve tracked state
|
|
163
|
+
const prevState = this.trackedFeatures.get(feature.id);
|
|
164
|
+
if (!prevState) {
|
|
165
|
+
// First time seeing this feature — initialize tracking
|
|
166
|
+
this.trackedFeatures.set(feature.id, {
|
|
167
|
+
prStatus: pr.status,
|
|
168
|
+
ciStatus: pr.ciStatus,
|
|
169
|
+
featureName: feature.name,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
const tracked = this.trackedFeatures.get(feature.id);
|
|
173
|
+
// Check PR status transition
|
|
174
|
+
if (prStatusInfo && prStatusInfo.state !== tracked.prStatus) {
|
|
175
|
+
const newPrStatus = prStatusInfo.state;
|
|
176
|
+
// eslint-disable-next-line no-console
|
|
177
|
+
console.log(`${TAG} PR #${pr.number} status changed: ${tracked.prStatus} -> ${newPrStatus} for "${feature.name}"`);
|
|
178
|
+
feature.pr = { ...pr, status: newPrStatus };
|
|
179
|
+
if (newPrStatus === PrStatus.Merged) {
|
|
180
|
+
feature.lifecycle = SdlcLifecycle.Maintain;
|
|
181
|
+
await this.completeAgentRun(feature);
|
|
182
|
+
this.emitNotification(NotificationEventType.PrMerged, feature.id, feature.agentRunId ?? '', feature.name, `PR #${pr.number} merged for ${feature.name}`, NotificationSeverity.Success);
|
|
183
|
+
}
|
|
184
|
+
else if (newPrStatus === PrStatus.Closed) {
|
|
185
|
+
this.emitNotification(NotificationEventType.PrClosed, feature.id, feature.agentRunId ?? '', feature.name, `PR #${pr.number} closed for ${feature.name}`, NotificationSeverity.Warning);
|
|
186
|
+
}
|
|
187
|
+
tracked.prStatus = newPrStatus;
|
|
188
|
+
needsUpdate = true;
|
|
189
|
+
}
|
|
190
|
+
// Check CI status transition
|
|
191
|
+
try {
|
|
192
|
+
const ciResult = await this.gitPrService.getCiStatus(feature.repositoryPath, feature.branch);
|
|
193
|
+
const newCiStatus = CI_STATUS_MAP[ciResult.status] ?? CiStatus.Pending;
|
|
194
|
+
if (newCiStatus !== tracked.ciStatus) {
|
|
195
|
+
// eslint-disable-next-line no-console
|
|
196
|
+
console.log(`${TAG} CI status changed: ${tracked.ciStatus ?? 'none'} -> ${newCiStatus} for "${feature.name}"`);
|
|
197
|
+
feature.pr = { ...(feature.pr ?? pr), ciStatus: newCiStatus };
|
|
198
|
+
if (newCiStatus === CiStatus.Success) {
|
|
199
|
+
this.emitNotification(NotificationEventType.PrChecksPassed, feature.id, feature.agentRunId ?? '', feature.name, `CI checks passed for ${feature.name}`, NotificationSeverity.Success);
|
|
200
|
+
}
|
|
201
|
+
else if (newCiStatus === CiStatus.Failure) {
|
|
202
|
+
this.emitNotification(NotificationEventType.PrChecksFailed, feature.id, feature.agentRunId ?? '', feature.name, `CI checks failed for ${feature.name}`, NotificationSeverity.Error);
|
|
203
|
+
}
|
|
204
|
+
tracked.ciStatus = newCiStatus;
|
|
205
|
+
needsUpdate = true;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
210
|
+
// eslint-disable-next-line no-console
|
|
211
|
+
console.warn(`${TAG} getCiStatus failed for "${feature.name}": ${msg}`);
|
|
212
|
+
}
|
|
213
|
+
if (needsUpdate) {
|
|
214
|
+
await this.featureRepo.update(feature);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/** Mark associated agent run as completed so the UI reflects "done" state. */
|
|
218
|
+
async completeAgentRun(feature) {
|
|
219
|
+
if (!feature.agentRunId)
|
|
220
|
+
return;
|
|
221
|
+
try {
|
|
222
|
+
await this.agentRunRepo.updateStatus(feature.agentRunId, AgentRunStatus.completed);
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
226
|
+
// eslint-disable-next-line no-console
|
|
227
|
+
console.warn(`${TAG} Failed to complete agent run ${feature.agentRunId}: ${msg}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
emitNotification(eventType, featureId, agentRunId, featureName, message, severity) {
|
|
231
|
+
const event = {
|
|
232
|
+
eventType,
|
|
233
|
+
featureId,
|
|
234
|
+
agentRunId,
|
|
235
|
+
featureName,
|
|
236
|
+
message,
|
|
237
|
+
severity,
|
|
238
|
+
timestamp: new Date().toISOString(),
|
|
239
|
+
};
|
|
240
|
+
this.notificationService.notify(event);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// --- Singleton accessors (follows NotificationWatcherService pattern) ---
|
|
244
|
+
let watcherInstance = null;
|
|
245
|
+
/**
|
|
246
|
+
* Initialize the PR sync watcher singleton.
|
|
247
|
+
* Must be called once during web server startup.
|
|
248
|
+
*
|
|
249
|
+
* @throws Error if the watcher is already initialized
|
|
250
|
+
*/
|
|
251
|
+
export function initializePrSyncWatcher(featureRepo, agentRunRepo, gitPrService, notificationService, pollIntervalMs) {
|
|
252
|
+
if (watcherInstance !== null) {
|
|
253
|
+
throw new Error('PR sync watcher already initialized. Cannot re-initialize.');
|
|
254
|
+
}
|
|
255
|
+
watcherInstance = new PrSyncWatcherService(featureRepo, agentRunRepo, gitPrService, notificationService, pollIntervalMs);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Get the PR sync watcher singleton.
|
|
259
|
+
*
|
|
260
|
+
* @returns The PR sync watcher service
|
|
261
|
+
* @throws Error if the watcher hasn't been initialized yet
|
|
262
|
+
*/
|
|
263
|
+
export function getPrSyncWatcher() {
|
|
264
|
+
if (watcherInstance === null) {
|
|
265
|
+
throw new Error('PR sync watcher not initialized. Call initializePrSyncWatcher() during web server startup.');
|
|
266
|
+
}
|
|
267
|
+
return watcherInstance;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Check if the PR sync watcher has been initialized.
|
|
271
|
+
*/
|
|
272
|
+
export function hasPrSyncWatcher() {
|
|
273
|
+
return watcherInstance !== null;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Reset the PR sync watcher singleton (for testing purposes only).
|
|
277
|
+
* Stops the watcher if running before resetting.
|
|
278
|
+
*
|
|
279
|
+
* @internal
|
|
280
|
+
*/
|
|
281
|
+
export function resetPrSyncWatcher() {
|
|
282
|
+
if (watcherInstance !== null) {
|
|
283
|
+
watcherInstance.stop();
|
|
284
|
+
}
|
|
285
|
+
watcherInstance = null;
|
|
286
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ui.command.d.ts","sourceRoot":"","sources":["../../../../../src/presentation/cli/commands/ui.command.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,OAAO,EAAwB,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"ui.command.d.ts","sourceRoot":"","sources":["../../../../../src/presentation/cli/commands/ui.command.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,OAAO,EAAwB,MAAM,WAAW,CAAC;AA+B1D;;GAEG;AACH,wBAAgB,eAAe,IAAI,OAAO,CAkFzC"}
|
|
@@ -20,6 +20,7 @@ import { container } from '../../../../packages/core/src/infrastructure/di/conta
|
|
|
20
20
|
import { setVersionEnvVars } from '../../../../packages/core/src/infrastructure/services/version.service.js';
|
|
21
21
|
import { resolveWebDir } from '../../../../packages/core/src/infrastructure/services/web-server.service.js';
|
|
22
22
|
import { initializeNotificationWatcher, getNotificationWatcher, } from '../../../../packages/core/src/infrastructure/services/notifications/notification-watcher.service.js';
|
|
23
|
+
import { initializePrSyncWatcher, getPrSyncWatcher, } from '../../../../packages/core/src/infrastructure/services/pr-sync/pr-sync-watcher.service.js';
|
|
23
24
|
import { BrowserOpenerService } from '../../../../packages/core/src/infrastructure/services/browser-opener.service.js';
|
|
24
25
|
import { colors, fmt, messages } from '../ui/index.js';
|
|
25
26
|
function parsePort(value) {
|
|
@@ -63,6 +64,10 @@ Examples:
|
|
|
63
64
|
const notificationService = container.resolve('INotificationService');
|
|
64
65
|
initializeNotificationWatcher(runRepo, phaseTimingRepo, featureRepo, notificationService);
|
|
65
66
|
getNotificationWatcher().start();
|
|
67
|
+
// Start PR sync watcher to detect PR/CI status transitions on GitHub
|
|
68
|
+
const gitPrService = container.resolve('IGitPrService');
|
|
69
|
+
initializePrSyncWatcher(featureRepo, runRepo, gitPrService, notificationService);
|
|
70
|
+
getPrSyncWatcher().start();
|
|
66
71
|
const url = `http://localhost:${port}`;
|
|
67
72
|
messages.success(`Server ready at ${fmt.code(url)}`);
|
|
68
73
|
messages.info('Press Ctrl+C to stop');
|
|
@@ -84,6 +89,7 @@ Examples:
|
|
|
84
89
|
// Force exit after 5s if graceful shutdown stalls
|
|
85
90
|
const forceExit = setTimeout(() => process.exit(0), 5000);
|
|
86
91
|
forceExit.unref();
|
|
92
|
+
getPrSyncWatcher().stop();
|
|
87
93
|
getNotificationWatcher().stop();
|
|
88
94
|
await service.stop();
|
|
89
95
|
process.exit(0);
|
package/dist/src/presentation/web/components/common/feature-node/derive-feature-state.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"derive-feature-state.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/components/common/feature-node/derive-feature-state.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAIL,qBAAqB,EACtB,MAAM,+BAA+B,CAAC;AACvC,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,KAAK,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AAE3F;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,QAAQ,GAAG,IAAI,GAAG,gBAAgB,CA4C9F;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAYvD;AAED,oGAAoG;AACpG,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,qBAAqB,GAAG,gBAAgB,
|
|
1
|
+
{"version":3,"file":"derive-feature-state.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/components/common/feature-node/derive-feature-state.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAIL,qBAAqB,EACtB,MAAM,+BAA+B,CAAC;AACvC,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,KAAK,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AAE3F;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,QAAQ,GAAG,IAAI,GAAG,gBAAgB,CA4C9F;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAYvD;AAED,oGAAoG;AACpG,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,qBAAqB,GAAG,gBAAgB,CAkBtF;AAYD,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,MAAM,GAAG,SAAS,GAC5B,qBAAqB,GAAG,SAAS,CAGnC"}
|
|
@@ -84,7 +84,13 @@ export function mapEventTypeToState(eventType) {
|
|
|
84
84
|
case NotificationEventType.AgentCompleted:
|
|
85
85
|
return 'done';
|
|
86
86
|
case NotificationEventType.AgentFailed:
|
|
87
|
+
case NotificationEventType.PrChecksFailed:
|
|
87
88
|
return 'error';
|
|
89
|
+
case NotificationEventType.PrMerged:
|
|
90
|
+
case NotificationEventType.PrChecksPassed:
|
|
91
|
+
return 'done';
|
|
92
|
+
case NotificationEventType.PrClosed:
|
|
93
|
+
return 'action-required';
|
|
88
94
|
}
|
|
89
95
|
}
|
|
90
96
|
/** Maps an SSE event phaseName to a FeatureLifecyclePhase. Mirrors page.tsx nodeToLifecyclePhase. */
|
|
@@ -18,6 +18,7 @@ import { initializeContainer, container } from '../../../packages/core/src/infra
|
|
|
18
18
|
import { InitializeSettingsUseCase } from '../../../packages/core/src/application/use-cases/settings/initialize-settings.use-case.js';
|
|
19
19
|
import { initializeSettings } from '../../../packages/core/src/infrastructure/services/settings.service.js';
|
|
20
20
|
import { initializeNotificationWatcher, getNotificationWatcher, } from '../../../packages/core/src/infrastructure/services/notifications/notification-watcher.service.js';
|
|
21
|
+
import { initializePrSyncWatcher, getPrSyncWatcher, } from '../../../packages/core/src/infrastructure/services/pr-sync/pr-sync-watcher.service.js';
|
|
21
22
|
const DEFAULT_PORT = 3000;
|
|
22
23
|
async function isPortAvailable(port) {
|
|
23
24
|
return new Promise((resolve) => {
|
|
@@ -61,6 +62,10 @@ async function main() {
|
|
|
61
62
|
const notificationService = container.resolve('INotificationService');
|
|
62
63
|
initializeNotificationWatcher(runRepo, phaseTimingRepo, featureRepo, notificationService);
|
|
63
64
|
getNotificationWatcher().start();
|
|
65
|
+
// Start PR sync watcher to detect PR/CI status transitions on GitHub
|
|
66
|
+
const gitPrService = container.resolve('IGitPrService');
|
|
67
|
+
initializePrSyncWatcher(featureRepo, runRepo, gitPrService, notificationService);
|
|
68
|
+
getPrSyncWatcher().start();
|
|
64
69
|
}
|
|
65
70
|
catch (error) {
|
|
66
71
|
console.warn('[dev-server] DI initialization failed — features will be empty:', error);
|
|
@@ -106,6 +111,12 @@ async function main() {
|
|
|
106
111
|
catch {
|
|
107
112
|
/* not initialized */
|
|
108
113
|
}
|
|
114
|
+
try {
|
|
115
|
+
getPrSyncWatcher().stop();
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
/* not initialized */
|
|
119
|
+
}
|
|
109
120
|
server.closeAllConnections();
|
|
110
121
|
await Promise.all([
|
|
111
122
|
new Promise((resolve) => server.close(() => resolve())),
|