codekin 0.3.7 → 0.4.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/README.md +2 -1
- package/dist/assets/index-BAdQqYEY.js +182 -0
- package/dist/assets/index-CeZYNLWt.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +8 -2
- package/server/dist/approval-manager.d.ts +44 -8
- package/server/dist/approval-manager.js +262 -23
- package/server/dist/approval-manager.js.map +1 -1
- package/server/dist/claude-process.d.ts +16 -0
- package/server/dist/claude-process.js +35 -10
- package/server/dist/claude-process.js.map +1 -1
- package/server/dist/commit-event-handler.d.ts +41 -0
- package/server/dist/commit-event-handler.js +99 -0
- package/server/dist/commit-event-handler.js.map +1 -0
- package/server/dist/commit-event-hooks.d.ts +35 -0
- package/server/dist/commit-event-hooks.js +177 -0
- package/server/dist/commit-event-hooks.js.map +1 -0
- package/server/dist/crypto-utils.js +10 -5
- package/server/dist/crypto-utils.js.map +1 -1
- package/server/dist/diff-parser.d.ts +23 -0
- package/server/dist/diff-parser.js +236 -0
- package/server/dist/diff-parser.js.map +1 -0
- package/server/dist/session-manager.d.ts +25 -8
- package/server/dist/session-manager.js +364 -29
- package/server/dist/session-manager.js.map +1 -1
- package/server/dist/session-routes.js +101 -4
- package/server/dist/session-routes.js.map +1 -1
- package/server/dist/stepflow-handler.js +17 -3
- package/server/dist/stepflow-handler.js.map +1 -1
- package/server/dist/tsconfig.tsbuildinfo +1 -1
- package/server/dist/types.d.ts +62 -1
- package/server/dist/upload-routes.d.ts +1 -1
- package/server/dist/upload-routes.js +40 -13
- package/server/dist/upload-routes.js.map +1 -1
- package/server/dist/webhook-workspace.js +5 -2
- package/server/dist/webhook-workspace.js.map +1 -1
- package/server/dist/workflow-loader.d.ts +6 -0
- package/server/dist/workflow-loader.js +11 -0
- package/server/dist/workflow-loader.js.map +1 -1
- package/server/dist/workflow-routes.d.ts +5 -1
- package/server/dist/workflow-routes.js +48 -7
- package/server/dist/workflow-routes.js.map +1 -1
- package/server/dist/ws-message-handler.js +20 -0
- package/server/dist/ws-message-handler.js.map +1 -1
- package/server/dist/ws-server.js +19 -3
- package/server/dist/ws-server.js.map +1 -1
- package/server/workflows/commit-review.md +22 -0
- package/server/workflows/docs-audit.weekly.md +97 -0
- package/dist/assets/index-Dc76fIdG.js +0 -174
- package/dist/assets/index-DoL2Uppj.css +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff-parser.js","sourceRoot":"","sources":["../diff-parser.ts"],"names":[],"mappings":"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"}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import type { WebSocket } from 'ws';
|
|
19
19
|
import { SessionArchive } from './session-archive.js';
|
|
20
|
-
import type { Session, SessionInfo, WsServerMessage } from './types.js';
|
|
20
|
+
import type { DiffFileStatus, DiffScope, Session, SessionInfo, WsServerMessage } from './types.js';
|
|
21
21
|
export interface CreateSessionOptions {
|
|
22
22
|
source?: 'manual' | 'webhook' | 'workflow' | 'stepflow';
|
|
23
23
|
id?: string;
|
|
@@ -25,22 +25,23 @@ export interface CreateSessionOptions {
|
|
|
25
25
|
model?: string;
|
|
26
26
|
}
|
|
27
27
|
export declare class SessionManager {
|
|
28
|
+
/** All active (non-archived) sessions, keyed by session UUID. */
|
|
28
29
|
private sessions;
|
|
29
|
-
/** SQLite archive for closed sessions. */
|
|
30
|
+
/** SQLite archive for closed sessions (persists conversation summaries across restarts). */
|
|
30
31
|
readonly archive: SessionArchive;
|
|
31
32
|
/** Exposed so ws-server can pass its port to child Claude processes. */
|
|
32
33
|
_serverPort: number;
|
|
33
34
|
/** Exposed so ws-server can pass the auth token to child Claude processes. */
|
|
34
35
|
_authToken: string;
|
|
35
|
-
/** Callback to broadcast a message to ALL connected WebSocket clients (set by ws-server). */
|
|
36
|
+
/** Callback to broadcast a message to ALL connected WebSocket clients (set by ws-server on startup). */
|
|
36
37
|
_globalBroadcast: ((msg: WsServerMessage) => void) | null;
|
|
37
|
-
/** Registered listeners notified when a session's Claude process exits. */
|
|
38
|
+
/** Registered listeners notified when a session's Claude process exits (used by webhook-handler for chained workflows). */
|
|
38
39
|
private _exitListeners;
|
|
39
|
-
/** Delegated approval logic. */
|
|
40
|
+
/** Delegated approval logic (auto-approve patterns, deny-lists, pattern management). */
|
|
40
41
|
private approvalManager;
|
|
41
|
-
/** Delegated naming logic. */
|
|
42
|
+
/** Delegated auto-naming logic (generates session names from first user message via Claude API). */
|
|
42
43
|
private sessionNaming;
|
|
43
|
-
/** Delegated persistence logic. */
|
|
44
|
+
/** Delegated persistence logic (saves/restores session metadata to disk across server restarts). */
|
|
44
45
|
private sessionPersistence;
|
|
45
46
|
constructor();
|
|
46
47
|
/** Check if a tool/command is auto-approved for a repo. */
|
|
@@ -53,6 +54,12 @@ export declare class SessionManager {
|
|
|
53
54
|
commands: string[];
|
|
54
55
|
patterns: string[];
|
|
55
56
|
};
|
|
57
|
+
/** Return approvals effective globally via cross-repo inference. */
|
|
58
|
+
getGlobalApprovals(): {
|
|
59
|
+
tools: Record<string, string[]>;
|
|
60
|
+
commands: Record<string, string[]>;
|
|
61
|
+
patterns: Record<string, string[]>;
|
|
62
|
+
};
|
|
56
63
|
/** Remove an auto-approval rule for a repo (workingDir) and persist to disk. */
|
|
57
64
|
removeApproval(workingDir: string, opts: {
|
|
58
65
|
tool?: string;
|
|
@@ -81,7 +88,7 @@ export declare class SessionManager {
|
|
|
81
88
|
/** Add a WebSocket client to a session. Returns the session or undefined if not found.
|
|
82
89
|
* Re-broadcasts any pending approval/control prompts so the joining client sees them. */
|
|
83
90
|
join(sessionId: string, ws: WebSocket): Session | undefined;
|
|
84
|
-
/** Remove a WebSocket client from a session. Auto-denies pending prompts if last client leaves. */
|
|
91
|
+
/** Remove a WebSocket client from a session. Auto-denies pending prompts if last client leaves (after grace period). */
|
|
85
92
|
leave(sessionId: string, ws: WebSocket): void;
|
|
86
93
|
/** Delete a session: kill its process, notify clients, remove from memory and disk. */
|
|
87
94
|
delete(sessionId: string): boolean;
|
|
@@ -183,6 +190,16 @@ export declare class SessionManager {
|
|
|
183
190
|
broadcast(session: Session, msg: WsServerMessage): void;
|
|
184
191
|
findSessionForClient(ws: WebSocket): Session | undefined;
|
|
185
192
|
removeClient(ws: WebSocket): void;
|
|
193
|
+
/**
|
|
194
|
+
* Run git diff in a session's workingDir and return structured results.
|
|
195
|
+
* Includes untracked file discovery for 'unstaged' and 'all' scopes.
|
|
196
|
+
*/
|
|
197
|
+
getDiff(sessionId: string, scope?: DiffScope): Promise<WsServerMessage>;
|
|
198
|
+
/**
|
|
199
|
+
* Discard changes in a session's workingDir per the given scope and paths.
|
|
200
|
+
* Returns a fresh diff_result after discarding.
|
|
201
|
+
*/
|
|
202
|
+
discardChanges(sessionId: string, scope: DiffScope, paths?: string[], statuses?: Record<string, DiffFileStatus>): Promise<WsServerMessage>;
|
|
186
203
|
/** Graceful shutdown: complete in-progress tasks, persist state, kill all processes. */
|
|
187
204
|
shutdown(): void;
|
|
188
205
|
/**
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
* - SessionPersistence: disk I/O for session state
|
|
17
17
|
*/
|
|
18
18
|
import { randomUUID } from 'crypto';
|
|
19
|
+
import { execFile } from 'child_process';
|
|
20
|
+
import { promises as fs } from 'fs';
|
|
21
|
+
import path from 'path';
|
|
22
|
+
import { promisify } from 'util';
|
|
19
23
|
import { ClaudeProcess } from './claude-process.js';
|
|
20
24
|
import { SessionArchive } from './session-archive.js';
|
|
21
25
|
import { cleanupWorkspace } from './webhook-workspace.js';
|
|
@@ -24,6 +28,79 @@ import { ApprovalManager } from './approval-manager.js';
|
|
|
24
28
|
import { SessionNaming } from './session-naming.js';
|
|
25
29
|
import { SessionPersistence } from './session-persistence.js';
|
|
26
30
|
import { deriveSessionToken } from './crypto-utils.js';
|
|
31
|
+
import { parseDiff, createUntrackedFileDiff } from './diff-parser.js';
|
|
32
|
+
const execFileAsync = promisify(execFile);
|
|
33
|
+
/** Max stdout for git commands (2 MB). */
|
|
34
|
+
const GIT_MAX_BUFFER = 2 * 1024 * 1024;
|
|
35
|
+
/** Timeout for git commands (10 seconds). */
|
|
36
|
+
const GIT_TIMEOUT_MS = 10_000;
|
|
37
|
+
/** Max paths per git command to stay under ARG_MAX (~128 KB on Linux). */
|
|
38
|
+
const GIT_PATH_CHUNK_SIZE = 200;
|
|
39
|
+
/** Run a git command as a fixed argv array (no shell interpolation). */
|
|
40
|
+
async function execGit(args, cwd) {
|
|
41
|
+
const { stdout } = await execFileAsync('git', args, {
|
|
42
|
+
cwd,
|
|
43
|
+
maxBuffer: GIT_MAX_BUFFER,
|
|
44
|
+
timeout: GIT_TIMEOUT_MS,
|
|
45
|
+
});
|
|
46
|
+
return stdout;
|
|
47
|
+
}
|
|
48
|
+
/** Run a git command with paths chunked to avoid E2BIG. Concatenates stdout. */
|
|
49
|
+
async function execGitChunked(baseArgs, paths, cwd) {
|
|
50
|
+
let result = '';
|
|
51
|
+
for (let i = 0; i < paths.length; i += GIT_PATH_CHUNK_SIZE) {
|
|
52
|
+
const chunk = paths.slice(i, i + GIT_PATH_CHUNK_SIZE);
|
|
53
|
+
result += await execGit([...baseArgs, '--', ...chunk], cwd);
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
/** Get file statuses from `git status --porcelain` for given paths (or all). */
|
|
58
|
+
async function getFileStatuses(cwd, paths) {
|
|
59
|
+
const args = ['status', '--porcelain', '-z'];
|
|
60
|
+
if (paths)
|
|
61
|
+
args.push('--', ...paths);
|
|
62
|
+
const raw = await execGit(args, cwd);
|
|
63
|
+
const result = {};
|
|
64
|
+
// git status --porcelain=v1 -z format: XY NUL path NUL
|
|
65
|
+
// XY is a two-character status code: X = index status, Y = worktree status.
|
|
66
|
+
// Examples: " M" = unstaged modification, "A " = staged addition, "??" = untracked.
|
|
67
|
+
// For renames/copies: XY NUL oldpath NUL newpath NUL
|
|
68
|
+
const parts = raw.split('\0');
|
|
69
|
+
let i = 0;
|
|
70
|
+
while (i < parts.length) {
|
|
71
|
+
const entry = parts[i];
|
|
72
|
+
if (entry.length < 3) {
|
|
73
|
+
i++;
|
|
74
|
+
continue;
|
|
75
|
+
} // skip empty trailing entries
|
|
76
|
+
const x = entry[0]; // index status
|
|
77
|
+
const y = entry[1]; // worktree status
|
|
78
|
+
const filePath = entry.slice(3);
|
|
79
|
+
if (x === 'R' || x === 'C') {
|
|
80
|
+
// Rename/copy: next NUL-separated part is the new path
|
|
81
|
+
const newPath = parts[i + 1] ?? filePath;
|
|
82
|
+
result[newPath] = 'renamed';
|
|
83
|
+
i += 2;
|
|
84
|
+
}
|
|
85
|
+
else if (x === 'D' || y === 'D') {
|
|
86
|
+
result[filePath] = 'deleted';
|
|
87
|
+
i++;
|
|
88
|
+
}
|
|
89
|
+
else if (x === '?' && y === '?') {
|
|
90
|
+
result[filePath] = 'added';
|
|
91
|
+
i++;
|
|
92
|
+
}
|
|
93
|
+
else if (x === 'A') {
|
|
94
|
+
result[filePath] = 'added';
|
|
95
|
+
i++;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
result[filePath] = 'modified';
|
|
99
|
+
i++;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
27
104
|
/** Max messages retained in a session's output history buffer. */
|
|
28
105
|
const MAX_HISTORY = 2000;
|
|
29
106
|
/** Max auto-restart attempts before requiring manual intervention. */
|
|
@@ -50,22 +127,23 @@ const API_RETRY_PATTERNS = [
|
|
|
50
127
|
/503/,
|
|
51
128
|
];
|
|
52
129
|
export class SessionManager {
|
|
130
|
+
/** All active (non-archived) sessions, keyed by session UUID. */
|
|
53
131
|
sessions = new Map();
|
|
54
|
-
/** SQLite archive for closed sessions. */
|
|
132
|
+
/** SQLite archive for closed sessions (persists conversation summaries across restarts). */
|
|
55
133
|
archive;
|
|
56
134
|
/** Exposed so ws-server can pass its port to child Claude processes. */
|
|
57
135
|
_serverPort = PORT;
|
|
58
136
|
/** Exposed so ws-server can pass the auth token to child Claude processes. */
|
|
59
137
|
_authToken = '';
|
|
60
|
-
/** Callback to broadcast a message to ALL connected WebSocket clients (set by ws-server). */
|
|
138
|
+
/** Callback to broadcast a message to ALL connected WebSocket clients (set by ws-server on startup). */
|
|
61
139
|
_globalBroadcast = null;
|
|
62
|
-
/** Registered listeners notified when a session's Claude process exits. */
|
|
140
|
+
/** Registered listeners notified when a session's Claude process exits (used by webhook-handler for chained workflows). */
|
|
63
141
|
_exitListeners = [];
|
|
64
|
-
/** Delegated approval logic. */
|
|
142
|
+
/** Delegated approval logic (auto-approve patterns, deny-lists, pattern management). */
|
|
65
143
|
approvalManager;
|
|
66
|
-
/** Delegated naming logic. */
|
|
144
|
+
/** Delegated auto-naming logic (generates session names from first user message via Claude API). */
|
|
67
145
|
sessionNaming;
|
|
68
|
-
/** Delegated persistence logic. */
|
|
146
|
+
/** Delegated persistence logic (saves/restores session metadata to disk across server restarts). */
|
|
69
147
|
sessionPersistence;
|
|
70
148
|
constructor() {
|
|
71
149
|
this.archive = new SessionArchive();
|
|
@@ -94,6 +172,10 @@ export class SessionManager {
|
|
|
94
172
|
getApprovals(workingDir) {
|
|
95
173
|
return this.approvalManager.getApprovals(workingDir);
|
|
96
174
|
}
|
|
175
|
+
/** Return approvals effective globally via cross-repo inference. */
|
|
176
|
+
getGlobalApprovals() {
|
|
177
|
+
return this.approvalManager.getGlobalApprovals();
|
|
178
|
+
}
|
|
97
179
|
/** Remove an auto-approval rule for a repo (workingDir) and persist to disk. */
|
|
98
180
|
removeApproval(workingDir, opts, skipPersist = false) {
|
|
99
181
|
return this.approvalManager.removeApproval(workingDir, opts, skipPersist);
|
|
@@ -157,6 +239,7 @@ export class SessionManager {
|
|
|
157
239
|
isProcessing: false,
|
|
158
240
|
pendingControlRequests: new Map(),
|
|
159
241
|
pendingToolApprovals: new Map(),
|
|
242
|
+
_leaveGraceTimer: null,
|
|
160
243
|
};
|
|
161
244
|
this.sessions.set(id, session);
|
|
162
245
|
this.persistToDisk();
|
|
@@ -201,6 +284,11 @@ export class SessionManager {
|
|
|
201
284
|
const session = this.sessions.get(sessionId);
|
|
202
285
|
if (!session)
|
|
203
286
|
return undefined;
|
|
287
|
+
// Cancel pending auto-deny from leave grace period
|
|
288
|
+
if (session._leaveGraceTimer) {
|
|
289
|
+
clearTimeout(session._leaveGraceTimer);
|
|
290
|
+
session._leaveGraceTimer = null;
|
|
291
|
+
}
|
|
204
292
|
session.clients.add(ws);
|
|
205
293
|
// Re-broadcast pending tool approval prompts (PreToolUse hook path)
|
|
206
294
|
for (const pending of session.pendingToolApprovals.values()) {
|
|
@@ -218,28 +306,37 @@ export class SessionManager {
|
|
|
218
306
|
}
|
|
219
307
|
return session;
|
|
220
308
|
}
|
|
221
|
-
/** Remove a WebSocket client from a session. Auto-denies pending prompts if last client leaves. */
|
|
309
|
+
/** Remove a WebSocket client from a session. Auto-denies pending prompts if last client leaves (after grace period). */
|
|
222
310
|
leave(sessionId, ws) {
|
|
223
311
|
const session = this.sessions.get(sessionId);
|
|
224
312
|
if (session) {
|
|
225
313
|
session.clients.delete(ws);
|
|
226
|
-
// If no clients remain,
|
|
314
|
+
// If no clients remain, wait a grace period before auto-denying.
|
|
315
|
+
// This prevents false denials when the user is just refreshing the page.
|
|
227
316
|
if (session.clients.size === 0) {
|
|
228
|
-
if (session.
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
session.
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
317
|
+
if (session._leaveGraceTimer)
|
|
318
|
+
clearTimeout(session._leaveGraceTimer);
|
|
319
|
+
session._leaveGraceTimer = setTimeout(() => {
|
|
320
|
+
session._leaveGraceTimer = null;
|
|
321
|
+
// Re-check: if still no clients after grace period, auto-deny
|
|
322
|
+
if (session.clients.size === 0) {
|
|
323
|
+
if (session.pendingControlRequests.size > 0) {
|
|
324
|
+
console.log(`[session] last client left, auto-denying ${session.pendingControlRequests.size} pending control requests`);
|
|
325
|
+
for (const [requestId] of session.pendingControlRequests) {
|
|
326
|
+
session.claudeProcess?.sendControlResponse(requestId, 'deny');
|
|
327
|
+
}
|
|
328
|
+
session.pendingControlRequests.clear();
|
|
329
|
+
}
|
|
330
|
+
if (session.pendingToolApprovals.size > 0) {
|
|
331
|
+
console.log(`[session] last client left, auto-denying ${session.pendingToolApprovals.size} pending tool approval(s)`);
|
|
332
|
+
for (const [reqId, pending] of session.pendingToolApprovals) {
|
|
333
|
+
pending.resolve({ allow: false, always: false });
|
|
334
|
+
this.broadcast(session, { type: 'prompt_dismiss', requestId: reqId });
|
|
335
|
+
}
|
|
336
|
+
session.pendingToolApprovals.clear();
|
|
337
|
+
}
|
|
240
338
|
}
|
|
241
|
-
|
|
242
|
-
}
|
|
339
|
+
}, 3000);
|
|
243
340
|
}
|
|
244
341
|
}
|
|
245
342
|
}
|
|
@@ -255,6 +352,8 @@ export class SessionManager {
|
|
|
255
352
|
clearTimeout(session._apiRetryTimer);
|
|
256
353
|
if (session._namingTimer)
|
|
257
354
|
clearTimeout(session._namingTimer);
|
|
355
|
+
if (session._leaveGraceTimer)
|
|
356
|
+
clearTimeout(session._leaveGraceTimer);
|
|
258
357
|
// Kill claude process if running
|
|
259
358
|
if (session.claudeProcess) {
|
|
260
359
|
session.claudeProcess.stop();
|
|
@@ -670,10 +769,40 @@ export class SessionManager {
|
|
|
670
769
|
if (!session)
|
|
671
770
|
return;
|
|
672
771
|
// Check for pending tool approval from PreToolUse hook
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
772
|
+
if (!requestId) {
|
|
773
|
+
const totalPending = session.pendingToolApprovals.size + session.pendingControlRequests.size;
|
|
774
|
+
if (totalPending === 1) {
|
|
775
|
+
// Exactly one pending prompt — safe to infer the target
|
|
776
|
+
const soleApproval = session.pendingToolApprovals.size === 1
|
|
777
|
+
? session.pendingToolApprovals.values().next().value
|
|
778
|
+
: undefined;
|
|
779
|
+
if (soleApproval) {
|
|
780
|
+
console.warn(`[prompt_response] no requestId, routing to sole pending tool approval: ${soleApproval.toolName}`);
|
|
781
|
+
this.resolveToolApproval(session, soleApproval, value);
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
const soleControl = session.pendingControlRequests.size === 1
|
|
785
|
+
? session.pendingControlRequests.values().next().value
|
|
786
|
+
: undefined;
|
|
787
|
+
if (soleControl) {
|
|
788
|
+
console.warn(`[prompt_response] no requestId, routing to sole pending control request: ${soleControl.toolName}`);
|
|
789
|
+
requestId = soleControl.requestId;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
else if (totalPending > 1) {
|
|
793
|
+
console.warn(`[prompt_response] no requestId with ${totalPending} pending prompts — rejecting to prevent misrouted response`);
|
|
794
|
+
this.broadcast(session, {
|
|
795
|
+
type: 'system_message',
|
|
796
|
+
subtype: 'error',
|
|
797
|
+
text: 'Prompt response could not be routed: multiple prompts pending. Please refresh and try again.',
|
|
798
|
+
});
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
else {
|
|
802
|
+
console.warn(`[prompt_response] no requestId, no pending prompts — forwarding as user message`);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
const approval = requestId ? session.pendingToolApprovals.get(requestId) : undefined;
|
|
677
806
|
if (approval) {
|
|
678
807
|
this.resolveToolApproval(session, approval, value);
|
|
679
808
|
return;
|
|
@@ -681,9 +810,7 @@ export class SessionManager {
|
|
|
681
810
|
if (!session.claudeProcess?.isAlive())
|
|
682
811
|
return;
|
|
683
812
|
// Find matching pending control request
|
|
684
|
-
const pending = requestId
|
|
685
|
-
? session.pendingControlRequests.get(requestId)
|
|
686
|
-
: session.pendingControlRequests.values().next().value; // fallback: oldest
|
|
813
|
+
const pending = requestId ? session.pendingControlRequests.get(requestId) : undefined;
|
|
687
814
|
if (pending) {
|
|
688
815
|
session.pendingControlRequests.delete(pending.requestId);
|
|
689
816
|
// Dismiss prompt on all other clients viewing this session
|
|
@@ -1042,6 +1169,214 @@ export class SessionManager {
|
|
|
1042
1169
|
session.clients.delete(ws);
|
|
1043
1170
|
}
|
|
1044
1171
|
}
|
|
1172
|
+
// ---------------------------------------------------------------------------
|
|
1173
|
+
// Diff viewer
|
|
1174
|
+
// ---------------------------------------------------------------------------
|
|
1175
|
+
/**
|
|
1176
|
+
* Run git diff in a session's workingDir and return structured results.
|
|
1177
|
+
* Includes untracked file discovery for 'unstaged' and 'all' scopes.
|
|
1178
|
+
*/
|
|
1179
|
+
async getDiff(sessionId, scope = 'all') {
|
|
1180
|
+
const session = this.sessions.get(sessionId);
|
|
1181
|
+
if (!session)
|
|
1182
|
+
return { type: 'diff_error', message: 'Session not found' };
|
|
1183
|
+
const cwd = session.workingDir;
|
|
1184
|
+
try {
|
|
1185
|
+
// Get branch name
|
|
1186
|
+
let branch;
|
|
1187
|
+
try {
|
|
1188
|
+
const branchResult = await execGit(['rev-parse', '--abbrev-ref', 'HEAD'], cwd);
|
|
1189
|
+
branch = branchResult.trim();
|
|
1190
|
+
if (branch === 'HEAD') {
|
|
1191
|
+
const shaResult = await execGit(['rev-parse', '--short', 'HEAD'], cwd);
|
|
1192
|
+
branch = `detached at ${shaResult.trim()}`;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
catch {
|
|
1196
|
+
branch = 'unknown';
|
|
1197
|
+
}
|
|
1198
|
+
// Build diff command based on scope
|
|
1199
|
+
const diffArgs = ['diff', '--find-renames', '--no-color', '--unified=3'];
|
|
1200
|
+
if (scope === 'staged') {
|
|
1201
|
+
diffArgs.push('--cached');
|
|
1202
|
+
}
|
|
1203
|
+
else if (scope === 'all') {
|
|
1204
|
+
diffArgs.push('HEAD');
|
|
1205
|
+
}
|
|
1206
|
+
// 'unstaged' uses bare `git diff` (working tree vs index)
|
|
1207
|
+
let rawDiff;
|
|
1208
|
+
try {
|
|
1209
|
+
rawDiff = await execGit(diffArgs, cwd);
|
|
1210
|
+
}
|
|
1211
|
+
catch {
|
|
1212
|
+
// git diff HEAD fails if no commits yet — fall back to staged + unstaged
|
|
1213
|
+
if (scope === 'all') {
|
|
1214
|
+
const [staged, unstaged] = await Promise.all([
|
|
1215
|
+
execGit(['diff', '--cached', '--find-renames', '--no-color', '--unified=3'], cwd).catch(() => ''),
|
|
1216
|
+
execGit(['diff', '--find-renames', '--no-color', '--unified=3'], cwd).catch(() => ''),
|
|
1217
|
+
]);
|
|
1218
|
+
rawDiff = staged + unstaged;
|
|
1219
|
+
}
|
|
1220
|
+
else {
|
|
1221
|
+
rawDiff = '';
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
const { files, truncated, truncationReason } = parseDiff(rawDiff);
|
|
1225
|
+
// Discover untracked files for 'unstaged' and 'all' scopes
|
|
1226
|
+
if (scope !== 'staged') {
|
|
1227
|
+
try {
|
|
1228
|
+
const untrackedRaw = await execGit(['ls-files', '--others', '--exclude-standard'], cwd);
|
|
1229
|
+
const untrackedPaths = untrackedRaw.trim().split('\n').filter(Boolean);
|
|
1230
|
+
for (const relPath of untrackedPaths) {
|
|
1231
|
+
try {
|
|
1232
|
+
const fullPath = path.join(cwd, relPath);
|
|
1233
|
+
// Check if binary by attempting to read as utf-8
|
|
1234
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
1235
|
+
files.push(createUntrackedFileDiff(relPath, content));
|
|
1236
|
+
}
|
|
1237
|
+
catch {
|
|
1238
|
+
// Binary or unreadable — add as binary
|
|
1239
|
+
files.push({
|
|
1240
|
+
path: relPath,
|
|
1241
|
+
status: 'added',
|
|
1242
|
+
isBinary: true,
|
|
1243
|
+
additions: 0,
|
|
1244
|
+
deletions: 0,
|
|
1245
|
+
hunks: [],
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
catch {
|
|
1251
|
+
// ls-files failed — skip untracked
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
const summary = {
|
|
1255
|
+
filesChanged: files.length,
|
|
1256
|
+
insertions: files.reduce((sum, f) => sum + f.additions, 0),
|
|
1257
|
+
deletions: files.reduce((sum, f) => sum + f.deletions, 0),
|
|
1258
|
+
truncated,
|
|
1259
|
+
truncationReason,
|
|
1260
|
+
};
|
|
1261
|
+
return { type: 'diff_result', files, summary, branch, scope };
|
|
1262
|
+
}
|
|
1263
|
+
catch (err) {
|
|
1264
|
+
const message = err instanceof Error ? err.message : 'Failed to get diff';
|
|
1265
|
+
return { type: 'diff_error', message };
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* Discard changes in a session's workingDir per the given scope and paths.
|
|
1270
|
+
* Returns a fresh diff_result after discarding.
|
|
1271
|
+
*/
|
|
1272
|
+
async discardChanges(sessionId, scope, paths, statuses) {
|
|
1273
|
+
const session = this.sessions.get(sessionId);
|
|
1274
|
+
if (!session)
|
|
1275
|
+
return { type: 'diff_error', message: 'Session not found' };
|
|
1276
|
+
const cwd = session.workingDir;
|
|
1277
|
+
try {
|
|
1278
|
+
// Validate paths — enforce separator boundary to prevent /repoX matching /repo
|
|
1279
|
+
if (paths) {
|
|
1280
|
+
const root = path.resolve(cwd) + path.sep;
|
|
1281
|
+
for (const p of paths) {
|
|
1282
|
+
if (p.includes('..') || path.isAbsolute(p)) {
|
|
1283
|
+
return { type: 'diff_error', message: `Invalid path: ${p}` };
|
|
1284
|
+
}
|
|
1285
|
+
const resolved = path.resolve(cwd, p);
|
|
1286
|
+
if (resolved !== path.resolve(cwd) && !resolved.startsWith(root)) {
|
|
1287
|
+
return { type: 'diff_error', message: `Path escapes working directory: ${p}` };
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
// Determine file statuses if not provided
|
|
1292
|
+
let fileStatuses = statuses ?? {};
|
|
1293
|
+
if (!statuses && paths) {
|
|
1294
|
+
fileStatuses = await getFileStatuses(cwd, paths);
|
|
1295
|
+
}
|
|
1296
|
+
else if (!statuses && !paths) {
|
|
1297
|
+
fileStatuses = await getFileStatuses(cwd);
|
|
1298
|
+
}
|
|
1299
|
+
const targetPaths = paths ?? Object.keys(fileStatuses);
|
|
1300
|
+
// Separate files by status for different handling
|
|
1301
|
+
const trackedPaths = [];
|
|
1302
|
+
const untrackedPaths = [];
|
|
1303
|
+
const stagedNewPaths = [];
|
|
1304
|
+
for (const p of targetPaths) {
|
|
1305
|
+
const status = fileStatuses[p];
|
|
1306
|
+
if (status === 'added') {
|
|
1307
|
+
// Determine if untracked or staged-new by checking the index
|
|
1308
|
+
try {
|
|
1309
|
+
const indexEntry = (await execGit(['ls-files', '--stage', '--', p], cwd)).trim();
|
|
1310
|
+
if (indexEntry) {
|
|
1311
|
+
stagedNewPaths.push(p);
|
|
1312
|
+
}
|
|
1313
|
+
else {
|
|
1314
|
+
untrackedPaths.push(p);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
catch {
|
|
1318
|
+
untrackedPaths.push(p);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
else {
|
|
1322
|
+
trackedPaths.push(p);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
// Handle tracked files (modified, deleted, renamed) with git restore
|
|
1326
|
+
if (trackedPaths.length > 0) {
|
|
1327
|
+
const restoreArgs = ['restore'];
|
|
1328
|
+
if (scope === 'staged') {
|
|
1329
|
+
restoreArgs.push('--staged');
|
|
1330
|
+
}
|
|
1331
|
+
else if (scope === 'all') {
|
|
1332
|
+
restoreArgs.push('--staged', '--worktree');
|
|
1333
|
+
}
|
|
1334
|
+
else {
|
|
1335
|
+
restoreArgs.push('--worktree');
|
|
1336
|
+
}
|
|
1337
|
+
try {
|
|
1338
|
+
await execGitChunked(restoreArgs, trackedPaths, cwd);
|
|
1339
|
+
}
|
|
1340
|
+
catch (err) {
|
|
1341
|
+
// Fallback for Git < 2.23
|
|
1342
|
+
console.warn('[discard] git restore failed, trying fallback:', err);
|
|
1343
|
+
if (scope === 'staged' || scope === 'all') {
|
|
1344
|
+
await execGitChunked(['reset', 'HEAD'], trackedPaths, cwd);
|
|
1345
|
+
}
|
|
1346
|
+
if (scope === 'unstaged' || scope === 'all') {
|
|
1347
|
+
await execGitChunked(['checkout'], trackedPaths, cwd);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
// Handle staged new files
|
|
1352
|
+
if (stagedNewPaths.length > 0) {
|
|
1353
|
+
if (scope === 'staged') {
|
|
1354
|
+
// Unstage only — leave on disk
|
|
1355
|
+
await execGitChunked(['rm', '--cached'], stagedNewPaths, cwd);
|
|
1356
|
+
}
|
|
1357
|
+
else if (scope === 'all') {
|
|
1358
|
+
// Remove from index and disk
|
|
1359
|
+
await execGitChunked(['rm', '--cached'], stagedNewPaths, cwd);
|
|
1360
|
+
for (const p of stagedNewPaths) {
|
|
1361
|
+
await fs.unlink(path.join(cwd, p)).catch(() => { });
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
// 'unstaged' scope: N/A for staged-new files
|
|
1365
|
+
}
|
|
1366
|
+
// Handle untracked files (delete from disk)
|
|
1367
|
+
if (untrackedPaths.length > 0 && scope !== 'staged') {
|
|
1368
|
+
for (const p of untrackedPaths) {
|
|
1369
|
+
await fs.unlink(path.join(cwd, p)).catch(() => { });
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
// Return fresh diff
|
|
1373
|
+
return await this.getDiff(sessionId, scope);
|
|
1374
|
+
}
|
|
1375
|
+
catch (err) {
|
|
1376
|
+
const message = err instanceof Error ? err.message : 'Failed to discard changes';
|
|
1377
|
+
return { type: 'diff_error', message };
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1045
1380
|
/** Graceful shutdown: complete in-progress tasks, persist state, kill all processes. */
|
|
1046
1381
|
shutdown() {
|
|
1047
1382
|
// Complete in-progress tasks for active sessions before persisting.
|