diff-easy 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 +22 -0
- package/README.md +142 -0
- package/dist/cli/cmux.d.ts +9 -0
- package/dist/cli/cmux.js +274 -0
- package/dist/cli/comment.d.ts +2 -0
- package/dist/cli/comment.js +91 -0
- package/dist/cli/github.d.ts +84 -0
- package/dist/cli/github.js +364 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +397 -0
- package/dist/cli/turns.d.ts +14 -0
- package/dist/cli/turns.js +333 -0
- package/dist/cli/utils.d.ts +27 -0
- package/dist/cli/utils.js +204 -0
- package/dist/client/assets/arc-CHYjdFTk.js +1 -0
- package/dist/client/assets/architecture-7EHR7CIX-weHBYDgM.js +1 -0
- package/dist/client/assets/architectureDiagram-3BPJPVTR-DRa2msuJ.js +36 -0
- package/dist/client/assets/array-BifhSqXX.js +1 -0
- package/dist/client/assets/blockDiagram-GPEHLZMM-BTdKsXy5.js +132 -0
- package/dist/client/assets/c4Diagram-AAUBKEIU-CwTvCkOg.js +10 -0
- package/dist/client/assets/channel-Cv40nQW1.js +1 -0
- package/dist/client/assets/chunk-2J33WTMH-BFsjrTtc.js +1 -0
- package/dist/client/assets/chunk-3OPIFGDE-V_xVw1dq.js +62 -0
- package/dist/client/assets/chunk-4BX2VUAB-DoLO6LLy.js +1 -0
- package/dist/client/assets/chunk-55IACEB6-CHgBwbdB.js +1 -0
- package/dist/client/assets/chunk-5ZQYHXKU-DMixAgQG.js +2 -0
- package/dist/client/assets/chunk-727SXJPM-C-3FZTmm.js +206 -0
- package/dist/client/assets/chunk-AQP2D5EJ-DpQZhXt-.js +231 -0
- package/dist/client/assets/chunk-BSJP7CBP-BnO9E_G4.js +1 -0
- package/dist/client/assets/chunk-CSCIHK7Q-BxItphyO.js +124 -0
- package/dist/client/assets/chunk-FMBD7UC4-4hEcfvfT.js +15 -0
- package/dist/client/assets/chunk-KSCS5N6A-DnArkpOI.js +10 -0
- package/dist/client/assets/chunk-L5ZTLDWV-DQoYJ1BW.js +1 -0
- package/dist/client/assets/chunk-LZXEDZCA-CdCevBX-.js +2 -0
- package/dist/client/assets/chunk-ND2GUHAM-Dt89U_nU.js +1 -0
- package/dist/client/assets/chunk-NNHCCRGN-DlpIbxXb.js +159 -0
- package/dist/client/assets/chunk-NZK2D7GU-C2Ev_f_w.js +1 -0
- package/dist/client/assets/chunk-O5CBEL6O-BeZ3MFnJ.js +70 -0
- package/dist/client/assets/chunk-QZHKN3VN-BmDOSWHp.js +1 -0
- package/dist/client/assets/chunk-WU5MYG2G-gLuInTk4.js +1 -0
- package/dist/client/assets/chunk-XPW4576I-DPbDc8_T.js +32 -0
- package/dist/client/assets/classDiagram-4FO5ZUOK-DOpVKUrb.js +1 -0
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-DOpVKUrb.js +1 -0
- package/dist/client/assets/cose-bilkent-S5V4N54A-CCPyalms.js +1 -0
- package/dist/client/assets/cytoscape.esm-FqbQrHcz.js +321 -0
- package/dist/client/assets/dagre-BM42HDAG-6jrYMjX3.js +4 -0
- package/dist/client/assets/dagre-Bx709z4p.js +1 -0
- package/dist/client/assets/defaultLocale-C8Fc0cco.js +1 -0
- package/dist/client/assets/diagram-2AECGRRQ-dCQ_KoXl.js +43 -0
- package/dist/client/assets/diagram-5GNKFQAL-B3Xj4-XE.js +10 -0
- package/dist/client/assets/diagram-KO2AKTUF-DaGgF2yL.js +3 -0
- package/dist/client/assets/diagram-LMA3HP47-CF_c4tPT.js +24 -0
- package/dist/client/assets/diagram-OG6HWLK6-BgBHbJkT.js +24 -0
- package/dist/client/assets/dist-7wVkzZne.js +1 -0
- package/dist/client/assets/erDiagram-TEJ5UH35-BZgX1grR.js +85 -0
- package/dist/client/assets/eventmodeling-FCH6USID-BaI6RNKr.js +1 -0
- package/dist/client/assets/flowDiagram-I6XJVG4X-BFNUa12V.js +162 -0
- package/dist/client/assets/ganttDiagram-6RSMTGT7-C8t6BThI.js +292 -0
- package/dist/client/assets/gitGraph-WXDBUCRP-BlicfDkM.js +1 -0
- package/dist/client/assets/gitGraphDiagram-PVQCEYII-BmnIXuXP.js +106 -0
- package/dist/client/assets/graphlib-B8gBHxth.js +1 -0
- package/dist/client/assets/index-BfVtbKTZ.css +2 -0
- package/dist/client/assets/index-p8zzfjdu.js +87 -0
- package/dist/client/assets/info-J43DQDTF-DHLG38N3.js +1 -0
- package/dist/client/assets/infoDiagram-5YYISTIA-oQ-lq-45.js +2 -0
- package/dist/client/assets/init-D6jRqBbL.js +1 -0
- package/dist/client/assets/ishikawaDiagram-YF4QCWOH-DrODlBLD.js +70 -0
- package/dist/client/assets/journeyDiagram-JHISSGLW-Bxg5wCQz.js +139 -0
- package/dist/client/assets/kanban-definition-UN3LZRKU-DAdvUDIB.js +89 -0
- package/dist/client/assets/katex-Vhh-h91d.js +257 -0
- package/dist/client/assets/line-D8kNd46u.js +1 -0
- package/dist/client/assets/linear-jwGZSrg4.js +1 -0
- package/dist/client/assets/mermaid-parser.core-PVPfUfyU.js +4 -0
- package/dist/client/assets/mermaid.core-DwdP6CFg.js +9 -0
- package/dist/client/assets/mindmap-definition-RKZ34NQL-DeOoL56Q.js +96 -0
- package/dist/client/assets/ordinal-hYBb2elL.js +1 -0
- package/dist/client/assets/packet-YPE3B663-BBEaxdJP.js +1 -0
- package/dist/client/assets/path-BWPyau1x.js +1 -0
- package/dist/client/assets/pie-LRSECV5Y-fvTRFm-2.js +1 -0
- package/dist/client/assets/pieDiagram-4H26LBE5-oep1px2r.js +30 -0
- package/dist/client/assets/prism-bash-D6zCJ74D.js +1 -0
- package/dist/client/assets/prism-clojure-_z-zoNcE.js +1 -0
- package/dist/client/assets/prism-csharp-XPn2bw0d.js +1 -0
- package/dist/client/assets/prism-dart-CDv16Fbu.js +1 -0
- package/dist/client/assets/prism-elixir-DhHhV5Rj.js +1 -0
- package/dist/client/assets/prism-haskell-5hq1Nh9c.js +1 -0
- package/dist/client/assets/prism-hcl-B6Z0Ouoo.js +1 -0
- package/dist/client/assets/prism-java-6wtusvIl.js +1 -0
- package/dist/client/assets/prism-markup-templating-KkSMBj6Y.js +1 -0
- package/dist/client/assets/prism-nix-C88dTX93.js +1 -0
- package/dist/client/assets/prism-perl-Lsbi34sH.js +1 -0
- package/dist/client/assets/prism-php-Co4fM40K.js +1 -0
- package/dist/client/assets/prism-protobuf-yzSHlhwy.js +1 -0
- package/dist/client/assets/prism-ruby-DFCASkuD.js +1 -0
- package/dist/client/assets/prism-scala-VkM3wCOL.js +1 -0
- package/dist/client/assets/prism-solidity-DTom4k5p.js +1 -0
- package/dist/client/assets/prism-sql-CYhVDMEE.js +1 -0
- package/dist/client/assets/prism-vim-BgpkjCK5.js +1 -0
- package/dist/client/assets/quadrantDiagram-W4KKPZXB-D9l66CHb.js +7 -0
- package/dist/client/assets/radar-GUYGQ44K-DAU5EPVd.js +1 -0
- package/dist/client/assets/requirementDiagram-4Y6WPE33-helpQlwb.js +84 -0
- package/dist/client/assets/rough.esm-CSKSodPl.js +1 -0
- package/dist/client/assets/sankeyDiagram-5OEKKPKP-C9sNuuvA.js +40 -0
- package/dist/client/assets/sequenceDiagram-3UESZ5HK-BP0bhCwQ.js +162 -0
- package/dist/client/assets/src-CDAPGf2l.js +1 -0
- package/dist/client/assets/stateDiagram-AJRCARHV-CsFizpla.js +1 -0
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Bvn8tanl.js +1 -0
- package/dist/client/assets/timeline-definition-PNZ67QCA-BnkmHs1F.js +120 -0
- package/dist/client/assets/treeView-BLDUP644-DD9HZO5W.js +1 -0
- package/dist/client/assets/treemap-LRROVOQU-BBKuQ1lo.js +1 -0
- package/dist/client/assets/vennDiagram-CIIHVFJN-BPFTHflF.js +34 -0
- package/dist/client/assets/wardley-L42UT6IY-y88gDkwb.js +1 -0
- package/dist/client/assets/wardleyDiagram-YWT4CUSO-BlcA7IVh.js +78 -0
- package/dist/client/assets/xychartDiagram-2RQKCTM6-DX5vUCRH.js +7 -0
- package/dist/client/favicon.png +0 -0
- package/dist/client/index.html +14 -0
- package/dist/client/logo.png +0 -0
- package/dist/client/site-data/blobs/080c0e6/cHVibGljL3NpdGUtZGF0YS9vZy1pbWFnZS5wbmc.png +0 -0
- package/dist/client/site-data/blobs/55f23a1/bGFuZGluZy9wdWJsaWMvZGlmaXQvbG9nby5wbmc.png +0 -0
- package/dist/client/site-data/blobs/66ff7c6/cHVibGljL2xvZ28ucG5n.png +0 -0
- package/dist/client/site-data/blobs/e6977fe/cHVibGljL2xvZ28ucG5n.png +0 -0
- package/dist/client/site-data/manifest.json +1 -0
- package/dist/client/site-data/og-image.png +0 -0
- package/dist/client/site-data/snapshots/55f23a1...080c0e6.json +1 -0
- package/dist/client/site-data/snapshots/66ff7c6...e6977fe.json +1 -0
- package/dist/client/site-data/snapshots/7d40fd4...a72112f-comments.json +1 -0
- package/dist/client/site-data/snapshots/7d40fd4...a72112f.json +1 -0
- package/dist/server/file-watcher.d.ts +33 -0
- package/dist/server/file-watcher.js +340 -0
- package/dist/server/generated-file-check.d.ts +17 -0
- package/dist/server/generated-file-check.js +280 -0
- package/dist/server/git-diff.d.ts +94 -0
- package/dist/server/git-diff.js +909 -0
- package/dist/server/grouping/cache.d.ts +3 -0
- package/dist/server/grouping/cache.js +22 -0
- package/dist/server/grouping/claudeCli.d.ts +10 -0
- package/dist/server/grouping/claudeCli.js +95 -0
- package/dist/server/grouping/digest.d.ts +15 -0
- package/dist/server/grouping/digest.js +38 -0
- package/dist/server/grouping/reconcile.d.ts +6 -0
- package/dist/server/grouping/reconcile.js +34 -0
- package/dist/server/grouping/types.d.ts +9 -0
- package/dist/server/grouping/types.js +1 -0
- package/dist/server/server.d.ts +37 -0
- package/dist/server/server.js +1283 -0
- package/dist/shared/grouping.d.ts +4 -0
- package/dist/shared/grouping.js +170 -0
- package/dist/types/diff.d.ts +263 -0
- package/dist/types/diff.js +1 -0
- package/dist/types/watch.d.ts +59 -0
- package/dist/types/watch.js +8 -0
- package/dist/utils/commentFormatting.d.ts +6 -0
- package/dist/utils/commentFormatting.js +90 -0
- package/dist/utils/commentImports.d.ts +11 -0
- package/dist/utils/commentImports.js +386 -0
- package/dist/utils/createId.d.ts +1 -0
- package/dist/utils/createId.js +5 -0
- package/dist/utils/diffMode.d.ts +3 -0
- package/dist/utils/diffMode.js +13 -0
- package/dist/utils/diffSelection.d.ts +13 -0
- package/dist/utils/diffSelection.js +39 -0
- package/dist/utils/editorOptions.d.ts +60 -0
- package/dist/utils/editorOptions.js +171 -0
- package/dist/utils/fileUtils.d.ts +12 -0
- package/dist/utils/fileUtils.js +21 -0
- package/dist/utils/hubConfig.d.ts +32 -0
- package/dist/utils/hubConfig.js +51 -0
- package/dist/utils/suggestionUtils.d.ts +19 -0
- package/dist/utils/suggestionUtils.js +31 -0
- package/dist/utils/turnLog.d.ts +47 -0
- package/dist/utils/turnLog.js +133 -0
- package/package.json +105 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
export const CUSTOM_EDITOR_ID = 'custom';
|
|
2
|
+
export const NONE_EDITOR_ID = 'none';
|
|
3
|
+
export const DEFAULT_EDITOR_ID = 'vscode';
|
|
4
|
+
export const EDITOR_OPTIONS = [
|
|
5
|
+
{
|
|
6
|
+
id: 'vscode',
|
|
7
|
+
label: 'VS Code',
|
|
8
|
+
command: 'code',
|
|
9
|
+
argsTemplate: '-g %file:%line',
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
id: 'cursor',
|
|
13
|
+
label: 'Cursor',
|
|
14
|
+
command: 'cursor',
|
|
15
|
+
argsTemplate: '-g %file:%line',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: 'zed',
|
|
19
|
+
label: 'Zed',
|
|
20
|
+
command: 'zed',
|
|
21
|
+
argsTemplate: '%file:%line',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'textmate',
|
|
25
|
+
label: 'TextMate',
|
|
26
|
+
command: 'mate',
|
|
27
|
+
argsTemplate: '-l %line %file',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 'bbedit',
|
|
31
|
+
label: 'BBEdit',
|
|
32
|
+
command: 'bbedit',
|
|
33
|
+
argsTemplate: '+%line %file',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'emacs',
|
|
37
|
+
label: 'Emacs',
|
|
38
|
+
command: 'emacsclient',
|
|
39
|
+
argsTemplate: '--no-wait +%line %file',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'macvim',
|
|
43
|
+
label: 'MacVim',
|
|
44
|
+
command: 'mvim',
|
|
45
|
+
argsTemplate: '--remote-silent +%line %file',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'sublime',
|
|
49
|
+
label: 'Sublime Text',
|
|
50
|
+
command: 'subl',
|
|
51
|
+
argsTemplate: '%file:%line',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'nova',
|
|
55
|
+
label: 'Nova',
|
|
56
|
+
command: 'nova',
|
|
57
|
+
argsTemplate: 'open %file -l %line',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'custom',
|
|
61
|
+
label: 'Custom…',
|
|
62
|
+
command: '',
|
|
63
|
+
argsTemplate: '',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'none',
|
|
67
|
+
label: 'Hide “Open in editor” button',
|
|
68
|
+
command: '',
|
|
69
|
+
argsTemplate: '',
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
export const DEFAULT_EDITOR_OPTION = EDITOR_OPTIONS.find((option) => option.id === DEFAULT_EDITOR_ID) ?? EDITOR_OPTIONS[0];
|
|
73
|
+
/**
|
|
74
|
+
* Case-insensitive lookup by id. Used mostly for the `DIFIT_EDITOR` / `EDITOR`
|
|
75
|
+
* environment-variable fallback on the server; the browser UI passes the full
|
|
76
|
+
* `{id, command, argsTemplate}` shape so it does not rely on this helper.
|
|
77
|
+
*/
|
|
78
|
+
export const resolveEditorOption = (input) => {
|
|
79
|
+
const normalized = (input ?? DEFAULT_EDITOR_ID).toLowerCase();
|
|
80
|
+
const matched = EDITOR_OPTIONS.find((option) => option.id === normalized);
|
|
81
|
+
return matched ?? DEFAULT_EDITOR_OPTION;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Tokenise a user-supplied arguments template string into individual CLI
|
|
85
|
+
* arguments. Supports single and double quoted segments so paths and flags
|
|
86
|
+
* containing spaces can be preserved.
|
|
87
|
+
*
|
|
88
|
+
* Inside double-quoted segments `\"` decodes to a literal `"` and `\\` to a
|
|
89
|
+
* literal `\`, mirroring common shell intuition. Other `\x` sequences pass
|
|
90
|
+
* through untouched so Lisp/regex fragments like `\s` or `\$` stay intact.
|
|
91
|
+
* Single-quoted segments are fully literal. The template is handed to
|
|
92
|
+
* `spawn` as argv directly – no shell is involved.
|
|
93
|
+
*/
|
|
94
|
+
export const parseEditorArgsTemplate = (template) => {
|
|
95
|
+
const input = template ?? '';
|
|
96
|
+
const tokens = [];
|
|
97
|
+
const n = input.length;
|
|
98
|
+
let i = 0;
|
|
99
|
+
while (i < n) {
|
|
100
|
+
while (i < n && /\s/.test(input[i] ?? ''))
|
|
101
|
+
i++;
|
|
102
|
+
if (i >= n)
|
|
103
|
+
break;
|
|
104
|
+
let token = '';
|
|
105
|
+
let inQuote = null;
|
|
106
|
+
while (i < n) {
|
|
107
|
+
const ch = input[i];
|
|
108
|
+
if (ch === undefined)
|
|
109
|
+
break;
|
|
110
|
+
if (inQuote === '"') {
|
|
111
|
+
if (ch === '\\' && i + 1 < n) {
|
|
112
|
+
const next = input[i + 1];
|
|
113
|
+
if (next === '"' || next === '\\') {
|
|
114
|
+
token += next;
|
|
115
|
+
i += 2;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (ch === inQuote) {
|
|
120
|
+
inQuote = null;
|
|
121
|
+
i++;
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
token += ch;
|
|
125
|
+
i++;
|
|
126
|
+
}
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (inQuote === "'") {
|
|
130
|
+
if (ch === inQuote) {
|
|
131
|
+
inQuote = null;
|
|
132
|
+
i++;
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
token += ch;
|
|
136
|
+
i++;
|
|
137
|
+
}
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (ch === '"' || ch === "'") {
|
|
141
|
+
inQuote = ch;
|
|
142
|
+
i++;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (/\s/.test(ch))
|
|
146
|
+
break;
|
|
147
|
+
token += ch;
|
|
148
|
+
i++;
|
|
149
|
+
}
|
|
150
|
+
tokens.push(token);
|
|
151
|
+
}
|
|
152
|
+
return tokens;
|
|
153
|
+
};
|
|
154
|
+
/**
|
|
155
|
+
* Build the final `{ command, args }` tuple to hand to `child_process.spawn`.
|
|
156
|
+
* Returns `null` when the command is empty so callers can surface a clear
|
|
157
|
+
* "not configured" error instead of spawning an empty process.
|
|
158
|
+
*
|
|
159
|
+
* `%file` and `%line` are substituted as-is inside each token, so composite
|
|
160
|
+
* tokens such as `"%file:%line"` become `"/abs/file.ts:42"`. When no line
|
|
161
|
+
* number is available the substitution falls back to `1`.
|
|
162
|
+
*/
|
|
163
|
+
export const buildEditorSpawnSpec = (input) => {
|
|
164
|
+
const command = input.command.trim();
|
|
165
|
+
if (!command)
|
|
166
|
+
return null;
|
|
167
|
+
const tokens = parseEditorArgsTemplate(input.argsTemplate);
|
|
168
|
+
const lineValue = String(input.lineNumber ?? 1);
|
|
169
|
+
const args = tokens.map((piece) => piece.replaceAll('%file', input.filePath).replaceAll('%line', lineValue));
|
|
170
|
+
return { command, args };
|
|
171
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get file extension from filename or filepath
|
|
3
|
+
* @param filename - The filename or filepath
|
|
4
|
+
* @returns The file extension in lowercase, or null if no extension
|
|
5
|
+
*/
|
|
6
|
+
export declare function getFileExtension(filename: string): string | null;
|
|
7
|
+
/**
|
|
8
|
+
* Get filename from filepath
|
|
9
|
+
* @param filepath - The filepath
|
|
10
|
+
* @returns The filename
|
|
11
|
+
*/
|
|
12
|
+
export declare function getFileName(filepath: string): string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get file extension from filename or filepath
|
|
3
|
+
* @param filename - The filename or filepath
|
|
4
|
+
* @returns The file extension in lowercase, or null if no extension
|
|
5
|
+
*/
|
|
6
|
+
export function getFileExtension(filename) {
|
|
7
|
+
if (!filename)
|
|
8
|
+
return null;
|
|
9
|
+
const extension = filename.split('.').pop()?.toLowerCase();
|
|
10
|
+
return extension || null;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Get filename from filepath
|
|
14
|
+
* @param filepath - The filepath
|
|
15
|
+
* @returns The filename
|
|
16
|
+
*/
|
|
17
|
+
export function getFileName(filepath) {
|
|
18
|
+
if (!filepath)
|
|
19
|
+
return '';
|
|
20
|
+
return filepath.split('/').pop() || '';
|
|
21
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hub configuration (diff-easy hub).
|
|
3
|
+
*
|
|
4
|
+
* Persists the user's workspace roots (scanned for repos), recently-opened
|
|
5
|
+
* folders/PRs, and per-repo "last seen turn" markers for activity badges.
|
|
6
|
+
* Stored at `$DIFIT_HOME/.difit/config.json` (DIFIT_HOME defaults to $HOME;
|
|
7
|
+
* the override exists so tests don't touch the real home dir).
|
|
8
|
+
*/
|
|
9
|
+
export interface RecentFolder {
|
|
10
|
+
repoId: string;
|
|
11
|
+
path: string;
|
|
12
|
+
name: string;
|
|
13
|
+
}
|
|
14
|
+
interface RecentPr {
|
|
15
|
+
url: string;
|
|
16
|
+
label: string;
|
|
17
|
+
repoId: string;
|
|
18
|
+
}
|
|
19
|
+
export interface HubConfig {
|
|
20
|
+
roots: string[];
|
|
21
|
+
recents: RecentFolder[];
|
|
22
|
+
recentPrs: RecentPr[];
|
|
23
|
+
/** repoId → timestamp of the newest turn the user has seen (for badges). */
|
|
24
|
+
lastSeen: Record<string, number>;
|
|
25
|
+
}
|
|
26
|
+
export declare function readHubConfig(): HubConfig;
|
|
27
|
+
export declare function writeHubConfig(config: HubConfig): void;
|
|
28
|
+
/** Add workspace roots (absolute, de-duplicated) to the config. */
|
|
29
|
+
export declare function addHubRoots(paths: string[]): HubConfig;
|
|
30
|
+
/** Record a recently-opened folder (most-recent first, capped). */
|
|
31
|
+
export declare function pushRecentFolder(config: HubConfig, folder: RecentFolder): void;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import { join, resolve } from 'path';
|
|
4
|
+
const DIR = '.difit';
|
|
5
|
+
const FILE = 'config.json';
|
|
6
|
+
const MAX_RECENTS = 20;
|
|
7
|
+
function baseDir() {
|
|
8
|
+
return process.env.DIFIT_HOME || homedir();
|
|
9
|
+
}
|
|
10
|
+
function configPath() {
|
|
11
|
+
return join(baseDir(), DIR, FILE);
|
|
12
|
+
}
|
|
13
|
+
function defaultConfig() {
|
|
14
|
+
return { roots: [], recents: [], recentPrs: [], lastSeen: {} };
|
|
15
|
+
}
|
|
16
|
+
export function readHubConfig() {
|
|
17
|
+
try {
|
|
18
|
+
const raw = readFileSync(configPath(), 'utf8');
|
|
19
|
+
const data = JSON.parse(raw);
|
|
20
|
+
return {
|
|
21
|
+
roots: Array.isArray(data.roots) ? data.roots : [],
|
|
22
|
+
recents: Array.isArray(data.recents) ? data.recents : [],
|
|
23
|
+
recentPrs: Array.isArray(data.recentPrs) ? data.recentPrs : [],
|
|
24
|
+
lastSeen: data.lastSeen && typeof data.lastSeen === 'object' ? data.lastSeen : {},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return defaultConfig();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function writeHubConfig(config) {
|
|
32
|
+
const dir = join(baseDir(), DIR);
|
|
33
|
+
if (!existsSync(dir))
|
|
34
|
+
mkdirSync(dir, { recursive: true });
|
|
35
|
+
writeFileSync(configPath(), `${JSON.stringify(config, null, 2)}\n`);
|
|
36
|
+
}
|
|
37
|
+
/** Add workspace roots (absolute, de-duplicated) to the config. */
|
|
38
|
+
export function addHubRoots(paths) {
|
|
39
|
+
const config = readHubConfig();
|
|
40
|
+
for (const p of paths) {
|
|
41
|
+
const abs = resolve(p);
|
|
42
|
+
if (!config.roots.includes(abs))
|
|
43
|
+
config.roots.push(abs);
|
|
44
|
+
}
|
|
45
|
+
writeHubConfig(config);
|
|
46
|
+
return config;
|
|
47
|
+
}
|
|
48
|
+
/** Record a recently-opened folder (most-recent first, capped). */
|
|
49
|
+
export function pushRecentFolder(config, folder) {
|
|
50
|
+
config.recents = [folder, ...config.recents.filter((r) => r.repoId !== folder.repoId)].slice(0, MAX_RECENTS);
|
|
51
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
interface SuggestionBlock {
|
|
2
|
+
suggestedCode: string;
|
|
3
|
+
startIndex: number;
|
|
4
|
+
endIndex: number;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Check if a comment body contains any suggestion blocks
|
|
8
|
+
*/
|
|
9
|
+
export declare function hasSuggestionBlock(body: string): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Parse all suggestion blocks from a comment body.
|
|
12
|
+
* Pure parser: only extracts suggestion blocks and their positions from the body text.
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseSuggestionBlocks(body: string): SuggestionBlock[];
|
|
15
|
+
/**
|
|
16
|
+
* Create a suggestion template with the given code
|
|
17
|
+
*/
|
|
18
|
+
export declare function createSuggestionTemplate(code: string): string;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if a comment body contains any suggestion blocks
|
|
3
|
+
*/
|
|
4
|
+
export function hasSuggestionBlock(body) {
|
|
5
|
+
return /```suggestion\n([\s\S]*?)```/.test(body);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Parse all suggestion blocks from a comment body.
|
|
9
|
+
* Pure parser: only extracts suggestion blocks and their positions from the body text.
|
|
10
|
+
*/
|
|
11
|
+
export function parseSuggestionBlocks(body) {
|
|
12
|
+
const blocks = [];
|
|
13
|
+
const regex = /```suggestion\n([\s\S]*?)```/g;
|
|
14
|
+
let match;
|
|
15
|
+
while ((match = regex.exec(body)) !== null) {
|
|
16
|
+
blocks.push({
|
|
17
|
+
suggestedCode: match[1].replace(/\n$/, ''), // Remove trailing newline
|
|
18
|
+
startIndex: match.index,
|
|
19
|
+
endIndex: match.index + match[0].length,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return blocks;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Create a suggestion template with the given code
|
|
26
|
+
*/
|
|
27
|
+
export function createSuggestionTemplate(code) {
|
|
28
|
+
// Ensure code ends with a newline for proper formatting
|
|
29
|
+
const normalizedCode = code.endsWith('\n') ? code : code + '\n';
|
|
30
|
+
return `\`\`\`suggestion\n${normalizedCode}\`\`\``;
|
|
31
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared turn-log model (diff-easy addition).
|
|
3
|
+
*
|
|
4
|
+
* A "turn" is one round of an AI-agent conversation. The capture hook
|
|
5
|
+
* (`diff-easy snapshot`) records a git snapshot at the start (UserPromptSubmit)
|
|
6
|
+
* and end (Stop) of each turn; the server reads this log to show the diff a
|
|
7
|
+
* given turn produced. Snapshots are anchored under a git ref so the objects
|
|
8
|
+
* survive garbage collection (see src/cli/turns.ts).
|
|
9
|
+
*/
|
|
10
|
+
export interface TurnRecord {
|
|
11
|
+
id: string;
|
|
12
|
+
sessionId: string;
|
|
13
|
+
/** Full user prompt that started the turn (UserPromptSubmit), if available. */
|
|
14
|
+
prompt: string | null;
|
|
15
|
+
/** Short single-line preview for the timeline UI. */
|
|
16
|
+
promptPreview: string | null;
|
|
17
|
+
/** Snapshot commit of the working tree at the start of the turn. */
|
|
18
|
+
commitBefore: string;
|
|
19
|
+
/** Snapshot commit at the end of the turn; null while the turn is open. */
|
|
20
|
+
commitAfter: string | null;
|
|
21
|
+
startedAt: number;
|
|
22
|
+
endedAt: number | null;
|
|
23
|
+
status: 'open' | 'closed';
|
|
24
|
+
}
|
|
25
|
+
export interface ServerInfo {
|
|
26
|
+
port: number;
|
|
27
|
+
pid: number;
|
|
28
|
+
startedAt: number;
|
|
29
|
+
}
|
|
30
|
+
export declare function readTurnLog(repoRoot: string): TurnRecord[];
|
|
31
|
+
export declare function writeTurnLog(repoRoot: string, turns: TurnRecord[]): void;
|
|
32
|
+
/**
|
|
33
|
+
* Count completed turns whose end time is newer than `since` (epoch ms). Open
|
|
34
|
+
* (in-progress) turns are excluded — they aren't yet a reviewable change. Used
|
|
35
|
+
* to compute the hub's per-repo "new turns" activity badge.
|
|
36
|
+
*/
|
|
37
|
+
export declare function countTurnsSince(turns: TurnRecord[], since: number): number;
|
|
38
|
+
/** Newest completed-turn end time (epoch ms), or 0 when there are none. */
|
|
39
|
+
export declare function latestTurnTimestamp(turns: TurnRecord[]): number;
|
|
40
|
+
export declare function readServerInfo(repoRoot: string): ServerInfo | null;
|
|
41
|
+
export declare function writeServerInfo(repoRoot: string, info: ServerInfo): void;
|
|
42
|
+
/** Remove the advertised server info (on shutdown, or when found to be stale). */
|
|
43
|
+
export declare function deleteServerInfo(repoRoot: string): void;
|
|
44
|
+
export declare function writeHubInfo(info: ServerInfo): void;
|
|
45
|
+
/** Read the global hub advertisement, or null if no hub is running. */
|
|
46
|
+
export declare function readHubInfo(): ServerInfo | null;
|
|
47
|
+
export declare function deleteHubInfo(): void;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
const TURN_DIR_NAME = '.difit-turns';
|
|
5
|
+
const TURN_LOG_NAME = 'log.json';
|
|
6
|
+
const SERVER_INFO_NAME = 'server.json';
|
|
7
|
+
function turnDir(repoRoot) {
|
|
8
|
+
return join(repoRoot, TURN_DIR_NAME);
|
|
9
|
+
}
|
|
10
|
+
function turnLogPath(repoRoot) {
|
|
11
|
+
return join(turnDir(repoRoot), TURN_LOG_NAME);
|
|
12
|
+
}
|
|
13
|
+
function serverInfoPath(repoRoot) {
|
|
14
|
+
return join(turnDir(repoRoot), SERVER_INFO_NAME);
|
|
15
|
+
}
|
|
16
|
+
function ensureTurnDir(repoRoot) {
|
|
17
|
+
const dir = turnDir(repoRoot);
|
|
18
|
+
if (!existsSync(dir)) {
|
|
19
|
+
mkdirSync(dir, { recursive: true });
|
|
20
|
+
// The whole directory is local-only state; keep it out of version control.
|
|
21
|
+
try {
|
|
22
|
+
writeFileSync(join(dir, '.gitignore'), '*\n');
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// best-effort
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function readTurnLog(repoRoot) {
|
|
30
|
+
try {
|
|
31
|
+
const raw = readFileSync(turnLogPath(repoRoot), 'utf8');
|
|
32
|
+
const data = JSON.parse(raw);
|
|
33
|
+
if (data && typeof data === 'object' && Array.isArray(data.turns)) {
|
|
34
|
+
return data.turns;
|
|
35
|
+
}
|
|
36
|
+
if (Array.isArray(data))
|
|
37
|
+
return data;
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function writeTurnLog(repoRoot, turns) {
|
|
45
|
+
ensureTurnDir(repoRoot);
|
|
46
|
+
const payload = { version: 1, turns };
|
|
47
|
+
writeFileSync(turnLogPath(repoRoot), `${JSON.stringify(payload, null, 2)}\n`);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Count completed turns whose end time is newer than `since` (epoch ms). Open
|
|
51
|
+
* (in-progress) turns are excluded — they aren't yet a reviewable change. Used
|
|
52
|
+
* to compute the hub's per-repo "new turns" activity badge.
|
|
53
|
+
*/
|
|
54
|
+
export function countTurnsSince(turns, since) {
|
|
55
|
+
return turns.filter((turn) => turn.endedAt != null && turn.endedAt > since).length;
|
|
56
|
+
}
|
|
57
|
+
/** Newest completed-turn end time (epoch ms), or 0 when there are none. */
|
|
58
|
+
export function latestTurnTimestamp(turns) {
|
|
59
|
+
let latest = 0;
|
|
60
|
+
for (const turn of turns) {
|
|
61
|
+
if (turn.endedAt != null && turn.endedAt > latest) {
|
|
62
|
+
latest = turn.endedAt;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return latest;
|
|
66
|
+
}
|
|
67
|
+
export function readServerInfo(repoRoot) {
|
|
68
|
+
try {
|
|
69
|
+
const raw = readFileSync(serverInfoPath(repoRoot), 'utf8');
|
|
70
|
+
const data = JSON.parse(raw);
|
|
71
|
+
if (data && typeof data === 'object' && typeof data.port === 'number') {
|
|
72
|
+
return data;
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export function writeServerInfo(repoRoot, info) {
|
|
81
|
+
ensureTurnDir(repoRoot);
|
|
82
|
+
writeFileSync(serverInfoPath(repoRoot), `${JSON.stringify(info, null, 2)}\n`);
|
|
83
|
+
}
|
|
84
|
+
/** Remove the advertised server info (on shutdown, or when found to be stale). */
|
|
85
|
+
export function deleteServerInfo(repoRoot) {
|
|
86
|
+
try {
|
|
87
|
+
rmSync(serverInfoPath(repoRoot), { force: true });
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// best-effort — a leftover file only costs a wasted liveness check
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// ----- Hub server discovery (diff-easy hub) -------------------------------
|
|
94
|
+
// The always-on hub serves many repos, so it advertises itself at a single
|
|
95
|
+
// global path (not per-repo) that the capture hook reads regardless of which
|
|
96
|
+
// repo the agent ran in.
|
|
97
|
+
const HUB_DIR_NAME = '.difit';
|
|
98
|
+
const HUB_INFO_NAME = 'hub.json';
|
|
99
|
+
/** Base dir for hub state. `DIFIT_HOME` overrides $HOME (used to isolate tests). */
|
|
100
|
+
function hubBaseDir() {
|
|
101
|
+
return process.env.DIFIT_HOME || homedir();
|
|
102
|
+
}
|
|
103
|
+
function hubInfoPath() {
|
|
104
|
+
return join(hubBaseDir(), HUB_DIR_NAME, HUB_INFO_NAME);
|
|
105
|
+
}
|
|
106
|
+
export function writeHubInfo(info) {
|
|
107
|
+
const dir = join(hubBaseDir(), HUB_DIR_NAME);
|
|
108
|
+
if (!existsSync(dir))
|
|
109
|
+
mkdirSync(dir, { recursive: true });
|
|
110
|
+
writeFileSync(hubInfoPath(), `${JSON.stringify(info, null, 2)}\n`);
|
|
111
|
+
}
|
|
112
|
+
/** Read the global hub advertisement, or null if no hub is running. */
|
|
113
|
+
export function readHubInfo() {
|
|
114
|
+
try {
|
|
115
|
+
const raw = readFileSync(hubInfoPath(), 'utf8');
|
|
116
|
+
const data = JSON.parse(raw);
|
|
117
|
+
if (data && typeof data === 'object' && typeof data.port === 'number') {
|
|
118
|
+
return data;
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
export function deleteHubInfo() {
|
|
127
|
+
try {
|
|
128
|
+
rmSync(hubInfoPath(), { force: true });
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// best-effort
|
|
132
|
+
}
|
|
133
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "diff-easy",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A local git diff viewer for AI-agent workflows: filter by file/type, search within diffs, and review the diff produced by each agent turn. Built on difit.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cli",
|
|
7
|
+
"code-review",
|
|
8
|
+
"diff",
|
|
9
|
+
"diff-viewer",
|
|
10
|
+
"git",
|
|
11
|
+
"github",
|
|
12
|
+
"react",
|
|
13
|
+
"review",
|
|
14
|
+
"tailwind"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": "Sai Mageshvar",
|
|
18
|
+
"bin": {
|
|
19
|
+
"diff-easy": "dist/cli/index.js"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"type": "module",
|
|
26
|
+
"main": "./dist/cli/index.js",
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"dev": "node scripts/dev.js",
|
|
32
|
+
"dev:site": "pnpm run build:cli && node scripts/export-site-data.js && vite --config vite.config.site.ts --open /",
|
|
33
|
+
"dev:cli": "tsc --project tsconfig.cli.json && NODE_ENV=development node dist/cli/index.js",
|
|
34
|
+
"clean": "node -e \"const fs=require('fs');['dist','tsconfig.cli.tsbuildinfo','tsconfig.tsbuildinfo'].forEach(f=>fs.rmSync(f,{recursive:true,force:true}))\"",
|
|
35
|
+
"build": "pnpm run clean && pnpm run build:cli && vite build && node scripts/prune-dist-tests.js",
|
|
36
|
+
"build:site": "pnpm run build:cli && GITHUB_PAGES=true vite build --config vite.config.site.ts",
|
|
37
|
+
"build:cli": "tsc --project tsconfig.cli.json",
|
|
38
|
+
"export:site-data": "node scripts/export-site-data.js",
|
|
39
|
+
"start": "pnpm run build && node dist/cli/index.js",
|
|
40
|
+
"check": "oxlint . --type-aware --type-check --deny-warnings --report-unused-disable-directives",
|
|
41
|
+
"check:fix": "oxlint . --type-aware --type-check --deny-warnings --report-unused-disable-directives --fix",
|
|
42
|
+
"format": "oxfmt --check .",
|
|
43
|
+
"format:fix": "oxfmt --write .",
|
|
44
|
+
"knip": "knip --use-tsconfig-files",
|
|
45
|
+
"test": "vitest run",
|
|
46
|
+
"test:watch": "vitest",
|
|
47
|
+
"prepare": "lefthook install",
|
|
48
|
+
"prepublishOnly": "NODE_ENV=production pnpm run build",
|
|
49
|
+
"perf": "node scripts/measure-performance.js",
|
|
50
|
+
"perf:small": "node scripts/measure-performance.js --size small",
|
|
51
|
+
"perf:medium": "node scripts/measure-performance.js --size medium",
|
|
52
|
+
"perf:large": "node scripts/measure-performance.js --size large",
|
|
53
|
+
"perf:xlarge": "node scripts/measure-performance.js --size xlarge",
|
|
54
|
+
"perf:compare": "node scripts/compare-performance.js"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"@floating-ui/react": "^0.27.16",
|
|
58
|
+
"@parcel/watcher": "^2.5.1",
|
|
59
|
+
"commander": "^14.0.0",
|
|
60
|
+
"diff": "^9.0.0",
|
|
61
|
+
"express": "^5.1.0",
|
|
62
|
+
"lucide-react": "^1.0.0",
|
|
63
|
+
"mermaid": "^11.13.0",
|
|
64
|
+
"open": "^11.0.0",
|
|
65
|
+
"prism-react-renderer": "^2.4.1",
|
|
66
|
+
"prismjs": "^1.30.0",
|
|
67
|
+
"react": "^19.1.0",
|
|
68
|
+
"react-dom": "^19.1.0",
|
|
69
|
+
"react-hotkeys-hook": "^5.2.4",
|
|
70
|
+
"react-markdown": "^10.1.0",
|
|
71
|
+
"remark-gfm": "^4.0.1",
|
|
72
|
+
"simple-git": "^3.28.0"
|
|
73
|
+
},
|
|
74
|
+
"devDependencies": {
|
|
75
|
+
"@tailwindcss/postcss": "^4.1.11",
|
|
76
|
+
"@testing-library/jest-dom": "^6.6.3",
|
|
77
|
+
"@testing-library/react": "^16.3.0",
|
|
78
|
+
"@testing-library/user-event": "^14.6.1",
|
|
79
|
+
"@tsconfig/strictest": "^2.0.5",
|
|
80
|
+
"@types/express": "^5.0.3",
|
|
81
|
+
"@types/node": "^24.0.8",
|
|
82
|
+
"@types/react": "^19.1.8",
|
|
83
|
+
"@types/react-dom": "^19.1.6",
|
|
84
|
+
"@vitejs/plugin-react": "^6.0.0",
|
|
85
|
+
"autoprefixer": "^10.4.21",
|
|
86
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
87
|
+
"happy-dom": "^20.0.0",
|
|
88
|
+
"knip": "^6.0.0",
|
|
89
|
+
"lefthook": "^2.0.0",
|
|
90
|
+
"oxfmt": "^0.53.0",
|
|
91
|
+
"oxlint": "^1.49.0",
|
|
92
|
+
"oxlint-tsgolint": "^0.23.0",
|
|
93
|
+
"playwright": "^1.54.1",
|
|
94
|
+
"postcss": "^8.5.6",
|
|
95
|
+
"tailwindcss": "^4.1.11",
|
|
96
|
+
"typescript": "^6.0.0",
|
|
97
|
+
"undici": "^8.0.0",
|
|
98
|
+
"vite": "^8.0.0",
|
|
99
|
+
"vitest": "^4.0.6"
|
|
100
|
+
},
|
|
101
|
+
"engines": {
|
|
102
|
+
"node": ">=21.0.0"
|
|
103
|
+
},
|
|
104
|
+
"packageManager": "pnpm@11.5.0"
|
|
105
|
+
}
|