@jmoyers/harness 0.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.
- package/LICENSE +21 -0
- package/README.md +145 -0
- package/native/ptyd/Cargo.lock +16 -0
- package/native/ptyd/Cargo.toml +7 -0
- package/native/ptyd/src/main.rs +257 -0
- package/package.json +90 -0
- package/scripts/build-ptyd.sh +73 -0
- package/scripts/control-plane-daemon.ts +277 -0
- package/scripts/cursor-hook-relay.ts +82 -0
- package/scripts/harness-animate.ts +469 -0
- package/scripts/harness-bin.js +77 -0
- package/scripts/harness-core.ts +1 -0
- package/scripts/harness-inspector.ts +439 -0
- package/scripts/harness.ts +2493 -0
- package/src/adapters/agent-session-state.ts +390 -0
- package/src/cli/gateway-record.ts +173 -0
- package/src/codex/live-session.ts +872 -0
- package/src/config/config-core.ts +1359 -0
- package/src/config/secrets-core.ts +170 -0
- package/src/control-plane/agent-realtime-api.ts +2441 -0
- package/src/control-plane/codex-session-stream.ts +392 -0
- package/src/control-plane/codex-telemetry.ts +1325 -0
- package/src/control-plane/lifecycle-hooks.ts +706 -0
- package/src/control-plane/session-summary.ts +380 -0
- package/src/control-plane/status/agent-status-reducer.ts +21 -0
- package/src/control-plane/status/reducer-base.ts +170 -0
- package/src/control-plane/status/reducers/claude-status-reducer.ts +37 -0
- package/src/control-plane/status/reducers/codex-status-reducer.ts +48 -0
- package/src/control-plane/status/reducers/critique-status-reducer.ts +15 -0
- package/src/control-plane/status/reducers/cursor-status-reducer.ts +37 -0
- package/src/control-plane/status/reducers/terminal-status-reducer.ts +15 -0
- package/src/control-plane/status/session-status-engine.ts +76 -0
- package/src/control-plane/stream-client.ts +396 -0
- package/src/control-plane/stream-command-parser.ts +1673 -0
- package/src/control-plane/stream-protocol.ts +1808 -0
- package/src/control-plane/stream-server-background.ts +486 -0
- package/src/control-plane/stream-server-command.ts +2557 -0
- package/src/control-plane/stream-server-connection.ts +234 -0
- package/src/control-plane/stream-server-observed-filter.ts +112 -0
- package/src/control-plane/stream-server-session-runtime.ts +566 -0
- package/src/control-plane/stream-server-state-store.ts +15 -0
- package/src/control-plane/stream-server.ts +3192 -0
- package/src/cursor/managed-hooks.ts +282 -0
- package/src/domain/conversations.ts +414 -0
- package/src/domain/directories.ts +78 -0
- package/src/domain/repositories.ts +123 -0
- package/src/domain/tasks.ts +148 -0
- package/src/domain/workspace.ts +156 -0
- package/src/events/normalized-events.ts +124 -0
- package/src/mux/ansi-integrity.ts +103 -0
- package/src/mux/control-plane-op-queue.ts +212 -0
- package/src/mux/conversation-rail.ts +339 -0
- package/src/mux/double-click.ts +78 -0
- package/src/mux/dual-pane-core.ts +435 -0
- package/src/mux/harness-core-ui.ts +817 -0
- package/src/mux/input-shortcuts.ts +667 -0
- package/src/mux/live-mux/actions-conversation.ts +344 -0
- package/src/mux/live-mux/actions-repository.ts +246 -0
- package/src/mux/live-mux/actions-task.ts +115 -0
- package/src/mux/live-mux/args.ts +142 -0
- package/src/mux/live-mux/command-menu.ts +298 -0
- package/src/mux/live-mux/control-plane-records.ts +546 -0
- package/src/mux/live-mux/conversation-state.ts +188 -0
- package/src/mux/live-mux/directory-resolution.ts +34 -0
- package/src/mux/live-mux/event-mapping.ts +96 -0
- package/src/mux/live-mux/gateway-profiler.ts +152 -0
- package/src/mux/live-mux/gateway-render-trace.ts +177 -0
- package/src/mux/live-mux/gateway-status-timeline.ts +166 -0
- package/src/mux/live-mux/git-parsing.ts +131 -0
- package/src/mux/live-mux/git-snapshot.ts +263 -0
- package/src/mux/live-mux/git-state.ts +136 -0
- package/src/mux/live-mux/global-shortcut-handlers.ts +143 -0
- package/src/mux/live-mux/home-pane-actions.ts +58 -0
- package/src/mux/live-mux/home-pane-drop.ts +44 -0
- package/src/mux/live-mux/home-pane-entity-click.ts +96 -0
- package/src/mux/live-mux/home-pane-pointer.ts +96 -0
- package/src/mux/live-mux/input-forwarding.ts +112 -0
- package/src/mux/live-mux/layout.ts +30 -0
- package/src/mux/live-mux/left-nav-activation.ts +103 -0
- package/src/mux/live-mux/left-nav.ts +85 -0
- package/src/mux/live-mux/left-rail-actions.ts +118 -0
- package/src/mux/live-mux/left-rail-conversation-click.ts +82 -0
- package/src/mux/live-mux/left-rail-pointer.ts +74 -0
- package/src/mux/live-mux/modal-command-menu-handler.ts +101 -0
- package/src/mux/live-mux/modal-conversation-handlers.ts +217 -0
- package/src/mux/live-mux/modal-input-reducers.ts +94 -0
- package/src/mux/live-mux/modal-overlays.ts +287 -0
- package/src/mux/live-mux/modal-pointer.ts +70 -0
- package/src/mux/live-mux/modal-prompt-handlers.ts +187 -0
- package/src/mux/live-mux/modal-task-editor-handler.ts +156 -0
- package/src/mux/live-mux/observed-stream.ts +87 -0
- package/src/mux/live-mux/palette-parsing.ts +128 -0
- package/src/mux/live-mux/pointer-routing.ts +108 -0
- package/src/mux/live-mux/process-usage.ts +53 -0
- package/src/mux/live-mux/project-pane-pointer.ts +44 -0
- package/src/mux/live-mux/rail-layout.ts +244 -0
- package/src/mux/live-mux/render-trace-analysis.ts +213 -0
- package/src/mux/live-mux/render-trace-state.ts +84 -0
- package/src/mux/live-mux/repository-folding.ts +207 -0
- package/src/mux/live-mux/runtime-shutdown.ts +51 -0
- package/src/mux/live-mux/selection.ts +411 -0
- package/src/mux/live-mux/startup-utils.ts +187 -0
- package/src/mux/live-mux/status-timeline-state.ts +82 -0
- package/src/mux/live-mux/task-pane-shortcuts.ts +206 -0
- package/src/mux/live-mux/terminal-palette.ts +79 -0
- package/src/mux/new-thread-prompt.ts +165 -0
- package/src/mux/project-tree.ts +295 -0
- package/src/mux/render-frame.ts +113 -0
- package/src/mux/runtime-wiring.ts +185 -0
- package/src/mux/selector-index.ts +160 -0
- package/src/mux/startup-sequencer.ts +238 -0
- package/src/mux/task-composer.ts +289 -0
- package/src/mux/task-focused-pane.ts +417 -0
- package/src/mux/task-screen-keybindings.ts +539 -0
- package/src/mux/terminal-input-modes.ts +35 -0
- package/src/mux/workspace-path.ts +55 -0
- package/src/mux/workspace-rail-model.ts +701 -0
- package/src/mux/workspace-rail.ts +247 -0
- package/src/perf/perf-core.ts +307 -0
- package/src/pty/pty_host.ts +217 -0
- package/src/pty/session-broker.ts +158 -0
- package/src/recording/terminal-recording.ts +383 -0
- package/src/services/control-plane.ts +567 -0
- package/src/services/conversation-lifecycle.ts +176 -0
- package/src/services/conversation-startup-hydration.ts +47 -0
- package/src/services/directory-hydration.ts +49 -0
- package/src/services/event-persistence.ts +104 -0
- package/src/services/mux-ui-state-persistence.ts +82 -0
- package/src/services/output-load-sampler.ts +231 -0
- package/src/services/process-usage-refresh.ts +88 -0
- package/src/services/recording.ts +75 -0
- package/src/services/render-trace-recorder.ts +177 -0
- package/src/services/runtime-control-actions.ts +123 -0
- package/src/services/runtime-control-plane-ops.ts +131 -0
- package/src/services/runtime-conversation-actions.ts +113 -0
- package/src/services/runtime-conversation-activation.ts +78 -0
- package/src/services/runtime-conversation-starter.ts +171 -0
- package/src/services/runtime-conversation-title-edit.ts +149 -0
- package/src/services/runtime-directory-actions.ts +164 -0
- package/src/services/runtime-envelope-handler.ts +198 -0
- package/src/services/runtime-git-state.ts +92 -0
- package/src/services/runtime-input-pipeline.ts +50 -0
- package/src/services/runtime-input-router.ts +202 -0
- package/src/services/runtime-layout-resize.ts +236 -0
- package/src/services/runtime-left-rail-render.ts +159 -0
- package/src/services/runtime-main-pane-input.ts +230 -0
- package/src/services/runtime-modal-input.ts +119 -0
- package/src/services/runtime-navigation-input.ts +207 -0
- package/src/services/runtime-process-wiring.ts +68 -0
- package/src/services/runtime-rail-input.ts +287 -0
- package/src/services/runtime-render-flush.ts +146 -0
- package/src/services/runtime-render-lifecycle.ts +104 -0
- package/src/services/runtime-render-orchestrator.ts +108 -0
- package/src/services/runtime-render-pipeline.ts +167 -0
- package/src/services/runtime-render-state.ts +72 -0
- package/src/services/runtime-repository-actions.ts +197 -0
- package/src/services/runtime-right-pane-render.ts +132 -0
- package/src/services/runtime-shutdown.ts +79 -0
- package/src/services/runtime-stream-subscriptions.ts +56 -0
- package/src/services/runtime-task-composer-persistence.ts +139 -0
- package/src/services/runtime-task-editor-actions.ts +83 -0
- package/src/services/runtime-task-pane-actions.ts +198 -0
- package/src/services/runtime-task-pane-shortcuts.ts +189 -0
- package/src/services/runtime-task-pane.ts +62 -0
- package/src/services/runtime-workspace-actions.ts +153 -0
- package/src/services/runtime-workspace-observed-events.ts +190 -0
- package/src/services/session-projection-instrumentation.ts +190 -0
- package/src/services/startup-background-probe.ts +91 -0
- package/src/services/startup-background-resume.ts +65 -0
- package/src/services/startup-orchestrator.ts +166 -0
- package/src/services/startup-output-tracker.ts +54 -0
- package/src/services/startup-paint-tracker.ts +115 -0
- package/src/services/startup-persisted-conversation-queue.ts +45 -0
- package/src/services/startup-settled-gate.ts +67 -0
- package/src/services/startup-shutdown.ts +53 -0
- package/src/services/startup-span-tracker.ts +77 -0
- package/src/services/startup-state-hydration.ts +94 -0
- package/src/services/startup-visibility.ts +35 -0
- package/src/services/status-timeline-recorder.ts +144 -0
- package/src/services/task-pane-selection-actions.ts +153 -0
- package/src/services/task-planning-hydration.ts +58 -0
- package/src/services/task-planning-observed-events.ts +89 -0
- package/src/services/workspace-observed-events.ts +113 -0
- package/src/store/control-plane-store-normalize.ts +760 -0
- package/src/store/control-plane-store-types.ts +224 -0
- package/src/store/control-plane-store.ts +2951 -0
- package/src/store/event-store.ts +253 -0
- package/src/store/sqlite.ts +81 -0
- package/src/terminal/compat-matrix.ts +345 -0
- package/src/terminal/differential-checkpoints.ts +132 -0
- package/src/terminal/parity-suite.ts +441 -0
- package/src/terminal/snapshot-oracle.ts +1840 -0
- package/src/ui/conversation-input-forwarder.ts +114 -0
- package/src/ui/conversation-selection-input.ts +103 -0
- package/src/ui/debug-footer-notice.ts +39 -0
- package/src/ui/global-shortcut-input.ts +126 -0
- package/src/ui/input-preflight.ts +68 -0
- package/src/ui/input-token-router.ts +312 -0
- package/src/ui/input.ts +238 -0
- package/src/ui/kit.ts +509 -0
- package/src/ui/left-nav-input.ts +80 -0
- package/src/ui/left-rail-pointer-input.ts +148 -0
- package/src/ui/main-pane-pointer-input.ts +150 -0
- package/src/ui/modals/manager.ts +192 -0
- package/src/ui/mux-theme.ts +529 -0
- package/src/ui/panes/conversation.ts +19 -0
- package/src/ui/panes/home-gridfire.ts +302 -0
- package/src/ui/panes/home.ts +109 -0
- package/src/ui/panes/left-rail.ts +12 -0
- package/src/ui/panes/project.ts +44 -0
- package/src/ui/pointer-routing-input.ts +158 -0
- package/src/ui/repository-fold-input.ts +91 -0
- package/src/ui/screen.ts +210 -0
- package/src/ui/surface.ts +224 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { resolveStatusTimelineStatePath } from './status-timeline-state.ts';
|
|
6
|
+
|
|
7
|
+
type GatewayStatusTimelineAction = 'start' | 'stop';
|
|
8
|
+
|
|
9
|
+
interface RunHarnessStatusTimelineCommandInput {
|
|
10
|
+
readonly invocationDirectory: string;
|
|
11
|
+
readonly harnessScriptPath: string;
|
|
12
|
+
readonly sessionName: string | null;
|
|
13
|
+
readonly action: GatewayStatusTimelineAction;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface RunHarnessStatusTimelineCommandResult {
|
|
17
|
+
readonly stdout: string;
|
|
18
|
+
readonly stderr: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ToggleGatewayStatusTimelineOptions {
|
|
22
|
+
readonly invocationDirectory: string;
|
|
23
|
+
readonly sessionName: string | null;
|
|
24
|
+
readonly statusTimelineStateExists?: (statusTimelineStatePath: string) => boolean;
|
|
25
|
+
readonly runHarnessStatusTimelineCommand?: (
|
|
26
|
+
input: RunHarnessStatusTimelineCommandInput,
|
|
27
|
+
) => Promise<RunHarnessStatusTimelineCommandResult>;
|
|
28
|
+
readonly harnessScriptPath?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface ToggleGatewayStatusTimelineResult {
|
|
32
|
+
readonly action: GatewayStatusTimelineAction;
|
|
33
|
+
readonly message: string;
|
|
34
|
+
readonly stdout: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
|
38
|
+
const DEFAULT_HARNESS_SCRIPT_PATH = resolve(SCRIPT_DIR, '../../../scripts/harness.ts');
|
|
39
|
+
|
|
40
|
+
function firstNonEmptyLine(text: string): string | null {
|
|
41
|
+
const lines = text
|
|
42
|
+
.split('\n')
|
|
43
|
+
.map((line) => line.trim())
|
|
44
|
+
.filter((line) => line.length > 0);
|
|
45
|
+
return lines[0] ?? null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function lineValueForPrefix(text: string, prefix: string): string | null {
|
|
49
|
+
for (const rawLine of text.split('\n')) {
|
|
50
|
+
const line = rawLine.trim();
|
|
51
|
+
if (!line.startsWith(prefix)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const value = line.slice(prefix.length).trim();
|
|
55
|
+
if (value.length > 0) {
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function resolveHarnessStatusTimelineCommandArgs(
|
|
63
|
+
action: GatewayStatusTimelineAction,
|
|
64
|
+
sessionName: string | null,
|
|
65
|
+
): readonly string[] {
|
|
66
|
+
if (sessionName === null) {
|
|
67
|
+
return ['status-timeline', action];
|
|
68
|
+
}
|
|
69
|
+
return ['--session', sessionName, 'status-timeline', action];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function summarizeStatusTimelineSuccess(
|
|
73
|
+
action: GatewayStatusTimelineAction,
|
|
74
|
+
stdout: string,
|
|
75
|
+
): string {
|
|
76
|
+
if (action === 'start') {
|
|
77
|
+
const timelinePath = lineValueForPrefix(stdout, 'status-timeline-target:');
|
|
78
|
+
if (timelinePath !== null) {
|
|
79
|
+
return `status: timeline=${timelinePath}`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const firstLine = firstNonEmptyLine(stdout);
|
|
83
|
+
if (firstLine !== null) {
|
|
84
|
+
return firstLine;
|
|
85
|
+
}
|
|
86
|
+
if (action === 'start') {
|
|
87
|
+
return 'status timeline started';
|
|
88
|
+
}
|
|
89
|
+
return 'status timeline stopped';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function summarizeStatusTimelineFailure(
|
|
93
|
+
action: GatewayStatusTimelineAction,
|
|
94
|
+
stderr: string,
|
|
95
|
+
stdout: string,
|
|
96
|
+
): string {
|
|
97
|
+
const detail = firstNonEmptyLine(stderr) ?? firstNonEmptyLine(stdout) ?? 'unknown error';
|
|
98
|
+
return `status timeline ${action} failed: ${detail}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function runHarnessStatusTimelineCommand(
|
|
102
|
+
input: RunHarnessStatusTimelineCommandInput,
|
|
103
|
+
): Promise<RunHarnessStatusTimelineCommandResult> {
|
|
104
|
+
const commandArgs = resolveHarnessStatusTimelineCommandArgs(input.action, input.sessionName);
|
|
105
|
+
const child = spawn(process.execPath, [input.harnessScriptPath, ...commandArgs], {
|
|
106
|
+
cwd: input.invocationDirectory,
|
|
107
|
+
env: {
|
|
108
|
+
...process.env,
|
|
109
|
+
HARNESS_INVOKE_CWD: input.invocationDirectory,
|
|
110
|
+
},
|
|
111
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
112
|
+
});
|
|
113
|
+
const stdoutChunks: Buffer[] = [];
|
|
114
|
+
const stderrChunks: Buffer[] = [];
|
|
115
|
+
child.stdout?.on('data', (chunk: Buffer | string) => {
|
|
116
|
+
stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
117
|
+
});
|
|
118
|
+
child.stderr?.on('data', (chunk: Buffer | string) => {
|
|
119
|
+
stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const [code, signal] = await new Promise<[number | null, NodeJS.Signals | null]>(
|
|
123
|
+
(resolveExit, rejectExit) => {
|
|
124
|
+
child.once('error', rejectExit);
|
|
125
|
+
child.once('exit', (exitCode, exitSignal) => {
|
|
126
|
+
resolveExit([exitCode, exitSignal]);
|
|
127
|
+
});
|
|
128
|
+
},
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const stdout = Buffer.concat(stdoutChunks).toString('utf8');
|
|
132
|
+
const stderr = Buffer.concat(stderrChunks).toString('utf8');
|
|
133
|
+
const exitCode = code ?? (signal === 'SIGINT' ? 130 : signal === 'SIGTERM' ? 143 : 1);
|
|
134
|
+
if (exitCode !== 0) {
|
|
135
|
+
throw new Error(summarizeStatusTimelineFailure(input.action, stderr, stdout));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
stdout,
|
|
140
|
+
stderr,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function toggleGatewayStatusTimeline(
|
|
145
|
+
options: ToggleGatewayStatusTimelineOptions,
|
|
146
|
+
): Promise<ToggleGatewayStatusTimelineResult> {
|
|
147
|
+
const statePath = resolveStatusTimelineStatePath(
|
|
148
|
+
options.invocationDirectory,
|
|
149
|
+
options.sessionName,
|
|
150
|
+
);
|
|
151
|
+
const isRunning = (options.statusTimelineStateExists ?? existsSync)(statePath);
|
|
152
|
+
const action: GatewayStatusTimelineAction = isRunning ? 'stop' : 'start';
|
|
153
|
+
const harnessScriptPath = options.harnessScriptPath ?? DEFAULT_HARNESS_SCRIPT_PATH;
|
|
154
|
+
const runCommand = options.runHarnessStatusTimelineCommand ?? runHarnessStatusTimelineCommand;
|
|
155
|
+
const result = await runCommand({
|
|
156
|
+
invocationDirectory: options.invocationDirectory,
|
|
157
|
+
harnessScriptPath,
|
|
158
|
+
sessionName: options.sessionName,
|
|
159
|
+
action,
|
|
160
|
+
});
|
|
161
|
+
return {
|
|
162
|
+
action,
|
|
163
|
+
message: summarizeStatusTimelineSuccess(action, result.stdout),
|
|
164
|
+
stdout: result.stdout,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
export function parseGitBranchFromStatusHeader(header: string | null): string {
|
|
2
|
+
if (header === null) {
|
|
3
|
+
return '(detached)';
|
|
4
|
+
}
|
|
5
|
+
const raw = header.trim();
|
|
6
|
+
if (raw.length === 0) {
|
|
7
|
+
return '(detached)';
|
|
8
|
+
}
|
|
9
|
+
if (raw.startsWith('No commits yet on')) {
|
|
10
|
+
const branch = raw.slice('No commits yet on'.length).trim();
|
|
11
|
+
return branch.length > 0 ? branch : '(detached)';
|
|
12
|
+
}
|
|
13
|
+
const [headPart = ''] = raw.split('...');
|
|
14
|
+
const head = headPart.trim();
|
|
15
|
+
if (head.length === 0 || head === 'HEAD' || head.startsWith('HEAD ')) {
|
|
16
|
+
return '(detached)';
|
|
17
|
+
}
|
|
18
|
+
return head;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function parseGitShortstatCounts(output: string): { additions: number; deletions: number } {
|
|
22
|
+
const additionsMatch = output.match(/(\d+)\s+insertions?\(\+\)/);
|
|
23
|
+
const deletionsMatch = output.match(/(\d+)\s+deletions?\(-\)/);
|
|
24
|
+
return {
|
|
25
|
+
additions: additionsMatch === null ? 0 : Number.parseInt(additionsMatch[1]!, 10),
|
|
26
|
+
deletions: deletionsMatch === null ? 0 : Number.parseInt(deletionsMatch[1]!, 10),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function normalizeGitHubRemoteUrl(value: string): string | null {
|
|
31
|
+
const trimmed = value.trim();
|
|
32
|
+
if (trimmed.length === 0) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const normalizePath = (rawPath: string): string | null => {
|
|
37
|
+
let path = rawPath.trim().replace(/^\/+/u, '').replace(/\/+$/u, '');
|
|
38
|
+
if (path.length === 0) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
if (path.endsWith('.git')) {
|
|
42
|
+
path = path.slice(0, -4);
|
|
43
|
+
}
|
|
44
|
+
const [owner = '', repository = ''] = path.split('/');
|
|
45
|
+
if (owner.length === 0 || repository.length === 0) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return `https://github.com/${owner.toLowerCase()}/${repository.toLowerCase()}`;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const scpMatch = trimmed.match(/^git@github\.com:(.+)$/iu);
|
|
52
|
+
if (scpMatch !== null) {
|
|
53
|
+
return normalizePath(scpMatch[1]!);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let candidate = trimmed;
|
|
57
|
+
if (candidate.startsWith('ssh://')) {
|
|
58
|
+
candidate = `https://${candidate.slice('ssh://'.length)}`;
|
|
59
|
+
} else if (candidate.startsWith('git://')) {
|
|
60
|
+
candidate = `https://${candidate.slice('git://'.length)}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let parsed: URL;
|
|
64
|
+
try {
|
|
65
|
+
parsed = new URL(candidate);
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
if (parsed.hostname.toLowerCase() !== 'github.com') {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
return normalizePath(parsed.pathname);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function repositoryNameFromGitHubRemoteUrl(remoteUrl: string): string {
|
|
76
|
+
const normalized = normalizeGitHubRemoteUrl(remoteUrl);
|
|
77
|
+
if (normalized === null) {
|
|
78
|
+
return remoteUrl;
|
|
79
|
+
}
|
|
80
|
+
const parts = normalized.split('/');
|
|
81
|
+
const name = parts[parts.length - 1]!;
|
|
82
|
+
return name;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function shouldShowGitHubPrActions(input: {
|
|
86
|
+
trackedBranch: string | null;
|
|
87
|
+
defaultBranch: string | null;
|
|
88
|
+
}): boolean {
|
|
89
|
+
const trackedBranch = input.trackedBranch?.trim() ?? '';
|
|
90
|
+
if (trackedBranch.length === 0 || trackedBranch === '(detached)') {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
const normalizedTrackedBranch = trackedBranch.toLowerCase();
|
|
94
|
+
const normalizedDefaultBranch = input.defaultBranch?.trim().toLowerCase() ?? '';
|
|
95
|
+
if (normalizedDefaultBranch.length > 0) {
|
|
96
|
+
return normalizedTrackedBranch !== normalizedDefaultBranch;
|
|
97
|
+
}
|
|
98
|
+
return normalizedTrackedBranch !== 'main';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function parseCommitCount(output: string): number | null {
|
|
102
|
+
const trimmed = output.trim();
|
|
103
|
+
if (trimmed.length === 0) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
107
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
return parsed;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function parseLastCommitLine(output: string): {
|
|
114
|
+
lastCommitAt: string | null;
|
|
115
|
+
shortCommitHash: string | null;
|
|
116
|
+
} {
|
|
117
|
+
const trimmed = output.trim();
|
|
118
|
+
if (trimmed.length === 0) {
|
|
119
|
+
return {
|
|
120
|
+
lastCommitAt: null,
|
|
121
|
+
shortCommitHash: null,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const [hashPart = '', tsPart = ''] = trimmed.split('\t');
|
|
125
|
+
const hash = hashPart.trim();
|
|
126
|
+
const timestamp = tsPart.trim();
|
|
127
|
+
return {
|
|
128
|
+
lastCommitAt: timestamp.length > 0 ? timestamp : null,
|
|
129
|
+
shortCommitHash: hash,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import {
|
|
4
|
+
normalizeGitHubRemoteUrl,
|
|
5
|
+
parseCommitCount,
|
|
6
|
+
parseGitBranchFromStatusHeader,
|
|
7
|
+
parseGitShortstatCounts,
|
|
8
|
+
parseLastCommitLine,
|
|
9
|
+
repositoryNameFromGitHubRemoteUrl,
|
|
10
|
+
} from './git-parsing.ts';
|
|
11
|
+
|
|
12
|
+
const execFileAsync = promisify(execFile);
|
|
13
|
+
|
|
14
|
+
interface GitSummary {
|
|
15
|
+
readonly branch: string;
|
|
16
|
+
readonly changedFiles: number;
|
|
17
|
+
readonly additions: number;
|
|
18
|
+
readonly deletions: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface GitRepositorySnapshot {
|
|
22
|
+
readonly normalizedRemoteUrl: string | null;
|
|
23
|
+
readonly commitCount: number | null;
|
|
24
|
+
readonly lastCommitAt: string | null;
|
|
25
|
+
readonly shortCommitHash: string | null;
|
|
26
|
+
readonly inferredName: string | null;
|
|
27
|
+
readonly defaultBranch: string | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface GitDirectorySnapshot {
|
|
31
|
+
readonly summary: GitSummary;
|
|
32
|
+
readonly repository: GitRepositorySnapshot;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface ReadGitDirectorySnapshotOptions {
|
|
36
|
+
readonly includeCommitCount?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface ProcessUsageSample {
|
|
40
|
+
readonly cpuPercent: number | null;
|
|
41
|
+
readonly memoryMb: number | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const GIT_SUMMARY_NOT_REPOSITORY: GitSummary = {
|
|
45
|
+
branch: '(not git)',
|
|
46
|
+
changedFiles: 0,
|
|
47
|
+
additions: 0,
|
|
48
|
+
deletions: 0,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const GIT_REPOSITORY_NONE: GitRepositorySnapshot = {
|
|
52
|
+
normalizedRemoteUrl: null,
|
|
53
|
+
commitCount: null,
|
|
54
|
+
lastCommitAt: null,
|
|
55
|
+
shortCommitHash: null,
|
|
56
|
+
inferredName: null,
|
|
57
|
+
defaultBranch: null,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
interface ExecFileResult {
|
|
61
|
+
readonly stdout: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface GitProcessRunnerOptions {
|
|
65
|
+
readonly cwd: string;
|
|
66
|
+
readonly encoding: 'utf8';
|
|
67
|
+
readonly timeout: number;
|
|
68
|
+
readonly maxBuffer: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface PsProcessRunnerOptions {
|
|
72
|
+
readonly encoding: 'utf8';
|
|
73
|
+
readonly timeout: number;
|
|
74
|
+
readonly maxBuffer: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export type GitProcessRunner = (
|
|
78
|
+
command: string,
|
|
79
|
+
args: readonly string[],
|
|
80
|
+
options: GitProcessRunnerOptions,
|
|
81
|
+
) => Promise<ExecFileResult>;
|
|
82
|
+
|
|
83
|
+
export type PsProcessRunner = (
|
|
84
|
+
command: string,
|
|
85
|
+
args: readonly string[],
|
|
86
|
+
options: PsProcessRunnerOptions,
|
|
87
|
+
) => Promise<ExecFileResult>;
|
|
88
|
+
|
|
89
|
+
export type GitCommandRunner = (cwd: string, args: readonly string[]) => Promise<string>;
|
|
90
|
+
|
|
91
|
+
const defaultGitProcessRunner: GitProcessRunner = async (command, args, options) => {
|
|
92
|
+
const result = await execFileAsync(command, [...args], options);
|
|
93
|
+
return {
|
|
94
|
+
stdout: result.stdout,
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const defaultPsProcessRunner: PsProcessRunner = async (command, args, options) => {
|
|
99
|
+
const result = await execFileAsync(command, [...args], options);
|
|
100
|
+
return {
|
|
101
|
+
stdout: result.stdout,
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export async function runGitCommand(
|
|
106
|
+
cwd: string,
|
|
107
|
+
args: readonly string[],
|
|
108
|
+
processRunner: GitProcessRunner = defaultGitProcessRunner,
|
|
109
|
+
): Promise<string> {
|
|
110
|
+
try {
|
|
111
|
+
const result = await processRunner('git', args, {
|
|
112
|
+
cwd,
|
|
113
|
+
encoding: 'utf8',
|
|
114
|
+
timeout: 1500,
|
|
115
|
+
maxBuffer: 1024 * 1024,
|
|
116
|
+
});
|
|
117
|
+
return result.stdout.trim();
|
|
118
|
+
} catch {
|
|
119
|
+
return '';
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function readNormalizedGitHubRemoteUrl(
|
|
124
|
+
cwd: string,
|
|
125
|
+
runCommand: GitCommandRunner,
|
|
126
|
+
): Promise<string | null> {
|
|
127
|
+
const originRemoteUrl = normalizeGitHubRemoteUrl(
|
|
128
|
+
await runCommand(cwd, ['remote', 'get-url', 'origin']),
|
|
129
|
+
);
|
|
130
|
+
if (originRemoteUrl !== null) {
|
|
131
|
+
return originRemoteUrl;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const remoteNames = (await runCommand(cwd, ['remote']))
|
|
135
|
+
.split('\n')
|
|
136
|
+
.map((name) => name.trim())
|
|
137
|
+
.filter((name) => name.length > 0);
|
|
138
|
+
for (const remoteName of remoteNames) {
|
|
139
|
+
const remoteUrl = normalizeGitHubRemoteUrl(
|
|
140
|
+
await runCommand(cwd, ['remote', 'get-url', remoteName]),
|
|
141
|
+
);
|
|
142
|
+
if (remoteUrl !== null) {
|
|
143
|
+
return remoteUrl;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function readGitDirectorySnapshot(
|
|
150
|
+
cwd: string,
|
|
151
|
+
runCommand: GitCommandRunner = runGitCommand,
|
|
152
|
+
options: ReadGitDirectorySnapshotOptions = {},
|
|
153
|
+
): Promise<GitDirectorySnapshot> {
|
|
154
|
+
const insideWorkTree = await runCommand(cwd, ['rev-parse', '--is-inside-work-tree']);
|
|
155
|
+
if (insideWorkTree !== 'true') {
|
|
156
|
+
return {
|
|
157
|
+
summary: GIT_SUMMARY_NOT_REPOSITORY,
|
|
158
|
+
repository: GIT_REPOSITORY_NONE,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const statusOutputPromise = runCommand(cwd, ['status', '--porcelain=1', '--branch']);
|
|
163
|
+
const unstagedShortstatPromise = runCommand(cwd, ['diff', '--shortstat']);
|
|
164
|
+
const stagedShortstatPromise = runCommand(cwd, ['diff', '--cached', '--shortstat']);
|
|
165
|
+
const remoteUrlPromise = readNormalizedGitHubRemoteUrl(cwd, runCommand);
|
|
166
|
+
const lastCommitPromise = runCommand(cwd, ['log', '-1', '--format=%ct %h']);
|
|
167
|
+
const commitCountPromise =
|
|
168
|
+
options.includeCommitCount === false
|
|
169
|
+
? Promise.resolve('')
|
|
170
|
+
: runCommand(cwd, ['rev-list', '--count', 'HEAD']);
|
|
171
|
+
|
|
172
|
+
const statusOutput = await statusOutputPromise;
|
|
173
|
+
const statusLines = statusOutput.split('\n').filter((line) => line.trim().length > 0);
|
|
174
|
+
const firstStatusLine = statusLines[0];
|
|
175
|
+
const headerLine =
|
|
176
|
+
firstStatusLine !== undefined && firstStatusLine.startsWith('## ')
|
|
177
|
+
? statusLines.shift()!.slice(3)
|
|
178
|
+
: null;
|
|
179
|
+
const branch = parseGitBranchFromStatusHeader(headerLine);
|
|
180
|
+
const changedFiles = statusLines.length;
|
|
181
|
+
|
|
182
|
+
const [unstagedShortstat, stagedShortstat, remoteUrlRaw, commitCountRaw, lastCommitRaw] =
|
|
183
|
+
await Promise.all([
|
|
184
|
+
unstagedShortstatPromise,
|
|
185
|
+
stagedShortstatPromise,
|
|
186
|
+
remoteUrlPromise,
|
|
187
|
+
commitCountPromise,
|
|
188
|
+
lastCommitPromise,
|
|
189
|
+
]);
|
|
190
|
+
|
|
191
|
+
const unstaged = parseGitShortstatCounts(unstagedShortstat);
|
|
192
|
+
const staged = parseGitShortstatCounts(stagedShortstat);
|
|
193
|
+
const normalizedRemoteUrl = remoteUrlRaw;
|
|
194
|
+
const commitCount = parseCommitCount(commitCountRaw);
|
|
195
|
+
const lastCommit = parseLastCommitLine(lastCommitRaw);
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
summary: {
|
|
199
|
+
branch,
|
|
200
|
+
changedFiles,
|
|
201
|
+
additions: unstaged.additions + staged.additions,
|
|
202
|
+
deletions: unstaged.deletions + staged.deletions,
|
|
203
|
+
},
|
|
204
|
+
repository: {
|
|
205
|
+
normalizedRemoteUrl,
|
|
206
|
+
commitCount,
|
|
207
|
+
lastCommitAt: lastCommit.lastCommitAt,
|
|
208
|
+
shortCommitHash: lastCommit.shortCommitHash,
|
|
209
|
+
inferredName:
|
|
210
|
+
normalizedRemoteUrl === null
|
|
211
|
+
? null
|
|
212
|
+
: repositoryNameFromGitHubRemoteUrl(normalizedRemoteUrl),
|
|
213
|
+
defaultBranch: branch === '(detached)' ? null : branch,
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export async function readProcessUsageSample(
|
|
219
|
+
processId: number | null,
|
|
220
|
+
processRunner: PsProcessRunner = defaultPsProcessRunner,
|
|
221
|
+
): Promise<ProcessUsageSample> {
|
|
222
|
+
if (processId === null) {
|
|
223
|
+
return {
|
|
224
|
+
cpuPercent: null,
|
|
225
|
+
memoryMb: null,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let stdout = '';
|
|
230
|
+
try {
|
|
231
|
+
const result = await processRunner('ps', ['-p', String(processId), '-o', '%cpu=,rss='], {
|
|
232
|
+
encoding: 'utf8',
|
|
233
|
+
timeout: 1000,
|
|
234
|
+
maxBuffer: 8 * 1024,
|
|
235
|
+
});
|
|
236
|
+
stdout = result.stdout;
|
|
237
|
+
} catch {
|
|
238
|
+
return {
|
|
239
|
+
cpuPercent: null,
|
|
240
|
+
memoryMb: null,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const line = stdout
|
|
245
|
+
.split('\n')
|
|
246
|
+
.map((entry) => entry.trim())
|
|
247
|
+
.filter((entry) => entry.length > 0)
|
|
248
|
+
.at(-1);
|
|
249
|
+
if (line === undefined) {
|
|
250
|
+
return {
|
|
251
|
+
cpuPercent: null,
|
|
252
|
+
memoryMb: null,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const parts = line.split(/\s+/);
|
|
257
|
+
const cpuPercentRaw = Number.parseFloat(String(parts[0]));
|
|
258
|
+
const memoryKbRaw = Number.parseInt(String(parts[1]), 10);
|
|
259
|
+
return {
|
|
260
|
+
cpuPercent: Number.isFinite(cpuPercentRaw) ? cpuPercentRaw : null,
|
|
261
|
+
memoryMb: Number.isFinite(memoryKbRaw) ? memoryKbRaw / 1024 : null,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { StreamObservedEvent } from '../../control-plane/stream-protocol.ts';
|
|
2
|
+
|
|
3
|
+
export interface GitSummary {
|
|
4
|
+
readonly branch: string;
|
|
5
|
+
readonly changedFiles: number;
|
|
6
|
+
readonly additions: number;
|
|
7
|
+
readonly deletions: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface GitRepositorySnapshot {
|
|
11
|
+
readonly normalizedRemoteUrl: string | null;
|
|
12
|
+
readonly commitCount: number | null;
|
|
13
|
+
readonly lastCommitAt: string | null;
|
|
14
|
+
readonly shortCommitHash: string | null;
|
|
15
|
+
readonly inferredName: string | null;
|
|
16
|
+
readonly defaultBranch: string | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function gitSummaryEqual(left: GitSummary, right: GitSummary): boolean {
|
|
20
|
+
return (
|
|
21
|
+
left.branch === right.branch &&
|
|
22
|
+
left.changedFiles === right.changedFiles &&
|
|
23
|
+
left.additions === right.additions &&
|
|
24
|
+
left.deletions === right.deletions
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function gitRepositorySnapshotEqual(
|
|
29
|
+
left: GitRepositorySnapshot,
|
|
30
|
+
right: GitRepositorySnapshot,
|
|
31
|
+
): boolean {
|
|
32
|
+
return (
|
|
33
|
+
left.normalizedRemoteUrl === right.normalizedRemoteUrl &&
|
|
34
|
+
left.commitCount === right.commitCount &&
|
|
35
|
+
left.lastCommitAt === right.lastCommitAt &&
|
|
36
|
+
left.shortCommitHash === right.shortCommitHash &&
|
|
37
|
+
left.defaultBranch === right.defaultBranch &&
|
|
38
|
+
left.inferredName === right.inferredName
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function deleteDirectoryGitState(
|
|
43
|
+
directoryId: string,
|
|
44
|
+
gitSummaryByDirectoryId: Map<string, GitSummary>,
|
|
45
|
+
directoryRepositorySnapshotByDirectoryId: Map<string, GitRepositorySnapshot>,
|
|
46
|
+
repositoryAssociationByDirectoryId: Map<string, string>,
|
|
47
|
+
): void {
|
|
48
|
+
gitSummaryByDirectoryId.delete(directoryId);
|
|
49
|
+
directoryRepositorySnapshotByDirectoryId.delete(directoryId);
|
|
50
|
+
repositoryAssociationByDirectoryId.delete(directoryId);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface ApplyObservedGitStatusEventOptions<TRepositoryRecord extends { repositoryId: string }> {
|
|
54
|
+
enabled: boolean;
|
|
55
|
+
observed: StreamObservedEvent;
|
|
56
|
+
gitSummaryByDirectoryId: Map<string, GitSummary>;
|
|
57
|
+
loadingSummary: GitSummary;
|
|
58
|
+
directoryRepositorySnapshotByDirectoryId: Map<string, GitRepositorySnapshot>;
|
|
59
|
+
emptyRepositorySnapshot: GitRepositorySnapshot;
|
|
60
|
+
repositoryAssociationByDirectoryId: Map<string, string>;
|
|
61
|
+
repositories: Map<string, TRepositoryRecord>;
|
|
62
|
+
parseRepositoryRecord: (input: unknown) => TRepositoryRecord | null;
|
|
63
|
+
repositoryRecordChanged: (
|
|
64
|
+
previous: TRepositoryRecord | undefined,
|
|
65
|
+
next: TRepositoryRecord,
|
|
66
|
+
) => boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface ApplyObservedGitStatusEventResult {
|
|
70
|
+
readonly handled: boolean;
|
|
71
|
+
readonly changed: boolean;
|
|
72
|
+
readonly repositoryRecordChanged: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function applyObservedGitStatusEvent<TRepositoryRecord extends { repositoryId: string }>(
|
|
76
|
+
options: ApplyObservedGitStatusEventOptions<TRepositoryRecord>,
|
|
77
|
+
): ApplyObservedGitStatusEventResult {
|
|
78
|
+
const {
|
|
79
|
+
enabled,
|
|
80
|
+
observed,
|
|
81
|
+
gitSummaryByDirectoryId,
|
|
82
|
+
loadingSummary,
|
|
83
|
+
directoryRepositorySnapshotByDirectoryId,
|
|
84
|
+
emptyRepositorySnapshot,
|
|
85
|
+
repositoryAssociationByDirectoryId,
|
|
86
|
+
repositories,
|
|
87
|
+
parseRepositoryRecord,
|
|
88
|
+
repositoryRecordChanged,
|
|
89
|
+
} = options;
|
|
90
|
+
if (!enabled || observed.type !== 'directory-git-updated') {
|
|
91
|
+
return {
|
|
92
|
+
handled: false,
|
|
93
|
+
changed: false,
|
|
94
|
+
repositoryRecordChanged: false,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const previousSummary = gitSummaryByDirectoryId.get(observed.directoryId) ?? loadingSummary;
|
|
99
|
+
const summaryChanged = !gitSummaryEqual(previousSummary, observed.summary);
|
|
100
|
+
gitSummaryByDirectoryId.set(observed.directoryId, observed.summary);
|
|
101
|
+
|
|
102
|
+
const previousRepositorySnapshot =
|
|
103
|
+
directoryRepositorySnapshotByDirectoryId.get(observed.directoryId) ?? emptyRepositorySnapshot;
|
|
104
|
+
const repositorySnapshotChanged = !gitRepositorySnapshotEqual(
|
|
105
|
+
previousRepositorySnapshot,
|
|
106
|
+
observed.repositorySnapshot,
|
|
107
|
+
);
|
|
108
|
+
directoryRepositorySnapshotByDirectoryId.set(observed.directoryId, observed.repositorySnapshot);
|
|
109
|
+
|
|
110
|
+
let associationChanged = false;
|
|
111
|
+
if (observed.repositoryId === null) {
|
|
112
|
+
associationChanged = repositoryAssociationByDirectoryId.delete(observed.directoryId);
|
|
113
|
+
} else {
|
|
114
|
+
const previousRepositoryId =
|
|
115
|
+
repositoryAssociationByDirectoryId.get(observed.directoryId) ?? null;
|
|
116
|
+
repositoryAssociationByDirectoryId.set(observed.directoryId, observed.repositoryId);
|
|
117
|
+
associationChanged = previousRepositoryId !== observed.repositoryId;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let changedRepositoryRecord = false;
|
|
121
|
+
if (observed.repository !== null) {
|
|
122
|
+
const repository = parseRepositoryRecord(observed.repository);
|
|
123
|
+
if (repository !== null) {
|
|
124
|
+
const previous = repositories.get(repository.repositoryId);
|
|
125
|
+
repositories.set(repository.repositoryId, repository);
|
|
126
|
+
changedRepositoryRecord = repositoryRecordChanged(previous, repository);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
handled: true,
|
|
132
|
+
changed:
|
|
133
|
+
summaryChanged || repositorySnapshotChanged || associationChanged || changedRepositoryRecord,
|
|
134
|
+
repositoryRecordChanged: changedRepositoryRecord,
|
|
135
|
+
};
|
|
136
|
+
}
|