@sumicom/quicksave 0.1.0 → 0.3.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/config.d.ts +5 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +13 -0
- package/dist/config.js.map +1 -1
- package/dist/connection/signaling.d.ts.map +1 -1
- package/dist/connection/signaling.js +5 -0
- package/dist/connection/signaling.js.map +1 -1
- package/dist/index.js +19 -1
- package/dist/index.js.map +1 -1
- package/package.json +22 -2
- package/dist/webrtc/connection.d.ts +0 -52
- package/dist/webrtc/connection.d.ts.map +0 -1
- package/dist/webrtc/connection.js +0 -255
- package/dist/webrtc/connection.js.map +0 -1
- package/dist/webrtc/signaling.d.ts +0 -41
- package/dist/webrtc/signaling.d.ts.map +0 -1
- package/dist/webrtc/signaling.js +0 -152
- package/dist/webrtc/signaling.js.map +0 -1
- package/src/ai/commitSummary.ts +0 -199
- package/src/config.ts +0 -97
- package/src/connection/connection.ts +0 -223
- package/src/connection/signaling.ts +0 -172
- package/src/git/operations.test.ts +0 -305
- package/src/git/operations.ts +0 -443
- package/src/handlers/messageHandler.test.ts +0 -294
- package/src/handlers/messageHandler.ts +0 -550
- package/src/index.ts +0 -151
- package/src/types/webrtc.d.ts +0 -38
- package/tsconfig.json +0 -13
- package/vitest.config.ts +0 -9
package/src/git/operations.ts
DELETED
|
@@ -1,443 +0,0 @@
|
|
|
1
|
-
import { simpleGit, SimpleGit, StatusResult } from 'simple-git';
|
|
2
|
-
import { readFile, writeFile, unlink, stat } from 'fs/promises';
|
|
3
|
-
import { join } from 'path';
|
|
4
|
-
import { tmpdir } from 'os';
|
|
5
|
-
import { randomBytes } from 'crypto';
|
|
6
|
-
import type {
|
|
7
|
-
GitStatus,
|
|
8
|
-
FileChange,
|
|
9
|
-
FileDiff,
|
|
10
|
-
DiffHunk,
|
|
11
|
-
Commit,
|
|
12
|
-
Branch,
|
|
13
|
-
FileStatus,
|
|
14
|
-
} from '@sumicom/quicksave-shared';
|
|
15
|
-
|
|
16
|
-
export interface GitOperationsOptions {
|
|
17
|
-
maxDiffFileSizeKB?: number;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export class GitOperations {
|
|
21
|
-
private git: SimpleGit;
|
|
22
|
-
private gitRoot: string | null = null;
|
|
23
|
-
private initialized = false;
|
|
24
|
-
private maxDiffFileSizeKB: number;
|
|
25
|
-
|
|
26
|
-
constructor(repoPath: string, options?: GitOperationsOptions) {
|
|
27
|
-
this.git = simpleGit(repoPath);
|
|
28
|
-
this.maxDiffFileSizeKB =
|
|
29
|
-
options?.maxDiffFileSizeKB ??
|
|
30
|
-
parseInt(process.env.QUICKSAVE_MAX_DIFF_SIZE_KB || '100', 10);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Initialize git to run from the git root directory
|
|
35
|
-
* This ensures relative paths work correctly
|
|
36
|
-
*/
|
|
37
|
-
private async ensureInitialized(): Promise<void> {
|
|
38
|
-
if (this.initialized) return;
|
|
39
|
-
|
|
40
|
-
const gitRoot = await this.getGitRoot();
|
|
41
|
-
this.git = simpleGit(gitRoot);
|
|
42
|
-
this.initialized = true;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Get the actual git repository root path
|
|
47
|
-
*/
|
|
48
|
-
private async getGitRoot(): Promise<string> {
|
|
49
|
-
if (this.gitRoot) {
|
|
50
|
-
return this.gitRoot;
|
|
51
|
-
}
|
|
52
|
-
this.gitRoot = (await this.git.revparse(['--show-toplevel'])).trim();
|
|
53
|
-
return this.gitRoot;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Get the current git status
|
|
58
|
-
*/
|
|
59
|
-
async getStatus(): Promise<GitStatus> {
|
|
60
|
-
await this.ensureInitialized();
|
|
61
|
-
const status = await this.git.status();
|
|
62
|
-
const branchInfo = await this.getBranchTracking();
|
|
63
|
-
|
|
64
|
-
return {
|
|
65
|
-
branch: status.current || 'HEAD',
|
|
66
|
-
ahead: branchInfo.ahead,
|
|
67
|
-
behind: branchInfo.behind,
|
|
68
|
-
staged: this.parseFileChanges(status, 'staged'),
|
|
69
|
-
unstaged: this.parseFileChanges(status, 'unstaged'),
|
|
70
|
-
untracked: status.not_added,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Get diff for a specific file
|
|
76
|
-
*/
|
|
77
|
-
async getDiff(path: string, staged: boolean = false): Promise<FileDiff> {
|
|
78
|
-
await this.ensureInitialized();
|
|
79
|
-
|
|
80
|
-
// Check file size before generating diff
|
|
81
|
-
const fileSizeKB = await this.getFileSizeKB(path);
|
|
82
|
-
if (fileSizeKB > this.maxDiffFileSizeKB) {
|
|
83
|
-
return {
|
|
84
|
-
path,
|
|
85
|
-
hunks: [],
|
|
86
|
-
isBinary: false,
|
|
87
|
-
truncated: true,
|
|
88
|
-
truncatedReason: `File exceeds ${this.maxDiffFileSizeKB}KB limit (${fileSizeKB}KB)`,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const status = await this.git.status();
|
|
93
|
-
const isUntracked = status.not_added.includes(path);
|
|
94
|
-
const isNewFile = status.created.includes(path);
|
|
95
|
-
|
|
96
|
-
console.log(`getDiff: path="${path}", isUntracked=${isUntracked}, isNewFile=${isNewFile}`);
|
|
97
|
-
console.log(`getDiff: not_added=[${status.not_added.join(', ')}]`);
|
|
98
|
-
|
|
99
|
-
// For untracked files, show the full content as additions
|
|
100
|
-
if (isUntracked) {
|
|
101
|
-
return this.getNewFileDiff(path);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// For new staged files, show the staged content from the index
|
|
105
|
-
if (staged && isNewFile) {
|
|
106
|
-
return this.getStagedNewFileDiff(path);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Normal diff
|
|
110
|
-
const args = staged
|
|
111
|
-
? ['diff', '--cached', '--', path]
|
|
112
|
-
: ['diff', '--', path];
|
|
113
|
-
const diffOutput = await this.git.raw(args);
|
|
114
|
-
|
|
115
|
-
// If diff is empty, return empty diff
|
|
116
|
-
if (!diffOutput.trim()) {
|
|
117
|
-
return {
|
|
118
|
-
path,
|
|
119
|
-
hunks: [],
|
|
120
|
-
isBinary: false,
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return this.parseDiff(path, diffOutput);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Get file size in KB
|
|
129
|
-
*/
|
|
130
|
-
private async getFileSizeKB(path: string): Promise<number> {
|
|
131
|
-
try {
|
|
132
|
-
const gitRoot = await this.getGitRoot();
|
|
133
|
-
const fullPath = join(gitRoot, path);
|
|
134
|
-
const stats = await stat(fullPath);
|
|
135
|
-
return Math.ceil(stats.size / 1024);
|
|
136
|
-
} catch {
|
|
137
|
-
return 0; // File doesn't exist or can't be read
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Get diff for a staged new file by reading content from the git index
|
|
143
|
-
*/
|
|
144
|
-
private async getStagedNewFileDiff(path: string): Promise<FileDiff> {
|
|
145
|
-
try {
|
|
146
|
-
const content = await this.git.raw(['show', `:${path}`]);
|
|
147
|
-
|
|
148
|
-
if (this.isBinaryContent(content)) {
|
|
149
|
-
return { path, hunks: [], isBinary: true };
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return this.createSyntheticDiff(path, content);
|
|
153
|
-
} catch {
|
|
154
|
-
// Fallback to reading from working tree
|
|
155
|
-
return this.getNewFileDiff(path);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Get diff representation for a new/untracked file (shows all content as additions)
|
|
161
|
-
*/
|
|
162
|
-
private async getNewFileDiff(path: string): Promise<FileDiff> {
|
|
163
|
-
const gitRoot = await this.getGitRoot();
|
|
164
|
-
const fullPath = join(gitRoot, path);
|
|
165
|
-
|
|
166
|
-
try {
|
|
167
|
-
const content = await readFile(fullPath, 'utf-8');
|
|
168
|
-
|
|
169
|
-
if (this.isBinaryContent(content)) {
|
|
170
|
-
return { path, hunks: [], isBinary: true };
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return this.createSyntheticDiff(path, content);
|
|
174
|
-
} catch (error) {
|
|
175
|
-
console.error(`Failed to read untracked file ${fullPath}:`, error);
|
|
176
|
-
return { path, hunks: [], isBinary: false };
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Create a synthetic diff showing all content as additions
|
|
182
|
-
*/
|
|
183
|
-
private createSyntheticDiff(path: string, content: string): FileDiff {
|
|
184
|
-
const lines = content.split(/\r?\n/);
|
|
185
|
-
|
|
186
|
-
// Remove trailing empty line if content ends with newline
|
|
187
|
-
if (lines.length > 0 && lines[lines.length - 1] === '') {
|
|
188
|
-
lines.pop();
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (lines.length === 0) {
|
|
192
|
-
return { path, hunks: [], isBinary: false };
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const hunkContent = lines.map(line => `+${line}`).join('\n');
|
|
196
|
-
const hunk: DiffHunk = {
|
|
197
|
-
oldStart: 0,
|
|
198
|
-
oldLines: 0,
|
|
199
|
-
newStart: 1,
|
|
200
|
-
newLines: lines.length,
|
|
201
|
-
content: `@@ -0,0 +1,${lines.length} @@\n${hunkContent}`,
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
return { path, hunks: [hunk], isBinary: false };
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Simple binary content detection
|
|
209
|
-
*/
|
|
210
|
-
private isBinaryContent(content: string): boolean {
|
|
211
|
-
return content.includes('\0');
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Stage files
|
|
216
|
-
*/
|
|
217
|
-
async stage(paths: string[]): Promise<void> {
|
|
218
|
-
await this.ensureInitialized();
|
|
219
|
-
await this.git.add(paths);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Unstage files
|
|
224
|
-
*/
|
|
225
|
-
async unstage(paths: string[]): Promise<void> {
|
|
226
|
-
await this.ensureInitialized();
|
|
227
|
-
await this.git.reset(['HEAD', '--', ...paths]);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Stage a patch (for line-level staging)
|
|
232
|
-
*/
|
|
233
|
-
async stagePatch(patch: string): Promise<void> {
|
|
234
|
-
await this.ensureInitialized();
|
|
235
|
-
const tempFile = await this.writeTempPatch(patch);
|
|
236
|
-
try {
|
|
237
|
-
await this.git.raw(['apply', '--cached', tempFile]);
|
|
238
|
-
} finally {
|
|
239
|
-
await this.cleanupTempFile(tempFile);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Unstage a patch (for line-level unstaging)
|
|
245
|
-
*/
|
|
246
|
-
async unstagePatch(patch: string): Promise<void> {
|
|
247
|
-
await this.ensureInitialized();
|
|
248
|
-
const tempFile = await this.writeTempPatch(patch);
|
|
249
|
-
try {
|
|
250
|
-
await this.git.raw(['apply', '--cached', '-R', tempFile]);
|
|
251
|
-
} finally {
|
|
252
|
-
await this.cleanupTempFile(tempFile);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Write a patch to a temporary file
|
|
258
|
-
*/
|
|
259
|
-
private async writeTempPatch(patch: string): Promise<string> {
|
|
260
|
-
const id = randomBytes(8).toString('hex');
|
|
261
|
-
const tempPath = join(tmpdir(), `quicksave-patch-${id}.patch`);
|
|
262
|
-
await writeFile(tempPath, patch, 'utf-8');
|
|
263
|
-
return tempPath;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Clean up a temporary file
|
|
268
|
-
*/
|
|
269
|
-
private async cleanupTempFile(path: string): Promise<void> {
|
|
270
|
-
try {
|
|
271
|
-
await unlink(path);
|
|
272
|
-
} catch {
|
|
273
|
-
// Ignore cleanup errors
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* Create a commit
|
|
279
|
-
*/
|
|
280
|
-
async commit(message: string, description?: string): Promise<string> {
|
|
281
|
-
const fullMessage = description ? `${message}\n\n${description}` : message;
|
|
282
|
-
const result = await this.git.commit(fullMessage);
|
|
283
|
-
return result.commit;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Get commit history
|
|
288
|
-
*/
|
|
289
|
-
async getLog(limit: number = 50): Promise<Commit[]> {
|
|
290
|
-
const log = await this.git.log({ maxCount: limit });
|
|
291
|
-
|
|
292
|
-
return log.all.map((entry) => ({
|
|
293
|
-
hash: entry.hash,
|
|
294
|
-
shortHash: entry.hash.slice(0, 7),
|
|
295
|
-
message: entry.message,
|
|
296
|
-
author: entry.author_name,
|
|
297
|
-
email: entry.author_email,
|
|
298
|
-
date: entry.date,
|
|
299
|
-
}));
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Get all branches
|
|
304
|
-
*/
|
|
305
|
-
async getBranches(): Promise<{ branches: Branch[]; current: string }> {
|
|
306
|
-
const branchSummary = await this.git.branch(['-a']);
|
|
307
|
-
|
|
308
|
-
const branches: Branch[] = Object.entries(branchSummary.branches).map(
|
|
309
|
-
([name, info]) => ({
|
|
310
|
-
name: info.name,
|
|
311
|
-
current: info.current,
|
|
312
|
-
remote: name.startsWith('remotes/') ? name.split('/')[1] : undefined,
|
|
313
|
-
})
|
|
314
|
-
);
|
|
315
|
-
|
|
316
|
-
return {
|
|
317
|
-
branches,
|
|
318
|
-
current: branchSummary.current,
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Checkout a branch
|
|
324
|
-
*/
|
|
325
|
-
async checkout(branch: string, create: boolean = false): Promise<void> {
|
|
326
|
-
if (create) {
|
|
327
|
-
await this.git.checkoutLocalBranch(branch);
|
|
328
|
-
} else {
|
|
329
|
-
await this.git.checkout(branch);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* Discard changes in files
|
|
335
|
-
*/
|
|
336
|
-
async discard(paths: string[]): Promise<void> {
|
|
337
|
-
await this.ensureInitialized();
|
|
338
|
-
await this.git.checkout(['--', ...paths]);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* Check if path is a valid git repository
|
|
343
|
-
*/
|
|
344
|
-
async isValidRepo(): Promise<boolean> {
|
|
345
|
-
try {
|
|
346
|
-
await this.git.status();
|
|
347
|
-
return true;
|
|
348
|
-
} catch {
|
|
349
|
-
return false;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// ============================================================================
|
|
354
|
-
// Private Helpers
|
|
355
|
-
// ============================================================================
|
|
356
|
-
|
|
357
|
-
private async getBranchTracking(): Promise<{ ahead: number; behind: number }> {
|
|
358
|
-
try {
|
|
359
|
-
const status = await this.git.status();
|
|
360
|
-
return {
|
|
361
|
-
ahead: status.ahead,
|
|
362
|
-
behind: status.behind,
|
|
363
|
-
};
|
|
364
|
-
} catch {
|
|
365
|
-
return { ahead: 0, behind: 0 };
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
private parseFileChanges(
|
|
370
|
-
status: StatusResult,
|
|
371
|
-
type: 'staged' | 'unstaged'
|
|
372
|
-
): FileChange[] {
|
|
373
|
-
const changes: FileChange[] = [];
|
|
374
|
-
|
|
375
|
-
if (type === 'staged') {
|
|
376
|
-
for (const file of status.staged) {
|
|
377
|
-
changes.push({
|
|
378
|
-
path: file,
|
|
379
|
-
status: this.getFileStatus(status, file),
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
} else {
|
|
383
|
-
// Use files array for accurate unstaged detection
|
|
384
|
-
// working_dir indicates the status in the working directory (unstaged changes)
|
|
385
|
-
for (const file of status.files) {
|
|
386
|
-
const wd = file.working_dir;
|
|
387
|
-
if (wd === 'M') {
|
|
388
|
-
changes.push({ path: file.path, status: 'modified' });
|
|
389
|
-
} else if (wd === 'D') {
|
|
390
|
-
changes.push({ path: file.path, status: 'deleted' });
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
return changes;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
private getFileStatus(status: StatusResult, file: string): FileStatus {
|
|
399
|
-
if (status.created.includes(file)) return 'added';
|
|
400
|
-
if (status.deleted.includes(file)) return 'deleted';
|
|
401
|
-
if (status.renamed.some((r) => r.to === file)) return 'renamed';
|
|
402
|
-
return 'modified';
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
private parseDiff(path: string, diffOutput: string): FileDiff {
|
|
406
|
-
const hunks: DiffHunk[] = [];
|
|
407
|
-
const lines = diffOutput.split(/\r?\n/);
|
|
408
|
-
|
|
409
|
-
let currentHunk: DiffHunk | null = null;
|
|
410
|
-
let hunkContent: string[] = [];
|
|
411
|
-
|
|
412
|
-
for (const line of lines) {
|
|
413
|
-
const hunkMatch = line.match(/^@@\s*-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s*@@/);
|
|
414
|
-
|
|
415
|
-
if (hunkMatch) {
|
|
416
|
-
if (currentHunk) {
|
|
417
|
-
currentHunk.content = hunkContent.join('\n');
|
|
418
|
-
hunks.push(currentHunk);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
currentHunk = {
|
|
422
|
-
oldStart: parseInt(hunkMatch[1], 10),
|
|
423
|
-
oldLines: parseInt(hunkMatch[2] || '1', 10),
|
|
424
|
-
newStart: parseInt(hunkMatch[3], 10),
|
|
425
|
-
newLines: parseInt(hunkMatch[4] || '1', 10),
|
|
426
|
-
content: '',
|
|
427
|
-
};
|
|
428
|
-
hunkContent = [line];
|
|
429
|
-
} else if (currentHunk && (line.startsWith('+') || line.startsWith('-') || line.startsWith(' '))) {
|
|
430
|
-
hunkContent.push(line);
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
if (currentHunk) {
|
|
435
|
-
currentHunk.content = hunkContent.join('\n');
|
|
436
|
-
hunks.push(currentHunk);
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
const isBinary = diffOutput.includes('Binary files') || diffOutput.includes('GIT binary patch');
|
|
440
|
-
|
|
441
|
-
return { path, hunks, isBinary };
|
|
442
|
-
}
|
|
443
|
-
}
|