nex-code 0.3.4 → 0.3.7
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 +34 -12
- package/dist/bundle.js +505 -0
- package/dist/nex-code.js +485 -0
- package/package.json +8 -6
- package/bin/nex-code.js +0 -99
- package/cli/agent.js +0 -835
- package/cli/compactor.js +0 -85
- package/cli/context-engine.js +0 -507
- package/cli/context.js +0 -98
- package/cli/costs.js +0 -290
- package/cli/diff.js +0 -366
- package/cli/file-history.js +0 -94
- package/cli/format.js +0 -211
- package/cli/fuzzy-match.js +0 -270
- package/cli/git.js +0 -211
- package/cli/hooks.js +0 -173
- package/cli/index.js +0 -1289
- package/cli/mcp.js +0 -284
- package/cli/memory.js +0 -170
- package/cli/ollama.js +0 -130
- package/cli/permissions.js +0 -124
- package/cli/picker.js +0 -201
- package/cli/planner.js +0 -282
- package/cli/providers/anthropic.js +0 -333
- package/cli/providers/base.js +0 -116
- package/cli/providers/gemini.js +0 -239
- package/cli/providers/local.js +0 -249
- package/cli/providers/ollama.js +0 -228
- package/cli/providers/openai.js +0 -237
- package/cli/providers/registry.js +0 -454
- package/cli/render.js +0 -495
- package/cli/safety.js +0 -241
- package/cli/session.js +0 -133
- package/cli/skills.js +0 -412
- package/cli/spinner.js +0 -371
- package/cli/sub-agent.js +0 -425
- package/cli/tasks.js +0 -179
- package/cli/tool-tiers.js +0 -164
- package/cli/tool-validator.js +0 -138
- package/cli/tools.js +0 -1050
- package/cli/ui.js +0 -93
package/cli/costs.js
DELETED
|
@@ -1,290 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* cli/costs.js — Token Cost Tracking + Dashboard + Budget Limits
|
|
3
|
-
* Tracks token usage per provider/model and estimates session costs.
|
|
4
|
-
* Supports per-provider cost limits with budget gating.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const fs = require('fs');
|
|
8
|
-
const path = require('path');
|
|
9
|
-
|
|
10
|
-
const PRICING = {
|
|
11
|
-
'openai': {
|
|
12
|
-
'gpt-4o': { input: 2.50, output: 10.00 },
|
|
13
|
-
'gpt-4o-mini': { input: 0.15, output: 0.60 },
|
|
14
|
-
'gpt-4.1': { input: 2.00, output: 8.00 },
|
|
15
|
-
'gpt-4.1-mini': { input: 0.40, output: 1.60 },
|
|
16
|
-
'gpt-4.1-nano': { input: 0.10, output: 0.40 },
|
|
17
|
-
'o1': { input: 15.00, output: 60.00 },
|
|
18
|
-
'o3': { input: 10.00, output: 40.00 },
|
|
19
|
-
'o3-mini': { input: 1.10, output: 4.40 },
|
|
20
|
-
'o4-mini': { input: 1.10, output: 4.40 },
|
|
21
|
-
},
|
|
22
|
-
'anthropic': {
|
|
23
|
-
'claude-sonnet': { input: 3.00, output: 15.00 },
|
|
24
|
-
'claude-opus': { input: 5.00, output: 25.00 },
|
|
25
|
-
'claude-haiku': { input: 0.80, output: 4.00 },
|
|
26
|
-
'claude-sonnet-4-5': { input: 3.00, output: 15.00 },
|
|
27
|
-
'claude-sonnet-4': { input: 3.00, output: 15.00 },
|
|
28
|
-
},
|
|
29
|
-
'gemini': {
|
|
30
|
-
'gemini-2.5-pro': { input: 1.25, output: 10.00 },
|
|
31
|
-
'gemini-2.5-flash': { input: 0.15, output: 0.60 },
|
|
32
|
-
'gemini-2.0-flash': { input: 0.10, output: 0.40 },
|
|
33
|
-
'gemini-2.0-flash-lite': { input: 0.075, output: 0.30 },
|
|
34
|
-
},
|
|
35
|
-
'ollama': {
|
|
36
|
-
'qwen3-coder:480b': { input: 0, output: 0 },
|
|
37
|
-
'qwen3-coder-next': { input: 0, output: 0 },
|
|
38
|
-
'devstral-2:123b': { input: 0, output: 0 },
|
|
39
|
-
'devstral-small-2:24b': { input: 0, output: 0 },
|
|
40
|
-
'kimi-k2.5': { input: 0, output: 0 },
|
|
41
|
-
'kimi-k2:1t': { input: 0, output: 0 },
|
|
42
|
-
'deepseek-v3.2': { input: 0, output: 0 },
|
|
43
|
-
'minimax-m2.5': { input: 0, output: 0 },
|
|
44
|
-
'glm-5': { input: 0, output: 0 },
|
|
45
|
-
'glm-4.7': { input: 0, output: 0 },
|
|
46
|
-
'gpt-oss:120b': { input: 0, output: 0 },
|
|
47
|
-
},
|
|
48
|
-
'local': {},
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
// Session usage accumulator
|
|
52
|
-
let usageLog = [];
|
|
53
|
-
|
|
54
|
-
// Per-provider cost limits (e.g. { anthropic: 5.00, openai: 10.00 })
|
|
55
|
-
let costLimits = {};
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Track token usage for a single API call.
|
|
59
|
-
* @param {string} provider
|
|
60
|
-
* @param {string} model
|
|
61
|
-
* @param {number} inputTokens
|
|
62
|
-
* @param {number} outputTokens
|
|
63
|
-
*/
|
|
64
|
-
function trackUsage(provider, model, inputTokens, outputTokens) {
|
|
65
|
-
usageLog.push({ provider, model, input: inputTokens, output: outputTokens });
|
|
66
|
-
|
|
67
|
-
// Check budget after tracking
|
|
68
|
-
if (costLimits[provider] !== undefined) {
|
|
69
|
-
const budget = checkBudget(provider);
|
|
70
|
-
if (!budget.allowed) {
|
|
71
|
-
process.stderr.write(`\x1b[33m\u26a0 Budget limit reached for ${provider}: $${budget.spent.toFixed(2)} / $${budget.limit.toFixed(2)}\x1b[0m\n`);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Get pricing for a provider/model pair.
|
|
78
|
-
* Returns { input: 0, output: 0 } for unknown models.
|
|
79
|
-
*/
|
|
80
|
-
function getPricing(provider, model) {
|
|
81
|
-
const providerPricing = PRICING[provider];
|
|
82
|
-
if (!providerPricing) return { input: 0, output: 0 };
|
|
83
|
-
return providerPricing[model] || { input: 0, output: 0 };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Calculate cost for a single usage entry.
|
|
88
|
-
*/
|
|
89
|
-
function calcCost(entry) {
|
|
90
|
-
const pricing = getPricing(entry.provider, entry.model);
|
|
91
|
-
return (entry.input * pricing.input + entry.output * pricing.output) / 1_000_000;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Get aggregated session costs.
|
|
96
|
-
* @returns {{ totalCost: number, totalInput: number, totalOutput: number, breakdown: Array }}
|
|
97
|
-
*/
|
|
98
|
-
function getSessionCosts() {
|
|
99
|
-
const byKey = {};
|
|
100
|
-
|
|
101
|
-
for (const entry of usageLog) {
|
|
102
|
-
const key = `${entry.provider}:${entry.model}`;
|
|
103
|
-
if (!byKey[key]) {
|
|
104
|
-
byKey[key] = { provider: entry.provider, model: entry.model, input: 0, output: 0 };
|
|
105
|
-
}
|
|
106
|
-
byKey[key].input += entry.input;
|
|
107
|
-
byKey[key].output += entry.output;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const breakdown = Object.values(byKey).map((b) => ({
|
|
111
|
-
...b,
|
|
112
|
-
cost: calcCost(b),
|
|
113
|
-
}));
|
|
114
|
-
|
|
115
|
-
const totalCost = breakdown.reduce((sum, b) => sum + b.cost, 0);
|
|
116
|
-
const totalInput = breakdown.reduce((sum, b) => sum + b.input, 0);
|
|
117
|
-
const totalOutput = breakdown.reduce((sum, b) => sum + b.output, 0);
|
|
118
|
-
|
|
119
|
-
return { totalCost, totalInput, totalOutput, breakdown };
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Format costs for display (/costs command).
|
|
124
|
-
* @returns {string}
|
|
125
|
-
*/
|
|
126
|
-
function formatCosts() {
|
|
127
|
-
const { totalCost, totalInput, totalOutput, breakdown } = getSessionCosts();
|
|
128
|
-
|
|
129
|
-
if (breakdown.length === 0) {
|
|
130
|
-
return 'No token usage recorded this session.';
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const lines = [];
|
|
134
|
-
lines.push('Session Token Usage:');
|
|
135
|
-
lines.push('');
|
|
136
|
-
|
|
137
|
-
for (const b of breakdown) {
|
|
138
|
-
const costStr = b.cost > 0 ? `$${b.cost.toFixed(4)}` : 'free';
|
|
139
|
-
lines.push(` ${b.provider}:${b.model}`);
|
|
140
|
-
lines.push(` Input: ${b.input.toLocaleString()} tokens`);
|
|
141
|
-
lines.push(` Output: ${b.output.toLocaleString()} tokens`);
|
|
142
|
-
lines.push(` Cost: ${costStr}`);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
lines.push('');
|
|
146
|
-
lines.push(` Total: ${totalInput.toLocaleString()} in + ${totalOutput.toLocaleString()} out = $${totalCost.toFixed(4)}`);
|
|
147
|
-
|
|
148
|
-
return lines.join('\n');
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Format a short cost hint for inline display after responses.
|
|
153
|
-
* @returns {string} e.g. "[~$0.003]" or "" if free
|
|
154
|
-
*/
|
|
155
|
-
function formatCostHint(provider, model, inputTokens, outputTokens) {
|
|
156
|
-
const pricing = getPricing(provider, model);
|
|
157
|
-
const cost = (inputTokens * pricing.input + outputTokens * pricing.output) / 1_000_000;
|
|
158
|
-
if (cost <= 0) return '';
|
|
159
|
-
return `[~$${cost.toFixed(4)}]`;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Reset all tracked usage (for testing or new sessions).
|
|
164
|
-
*/
|
|
165
|
-
function resetCosts() {
|
|
166
|
-
usageLog = [];
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// ─── Cost Limits ───────────────────────────────────────────────
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Set a cost limit for a provider (in dollars).
|
|
173
|
-
*/
|
|
174
|
-
function setCostLimit(provider, maxDollars) {
|
|
175
|
-
costLimits[provider] = maxDollars;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Remove a cost limit for a provider.
|
|
180
|
-
*/
|
|
181
|
-
function removeCostLimit(provider) {
|
|
182
|
-
delete costLimits[provider];
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Get all cost limits.
|
|
187
|
-
* @returns {object}
|
|
188
|
-
*/
|
|
189
|
-
function getCostLimits() {
|
|
190
|
-
return { ...costLimits };
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Get total spend for a specific provider in this session.
|
|
195
|
-
*/
|
|
196
|
-
function getProviderSpend(provider) {
|
|
197
|
-
let total = 0;
|
|
198
|
-
for (const entry of usageLog) {
|
|
199
|
-
if (entry.provider === provider) {
|
|
200
|
-
total += calcCost(entry);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
return total;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Check if a provider is within budget.
|
|
208
|
-
* @returns {{ allowed: boolean, spent: number, limit: number|null, remaining: number|null }}
|
|
209
|
-
*/
|
|
210
|
-
function checkBudget(provider) {
|
|
211
|
-
const spent = getProviderSpend(provider);
|
|
212
|
-
const limit = costLimits[provider];
|
|
213
|
-
|
|
214
|
-
if (limit === undefined) {
|
|
215
|
-
return { allowed: true, spent, limit: null, remaining: null };
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const remaining = Math.max(0, limit - spent);
|
|
219
|
-
return {
|
|
220
|
-
allowed: spent < limit,
|
|
221
|
-
spent,
|
|
222
|
-
limit,
|
|
223
|
-
remaining,
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Load cost limits from .nex/config.json
|
|
229
|
-
*/
|
|
230
|
-
function loadCostLimits() {
|
|
231
|
-
const configPath = path.join(process.cwd(), '.nex', 'config.json');
|
|
232
|
-
if (!fs.existsSync(configPath)) return;
|
|
233
|
-
try {
|
|
234
|
-
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
235
|
-
if (config.costLimits && typeof config.costLimits === 'object') {
|
|
236
|
-
costLimits = { ...config.costLimits };
|
|
237
|
-
}
|
|
238
|
-
} catch {
|
|
239
|
-
// ignore corrupt config
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Save cost limits to .nex/config.json
|
|
245
|
-
*/
|
|
246
|
-
function saveCostLimits() {
|
|
247
|
-
const configDir = path.join(process.cwd(), '.nex');
|
|
248
|
-
const configPath = path.join(configDir, 'config.json');
|
|
249
|
-
|
|
250
|
-
let config = {};
|
|
251
|
-
if (fs.existsSync(configPath)) {
|
|
252
|
-
try {
|
|
253
|
-
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
254
|
-
} catch {
|
|
255
|
-
config = {};
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
config.costLimits = costLimits;
|
|
260
|
-
if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
|
|
261
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* Reset cost limits (for testing).
|
|
266
|
-
*/
|
|
267
|
-
function resetCostLimits() {
|
|
268
|
-
costLimits = {};
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// Load cost limits on init
|
|
272
|
-
loadCostLimits();
|
|
273
|
-
|
|
274
|
-
module.exports = {
|
|
275
|
-
PRICING,
|
|
276
|
-
trackUsage,
|
|
277
|
-
getSessionCosts,
|
|
278
|
-
formatCosts,
|
|
279
|
-
formatCostHint,
|
|
280
|
-
resetCosts,
|
|
281
|
-
getPricing,
|
|
282
|
-
setCostLimit,
|
|
283
|
-
removeCostLimit,
|
|
284
|
-
getCostLimits,
|
|
285
|
-
getProviderSpend,
|
|
286
|
-
checkBudget,
|
|
287
|
-
loadCostLimits,
|
|
288
|
-
saveCostLimits,
|
|
289
|
-
resetCostLimits,
|
|
290
|
-
};
|
package/cli/diff.js
DELETED
|
@@ -1,366 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* cli/diff.js — Diff Display + Confirmation
|
|
3
|
-
* Minimal Myers-diff-lite for line-by-line comparison.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const path = require('path');
|
|
7
|
-
const { C } = require('./ui');
|
|
8
|
-
const { confirm, getAutoConfirm } = require('./safety');
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Simple LCS-based line diff (no external deps)
|
|
12
|
-
* Returns array of { type: 'same'|'add'|'remove', line: string }
|
|
13
|
-
*/
|
|
14
|
-
const DIFF_LINE_LIMIT = 2000;
|
|
15
|
-
|
|
16
|
-
function diffLines(oldText, newText) {
|
|
17
|
-
const oldLines = oldText.split('\n');
|
|
18
|
-
const newLines = newText.split('\n');
|
|
19
|
-
const result = [];
|
|
20
|
-
|
|
21
|
-
// Guard against OOM on huge files — fall back to simple summary
|
|
22
|
-
const m = oldLines.length;
|
|
23
|
-
const n = newLines.length;
|
|
24
|
-
if (m > DIFF_LINE_LIMIT || n > DIFF_LINE_LIMIT) {
|
|
25
|
-
// Produce a simplified diff: all old lines removed, all new lines added
|
|
26
|
-
for (const line of oldLines) result.push({ type: 'remove', line });
|
|
27
|
-
for (const line of newLines) result.push({ type: 'add', line });
|
|
28
|
-
return result;
|
|
29
|
-
}
|
|
30
|
-
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
31
|
-
|
|
32
|
-
for (let i = 1; i <= m; i++) {
|
|
33
|
-
for (let j = 1; j <= n; j++) {
|
|
34
|
-
if (oldLines[i - 1] === newLines[j - 1]) {
|
|
35
|
-
dp[i][j] = dp[i - 1][j - 1] + 1;
|
|
36
|
-
} else {
|
|
37
|
-
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Backtrack
|
|
43
|
-
let i = m,
|
|
44
|
-
j = n;
|
|
45
|
-
const ops = [];
|
|
46
|
-
while (i > 0 || j > 0) {
|
|
47
|
-
if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
|
|
48
|
-
ops.unshift({ type: 'same', line: oldLines[i - 1] });
|
|
49
|
-
i--;
|
|
50
|
-
j--;
|
|
51
|
-
} else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
|
|
52
|
-
ops.unshift({ type: 'add', line: newLines[j - 1] });
|
|
53
|
-
j--;
|
|
54
|
-
} else {
|
|
55
|
-
ops.unshift({ type: 'remove', line: oldLines[i - 1] });
|
|
56
|
-
i--;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return ops;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Show colored diff for edit_file (old_text → new_text) with context lines
|
|
65
|
-
*/
|
|
66
|
-
function showEditDiff(filePath, oldText, newText, contextLines = 3) {
|
|
67
|
-
console.log(`\n${C.bold}${C.cyan} Diff: ${filePath}${C.reset}`);
|
|
68
|
-
const ops = diffLines(oldText, newText);
|
|
69
|
-
|
|
70
|
-
// Find changed regions and show with context
|
|
71
|
-
const changed = [];
|
|
72
|
-
ops.forEach((op, idx) => {
|
|
73
|
-
if (op.type !== 'same') changed.push(idx);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
if (changed.length === 0) {
|
|
77
|
-
console.log(`${C.gray} (no changes)${C.reset}`);
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const showFrom = Math.max(0, changed[0] - contextLines);
|
|
82
|
-
const showTo = Math.min(ops.length, changed[changed.length - 1] + contextLines + 1);
|
|
83
|
-
|
|
84
|
-
if (showFrom > 0) console.log(`${C.gray} ...${C.reset}`);
|
|
85
|
-
|
|
86
|
-
for (let k = showFrom; k < showTo; k++) {
|
|
87
|
-
const op = ops[k];
|
|
88
|
-
switch (op.type) {
|
|
89
|
-
case 'remove':
|
|
90
|
-
console.log(`${C.red} - ${op.line}${C.reset}`);
|
|
91
|
-
break;
|
|
92
|
-
case 'add':
|
|
93
|
-
console.log(`${C.green} + ${op.line}${C.reset}`);
|
|
94
|
-
break;
|
|
95
|
-
default:
|
|
96
|
-
console.log(`${C.gray} ${op.line}${C.reset}`);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (showTo < ops.length) console.log(`${C.gray} ...${C.reset}`);
|
|
101
|
-
console.log();
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Show diff for write_file when file already exists
|
|
106
|
-
*/
|
|
107
|
-
function showWriteDiff(filePath, oldContent, newContent) {
|
|
108
|
-
console.log(`\n${C.bold}${C.cyan} File exists — showing changes: ${filePath}${C.reset}`);
|
|
109
|
-
const ops = diffLines(oldContent, newContent);
|
|
110
|
-
|
|
111
|
-
let changes = 0;
|
|
112
|
-
for (const op of ops) {
|
|
113
|
-
if (op.type !== 'same') changes++;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (changes === 0) {
|
|
117
|
-
console.log(`${C.gray} (identical content)${C.reset}`);
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Show up to 30 diff lines
|
|
122
|
-
let shown = 0;
|
|
123
|
-
for (const op of ops) {
|
|
124
|
-
if (shown >= 30) {
|
|
125
|
-
console.log(`${C.gray} ...(${changes - shown} more changes)${C.reset}`);
|
|
126
|
-
break;
|
|
127
|
-
}
|
|
128
|
-
switch (op.type) {
|
|
129
|
-
case 'remove':
|
|
130
|
-
console.log(`${C.red} - ${op.line}${C.reset}`);
|
|
131
|
-
shown++;
|
|
132
|
-
break;
|
|
133
|
-
case 'add':
|
|
134
|
-
console.log(`${C.green} + ${op.line}${C.reset}`);
|
|
135
|
-
shown++;
|
|
136
|
-
break;
|
|
137
|
-
default:
|
|
138
|
-
if (shown > 0) {
|
|
139
|
-
// Only show context around changes
|
|
140
|
-
console.log(`${C.gray} ${op.line}${C.reset}`);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
console.log();
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Show preview for new file creation
|
|
149
|
-
*/
|
|
150
|
-
function showNewFilePreview(filePath, content) {
|
|
151
|
-
console.log(`\n${C.bold}${C.cyan} New file: ${filePath}${C.reset}`);
|
|
152
|
-
const lines = content.split('\n');
|
|
153
|
-
const preview = lines.slice(0, 20);
|
|
154
|
-
for (const line of preview) {
|
|
155
|
-
console.log(`${C.green} + ${line}${C.reset}`);
|
|
156
|
-
}
|
|
157
|
-
if (lines.length > 20) {
|
|
158
|
-
console.log(`${C.gray} ...+${lines.length - 20} more lines${C.reset}`);
|
|
159
|
-
}
|
|
160
|
-
console.log();
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Confirm file change (edit or write)
|
|
165
|
-
*/
|
|
166
|
-
async function confirmFileChange(action) {
|
|
167
|
-
if (getAutoConfirm()) return true;
|
|
168
|
-
return confirm(` ${action}?`);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Show side-by-side diff view for two texts
|
|
173
|
-
* @param {string} filePath
|
|
174
|
-
* @param {string} oldText
|
|
175
|
-
* @param {string} newText
|
|
176
|
-
* @param {number} width - total terminal width (default: 80)
|
|
177
|
-
*/
|
|
178
|
-
function showSideBySideDiff(filePath, oldText, newText, width) {
|
|
179
|
-
const termWidth = width || process.stdout.columns || 80;
|
|
180
|
-
const colWidth = Math.floor((termWidth - 3) / 2); // 3 for separator "│"
|
|
181
|
-
|
|
182
|
-
console.log(`\n${C.bold}${C.cyan} Side-by-side: ${filePath}${C.reset}`);
|
|
183
|
-
console.log(` ${C.dim}${'─'.repeat(colWidth)}┬${'─'.repeat(colWidth)}${C.reset}`);
|
|
184
|
-
|
|
185
|
-
const ops = diffLines(oldText, newText);
|
|
186
|
-
|
|
187
|
-
// Pair up removals and additions
|
|
188
|
-
const pairs = [];
|
|
189
|
-
let i = 0;
|
|
190
|
-
while (i < ops.length) {
|
|
191
|
-
if (ops[i].type === 'same') {
|
|
192
|
-
pairs.push({ left: ops[i].line, right: ops[i].line, type: 'same' });
|
|
193
|
-
i++;
|
|
194
|
-
} else if (ops[i].type === 'remove') {
|
|
195
|
-
// Collect consecutive removes, then matching adds
|
|
196
|
-
const removes = [];
|
|
197
|
-
while (i < ops.length && ops[i].type === 'remove') {
|
|
198
|
-
removes.push(ops[i].line);
|
|
199
|
-
i++;
|
|
200
|
-
}
|
|
201
|
-
const adds = [];
|
|
202
|
-
while (i < ops.length && ops[i].type === 'add') {
|
|
203
|
-
adds.push(ops[i].line);
|
|
204
|
-
i++;
|
|
205
|
-
}
|
|
206
|
-
const maxLen = Math.max(removes.length, adds.length);
|
|
207
|
-
for (let j = 0; j < maxLen; j++) {
|
|
208
|
-
pairs.push({
|
|
209
|
-
left: j < removes.length ? removes[j] : '',
|
|
210
|
-
right: j < adds.length ? adds[j] : '',
|
|
211
|
-
type: 'changed',
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
} else if (ops[i].type === 'add') {
|
|
215
|
-
pairs.push({ left: '', right: ops[i].line, type: 'changed' });
|
|
216
|
-
i++;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Show max 40 pairs around changes
|
|
221
|
-
const changedIdxs = pairs.map((p, idx) => p.type !== 'same' ? idx : -1).filter(x => x >= 0);
|
|
222
|
-
if (changedIdxs.length === 0) {
|
|
223
|
-
console.log(` ${C.gray}(no changes)${C.reset}`);
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const showFrom = Math.max(0, changedIdxs[0] - 2);
|
|
228
|
-
const showTo = Math.min(pairs.length, changedIdxs[changedIdxs.length - 1] + 3);
|
|
229
|
-
|
|
230
|
-
const pad = (s, w) => {
|
|
231
|
-
const visible = s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
232
|
-
return visible.length >= w ? s.substring(0, w) : s + ' '.repeat(w - visible.length);
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
if (showFrom > 0) console.log(` ${C.dim}${'·'.repeat(colWidth)}┊${'·'.repeat(colWidth)}${C.reset}`);
|
|
236
|
-
|
|
237
|
-
for (let k = showFrom; k < showTo; k++) {
|
|
238
|
-
const p = pairs[k];
|
|
239
|
-
if (p.type === 'same') {
|
|
240
|
-
console.log(` ${C.gray}${pad(p.left, colWidth)}${C.reset}│${C.gray}${pad(p.right, colWidth)}${C.reset}`);
|
|
241
|
-
} else {
|
|
242
|
-
const leftCol = p.left ? `${C.red}${pad(p.left, colWidth)}${C.reset}` : `${pad('', colWidth)}`;
|
|
243
|
-
const rightCol = p.right ? `${C.green}${pad(p.right, colWidth)}${C.reset}` : `${pad('', colWidth)}`;
|
|
244
|
-
console.log(` ${leftCol}│${rightCol}`);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
if (showTo < pairs.length) console.log(` ${C.dim}${'·'.repeat(colWidth)}┊${'·'.repeat(colWidth)}${C.reset}`);
|
|
249
|
-
console.log(` ${C.dim}${'─'.repeat(colWidth)}┴${'─'.repeat(colWidth)}${C.reset}\n`);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Claude Code-style diff display
|
|
254
|
-
* Header: ⏺ Update(relPath) or ⏺ Create(relPath)
|
|
255
|
-
* Summary: ⎿ Added N lines, removed M lines
|
|
256
|
-
* Numbered context/change lines with ··· hunk separators
|
|
257
|
-
*/
|
|
258
|
-
function showClaudeDiff(filePath, oldContent, newContent, options = {}) {
|
|
259
|
-
const label = options.label || 'Update';
|
|
260
|
-
const contextSize = options.context || 3;
|
|
261
|
-
const relPath = path.isAbsolute(filePath) ? path.relative(process.cwd(), filePath) : filePath;
|
|
262
|
-
|
|
263
|
-
const ops = diffLines(oldContent, newContent);
|
|
264
|
-
|
|
265
|
-
// Assign line numbers to each op
|
|
266
|
-
let oldLine = 1, newLine = 1;
|
|
267
|
-
for (const op of ops) {
|
|
268
|
-
if (op.type === 'same') {
|
|
269
|
-
op.oldLine = oldLine++;
|
|
270
|
-
op.newLine = newLine++;
|
|
271
|
-
} else if (op.type === 'remove') {
|
|
272
|
-
op.oldLine = oldLine++;
|
|
273
|
-
op.newLine = null;
|
|
274
|
-
} else {
|
|
275
|
-
op.oldLine = null;
|
|
276
|
-
op.newLine = newLine++;
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Count additions and removals
|
|
281
|
-
let added = 0, removed = 0;
|
|
282
|
-
for (const op of ops) {
|
|
283
|
-
if (op.type === 'add') added++;
|
|
284
|
-
else if (op.type === 'remove') removed++;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Header
|
|
288
|
-
console.log(`\n${C.cyan}⏺${C.reset} ${C.bold}${label}(${relPath})${C.reset}`);
|
|
289
|
-
|
|
290
|
-
if (added === 0 && removed === 0) {
|
|
291
|
-
console.log(` ${C.dim}⎿ (no changes)${C.reset}\n`);
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Summary
|
|
296
|
-
const parts = [];
|
|
297
|
-
if (added > 0) parts.push(`Added ${added} line${added !== 1 ? 's' : ''}`);
|
|
298
|
-
if (removed > 0) parts.push(`removed ${removed} line${removed !== 1 ? 's' : ''}`);
|
|
299
|
-
console.log(` ${C.dim}⎿ ${parts.join(', ')}${C.reset}`);
|
|
300
|
-
|
|
301
|
-
// Build hunks: groups of changes with surrounding context
|
|
302
|
-
const changeIndices = [];
|
|
303
|
-
ops.forEach((op, i) => { if (op.type !== 'same') changeIndices.push(i); });
|
|
304
|
-
|
|
305
|
-
const hunks = [];
|
|
306
|
-
let hunkStart = null, hunkEnd = null;
|
|
307
|
-
for (const ci of changeIndices) {
|
|
308
|
-
const start = Math.max(0, ci - contextSize);
|
|
309
|
-
const end = Math.min(ops.length - 1, ci + contextSize);
|
|
310
|
-
if (hunkStart === null) {
|
|
311
|
-
hunkStart = start;
|
|
312
|
-
hunkEnd = end;
|
|
313
|
-
} else if (start <= hunkEnd + 1) {
|
|
314
|
-
hunkEnd = end;
|
|
315
|
-
} else {
|
|
316
|
-
hunks.push([hunkStart, hunkEnd]);
|
|
317
|
-
hunkStart = start;
|
|
318
|
-
hunkEnd = end;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
if (hunkStart !== null) hunks.push([hunkStart, hunkEnd]);
|
|
322
|
-
|
|
323
|
-
const PAD = ' '; // 6-char left padding
|
|
324
|
-
|
|
325
|
-
for (let h = 0; h < hunks.length; h++) {
|
|
326
|
-
if (h > 0) console.log(`${PAD}${C.dim}···${C.reset}`);
|
|
327
|
-
const [from, to] = hunks[h];
|
|
328
|
-
for (let i = from; i <= to; i++) {
|
|
329
|
-
const op = ops[i];
|
|
330
|
-
const lineNum = op.newLine != null ? op.newLine : op.oldLine;
|
|
331
|
-
const numStr = String(lineNum).padStart(4);
|
|
332
|
-
if (op.type === 'remove') {
|
|
333
|
-
console.log(`${PAD}${C.red}${numStr} -${op.line}${C.reset}`);
|
|
334
|
-
} else if (op.type === 'add') {
|
|
335
|
-
console.log(`${PAD}${C.green}${numStr} +${op.line}${C.reset}`);
|
|
336
|
-
} else {
|
|
337
|
-
console.log(`${PAD}${C.dim}${numStr}${C.reset} ${op.line}`);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
console.log();
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* Claude Code-style new file display
|
|
346
|
-
*/
|
|
347
|
-
function showClaudeNewFile(filePath, content) {
|
|
348
|
-
const relPath = path.isAbsolute(filePath) ? path.relative(process.cwd(), filePath) : filePath;
|
|
349
|
-
const lines = content.split('\n');
|
|
350
|
-
|
|
351
|
-
console.log(`\n${C.cyan}⏺${C.reset} ${C.bold}Create(${relPath})${C.reset}`);
|
|
352
|
-
console.log(` ${C.dim}⎿ ${lines.length} line${lines.length !== 1 ? 's' : ''}${C.reset}`);
|
|
353
|
-
|
|
354
|
-
const PAD = ' ';
|
|
355
|
-
const show = Math.min(lines.length, 20);
|
|
356
|
-
for (let i = 0; i < show; i++) {
|
|
357
|
-
const numStr = String(i + 1).padStart(4);
|
|
358
|
-
console.log(`${PAD}${C.green}${numStr} +${lines[i]}${C.reset}`);
|
|
359
|
-
}
|
|
360
|
-
if (lines.length > 20) {
|
|
361
|
-
console.log(`${PAD}${C.dim} ...+${lines.length - 20} more lines${C.reset}`);
|
|
362
|
-
}
|
|
363
|
-
console.log();
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
module.exports = { diffLines, showEditDiff, showWriteDiff, showNewFilePreview, confirmFileChange, showSideBySideDiff, showClaudeDiff, showClaudeNewFile };
|