@generacy-ai/generacy 0.5.0 → 0.7.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/dist/cli/commands/cockpit/clarification-answer-marker.d.ts +8 -0
- package/dist/cli/commands/cockpit/clarification-answer-marker.d.ts.map +1 -0
- package/dist/cli/commands/cockpit/clarification-answer-marker.js +76 -0
- package/dist/cli/commands/cockpit/clarification-answer-marker.js.map +1 -0
- package/dist/cli/commands/cockpit/clarification-comment-finder.d.ts +0 -10
- package/dist/cli/commands/cockpit/clarification-comment-finder.d.ts.map +1 -1
- package/dist/cli/commands/cockpit/clarification-comment-finder.js +57 -4
- package/dist/cli/commands/cockpit/clarification-comment-finder.js.map +1 -1
- package/dist/cli/commands/cockpit/clarify-relay.d.ts +38 -0
- package/dist/cli/commands/cockpit/clarify-relay.d.ts.map +1 -0
- package/dist/cli/commands/cockpit/clarify-relay.js +120 -0
- package/dist/cli/commands/cockpit/clarify-relay.js.map +1 -0
- package/dist/cli/commands/cockpit/doorbell/aggregate-on-demand.d.ts +43 -0
- package/dist/cli/commands/cockpit/doorbell/aggregate-on-demand.d.ts.map +1 -0
- package/dist/cli/commands/cockpit/doorbell/aggregate-on-demand.js +72 -0
- package/dist/cli/commands/cockpit/doorbell/aggregate-on-demand.js.map +1 -0
- package/dist/cli/commands/cockpit/doorbell/channel-discovery.d.ts +103 -0
- package/dist/cli/commands/cockpit/doorbell/channel-discovery.d.ts.map +1 -0
- package/dist/cli/commands/cockpit/doorbell/channel-discovery.js +168 -0
- package/dist/cli/commands/cockpit/doorbell/channel-discovery.js.map +1 -0
- package/dist/cli/commands/cockpit/doorbell/smee-source.d.ts +73 -0
- package/dist/cli/commands/cockpit/doorbell/smee-source.d.ts.map +1 -0
- package/dist/cli/commands/cockpit/doorbell/smee-source.js +405 -0
- package/dist/cli/commands/cockpit/doorbell/smee-source.js.map +1 -0
- package/dist/cli/commands/cockpit/doorbell/source-selector.d.ts +61 -0
- package/dist/cli/commands/cockpit/doorbell/source-selector.d.ts.map +1 -0
- package/dist/cli/commands/cockpit/doorbell/source-selector.js +208 -0
- package/dist/cli/commands/cockpit/doorbell/source-selector.js.map +1 -0
- package/dist/cli/commands/cockpit/doorbell/sse-parser.d.ts +15 -0
- package/dist/cli/commands/cockpit/doorbell/sse-parser.d.ts.map +1 -0
- package/dist/cli/commands/cockpit/doorbell/sse-parser.js +43 -0
- package/dist/cli/commands/cockpit/doorbell/sse-parser.js.map +1 -0
- package/dist/cli/commands/cockpit/doorbell/startup-retry.d.ts +55 -0
- package/dist/cli/commands/cockpit/doorbell/startup-retry.d.ts.map +1 -0
- package/dist/cli/commands/cockpit/doorbell/startup-retry.js +159 -0
- package/dist/cli/commands/cockpit/doorbell/startup-retry.js.map +1 -0
- package/dist/cli/commands/cockpit/doorbell/subscribe.d.ts +19 -0
- package/dist/cli/commands/cockpit/doorbell/subscribe.d.ts.map +1 -0
- package/dist/cli/commands/cockpit/doorbell/subscribe.js +54 -0
- package/dist/cli/commands/cockpit/doorbell/subscribe.js.map +1 -0
- package/dist/cli/commands/cockpit/doorbell/webhook-target-resolver.d.ts +20 -0
- package/dist/cli/commands/cockpit/doorbell/webhook-target-resolver.d.ts.map +1 -0
- package/dist/cli/commands/cockpit/doorbell/webhook-target-resolver.js +51 -0
- package/dist/cli/commands/cockpit/doorbell/webhook-target-resolver.js.map +1 -0
- package/dist/cli/commands/cockpit/doorbell/webhook-to-event.d.ts +28 -0
- package/dist/cli/commands/cockpit/doorbell/webhook-to-event.d.ts.map +1 -0
- package/dist/cli/commands/cockpit/doorbell/webhook-to-event.js +162 -0
- package/dist/cli/commands/cockpit/doorbell/webhook-to-event.js.map +1 -0
- package/dist/cli/commands/cockpit/doorbell.d.ts +58 -0
- package/dist/cli/commands/cockpit/doorbell.d.ts.map +1 -0
- package/dist/cli/commands/cockpit/doorbell.js +519 -0
- package/dist/cli/commands/cockpit/doorbell.js.map +1 -0
- package/dist/cli/commands/cockpit/index.d.ts +1 -0
- package/dist/cli/commands/cockpit/index.d.ts.map +1 -1
- package/dist/cli/commands/cockpit/index.js +3 -0
- package/dist/cli/commands/cockpit/index.js.map +1 -1
- package/dist/cli/commands/cockpit/mcp/event-bus-registry.d.ts +3 -2
- package/dist/cli/commands/cockpit/mcp/event-bus-registry.d.ts.map +1 -1
- package/dist/cli/commands/cockpit/mcp/event-bus-registry.js +41 -12
- package/dist/cli/commands/cockpit/mcp/event-bus-registry.js.map +1 -1
- package/dist/cli/commands/cockpit/mcp/event-bus.d.ts +6 -0
- package/dist/cli/commands/cockpit/mcp/event-bus.d.ts.map +1 -1
- package/dist/cli/commands/cockpit/mcp/event-bus.js +7 -1
- package/dist/cli/commands/cockpit/mcp/event-bus.js.map +1 -1
- package/dist/cli/commands/cockpit/mcp/schemas.d.ts +48 -0
- package/dist/cli/commands/cockpit/mcp/schemas.d.ts.map +1 -1
- package/dist/cli/commands/cockpit/mcp/schemas.js +28 -0
- package/dist/cli/commands/cockpit/mcp/schemas.js.map +1 -1
- package/dist/cli/commands/cockpit/mcp/server.d.ts.map +1 -1
- package/dist/cli/commands/cockpit/mcp/server.js +6 -1
- package/dist/cli/commands/cockpit/mcp/server.js.map +1 -1
- package/dist/cli/commands/cockpit/mcp/tools/cockpit_relay_clarify_answers.d.ts +45 -0
- package/dist/cli/commands/cockpit/mcp/tools/cockpit_relay_clarify_answers.d.ts.map +1 -0
- package/dist/cli/commands/cockpit/mcp/tools/cockpit_relay_clarify_answers.js +45 -0
- package/dist/cli/commands/cockpit/mcp/tools/cockpit_relay_clarify_answers.js.map +1 -0
- package/dist/cli/commands/cockpit/resume.d.ts +12 -6
- package/dist/cli/commands/cockpit/resume.d.ts.map +1 -1
- package/dist/cli/commands/cockpit/resume.js +27 -11
- package/dist/cli/commands/cockpit/resume.js.map +1 -1
- package/dist/cli/commands/cockpit/watch/diff.d.ts +1 -0
- package/dist/cli/commands/cockpit/watch/diff.d.ts.map +1 -1
- package/dist/cli/commands/cockpit/watch/diff.js.map +1 -1
- package/dist/cli/commands/cockpit/watch/emit.d.ts +3 -0
- package/dist/cli/commands/cockpit/watch/emit.d.ts.map +1 -1
- package/dist/cli/commands/cockpit/watch/emit.js +1 -0
- package/dist/cli/commands/cockpit/watch/emit.js.map +1 -1
- package/dist/cli/commands/cockpit/watch/poll-loop.d.ts +2 -0
- package/dist/cli/commands/cockpit/watch/poll-loop.d.ts.map +1 -1
- package/dist/cli/commands/cockpit/watch/poll-loop.js +38 -6
- package/dist/cli/commands/cockpit/watch/poll-loop.js.map +1 -1
- package/dist/cli/commands/cockpit/watch/pr-state.d.ts +15 -1
- package/dist/cli/commands/cockpit/watch/pr-state.d.ts.map +1 -1
- package/dist/cli/commands/cockpit/watch/pr-state.js +38 -0
- package/dist/cli/commands/cockpit/watch/pr-state.js.map +1 -1
- package/dist/cli/commands/cockpit/watch/snapshot.d.ts +6 -1
- package/dist/cli/commands/cockpit/watch/snapshot.d.ts.map +1 -1
- package/dist/cli/commands/cockpit/watch/snapshot.js +3 -1
- package/dist/cli/commands/cockpit/watch/snapshot.js.map +1 -1
- package/dist/cli/commands/cockpit/watch/stream-event.d.ts +3 -0
- package/dist/cli/commands/cockpit/watch/stream-event.d.ts.map +1 -1
- package/dist/cli/commands/cockpit/watch.d.ts +2 -1
- package/dist/cli/commands/cockpit/watch.d.ts.map +1 -1
- package/dist/cli/commands/cockpit/watch.js +25 -13
- package/dist/cli/commands/cockpit/watch.js.map +1 -1
- package/package.json +6 -5
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { dirname, join, parse as parsePath } from 'node:path';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
export const DEFAULT_CHANNEL_FILE_PATH = '/var/lib/generacy/smee-channel';
|
|
4
|
+
export const DEFAULT_WORKSPACE_MIRROR_PATH = '/workspaces/.generacy/cockpit/smee-channel';
|
|
5
|
+
export const DEFAULT_WEBHOOK_CONFIG_TIMEOUT_MS = 5_000;
|
|
6
|
+
// Copied verbatim from packages/orchestrator/src/services/smee-channel-resolver.ts:27
|
|
7
|
+
// to avoid an orchestrator import in the CLI.
|
|
8
|
+
export const SMEE_URL_PATTERN = /^https:\/\/smee\.io\/[A-Za-z0-9_-]+$/;
|
|
9
|
+
export const SmeeHookSchema = z
|
|
10
|
+
.object({
|
|
11
|
+
id: z.number().int(),
|
|
12
|
+
active: z.boolean(),
|
|
13
|
+
config: z.object({ url: z.string() }),
|
|
14
|
+
updated_at: z.string(),
|
|
15
|
+
})
|
|
16
|
+
.passthrough();
|
|
17
|
+
/**
|
|
18
|
+
* Pure tie-break for `/hooks` payloads (FR-005).
|
|
19
|
+
* 1. Keep only `active === true`.
|
|
20
|
+
* 2. Keep only entries whose `config.url` matches `SMEE_URL_PATTERN`.
|
|
21
|
+
* 3. Sort by `Date.parse(updated_at)` desc; `NaN` sorts last (`-Infinity`).
|
|
22
|
+
* 4. Return the first entry or `null`.
|
|
23
|
+
*/
|
|
24
|
+
export function pickSmeeHook(hooks) {
|
|
25
|
+
const candidates = hooks.filter((h) => h.active === true && SMEE_URL_PATTERN.test(h.config.url));
|
|
26
|
+
const scored = candidates.map((h) => {
|
|
27
|
+
const t = Date.parse(h.updated_at);
|
|
28
|
+
return { h, t: Number.isNaN(t) ? -Infinity : t };
|
|
29
|
+
});
|
|
30
|
+
scored.sort((a, b) => b.t - a.t);
|
|
31
|
+
return scored[0]?.h ?? null;
|
|
32
|
+
}
|
|
33
|
+
const ENV_KEY = 'COCKPIT_DOORBELL_SMEE_URL';
|
|
34
|
+
const WALKUP_SUFFIX = join('.generacy', 'cockpit', 'smee-channel');
|
|
35
|
+
function warn(input, msg) {
|
|
36
|
+
input.logger?.warn?.(msg);
|
|
37
|
+
}
|
|
38
|
+
function isEnoent(err) {
|
|
39
|
+
if (err == null || typeof err !== 'object')
|
|
40
|
+
return false;
|
|
41
|
+
const code = err.code;
|
|
42
|
+
return code === 'ENOENT';
|
|
43
|
+
}
|
|
44
|
+
function errMessage(err) {
|
|
45
|
+
return err instanceof Error ? err.message : String(err);
|
|
46
|
+
}
|
|
47
|
+
async function tryReadUrl(input, path, onNonEnoentError, onMalformed) {
|
|
48
|
+
let raw;
|
|
49
|
+
try {
|
|
50
|
+
raw = await input.fs.readFile(path, 'utf-8');
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
if (isEnoent(err))
|
|
54
|
+
return null;
|
|
55
|
+
onNonEnoentError(errMessage(err));
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const trimmed = raw.trim();
|
|
59
|
+
if (SMEE_URL_PATTERN.test(trimmed)) {
|
|
60
|
+
return trimmed;
|
|
61
|
+
}
|
|
62
|
+
onMalformed(trimmed);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
async function runWebhookConfigStage(input) {
|
|
66
|
+
if (input.runner == null)
|
|
67
|
+
return null;
|
|
68
|
+
const targets = input.targets;
|
|
69
|
+
if (targets == null || targets.length === 0)
|
|
70
|
+
return null;
|
|
71
|
+
const timeoutMs = input.webhookConfigTimeoutMs != null && input.webhookConfigTimeoutMs > 0
|
|
72
|
+
? input.webhookConfigTimeoutMs
|
|
73
|
+
: DEFAULT_WEBHOOK_CONFIG_TIMEOUT_MS;
|
|
74
|
+
const runner = input.runner;
|
|
75
|
+
for (const target of targets) {
|
|
76
|
+
const label = `${target.owner}/${target.repo}`;
|
|
77
|
+
let result;
|
|
78
|
+
try {
|
|
79
|
+
result = await runner('gh', ['api', `/repos/${target.owner}/${target.repo}/hooks`], { timeoutMs });
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
warn(input, `cockpit doorbell: webhook-config stage failed for ${label}: exit=1 (${errMessage(err)})`);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (result.exitCode !== 0) {
|
|
86
|
+
warn(input, `cockpit doorbell: webhook-config stage failed for ${label}: exit=${result.exitCode}`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
let raw;
|
|
90
|
+
try {
|
|
91
|
+
raw = JSON.parse(result.stdout);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
warn(input, `cockpit doorbell: webhook-config stage: malformed JSON for ${label}`);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const parsed = z.array(SmeeHookSchema).safeParse(raw);
|
|
98
|
+
if (!parsed.success) {
|
|
99
|
+
warn(input, `cockpit doorbell: webhook-config stage: unexpected /hooks shape for ${label}`);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const hook = pickSmeeHook(parsed.data);
|
|
103
|
+
if (hook == null)
|
|
104
|
+
continue; // routine — no smee hook here, try next
|
|
105
|
+
return hook.config.url;
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
export async function discoverChannelUrl(input) {
|
|
110
|
+
// Stage 1: env override.
|
|
111
|
+
const envRaw = input.env[ENV_KEY];
|
|
112
|
+
const envPresent = envRaw != null && envRaw !== '';
|
|
113
|
+
if (envPresent) {
|
|
114
|
+
if (SMEE_URL_PATTERN.test(envRaw)) {
|
|
115
|
+
return { url: envRaw, source: 'env' };
|
|
116
|
+
}
|
|
117
|
+
warn(input, `cockpit doorbell: ${ENV_KEY} does not match smee URL pattern; falling through to channel file`);
|
|
118
|
+
}
|
|
119
|
+
// Stage 2: webhook-config (gh api /hooks). Silent no-op when runner or
|
|
120
|
+
// targets are absent — the FS stages below still run.
|
|
121
|
+
const webhookUrl = await runWebhookConfigStage(input);
|
|
122
|
+
if (webhookUrl != null) {
|
|
123
|
+
return { url: webhookUrl, source: 'webhook-config' };
|
|
124
|
+
}
|
|
125
|
+
// Stage 3: walk-up scan starting at cwd.
|
|
126
|
+
const startCwd = input.cwd ?? process.cwd();
|
|
127
|
+
const root = parsePath(startCwd).root;
|
|
128
|
+
let dir = startCwd;
|
|
129
|
+
// Bound the walk: worst case ~few dozen hops, so no infinite-loop guard beyond
|
|
130
|
+
// the root check is required — path.dirname eventually stabilizes at root.
|
|
131
|
+
for (;;) {
|
|
132
|
+
const candidate = join(dir, WALKUP_SUFFIX);
|
|
133
|
+
const url = await tryReadUrl(input, candidate, (msg) => warn(input, `cockpit doorbell: walk-up read failed at ${dir}: ${msg}`), () => warn(input, `cockpit doorbell: channel content at ${candidate} does not match smee URL pattern`));
|
|
134
|
+
if (url != null) {
|
|
135
|
+
return { url, source: 'workspace-walkup' };
|
|
136
|
+
}
|
|
137
|
+
if (dir === root)
|
|
138
|
+
break;
|
|
139
|
+
const parent = dirname(dir);
|
|
140
|
+
if (parent === dir)
|
|
141
|
+
break;
|
|
142
|
+
dir = parent;
|
|
143
|
+
}
|
|
144
|
+
// Stage 4: absolute workspace mirror path.
|
|
145
|
+
const mirrorPath = input.workspaceMirrorPath ?? DEFAULT_WORKSPACE_MIRROR_PATH;
|
|
146
|
+
const mirrorUrl = await tryReadUrl(input, mirrorPath, (msg) => warn(input, `cockpit doorbell: failed to read workspace mirror at ${mirrorPath}: ${msg}`), () => warn(input, `cockpit doorbell: channel content at ${mirrorPath} does not match smee URL pattern`));
|
|
147
|
+
if (mirrorUrl != null) {
|
|
148
|
+
return { url: mirrorUrl, source: 'workspace-absolute' };
|
|
149
|
+
}
|
|
150
|
+
// Stage 5: cluster-internal channel file.
|
|
151
|
+
let raw;
|
|
152
|
+
try {
|
|
153
|
+
raw = await input.fs.readFile(input.channelFilePath, 'utf-8');
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
if (isEnoent(err))
|
|
157
|
+
return null;
|
|
158
|
+
warn(input, `cockpit doorbell: failed to read channel file ${input.channelFilePath}: ${errMessage(err)}`);
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
const trimmed = raw.trim();
|
|
162
|
+
if (SMEE_URL_PATTERN.test(trimmed)) {
|
|
163
|
+
return { url: trimmed, source: 'file' };
|
|
164
|
+
}
|
|
165
|
+
warn(input, `cockpit doorbell: channel file ${input.channelFilePath} content does not match smee URL pattern`);
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=channel-discovery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel-discovery.js","sourceRoot":"","sources":["../../../../../src/cli/commands/cockpit/doorbell/channel-discovery.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `SmeeDoorbellSource` — SSE consumer for the smee.io channel. Filters
|
|
3
|
+
* payloads by the epic's ref set and emits `CockpitStreamEvent`s to a
|
|
4
|
+
* caller-supplied sink. Models the reconnect ladder on
|
|
5
|
+
* `packages/orchestrator/src/services/smee-receiver.ts`.
|
|
6
|
+
*
|
|
7
|
+
* Contract: `specs/978-summary-generacy-cockpit/contracts/smee-doorbell-source.md`.
|
|
8
|
+
*/
|
|
9
|
+
import { type CommandRunner, type GhWrapper } from '@generacy-ai/cockpit';
|
|
10
|
+
import type { CockpitStreamEvent } from '../watch/stream-event.js';
|
|
11
|
+
export declare const DEFAULT_BASE_RECONNECT_DELAY_MS = 5000;
|
|
12
|
+
export declare const DEFAULT_REFRESH_DEBOUNCE_MS = 500;
|
|
13
|
+
export declare const DEFAULT_SAFETY_NET_INTERVAL_MS = 600000;
|
|
14
|
+
export interface SmeeDoorbellSourceOptions {
|
|
15
|
+
channelUrl: string;
|
|
16
|
+
epicRef: string;
|
|
17
|
+
gh: GhWrapper;
|
|
18
|
+
runner?: CommandRunner;
|
|
19
|
+
logger: {
|
|
20
|
+
warn: (msg: string) => void;
|
|
21
|
+
info?: (msg: string) => void;
|
|
22
|
+
};
|
|
23
|
+
onEvent: (event: CockpitStreamEvent) => Promise<void>;
|
|
24
|
+
onReconnectAttempt: (failedAttempts: number) => void;
|
|
25
|
+
onReconnectSuccess: () => void;
|
|
26
|
+
onSseBytes?: () => void;
|
|
27
|
+
onRefSetRefreshFailure?: (err: unknown) => void;
|
|
28
|
+
now?: () => number;
|
|
29
|
+
fetch?: typeof globalThis.fetch;
|
|
30
|
+
refreshDebounceMs?: number;
|
|
31
|
+
safetyNetIntervalMs?: number;
|
|
32
|
+
baseReconnectDelayMs?: number;
|
|
33
|
+
}
|
|
34
|
+
export declare class SmeeDoorbellSource {
|
|
35
|
+
private readonly channelUrl;
|
|
36
|
+
private readonly epicRef;
|
|
37
|
+
private readonly gh;
|
|
38
|
+
private readonly logger;
|
|
39
|
+
private readonly onEvent;
|
|
40
|
+
private readonly onReconnectAttempt;
|
|
41
|
+
private readonly onReconnectSuccess;
|
|
42
|
+
private readonly onSseBytes?;
|
|
43
|
+
private readonly onRefSetRefreshFailure?;
|
|
44
|
+
private readonly now;
|
|
45
|
+
private readonly fetchImpl;
|
|
46
|
+
private readonly refreshDebounceMs;
|
|
47
|
+
private readonly safetyNetIntervalMs;
|
|
48
|
+
private readonly baseReconnectDelayMs;
|
|
49
|
+
private refSet;
|
|
50
|
+
private aggState;
|
|
51
|
+
private prev;
|
|
52
|
+
private currentResolved;
|
|
53
|
+
private reconnectAttempt;
|
|
54
|
+
private running;
|
|
55
|
+
private abortController;
|
|
56
|
+
private refreshTimer;
|
|
57
|
+
private refreshDebounceTimer;
|
|
58
|
+
private aggregateDebounceTimer;
|
|
59
|
+
private pendingAggregateTrigger;
|
|
60
|
+
private runLoopPromise;
|
|
61
|
+
constructor(options: SmeeDoorbellSourceOptions);
|
|
62
|
+
start(): Promise<void>;
|
|
63
|
+
stop(): Promise<void>;
|
|
64
|
+
private sleep;
|
|
65
|
+
private runLoop;
|
|
66
|
+
private connect;
|
|
67
|
+
private processEventBlock;
|
|
68
|
+
private scheduleRefSetRefresh;
|
|
69
|
+
private refreshRefSet;
|
|
70
|
+
private scheduleAggregateRefresh;
|
|
71
|
+
private runAggregateRefresh;
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=smee-source.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"smee-source.d.ts","sourceRoot":"","sources":["../../../../../src/cli/commands/cockpit/doorbell/smee-source.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `SmeeDoorbellSource` — SSE consumer for the smee.io channel. Filters
|
|
3
|
+
* payloads by the epic's ref set and emits `CockpitStreamEvent`s to a
|
|
4
|
+
* caller-supplied sink. Models the reconnect ladder on
|
|
5
|
+
* `packages/orchestrator/src/services/smee-receiver.ts`.
|
|
6
|
+
*
|
|
7
|
+
* Contract: `specs/978-summary-generacy-cockpit/contracts/smee-doorbell-source.md`.
|
|
8
|
+
*/
|
|
9
|
+
import { resolveEpic, } from '@generacy-ai/cockpit';
|
|
10
|
+
import { calculateBackoffDelay } from '@generacy-ai/smee-backoff';
|
|
11
|
+
import { initialAggregateState } from '../watch/aggregate.js';
|
|
12
|
+
import { snapshotKey } from '../watch/snapshot.js';
|
|
13
|
+
import { parseSseEventBlock } from './sse-parser.js';
|
|
14
|
+
import { webhookToStreamEvent, } from './webhook-to-event.js';
|
|
15
|
+
import { maybeRefreshAggregate, } from './aggregate-on-demand.js';
|
|
16
|
+
export const DEFAULT_BASE_RECONNECT_DELAY_MS = 5_000;
|
|
17
|
+
export const DEFAULT_REFRESH_DEBOUNCE_MS = 500;
|
|
18
|
+
export const DEFAULT_SAFETY_NET_INTERVAL_MS = 600_000;
|
|
19
|
+
const AGGREGATE_TRIGGER_DEBOUNCE_MS = 500;
|
|
20
|
+
function repoRefsToSets(allRefs) {
|
|
21
|
+
const issues = new Set();
|
|
22
|
+
const prs = new Set();
|
|
23
|
+
const repos = new Set();
|
|
24
|
+
for (const ref of allRefs) {
|
|
25
|
+
const key = `${ref.repo}#${ref.number}`;
|
|
26
|
+
issues.add(key);
|
|
27
|
+
prs.add(key);
|
|
28
|
+
repos.add(ref.repo);
|
|
29
|
+
}
|
|
30
|
+
return { issues, prs, repos };
|
|
31
|
+
}
|
|
32
|
+
function buildRefSet(resolved) {
|
|
33
|
+
const sets = repoRefsToSets(resolved.parsed.allRefs);
|
|
34
|
+
sets.issues.add(`${resolved.epic.repo}#${resolved.epic.number}`);
|
|
35
|
+
sets.repos.add(resolved.epic.repo);
|
|
36
|
+
return {
|
|
37
|
+
epicRef: `${resolved.epic.repo}#${resolved.epic.number}`,
|
|
38
|
+
epicNumber: resolved.epic.number,
|
|
39
|
+
epicRepo: resolved.epic.repo,
|
|
40
|
+
issues: sets.issues,
|
|
41
|
+
prs: sets.prs,
|
|
42
|
+
watchedRepos: sets.repos,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function deriveTrigger(payload) {
|
|
46
|
+
if (payload.githubEvent === 'issues' && payload.action === 'labeled') {
|
|
47
|
+
const labelObj = payload.body['label'];
|
|
48
|
+
if (labelObj != null && typeof labelObj === 'object') {
|
|
49
|
+
const name = labelObj['name'];
|
|
50
|
+
if (typeof name === 'string' && name.startsWith('completed:')) {
|
|
51
|
+
return { kind: 'completed-label', label: name };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (payload.githubEvent === 'issues' && payload.action === 'closed') {
|
|
56
|
+
return { kind: 'issue-closed' };
|
|
57
|
+
}
|
|
58
|
+
if (payload.githubEvent === 'pull_request' && payload.action === 'closed') {
|
|
59
|
+
return { kind: 'pr-closed' };
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
function mapChecks(rollup) {
|
|
64
|
+
switch (rollup) {
|
|
65
|
+
case 'success':
|
|
66
|
+
return 'green';
|
|
67
|
+
case 'failure':
|
|
68
|
+
case 'error':
|
|
69
|
+
return 'red';
|
|
70
|
+
case 'pending':
|
|
71
|
+
case 'none':
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function isEpicPayload(payload, epicNumber) {
|
|
76
|
+
if (payload.githubEvent !== 'issues')
|
|
77
|
+
return false;
|
|
78
|
+
if (payload.action !== 'edited' &&
|
|
79
|
+
payload.action !== 'labeled' &&
|
|
80
|
+
payload.action !== 'unlabeled') {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
const issueObj = payload.body['issue'];
|
|
84
|
+
if (issueObj == null || typeof issueObj !== 'object')
|
|
85
|
+
return false;
|
|
86
|
+
const num = issueObj['number'];
|
|
87
|
+
return num === epicNumber;
|
|
88
|
+
}
|
|
89
|
+
export class SmeeDoorbellSource {
|
|
90
|
+
channelUrl;
|
|
91
|
+
epicRef;
|
|
92
|
+
gh;
|
|
93
|
+
logger;
|
|
94
|
+
onEvent;
|
|
95
|
+
onReconnectAttempt;
|
|
96
|
+
onReconnectSuccess;
|
|
97
|
+
onSseBytes;
|
|
98
|
+
onRefSetRefreshFailure;
|
|
99
|
+
now;
|
|
100
|
+
fetchImpl;
|
|
101
|
+
refreshDebounceMs;
|
|
102
|
+
safetyNetIntervalMs;
|
|
103
|
+
baseReconnectDelayMs;
|
|
104
|
+
refSet = null;
|
|
105
|
+
aggState = initialAggregateState();
|
|
106
|
+
prev = new Map();
|
|
107
|
+
currentResolved = null;
|
|
108
|
+
reconnectAttempt = 0;
|
|
109
|
+
running = false;
|
|
110
|
+
abortController = null;
|
|
111
|
+
refreshTimer = null;
|
|
112
|
+
refreshDebounceTimer = null;
|
|
113
|
+
aggregateDebounceTimer = null;
|
|
114
|
+
pendingAggregateTrigger = null;
|
|
115
|
+
runLoopPromise = null;
|
|
116
|
+
constructor(options) {
|
|
117
|
+
this.channelUrl = options.channelUrl;
|
|
118
|
+
this.epicRef = options.epicRef;
|
|
119
|
+
this.gh = options.gh;
|
|
120
|
+
this.logger = options.logger;
|
|
121
|
+
this.onEvent = options.onEvent;
|
|
122
|
+
this.onReconnectAttempt = options.onReconnectAttempt;
|
|
123
|
+
this.onReconnectSuccess = options.onReconnectSuccess;
|
|
124
|
+
if (options.onSseBytes != null) {
|
|
125
|
+
this.onSseBytes = options.onSseBytes;
|
|
126
|
+
}
|
|
127
|
+
if (options.onRefSetRefreshFailure != null) {
|
|
128
|
+
this.onRefSetRefreshFailure = options.onRefSetRefreshFailure;
|
|
129
|
+
}
|
|
130
|
+
this.now = options.now ?? (() => Date.now());
|
|
131
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
132
|
+
this.refreshDebounceMs = options.refreshDebounceMs ?? DEFAULT_REFRESH_DEBOUNCE_MS;
|
|
133
|
+
this.safetyNetIntervalMs =
|
|
134
|
+
options.safetyNetIntervalMs ?? DEFAULT_SAFETY_NET_INTERVAL_MS;
|
|
135
|
+
this.baseReconnectDelayMs =
|
|
136
|
+
options.baseReconnectDelayMs ?? DEFAULT_BASE_RECONNECT_DELAY_MS;
|
|
137
|
+
}
|
|
138
|
+
async start() {
|
|
139
|
+
if (this.running)
|
|
140
|
+
return;
|
|
141
|
+
// Blocking startup ref-set refresh: propagate resolveEpic failures so the
|
|
142
|
+
// caller (`runSmeeMode`) can demote to poll-fallback.
|
|
143
|
+
const resolved = await resolveEpic({
|
|
144
|
+
epicRef: this.epicRef,
|
|
145
|
+
gh: this.gh,
|
|
146
|
+
logger: this.logger,
|
|
147
|
+
});
|
|
148
|
+
this.currentResolved = resolved;
|
|
149
|
+
this.refSet = buildRefSet(resolved);
|
|
150
|
+
this.running = true;
|
|
151
|
+
this.abortController = new AbortController();
|
|
152
|
+
this.refreshTimer = setInterval(() => this.refreshRefSet(), this.safetyNetIntervalMs);
|
|
153
|
+
if (typeof this.refreshTimer.unref === 'function')
|
|
154
|
+
this.refreshTimer.unref();
|
|
155
|
+
this.runLoopPromise = this.runLoop();
|
|
156
|
+
}
|
|
157
|
+
async stop() {
|
|
158
|
+
if (!this.running)
|
|
159
|
+
return;
|
|
160
|
+
this.running = false;
|
|
161
|
+
if (this.abortController != null) {
|
|
162
|
+
this.abortController.abort();
|
|
163
|
+
this.abortController = null;
|
|
164
|
+
}
|
|
165
|
+
if (this.refreshTimer != null) {
|
|
166
|
+
clearInterval(this.refreshTimer);
|
|
167
|
+
this.refreshTimer = null;
|
|
168
|
+
}
|
|
169
|
+
if (this.refreshDebounceTimer != null) {
|
|
170
|
+
clearTimeout(this.refreshDebounceTimer);
|
|
171
|
+
this.refreshDebounceTimer = null;
|
|
172
|
+
}
|
|
173
|
+
if (this.aggregateDebounceTimer != null) {
|
|
174
|
+
clearTimeout(this.aggregateDebounceTimer);
|
|
175
|
+
this.aggregateDebounceTimer = null;
|
|
176
|
+
}
|
|
177
|
+
if (this.runLoopPromise != null) {
|
|
178
|
+
try {
|
|
179
|
+
await this.runLoopPromise;
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
/* drain */
|
|
183
|
+
}
|
|
184
|
+
this.runLoopPromise = null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
sleep(ms, signal) {
|
|
188
|
+
return new Promise((resolve) => {
|
|
189
|
+
if (signal.aborted) {
|
|
190
|
+
resolve();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const timer = setTimeout(resolve, ms);
|
|
194
|
+
if (typeof timer.unref === 'function')
|
|
195
|
+
timer.unref();
|
|
196
|
+
const onAbort = () => {
|
|
197
|
+
clearTimeout(timer);
|
|
198
|
+
resolve();
|
|
199
|
+
};
|
|
200
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
async runLoop() {
|
|
204
|
+
const signal = this.abortController?.signal;
|
|
205
|
+
if (signal == null)
|
|
206
|
+
return;
|
|
207
|
+
while (this.running && !signal.aborted) {
|
|
208
|
+
let sleepMs = calculateBackoffDelay(this.reconnectAttempt, {
|
|
209
|
+
base: this.baseReconnectDelayMs,
|
|
210
|
+
cap: 30_000,
|
|
211
|
+
});
|
|
212
|
+
try {
|
|
213
|
+
await this.connect(signal);
|
|
214
|
+
this.reconnectAttempt = 0;
|
|
215
|
+
sleepMs = calculateBackoffDelay(this.reconnectAttempt, {
|
|
216
|
+
base: this.baseReconnectDelayMs,
|
|
217
|
+
cap: 30_000,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
if (signal.aborted)
|
|
222
|
+
break;
|
|
223
|
+
this.reconnectAttempt++;
|
|
224
|
+
sleepMs = calculateBackoffDelay(this.reconnectAttempt, {
|
|
225
|
+
base: this.baseReconnectDelayMs,
|
|
226
|
+
cap: 30_000,
|
|
227
|
+
});
|
|
228
|
+
this.logger.warn(`cockpit doorbell: smee connection lost, reconnecting in ${sleepMs}ms (attempt ${this.reconnectAttempt}): ${err instanceof Error ? err.message : String(err)}`);
|
|
229
|
+
try {
|
|
230
|
+
this.onReconnectAttempt(this.reconnectAttempt);
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
/* callback failures don't stop the loop */
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (this.running && !signal.aborted) {
|
|
237
|
+
await this.sleep(sleepMs, signal);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
async connect(signal) {
|
|
242
|
+
const response = await this.fetchImpl(this.channelUrl, {
|
|
243
|
+
headers: { Accept: 'text/event-stream' },
|
|
244
|
+
signal,
|
|
245
|
+
});
|
|
246
|
+
if (!response.ok) {
|
|
247
|
+
throw new Error(`smee connection failed: ${response.status} ${response.statusText}`);
|
|
248
|
+
}
|
|
249
|
+
if (response.body == null) {
|
|
250
|
+
throw new Error('smee response has no body');
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
this.onReconnectSuccess();
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
/* callback failures don't stop the loop */
|
|
257
|
+
}
|
|
258
|
+
const reader = response.body.getReader();
|
|
259
|
+
const decoder = new TextDecoder();
|
|
260
|
+
let buffer = '';
|
|
261
|
+
try {
|
|
262
|
+
while (!signal.aborted) {
|
|
263
|
+
const { done, value } = await reader.read();
|
|
264
|
+
if (done)
|
|
265
|
+
break;
|
|
266
|
+
if (value != null && value.length > 0) {
|
|
267
|
+
try {
|
|
268
|
+
this.onSseBytes?.();
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
/* callback failures don't stop the loop */
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
buffer += decoder.decode(value, { stream: true });
|
|
275
|
+
const events = buffer.split('\n\n');
|
|
276
|
+
buffer = events.pop() ?? '';
|
|
277
|
+
for (const block of events) {
|
|
278
|
+
if (!block.trim())
|
|
279
|
+
continue;
|
|
280
|
+
await this.processEventBlock(block);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
finally {
|
|
285
|
+
reader.releaseLock();
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
async processEventBlock(block) {
|
|
289
|
+
const payload = parseSseEventBlock(block);
|
|
290
|
+
if (payload == null)
|
|
291
|
+
return;
|
|
292
|
+
if (this.refSet == null)
|
|
293
|
+
return;
|
|
294
|
+
if (isEpicPayload(payload, this.refSet.epicNumber)) {
|
|
295
|
+
this.scheduleRefSetRefresh();
|
|
296
|
+
}
|
|
297
|
+
const result = webhookToStreamEvent(payload.githubEvent, payload.action, payload.body, this.refSet, () => new Date(this.now()).toISOString());
|
|
298
|
+
if (result != null) {
|
|
299
|
+
const events = Array.isArray(result) ? result : [result];
|
|
300
|
+
for (const ev of events) {
|
|
301
|
+
let stamped = ev;
|
|
302
|
+
if (ev.type === 'issue-transition' &&
|
|
303
|
+
(ev.event === 'pr-checks' ||
|
|
304
|
+
(ev.event === 'label-change' && ev.sourceLabel === 'completed:validate'))) {
|
|
305
|
+
// no gh calls — read-through PrSnapshot cache only (FR-005)
|
|
306
|
+
const snap = this.prev.get(snapshotKey(ev.repo, 'pr', ev.number));
|
|
307
|
+
if (snap?.kind === 'pr') {
|
|
308
|
+
const wire = mapChecks(snap.checksRollup);
|
|
309
|
+
if (wire !== undefined) {
|
|
310
|
+
stamped = { ...ev, checks: wire };
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
try {
|
|
315
|
+
await this.onEvent(stamped);
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
this.logger.warn(`cockpit doorbell: onEvent sink rejected: ${err instanceof Error ? err.message : String(err)}`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const trigger = deriveTrigger(payload);
|
|
323
|
+
if (trigger != null) {
|
|
324
|
+
this.scheduleAggregateRefresh(trigger);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
scheduleRefSetRefresh() {
|
|
328
|
+
if (this.refreshDebounceTimer != null)
|
|
329
|
+
clearTimeout(this.refreshDebounceTimer);
|
|
330
|
+
this.refreshDebounceTimer = setTimeout(() => {
|
|
331
|
+
this.refreshDebounceTimer = null;
|
|
332
|
+
void this.refreshRefSet();
|
|
333
|
+
}, this.refreshDebounceMs);
|
|
334
|
+
if (typeof this.refreshDebounceTimer.unref === 'function') {
|
|
335
|
+
this.refreshDebounceTimer.unref();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
async refreshRefSet() {
|
|
339
|
+
if (!this.running)
|
|
340
|
+
return;
|
|
341
|
+
try {
|
|
342
|
+
const resolved = await resolveEpic({
|
|
343
|
+
epicRef: this.epicRef,
|
|
344
|
+
gh: this.gh,
|
|
345
|
+
logger: this.logger,
|
|
346
|
+
});
|
|
347
|
+
this.currentResolved = resolved;
|
|
348
|
+
this.refSet = buildRefSet(resolved);
|
|
349
|
+
}
|
|
350
|
+
catch (err) {
|
|
351
|
+
this.logger.warn(`cockpit doorbell: ref-set refresh failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
352
|
+
if (this.onRefSetRefreshFailure != null) {
|
|
353
|
+
try {
|
|
354
|
+
this.onRefSetRefreshFailure(err);
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
/* callback errors are swallowed */
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
scheduleAggregateRefresh(trigger) {
|
|
363
|
+
this.pendingAggregateTrigger = trigger;
|
|
364
|
+
if (this.aggregateDebounceTimer != null) {
|
|
365
|
+
clearTimeout(this.aggregateDebounceTimer);
|
|
366
|
+
}
|
|
367
|
+
this.aggregateDebounceTimer = setTimeout(() => {
|
|
368
|
+
this.aggregateDebounceTimer = null;
|
|
369
|
+
const next = this.pendingAggregateTrigger;
|
|
370
|
+
this.pendingAggregateTrigger = null;
|
|
371
|
+
void this.runAggregateRefresh(next);
|
|
372
|
+
}, AGGREGATE_TRIGGER_DEBOUNCE_MS);
|
|
373
|
+
if (typeof this.aggregateDebounceTimer.unref === 'function') {
|
|
374
|
+
this.aggregateDebounceTimer.unref();
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
async runAggregateRefresh(trigger) {
|
|
378
|
+
if (!this.running || this.refSet == null)
|
|
379
|
+
return;
|
|
380
|
+
const output = await maybeRefreshAggregate({
|
|
381
|
+
trigger,
|
|
382
|
+
epicRef: this.epicRef,
|
|
383
|
+
epicRepo: this.refSet.epicRepo,
|
|
384
|
+
epicNumber: this.refSet.epicNumber,
|
|
385
|
+
prevAgg: this.aggState,
|
|
386
|
+
prev: this.prev,
|
|
387
|
+
currentResolved: this.currentResolved,
|
|
388
|
+
gh: this.gh,
|
|
389
|
+
logger: this.logger,
|
|
390
|
+
now: () => new Date(this.now()).toISOString(),
|
|
391
|
+
});
|
|
392
|
+
this.aggState = output.nextAgg;
|
|
393
|
+
this.prev = output.nextPrev;
|
|
394
|
+
this.currentResolved = output.nextResolved;
|
|
395
|
+
for (const ev of output.events) {
|
|
396
|
+
try {
|
|
397
|
+
await this.onEvent(ev);
|
|
398
|
+
}
|
|
399
|
+
catch (err) {
|
|
400
|
+
this.logger.warn(`cockpit doorbell: aggregate onEvent sink rejected: ${err instanceof Error ? err.message : String(err)}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
//# sourceMappingURL=smee-source.js.map
|