codewalk 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/bin/codewalk.js +2 -0
- package/dist/commands/init.d.ts +5 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +163 -0
- package/dist/commands/visualize.d.ts +5 -0
- package/dist/commands/visualize.d.ts.map +1 -0
- package/dist/commands/visualize.js +79 -0
- package/dist/highlights-eq9cgrbb.scm +604 -0
- package/dist/highlights-ghv9g403.scm +205 -0
- package/dist/highlights-hk7bwhj4.scm +284 -0
- package/dist/highlights-r812a2qc.scm +150 -0
- package/dist/highlights-x6tmsnaa.scm +115 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21435 -0
- package/dist/injections-73j83es3.scm +27 -0
- package/dist/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
- package/dist/tree-sitter-markdown-411r6y9b.wasm +0 -0
- package/dist/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
- package/dist/tree-sitter-typescript-zxjzwt75.wasm +0 -0
- package/dist/tree-sitter-zig-e78zbjpm.wasm +0 -0
- package/dist/tui/app.d.ts +13 -0
- package/dist/tui/app.d.ts.map +1 -0
- package/dist/tui/app.js +11 -0
- package/dist/tui/tree-view.d.ts +24 -0
- package/dist/tui/tree-view.d.ts.map +1 -0
- package/dist/tui/tree-view.js +274 -0
- package/dist/types/codewalker.d.ts +15 -0
- package/dist/types/codewalker.d.ts.map +1 -0
- package/dist/types/codewalker.js +1 -0
- package/dist/utils/git.d.ts +24 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +132 -0
- package/dist/utils/tracking.d.ts +23 -0
- package/dist/utils/tracking.d.ts.map +1 -0
- package/dist/utils/tracking.js +85 -0
- package/package.json +41 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
; Query from: https://raw.githubusercontent.com/nvim-treesitter/nvim-treesitter/refs/heads/master/queries/markdown/injections.scm
|
|
2
|
+
(fenced_code_block
|
|
3
|
+
(info_string
|
|
4
|
+
(language) @_lang)
|
|
5
|
+
(code_fence_content) @injection.content
|
|
6
|
+
(#set-lang-from-info-string! @_lang))
|
|
7
|
+
|
|
8
|
+
((html_block) @injection.content
|
|
9
|
+
(#set! injection.language "html")
|
|
10
|
+
(#set! injection.combined)
|
|
11
|
+
(#set! injection.include-children))
|
|
12
|
+
|
|
13
|
+
((minus_metadata) @injection.content
|
|
14
|
+
(#set! injection.language "yaml")
|
|
15
|
+
(#offset! @injection.content 1 0 -1 0)
|
|
16
|
+
(#set! injection.include-children))
|
|
17
|
+
|
|
18
|
+
((plus_metadata) @injection.content
|
|
19
|
+
(#set! injection.language "toml")
|
|
20
|
+
(#offset! @injection.content 1 0 -1 0)
|
|
21
|
+
(#set! injection.include-children))
|
|
22
|
+
|
|
23
|
+
([
|
|
24
|
+
(inline)
|
|
25
|
+
(pipe_table_cell)
|
|
26
|
+
] @injection.content
|
|
27
|
+
(#set! injection.language "markdown_inline"))
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createCliRenderer, type CliRenderer, BoxRenderable, TextRenderable, ScrollBoxRenderable, DiffRenderable } from '@opentui/core';
|
|
2
|
+
import type { ReasoningGroup } from '../utils/tracking.js';
|
|
3
|
+
export interface AppState {
|
|
4
|
+
branch: string;
|
|
5
|
+
reasoningGroups: ReasoningGroup[];
|
|
6
|
+
selectedIndex: number;
|
|
7
|
+
expandedReasonings: Set<number>;
|
|
8
|
+
expandedFiles: Set<string>;
|
|
9
|
+
}
|
|
10
|
+
export declare function createAppState(branch: string, reasoningGroups: ReasoningGroup[]): AppState;
|
|
11
|
+
export { createCliRenderer, BoxRenderable, TextRenderable, ScrollBoxRenderable, DiffRenderable };
|
|
12
|
+
export type { CliRenderer };
|
|
13
|
+
//# sourceMappingURL=app.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../src/tui/app.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,KAAK,WAAW,EAChB,aAAa,EACb,cAAc,EACd,mBAAmB,EACnB,cAAc,EACf,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,cAAc,EAAE,CAAC;IAClC,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAChC,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAC5B;AAED,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,EACd,eAAe,EAAE,cAAc,EAAE,GAChC,QAAQ,CAQV;AAED,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,cAAc,EAAE,mBAAmB,EAAE,cAAc,EAAE,CAAC;AACjG,YAAY,EAAE,WAAW,EAAE,CAAC"}
|
package/dist/tui/app.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createCliRenderer, BoxRenderable, TextRenderable, ScrollBoxRenderable, DiffRenderable, } from '@opentui/core';
|
|
2
|
+
export function createAppState(branch, reasoningGroups) {
|
|
3
|
+
return {
|
|
4
|
+
branch,
|
|
5
|
+
reasoningGroups,
|
|
6
|
+
selectedIndex: 0,
|
|
7
|
+
expandedReasonings: new Set(),
|
|
8
|
+
expandedFiles: new Set(),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export { createCliRenderer, BoxRenderable, TextRenderable, ScrollBoxRenderable, DiffRenderable };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type CliRenderer } from '@opentui/core';
|
|
2
|
+
import type { AppState } from './app.js';
|
|
3
|
+
export declare class TreeView {
|
|
4
|
+
private renderer;
|
|
5
|
+
private state;
|
|
6
|
+
private rootBox;
|
|
7
|
+
private headerBox;
|
|
8
|
+
private scrollBox;
|
|
9
|
+
private footerBox;
|
|
10
|
+
private selectableItems;
|
|
11
|
+
constructor(renderer: CliRenderer, state: AppState);
|
|
12
|
+
private buildUI;
|
|
13
|
+
private buildHeader;
|
|
14
|
+
private buildContent;
|
|
15
|
+
private buildFooter;
|
|
16
|
+
private isReasoningSelected;
|
|
17
|
+
private isFileSelected;
|
|
18
|
+
getItemCount(): number;
|
|
19
|
+
toggleExpand(): void;
|
|
20
|
+
moveSelection(delta: number): void;
|
|
21
|
+
refresh(): void;
|
|
22
|
+
destroy(): void;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=tree-view.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tree-view.d.ts","sourceRoot":"","sources":["../../src/tui/tree-view.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,WAAW,EACjB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAgDzC,qBAAa,QAAQ;IACnB,OAAO,CAAC,QAAQ,CAAc;IAC9B,OAAO,CAAC,KAAK,CAAW;IACxB,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,SAAS,CAAgB;IACjC,OAAO,CAAC,SAAS,CAAsB;IACvC,OAAO,CAAC,SAAS,CAAgB;IACjC,OAAO,CAAC,eAAe,CAAwB;gBAEnC,QAAQ,EAAE,WAAW,EAAE,KAAK,EAAE,QAAQ;IAiDlD,OAAO,CAAC,OAAO;IAoBf,OAAO,CAAC,WAAW;IAiBnB,OAAO,CAAC,YAAY;IAwGpB,OAAO,CAAC,WAAW;IAiBnB,OAAO,CAAC,mBAAmB;IAK3B,OAAO,CAAC,cAAc;IAKf,YAAY,IAAI,MAAM;IAItB,YAAY,IAAI,IAAI;IA4BpB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAQlC,OAAO,IAAI,IAAI;IAIf,OAAO,IAAI,IAAI;CAGvB"}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { BoxRenderable, TextRenderable, ScrollBoxRenderable, DiffRenderable, } from '@opentui/core';
|
|
2
|
+
// Convert hunks to unified diff format for DiffRenderable
|
|
3
|
+
function hunksToUnifiedDiff(filePath, hunks) {
|
|
4
|
+
let diff = `--- a/${filePath}\n+++ b/${filePath}\n`;
|
|
5
|
+
for (const hunk of hunks) {
|
|
6
|
+
diff += hunk.header + '\n';
|
|
7
|
+
diff += hunk.content;
|
|
8
|
+
}
|
|
9
|
+
return diff;
|
|
10
|
+
}
|
|
11
|
+
// Get file extension for syntax highlighting
|
|
12
|
+
function getFileType(filePath) {
|
|
13
|
+
const ext = filePath.split('.').pop()?.toLowerCase();
|
|
14
|
+
const typeMap = {
|
|
15
|
+
ts: 'typescript',
|
|
16
|
+
tsx: 'tsx',
|
|
17
|
+
js: 'javascript',
|
|
18
|
+
jsx: 'jsx',
|
|
19
|
+
py: 'python',
|
|
20
|
+
rb: 'ruby',
|
|
21
|
+
go: 'go',
|
|
22
|
+
rs: 'rust',
|
|
23
|
+
java: 'java',
|
|
24
|
+
c: 'c',
|
|
25
|
+
cpp: 'cpp',
|
|
26
|
+
h: 'c',
|
|
27
|
+
hpp: 'cpp',
|
|
28
|
+
css: 'css',
|
|
29
|
+
scss: 'scss',
|
|
30
|
+
html: 'html',
|
|
31
|
+
json: 'json',
|
|
32
|
+
md: 'markdown',
|
|
33
|
+
yaml: 'yaml',
|
|
34
|
+
yml: 'yaml',
|
|
35
|
+
};
|
|
36
|
+
return ext ? typeMap[ext] : undefined;
|
|
37
|
+
}
|
|
38
|
+
export class TreeView {
|
|
39
|
+
renderer;
|
|
40
|
+
state;
|
|
41
|
+
rootBox;
|
|
42
|
+
headerBox;
|
|
43
|
+
scrollBox;
|
|
44
|
+
footerBox;
|
|
45
|
+
selectableItems = [];
|
|
46
|
+
constructor(renderer, state) {
|
|
47
|
+
this.renderer = renderer;
|
|
48
|
+
this.state = state;
|
|
49
|
+
// Root container - full screen, vertical flex
|
|
50
|
+
this.rootBox = new BoxRenderable(renderer, {
|
|
51
|
+
width: '100%',
|
|
52
|
+
height: '100%',
|
|
53
|
+
flexDirection: 'column',
|
|
54
|
+
});
|
|
55
|
+
// Header - fixed at top
|
|
56
|
+
this.headerBox = new BoxRenderable(renderer, {
|
|
57
|
+
width: '100%',
|
|
58
|
+
height: 3,
|
|
59
|
+
border: true,
|
|
60
|
+
borderStyle: 'single',
|
|
61
|
+
borderColor: '#555555',
|
|
62
|
+
backgroundColor: '#1a1a2e',
|
|
63
|
+
});
|
|
64
|
+
// Scrollable content area
|
|
65
|
+
this.scrollBox = new ScrollBoxRenderable(renderer, {
|
|
66
|
+
width: '100%',
|
|
67
|
+
flexGrow: 1,
|
|
68
|
+
scrollY: true,
|
|
69
|
+
backgroundColor: '#0f0f1a',
|
|
70
|
+
});
|
|
71
|
+
// Footer - fixed at bottom
|
|
72
|
+
this.footerBox = new BoxRenderable(renderer, {
|
|
73
|
+
width: '100%',
|
|
74
|
+
height: 2,
|
|
75
|
+
border: true,
|
|
76
|
+
borderStyle: 'single',
|
|
77
|
+
borderColor: '#555555',
|
|
78
|
+
backgroundColor: '#1a1a2e',
|
|
79
|
+
paddingLeft: 1,
|
|
80
|
+
});
|
|
81
|
+
this.rootBox.add(this.headerBox);
|
|
82
|
+
this.rootBox.add(this.scrollBox);
|
|
83
|
+
this.rootBox.add(this.footerBox);
|
|
84
|
+
renderer.root.add(this.rootBox);
|
|
85
|
+
this.buildUI();
|
|
86
|
+
}
|
|
87
|
+
buildUI() {
|
|
88
|
+
// Clear previous content
|
|
89
|
+
this.selectableItems = [];
|
|
90
|
+
// Clear scrollbox content
|
|
91
|
+
const children = this.scrollBox.getChildren();
|
|
92
|
+
for (const child of children) {
|
|
93
|
+
this.scrollBox.remove(child.id);
|
|
94
|
+
}
|
|
95
|
+
// Build header
|
|
96
|
+
this.buildHeader();
|
|
97
|
+
// Build content
|
|
98
|
+
this.buildContent();
|
|
99
|
+
// Build footer
|
|
100
|
+
this.buildFooter();
|
|
101
|
+
}
|
|
102
|
+
buildHeader() {
|
|
103
|
+
// Clear header
|
|
104
|
+
const headerChildren = this.headerBox.getChildren();
|
|
105
|
+
for (const child of headerChildren) {
|
|
106
|
+
this.headerBox.remove(child.id);
|
|
107
|
+
}
|
|
108
|
+
const totalChanges = this.state.reasoningGroups.length;
|
|
109
|
+
const headerText = new TextRenderable(this.renderer, {
|
|
110
|
+
content: ` CodeWalker - ${this.state.branch} (${totalChanges} logical changes)`,
|
|
111
|
+
fg: '#88ccff',
|
|
112
|
+
paddingTop: 0,
|
|
113
|
+
paddingLeft: 1,
|
|
114
|
+
});
|
|
115
|
+
this.headerBox.add(headerText);
|
|
116
|
+
}
|
|
117
|
+
buildContent() {
|
|
118
|
+
this.state.reasoningGroups.forEach((group, reasoningIdx) => {
|
|
119
|
+
const isExpanded = this.state.expandedReasonings.has(reasoningIdx);
|
|
120
|
+
const isSelected = this.isReasoningSelected(reasoningIdx);
|
|
121
|
+
// Reasoning container
|
|
122
|
+
const reasoningBox = new BoxRenderable(this.renderer, {
|
|
123
|
+
width: '100%',
|
|
124
|
+
flexDirection: 'column',
|
|
125
|
+
paddingLeft: 1,
|
|
126
|
+
paddingTop: 1,
|
|
127
|
+
backgroundColor: isSelected ? '#2a2a4e' : undefined,
|
|
128
|
+
});
|
|
129
|
+
// Reasoning header with arrow and text
|
|
130
|
+
const arrow = isExpanded ? '▼' : '▶';
|
|
131
|
+
const fileCount = group.files.length;
|
|
132
|
+
const reasoningHeader = new TextRenderable(this.renderer, {
|
|
133
|
+
content: `${arrow} ${group.reasoning} (${fileCount} file${fileCount !== 1 ? 's' : ''})`,
|
|
134
|
+
fg: isSelected ? '#ffffff' : '#cccccc',
|
|
135
|
+
width: '100%',
|
|
136
|
+
});
|
|
137
|
+
reasoningBox.add(reasoningHeader);
|
|
138
|
+
this.selectableItems.push({
|
|
139
|
+
type: 'reasoning',
|
|
140
|
+
reasoningIdx,
|
|
141
|
+
renderable: reasoningBox,
|
|
142
|
+
});
|
|
143
|
+
// If expanded, show files
|
|
144
|
+
if (isExpanded) {
|
|
145
|
+
group.files.forEach((file) => {
|
|
146
|
+
const fileKey = `${reasoningIdx}|${file.path}`;
|
|
147
|
+
const isFileExpanded = this.state.expandedFiles.has(fileKey);
|
|
148
|
+
const isFileSelected = this.isFileSelected(reasoningIdx, file.path);
|
|
149
|
+
// File container
|
|
150
|
+
const fileBox = new BoxRenderable(this.renderer, {
|
|
151
|
+
width: '100%',
|
|
152
|
+
flexDirection: 'column',
|
|
153
|
+
paddingLeft: 3,
|
|
154
|
+
paddingTop: 1,
|
|
155
|
+
backgroundColor: isFileSelected ? '#2a2a4e' : undefined,
|
|
156
|
+
});
|
|
157
|
+
// File header
|
|
158
|
+
const fileArrow = isFileExpanded ? '▼' : '▶';
|
|
159
|
+
const fileHeader = new TextRenderable(this.renderer, {
|
|
160
|
+
content: `${fileArrow} ${file.path}`,
|
|
161
|
+
fg: isFileSelected ? '#88ccff' : '#6699cc',
|
|
162
|
+
});
|
|
163
|
+
fileBox.add(fileHeader);
|
|
164
|
+
this.selectableItems.push({
|
|
165
|
+
type: 'file',
|
|
166
|
+
reasoningIdx,
|
|
167
|
+
filePath: file.path,
|
|
168
|
+
renderable: fileBox,
|
|
169
|
+
});
|
|
170
|
+
// If file is expanded, show diff
|
|
171
|
+
if (isFileExpanded && file.hunks.length > 0) {
|
|
172
|
+
const diffContent = hunksToUnifiedDiff(file.path, file.hunks);
|
|
173
|
+
const fileType = getFileType(file.path);
|
|
174
|
+
const diffBox = new BoxRenderable(this.renderer, {
|
|
175
|
+
width: '100%',
|
|
176
|
+
border: true,
|
|
177
|
+
borderStyle: 'single',
|
|
178
|
+
borderColor: '#444444',
|
|
179
|
+
title: file.path,
|
|
180
|
+
titleAlignment: 'left',
|
|
181
|
+
marginLeft: 2,
|
|
182
|
+
marginTop: 1,
|
|
183
|
+
marginBottom: 1,
|
|
184
|
+
});
|
|
185
|
+
const diffRenderable = new DiffRenderable(this.renderer, {
|
|
186
|
+
diff: diffContent,
|
|
187
|
+
view: 'unified',
|
|
188
|
+
showLineNumbers: true,
|
|
189
|
+
filetype: fileType,
|
|
190
|
+
addedBg: '#1a3d1a',
|
|
191
|
+
removedBg: '#3d1a1a',
|
|
192
|
+
contextBg: '#1a1a2e',
|
|
193
|
+
addedSignColor: '#22cc22',
|
|
194
|
+
removedSignColor: '#cc2222',
|
|
195
|
+
lineNumberFg: '#666666',
|
|
196
|
+
width: '100%',
|
|
197
|
+
});
|
|
198
|
+
diffBox.add(diffRenderable);
|
|
199
|
+
fileBox.add(diffBox);
|
|
200
|
+
}
|
|
201
|
+
reasoningBox.add(fileBox);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
this.scrollBox.add(reasoningBox);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
buildFooter() {
|
|
208
|
+
// Clear footer
|
|
209
|
+
const footerChildren = this.footerBox.getChildren();
|
|
210
|
+
for (const child of footerChildren) {
|
|
211
|
+
this.footerBox.remove(child.id);
|
|
212
|
+
}
|
|
213
|
+
const totalItems = this.selectableItems.length;
|
|
214
|
+
const currentPos = this.state.selectedIndex + 1;
|
|
215
|
+
const footerText = new TextRenderable(this.renderer, {
|
|
216
|
+
content: `j/k: navigate │ Enter: expand/collapse │ q: quit [${currentPos}/${totalItems}]`,
|
|
217
|
+
fg: '#888888',
|
|
218
|
+
});
|
|
219
|
+
this.footerBox.add(footerText);
|
|
220
|
+
}
|
|
221
|
+
isReasoningSelected(reasoningIdx) {
|
|
222
|
+
const item = this.selectableItems[this.state.selectedIndex];
|
|
223
|
+
return item?.type === 'reasoning' && item.reasoningIdx === reasoningIdx;
|
|
224
|
+
}
|
|
225
|
+
isFileSelected(reasoningIdx, filePath) {
|
|
226
|
+
const item = this.selectableItems[this.state.selectedIndex];
|
|
227
|
+
return item?.type === 'file' && item.reasoningIdx === reasoningIdx && item.filePath === filePath;
|
|
228
|
+
}
|
|
229
|
+
getItemCount() {
|
|
230
|
+
return this.selectableItems.length;
|
|
231
|
+
}
|
|
232
|
+
toggleExpand() {
|
|
233
|
+
const item = this.selectableItems[this.state.selectedIndex];
|
|
234
|
+
if (!item)
|
|
235
|
+
return;
|
|
236
|
+
if (item.type === 'reasoning') {
|
|
237
|
+
if (this.state.expandedReasonings.has(item.reasoningIdx)) {
|
|
238
|
+
this.state.expandedReasonings.delete(item.reasoningIdx);
|
|
239
|
+
// Collapse all files in this reasoning
|
|
240
|
+
for (const key of this.state.expandedFiles) {
|
|
241
|
+
if (key.startsWith(`${item.reasoningIdx}|`)) {
|
|
242
|
+
this.state.expandedFiles.delete(key);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
this.state.expandedReasonings.add(item.reasoningIdx);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
else if (item.type === 'file' && item.filePath) {
|
|
251
|
+
const fileKey = `${item.reasoningIdx}|${item.filePath}`;
|
|
252
|
+
if (this.state.expandedFiles.has(fileKey)) {
|
|
253
|
+
this.state.expandedFiles.delete(fileKey);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
this.state.expandedFiles.add(fileKey);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
this.buildUI();
|
|
260
|
+
}
|
|
261
|
+
moveSelection(delta) {
|
|
262
|
+
const newIndex = this.state.selectedIndex + delta;
|
|
263
|
+
if (newIndex >= 0 && newIndex < this.selectableItems.length) {
|
|
264
|
+
this.state.selectedIndex = newIndex;
|
|
265
|
+
this.buildUI();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
refresh() {
|
|
269
|
+
this.buildUI();
|
|
270
|
+
}
|
|
271
|
+
destroy() {
|
|
272
|
+
this.renderer.root.remove(this.rootBox.id);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type Changeset = {
|
|
2
|
+
version: number;
|
|
3
|
+
commit: string;
|
|
4
|
+
author: string;
|
|
5
|
+
changes: Change[];
|
|
6
|
+
};
|
|
7
|
+
export type Change = {
|
|
8
|
+
reasoning: string;
|
|
9
|
+
files: FileChange[];
|
|
10
|
+
};
|
|
11
|
+
export type FileChange = {
|
|
12
|
+
path: string;
|
|
13
|
+
hunks: number[];
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=codewalker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codewalker.d.ts","sourceRoot":"","sources":["../../src/types/codewalker.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,SAAS,GAAG;IAEtB,OAAO,EAAE,MAAM,CAAC;IAGhB,MAAM,EAAE,MAAM,CAAC;IAGf,MAAM,EAAE,MAAM,CAAC;IAGf,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,MAAM,GAAG;IAGnB,SAAS,EAAE,MAAM,CAAC;IAGlB,KAAK,EAAE,UAAU,EAAE,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IAEvB,IAAI,EAAE,MAAM,CAAC;IAKb,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface CommitInfo {
|
|
2
|
+
sha: string;
|
|
3
|
+
shortSha: string;
|
|
4
|
+
author: string;
|
|
5
|
+
message: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ParsedHunk {
|
|
8
|
+
hunkNumber: number;
|
|
9
|
+
header: string;
|
|
10
|
+
content: string;
|
|
11
|
+
}
|
|
12
|
+
export interface FileDiff {
|
|
13
|
+
path: string;
|
|
14
|
+
hunks: ParsedHunk[];
|
|
15
|
+
}
|
|
16
|
+
export declare function getCurrentBranch(cwd: string): string;
|
|
17
|
+
export declare function getCommitList(cwd: string): CommitInfo[];
|
|
18
|
+
export declare function isGitRepo(cwd: string): boolean;
|
|
19
|
+
export declare function getMainBranch(cwd: string): string;
|
|
20
|
+
export declare function getMergeBase(cwd: string, branch1: string, branch2: string): string;
|
|
21
|
+
export declare function getCommitDiff(cwd: string, commitSha: string): string;
|
|
22
|
+
export declare function parseDiffIntoFiles(diffOutput: string): FileDiff[];
|
|
23
|
+
export declare function getCommitFileDiffs(cwd: string, commitSha: string): FileDiff[];
|
|
24
|
+
//# sourceMappingURL=git.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../../src/utils/git.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,UAAU,EAAE,CAAC;CACrB;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CASpD;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,EAAE,CAuBvD;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAO9C;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAWjD;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CASlF;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAUpE;AAED,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,QAAQ,EAAE,CAsDjE;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,QAAQ,EAAE,CAG7E"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
export function getCurrentBranch(cwd) {
|
|
3
|
+
try {
|
|
4
|
+
return execSync('git rev-parse --abbrev-ref HEAD', {
|
|
5
|
+
cwd,
|
|
6
|
+
encoding: 'utf-8',
|
|
7
|
+
}).trim();
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
throw new Error('Not a git repository or git is not installed');
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function getCommitList(cwd) {
|
|
14
|
+
try {
|
|
15
|
+
const output = execSync('git log --format="%H|%h|%an|%s" --first-parent', {
|
|
16
|
+
cwd,
|
|
17
|
+
encoding: 'utf-8',
|
|
18
|
+
});
|
|
19
|
+
return output
|
|
20
|
+
.trim()
|
|
21
|
+
.split('\n')
|
|
22
|
+
.filter(Boolean)
|
|
23
|
+
.map((line) => {
|
|
24
|
+
const [sha, shortSha, author, ...messageParts] = line.split('|');
|
|
25
|
+
return {
|
|
26
|
+
sha,
|
|
27
|
+
shortSha,
|
|
28
|
+
author,
|
|
29
|
+
message: messageParts.join('|'),
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
throw new Error('Failed to get commit list');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function isGitRepo(cwd) {
|
|
38
|
+
try {
|
|
39
|
+
execSync('git rev-parse --git-dir', { cwd, encoding: 'utf-8' });
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function getMainBranch(cwd) {
|
|
47
|
+
try {
|
|
48
|
+
// Try to get the default branch from origin
|
|
49
|
+
const result = execSync('git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null || echo "refs/heads/main"', {
|
|
50
|
+
cwd,
|
|
51
|
+
encoding: 'utf-8',
|
|
52
|
+
}).trim();
|
|
53
|
+
return result.replace('refs/remotes/origin/', '').replace('refs/heads/', '');
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return 'main';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function getMergeBase(cwd, branch1, branch2) {
|
|
60
|
+
try {
|
|
61
|
+
return execSync(`git merge-base ${branch1} ${branch2}`, {
|
|
62
|
+
cwd,
|
|
63
|
+
encoding: 'utf-8',
|
|
64
|
+
}).trim();
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
throw new Error(`Failed to find merge base between ${branch1} and ${branch2}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export function getCommitDiff(cwd, commitSha) {
|
|
71
|
+
try {
|
|
72
|
+
return execSync(`git show ${commitSha} --format="" --patch`, {
|
|
73
|
+
cwd,
|
|
74
|
+
encoding: 'utf-8',
|
|
75
|
+
maxBuffer: 10 * 1024 * 1024, // 10MB buffer for large diffs
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return '';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export function parseDiffIntoFiles(diffOutput) {
|
|
83
|
+
const files = [];
|
|
84
|
+
// Split by file headers (diff --git a/... b/...)
|
|
85
|
+
const fileChunks = diffOutput.split(/^diff --git /m).filter(Boolean);
|
|
86
|
+
for (const chunk of fileChunks) {
|
|
87
|
+
const lines = chunk.split('\n');
|
|
88
|
+
// Extract file path from the first line (a/path b/path)
|
|
89
|
+
const headerMatch = lines[0]?.match(/a\/(.+?) b\/(.+)/);
|
|
90
|
+
if (!headerMatch)
|
|
91
|
+
continue;
|
|
92
|
+
const filePath = headerMatch[2];
|
|
93
|
+
const hunks = [];
|
|
94
|
+
let currentHunk = null;
|
|
95
|
+
let hunkNumber = 0;
|
|
96
|
+
for (let i = 1; i < lines.length; i++) {
|
|
97
|
+
const line = lines[i];
|
|
98
|
+
// Start of a new hunk
|
|
99
|
+
if (line.startsWith('@@')) {
|
|
100
|
+
if (currentHunk) {
|
|
101
|
+
hunks.push(currentHunk);
|
|
102
|
+
}
|
|
103
|
+
hunkNumber++;
|
|
104
|
+
currentHunk = {
|
|
105
|
+
hunkNumber,
|
|
106
|
+
header: line,
|
|
107
|
+
content: '',
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
else if (currentHunk) {
|
|
111
|
+
// Skip binary file markers and other metadata
|
|
112
|
+
if (line.startsWith('Binary files') || line.startsWith('index ') ||
|
|
113
|
+
line.startsWith('---') || line.startsWith('+++') ||
|
|
114
|
+
line.startsWith('new file') || line.startsWith('deleted file')) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
currentHunk.content += line + '\n';
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (currentHunk) {
|
|
121
|
+
hunks.push(currentHunk);
|
|
122
|
+
}
|
|
123
|
+
if (hunks.length > 0) {
|
|
124
|
+
files.push({ path: filePath, hunks });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return files;
|
|
128
|
+
}
|
|
129
|
+
export function getCommitFileDiffs(cwd, commitSha) {
|
|
130
|
+
const diffOutput = getCommitDiff(cwd, commitSha);
|
|
131
|
+
return parseDiffIntoFiles(diffOutput);
|
|
132
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Changeset } from '../types/codewalker.js';
|
|
2
|
+
import type { CommitInfo, ParsedHunk } from './git.js';
|
|
3
|
+
export interface TrackedCommit {
|
|
4
|
+
commit: CommitInfo;
|
|
5
|
+
tracking: Changeset | null;
|
|
6
|
+
}
|
|
7
|
+
export interface FileWithHunks {
|
|
8
|
+
path: string;
|
|
9
|
+
hunks: ParsedHunk[];
|
|
10
|
+
hunkNumbers: number[];
|
|
11
|
+
}
|
|
12
|
+
export interface ReasoningGroup {
|
|
13
|
+
reasoning: string;
|
|
14
|
+
files: FileWithHunks[];
|
|
15
|
+
}
|
|
16
|
+
export declare function loadTrackingFiles(cwd: string, commits: CommitInfo[]): Promise<TrackedCommit[]>;
|
|
17
|
+
export declare function getTrackedCommits(trackedCommits: TrackedCommit[]): TrackedCommit[];
|
|
18
|
+
/**
|
|
19
|
+
* Aggregates all tracking data into reasoning groups with actual diff hunks.
|
|
20
|
+
* This is the "By Reasoning" view - grouping changes by their logical purpose.
|
|
21
|
+
*/
|
|
22
|
+
export declare function aggregateByReasoning(cwd: string, trackedCommits: TrackedCommit[]): ReasoningGroup[];
|
|
23
|
+
//# sourceMappingURL=tracking.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tracking.d.ts","sourceRoot":"","sources":["../../src/utils/tracking.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAE,UAAU,EAAY,UAAU,EAAE,MAAM,UAAU,CAAC;AAGjE,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,EAAE,SAAS,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,aAAa,EAAE,CAAC;CACxB;AAED,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,UAAU,EAAE,GACpB,OAAO,CAAC,aAAa,EAAE,CAAC,CAkB1B;AAED,wBAAgB,iBAAiB,CAAC,cAAc,EAAE,aAAa,EAAE,GAAG,aAAa,EAAE,CAElF;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,MAAM,EACX,cAAc,EAAE,aAAa,EAAE,GAC9B,cAAc,EAAE,CAiElB"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { getCommitFileDiffs } from './git.js';
|
|
4
|
+
export async function loadTrackingFiles(cwd, commits) {
|
|
5
|
+
const result = [];
|
|
6
|
+
for (const commit of commits) {
|
|
7
|
+
const trackingPath = path.join(cwd, '.codewalker', `${commit.shortSha}.json`);
|
|
8
|
+
let tracking = null;
|
|
9
|
+
try {
|
|
10
|
+
const content = await fs.readFile(trackingPath, 'utf-8');
|
|
11
|
+
tracking = JSON.parse(content);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
// No tracking file for this commit
|
|
15
|
+
}
|
|
16
|
+
result.push({ commit, tracking });
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
export function getTrackedCommits(trackedCommits) {
|
|
21
|
+
return trackedCommits.filter((tc) => tc.tracking !== null);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Aggregates all tracking data into reasoning groups with actual diff hunks.
|
|
25
|
+
* This is the "By Reasoning" view - grouping changes by their logical purpose.
|
|
26
|
+
*/
|
|
27
|
+
export function aggregateByReasoning(cwd, trackedCommits) {
|
|
28
|
+
const reasoningMap = new Map();
|
|
29
|
+
// Build a map of commit SHA to file diffs for quick lookup
|
|
30
|
+
const commitDiffs = new Map();
|
|
31
|
+
for (const tc of trackedCommits) {
|
|
32
|
+
if (!tc.tracking)
|
|
33
|
+
continue;
|
|
34
|
+
// Get diffs for this commit (lazy load)
|
|
35
|
+
if (!commitDiffs.has(tc.commit.shortSha)) {
|
|
36
|
+
commitDiffs.set(tc.commit.shortSha, getCommitFileDiffs(cwd, tc.commit.shortSha));
|
|
37
|
+
}
|
|
38
|
+
const fileDiffs = commitDiffs.get(tc.commit.shortSha) || [];
|
|
39
|
+
for (const change of tc.tracking.changes) {
|
|
40
|
+
// Use reasoning as the key (could also include commit if you want per-commit grouping)
|
|
41
|
+
const key = change.reasoning;
|
|
42
|
+
if (!reasoningMap.has(key)) {
|
|
43
|
+
reasoningMap.set(key, {
|
|
44
|
+
reasoning: change.reasoning,
|
|
45
|
+
files: [],
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
const group = reasoningMap.get(key);
|
|
49
|
+
for (const fileChange of change.files) {
|
|
50
|
+
// Find the diff for this file
|
|
51
|
+
const fileDiff = fileDiffs.find((fd) => fd.path === fileChange.path);
|
|
52
|
+
if (!fileDiff)
|
|
53
|
+
continue;
|
|
54
|
+
// Get only the hunks specified in the tracking file
|
|
55
|
+
const selectedHunks = fileChange.hunks
|
|
56
|
+
.map((hunkNum) => fileDiff.hunks.find((h) => h.hunkNumber === hunkNum))
|
|
57
|
+
.filter((h) => h != null);
|
|
58
|
+
if (selectedHunks.length === 0)
|
|
59
|
+
continue;
|
|
60
|
+
// Check if this file is already in the group
|
|
61
|
+
const existingFile = group.files.find((f) => f.path === fileChange.path);
|
|
62
|
+
if (existingFile) {
|
|
63
|
+
// Merge hunks (avoid duplicates)
|
|
64
|
+
for (const hunk of selectedHunks) {
|
|
65
|
+
if (!existingFile.hunks.find((h) => h.hunkNumber === hunk.hunkNumber)) {
|
|
66
|
+
existingFile.hunks.push(hunk);
|
|
67
|
+
existingFile.hunkNumbers.push(hunk.hunkNumber);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
group.files.push({
|
|
73
|
+
path: fileChange.path,
|
|
74
|
+
hunks: selectedHunks,
|
|
75
|
+
hunkNumbers: fileChange.hunks,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Convert map to array and sort by number of files (most impactful first)
|
|
82
|
+
return Array.from(reasoningMap.values())
|
|
83
|
+
.filter((group) => group.files.length > 0)
|
|
84
|
+
.sort((a, b) => b.files.length - a.files.length);
|
|
85
|
+
}
|