termux-dev 1.4.19 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/assets/banner.svg +1 -1
- package/assets/preview.png +0 -0
- package/bin/devx.js +0 -0
- package/dist/cli/banner.js +41 -0
- package/dist/cli/diff.js +64 -0
- package/dist/cli/doctor.js +2 -1
- package/dist/cli/headless.js +8 -5
- package/dist/cli/history_search.js +111 -0
- package/dist/cli/index.js +86 -106
- package/dist/cli/modeborder.js +31 -0
- package/dist/cli/prompt.js +66 -37
- package/dist/cli/prompt_palette.js +37 -0
- package/dist/cli/server.js +2 -1
- package/dist/cli/statusline.js +107 -0
- package/dist/cli/telemetry.js +105 -0
- package/dist/core/types.js +1 -1
- package/dist/mcp/client.js +2 -1
- package/dist/permissions/guard.js +9 -4
- package/dist/providers/openai.js +5 -4
- package/dist/tools/fs.js +41 -7
- package/dist/tools/packages.js +35 -11
- package/dist/tools/search.js +24 -5
- package/package.json +5 -2
- package/dist/cli/index (# Edit conflict 2026-08-29 pe1jzcC #).js +0 -1966
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import { getCurrentTheme } from './theme.js';
|
|
4
|
+
export class GitStatusCache {
|
|
5
|
+
static cachedInfo = undefined;
|
|
6
|
+
static lastCheckTime = 0;
|
|
7
|
+
static TTL_MS = 10000; // 10s fallback TTL
|
|
8
|
+
static getStatus(forceRefresh = false) {
|
|
9
|
+
const now = Date.now();
|
|
10
|
+
if (!forceRefresh && GitStatusCache.cachedInfo !== undefined && (now - GitStatusCache.lastCheckTime < GitStatusCache.TTL_MS)) {
|
|
11
|
+
return GitStatusCache.cachedInfo;
|
|
12
|
+
}
|
|
13
|
+
GitStatusCache.lastCheckTime = now;
|
|
14
|
+
try {
|
|
15
|
+
const branch = execSync('git branch --show-current', {
|
|
16
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
17
|
+
encoding: 'utf8',
|
|
18
|
+
timeout: 1000
|
|
19
|
+
}).trim();
|
|
20
|
+
if (!branch) {
|
|
21
|
+
GitStatusCache.cachedInfo = null;
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const status = execSync('git status --porcelain', {
|
|
25
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
26
|
+
encoding: 'utf8',
|
|
27
|
+
timeout: 1000
|
|
28
|
+
}).trim();
|
|
29
|
+
const isDirty = status.length > 0;
|
|
30
|
+
GitStatusCache.cachedInfo = { branch, isDirty };
|
|
31
|
+
return GitStatusCache.cachedInfo;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
GitStatusCache.cachedInfo = null;
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
static invalidate() {
|
|
39
|
+
GitStatusCache.cachedInfo = undefined;
|
|
40
|
+
GitStatusCache.lastCheckTime = 0;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function renderProgressBar(percent, barWidth = 10) {
|
|
44
|
+
const clamped = Math.max(0, Math.min(100, percent));
|
|
45
|
+
const filledCount = Math.round((clamped / 100) * barWidth);
|
|
46
|
+
const emptyCount = Math.max(0, barWidth - filledCount);
|
|
47
|
+
const bar = '█'.repeat(filledCount) + '░'.repeat(emptyCount);
|
|
48
|
+
if (clamped >= 90)
|
|
49
|
+
return pc.red(`[${bar}] ${clamped}%`);
|
|
50
|
+
if (clamped >= 75)
|
|
51
|
+
return pc.yellow(`[${bar}] ${clamped}%`);
|
|
52
|
+
return pc.cyan(`[${bar}] ${clamped}%`);
|
|
53
|
+
}
|
|
54
|
+
export function formatCostBadge(cost, maxCostUSD) {
|
|
55
|
+
const costFormatted = `$${cost.toFixed(4)}`;
|
|
56
|
+
if (!maxCostUSD || maxCostUSD <= 0) {
|
|
57
|
+
return pc.green(costFormatted);
|
|
58
|
+
}
|
|
59
|
+
const ratio = cost / maxCostUSD;
|
|
60
|
+
if (ratio >= 1.0) {
|
|
61
|
+
return pc.red(costFormatted);
|
|
62
|
+
}
|
|
63
|
+
if (ratio >= 0.8) {
|
|
64
|
+
return pc.yellow(costFormatted);
|
|
65
|
+
}
|
|
66
|
+
return pc.green(costFormatted);
|
|
67
|
+
}
|
|
68
|
+
export function renderPowerlineStatus(opts) {
|
|
69
|
+
const cols = opts.cols || process.stdout.columns || 80;
|
|
70
|
+
const theme = getCurrentTheme();
|
|
71
|
+
// Mode badge
|
|
72
|
+
let modeBadge = '';
|
|
73
|
+
if (opts.isYolo) {
|
|
74
|
+
modeBadge = pc.bgRed(pc.bold(pc.white(' YOLO ')));
|
|
75
|
+
}
|
|
76
|
+
else if (opts.mode === 'PLAN') {
|
|
77
|
+
modeBadge = pc.bgCyan(pc.bold(pc.black(' PLAN ')));
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
modeBadge = pc.bgGreen(pc.bold(pc.black(' AGENT ')));
|
|
81
|
+
}
|
|
82
|
+
// Model shortening for narrow screens
|
|
83
|
+
let displayModel = opts.model;
|
|
84
|
+
if (cols < 75 && displayModel.length > 18) {
|
|
85
|
+
const parts = displayModel.split('/');
|
|
86
|
+
displayModel = parts.length > 1 ? parts.slice(1).join('/') : displayModel;
|
|
87
|
+
if (displayModel.length > 18) {
|
|
88
|
+
displayModel = displayModel.slice(0, 15) + '…';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Git info
|
|
92
|
+
const git = GitStatusCache.getStatus();
|
|
93
|
+
const gitStr = git ? ` · ${pc.magenta(git.branch)}${git.isDirty ? pc.yellow('✱') : ''}` : '';
|
|
94
|
+
// Context progress
|
|
95
|
+
const usagePercent = Math.min(100, Math.round((opts.currentTokens / Math.max(1, opts.maxTokens)) * 100));
|
|
96
|
+
const barWidth = cols < 60 ? 6 : 10;
|
|
97
|
+
const progressStr = renderProgressBar(usagePercent, barWidth);
|
|
98
|
+
const costStr = formatCostBadge(opts.cost, opts.maxCostUSD);
|
|
99
|
+
if (cols < 65) {
|
|
100
|
+
// 2-line layout for mobile Termux
|
|
101
|
+
const line1 = `${modeBadge} ${pc.bold(displayModel)}${gitStr}`;
|
|
102
|
+
const line2 = `${pc.dim('│')} Context ${progressStr} · ${costStr}`;
|
|
103
|
+
return `${line1}\n${line2}`;
|
|
104
|
+
}
|
|
105
|
+
// 1-line powerline layout for desktop
|
|
106
|
+
return `${modeBadge} ${pc.bold(displayModel)}${gitStr} · Context ${progressStr} · ${costStr}`;
|
|
107
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
export class LiveTelemetryTracker {
|
|
3
|
+
startTime = 0;
|
|
4
|
+
phase = 'connecting';
|
|
5
|
+
totalTokens = 0;
|
|
6
|
+
samples = [];
|
|
7
|
+
windowMs = 2000;
|
|
8
|
+
constructor(windowMs = 2000) {
|
|
9
|
+
this.windowMs = windowMs;
|
|
10
|
+
}
|
|
11
|
+
start(phase = 'connecting') {
|
|
12
|
+
this.startTime = Date.now();
|
|
13
|
+
this.phase = phase;
|
|
14
|
+
this.totalTokens = 0;
|
|
15
|
+
this.samples = [];
|
|
16
|
+
}
|
|
17
|
+
setPhase(phase) {
|
|
18
|
+
this.phase = phase;
|
|
19
|
+
}
|
|
20
|
+
recordTokens(count) {
|
|
21
|
+
if (count <= 0)
|
|
22
|
+
return;
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
this.totalTokens += count;
|
|
25
|
+
this.samples.push({ timestamp: now, tokens: count });
|
|
26
|
+
this.pruneOldSamples(now);
|
|
27
|
+
}
|
|
28
|
+
estimateTokensFromText(text) {
|
|
29
|
+
if (!text)
|
|
30
|
+
return 0;
|
|
31
|
+
// Fast token estimation: ~0.8 tokens per Cyrillic char, ~0.25 per ASCII
|
|
32
|
+
let tokens = 0;
|
|
33
|
+
for (let i = 0; i < text.length; i++) {
|
|
34
|
+
const code = text.charCodeAt(i);
|
|
35
|
+
if (code >= 0x0400 && code <= 0x04ff) {
|
|
36
|
+
tokens += 0.8;
|
|
37
|
+
}
|
|
38
|
+
else if (code > 0x07ff) {
|
|
39
|
+
tokens += 1.2;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
tokens += 0.25;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return Math.max(1, Math.round(tokens));
|
|
46
|
+
}
|
|
47
|
+
pruneOldSamples(now) {
|
|
48
|
+
const threshold = now - this.windowMs;
|
|
49
|
+
while (this.samples.length > 0 && this.samples[0].timestamp < threshold) {
|
|
50
|
+
this.samples.shift();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
getTokensPerSecond() {
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
this.pruneOldSamples(now);
|
|
56
|
+
if (this.samples.length === 0)
|
|
57
|
+
return 0;
|
|
58
|
+
const windowTokens = this.samples.reduce((acc, s) => acc + s.tokens, 0);
|
|
59
|
+
const earliest = this.samples[0].timestamp;
|
|
60
|
+
const durationSec = Math.max(0.2, (now - earliest) / 1000);
|
|
61
|
+
return Math.round(windowTokens / durationSec);
|
|
62
|
+
}
|
|
63
|
+
getElapsedSeconds() {
|
|
64
|
+
if (this.startTime === 0)
|
|
65
|
+
return 0;
|
|
66
|
+
return Math.max(0, Math.round((Date.now() - this.startTime) / 1000));
|
|
67
|
+
}
|
|
68
|
+
getTotalTokens() {
|
|
69
|
+
return this.totalTokens;
|
|
70
|
+
}
|
|
71
|
+
static formatTokenCount(n) {
|
|
72
|
+
if (n >= 1_000_000)
|
|
73
|
+
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
74
|
+
if (n >= 1_000)
|
|
75
|
+
return `${(n / 1_000).toFixed(1)}k`;
|
|
76
|
+
return `${Math.round(n)}`;
|
|
77
|
+
}
|
|
78
|
+
formatLabel() {
|
|
79
|
+
const secs = `${this.getElapsedSeconds()}s`;
|
|
80
|
+
const tokens = LiveTelemetryTracker.formatTokenCount(this.totalTokens);
|
|
81
|
+
const tps = this.getTokensPerSecond();
|
|
82
|
+
if (this.phase === 'connecting') {
|
|
83
|
+
return `connecting… ${pc.dim(secs)}`;
|
|
84
|
+
}
|
|
85
|
+
if (this.phase === 'thinking') {
|
|
86
|
+
if (this.totalTokens > 0) {
|
|
87
|
+
return `thinking… ${pc.dim(`${secs} · ${tokens} tok · ${tps} tok/s`)}`;
|
|
88
|
+
}
|
|
89
|
+
return `thinking… ${pc.dim(secs)}`;
|
|
90
|
+
}
|
|
91
|
+
if (this.phase === 'generating') {
|
|
92
|
+
if (this.totalTokens > 0) {
|
|
93
|
+
return `generating… ${pc.dim(`${secs} · ${tokens} tok · ${tps} tok/s`)}`;
|
|
94
|
+
}
|
|
95
|
+
return `generating… ${pc.dim(secs)}`;
|
|
96
|
+
}
|
|
97
|
+
return `executing… ${pc.dim(secs)}`;
|
|
98
|
+
}
|
|
99
|
+
reset() {
|
|
100
|
+
this.startTime = 0;
|
|
101
|
+
this.phase = 'connecting';
|
|
102
|
+
this.totalTokens = 0;
|
|
103
|
+
this.samples = [];
|
|
104
|
+
}
|
|
105
|
+
}
|
package/dist/core/types.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
export const DEVX_VERSION = '1.5.0';
|
package/dist/mcp/client.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
|
+
import { DEVX_VERSION } from '../core/types.js';
|
|
2
3
|
export class MCPClient {
|
|
3
4
|
name;
|
|
4
5
|
config;
|
|
@@ -63,7 +64,7 @@ export class MCPClient {
|
|
|
63
64
|
capabilities: {},
|
|
64
65
|
clientInfo: {
|
|
65
66
|
name: 'devx',
|
|
66
|
-
version:
|
|
67
|
+
version: DEVX_VERSION
|
|
67
68
|
}
|
|
68
69
|
});
|
|
69
70
|
if (!initResult) {
|
|
@@ -25,14 +25,19 @@ function isDangerousBashCommand(commandStr) {
|
|
|
25
25
|
return false;
|
|
26
26
|
// Normalize whitespace: collapse multiple spaces/tabs into single space
|
|
27
27
|
const norm = commandStr.toLowerCase().replace(/\s+/g, ' ').trim();
|
|
28
|
-
// Pattern matching rm with combined flags (-rf, -fr, -r -f, etc.) targeting root, home, termux prefix, or
|
|
29
|
-
const isDangerousRm = /rm\s+-(?:[a-z]*r[a-z]*f|[a-z]*f[a-z]*r)\s+(
|
|
30
|
-
/rm\s+-[a-z]*r[a-z]*\s+-[a-z]*f[a-z]*\s+(
|
|
31
|
-
/rm\s+-[a-z]*f[a-z]*\s+-[a-z]*r[a-z]*\s+(
|
|
28
|
+
// Pattern matching rm with combined flags (-rf, -fr, -r -f, etc.) targeting root, home, termux prefix, glob, or current/parent dir
|
|
29
|
+
const isDangerousRm = /rm\s+-(?:[a-z]*r[a-z]*f|[a-z]*f[a-z]*r)\s+(?:\/|\/\*|~|~\/|\*|\.|\.\/|\.\.\/?.*|\$home|\$prefix|\/data\/data\/com\.termux)/i.test(norm) ||
|
|
30
|
+
/rm\s+-[a-z]*r[a-z]*\s+-[a-z]*f[a-z]*\s+(?:\/|\/\*|~|~\/|\*|\.|\.\/|\.\.\/?.*|\$home|\$prefix|\/data\/data\/com\.termux)/i.test(norm) ||
|
|
31
|
+
/rm\s+-[a-z]*f[a-z]*\s+-[a-z]*r[a-z]*\s+(?:\/|\/\*|~|~\/|\*|\.|\.\/|\.\.\/?.*|\$home|\$prefix|\/data\/data\/com\.termux)/i.test(norm);
|
|
32
32
|
return (isDangerousRm ||
|
|
33
33
|
norm.includes('rm -rf /') ||
|
|
34
34
|
norm.includes('rm -rf ~') ||
|
|
35
35
|
norm.includes('rm -rf *') ||
|
|
36
|
+
norm.includes('rm -rf .') ||
|
|
37
|
+
norm.includes('rm -rf ./') ||
|
|
38
|
+
norm.includes('rm -rf ..') ||
|
|
39
|
+
norm.includes('rm -fr .') ||
|
|
40
|
+
norm.includes('rm -fr ./') ||
|
|
36
41
|
norm.includes('rm -rf $prefix') ||
|
|
37
42
|
norm.includes('rm -rf $home') ||
|
|
38
43
|
norm.includes('rm -rf /data/data/com.termux') ||
|
package/dist/providers/openai.js
CHANGED
|
@@ -220,11 +220,12 @@ export class OpenAIProvider {
|
|
|
220
220
|
const trimmed = line.trim();
|
|
221
221
|
if (!trimmed || trimmed.startsWith(':'))
|
|
222
222
|
continue;
|
|
223
|
-
if (trimmed
|
|
224
|
-
|
|
225
|
-
|
|
223
|
+
if (trimmed.startsWith('data:')) {
|
|
224
|
+
const dataPayload = trimmed.replace(/^data:\s*/, '');
|
|
225
|
+
if (dataPayload === '[DONE]')
|
|
226
|
+
continue;
|
|
226
227
|
try {
|
|
227
|
-
const json = JSON.parse(
|
|
228
|
+
const json = JSON.parse(dataPayload);
|
|
228
229
|
if (json.error) {
|
|
229
230
|
const errMsg = json.error.message || json.error.code || JSON.stringify(json.error);
|
|
230
231
|
throw new Error(`Provider Stream Error: ${errMsg}`);
|
package/dist/tools/fs.js
CHANGED
|
@@ -108,7 +108,8 @@ export const editFileTool = {
|
|
|
108
108
|
properties: {
|
|
109
109
|
path: { type: 'string', description: 'Path to the file to edit' },
|
|
110
110
|
target: { type: 'string', description: 'Exact string or block of code to replace' },
|
|
111
|
-
replacement: { type: 'string', description: 'New string or block of code to replace with' }
|
|
111
|
+
replacement: { type: 'string', description: 'New string or block of code to replace with' },
|
|
112
|
+
replaceAll: { type: 'boolean', description: 'If true, replaces all occurrences of target in the file. Default is false.' }
|
|
112
113
|
},
|
|
113
114
|
required: ['path', 'target', 'replacement']
|
|
114
115
|
}
|
|
@@ -138,14 +139,48 @@ export const editFileTool = {
|
|
|
138
139
|
throw new Error(`Target text to replace was not found in ${args.path}. Ensure exact indentation and characters match.`);
|
|
139
140
|
}
|
|
140
141
|
}
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
142
|
+
// Find all matching occurrences
|
|
143
|
+
const matches = [];
|
|
144
|
+
let searchPos = 0;
|
|
145
|
+
while (searchPos < content.length) {
|
|
146
|
+
const idx = content.indexOf(target, searchPos);
|
|
147
|
+
if (idx === -1)
|
|
148
|
+
break;
|
|
149
|
+
matches.push(idx);
|
|
150
|
+
searchPos = idx + Math.max(1, target.length);
|
|
151
|
+
}
|
|
152
|
+
const isReplaceAll = args.replaceAll === true || args.replaceAll === 'true';
|
|
153
|
+
if (matches.length > 1 && !isReplaceAll) {
|
|
154
|
+
const lineNumbers = matches.map(idx => content.slice(0, idx).split(/\r?\n/).length);
|
|
155
|
+
throw new Error(`Target text matches ${matches.length} occurrences in ${args.path} at lines: [${lineNumbers.join(', ')}]. ` +
|
|
156
|
+
`Please provide more surrounding lines/context to make the target block unique, or set replaceAll: true to replace all instances.`);
|
|
157
|
+
}
|
|
158
|
+
const hasCRLF = oldContent.includes('\r\n');
|
|
159
|
+
const removedArr = target.split(/\r?\n/);
|
|
160
|
+
const addedArr = args.replacement.split(/\r?\n/);
|
|
161
|
+
if (isReplaceAll && matches.length > 1) {
|
|
162
|
+
let newContent = content.split(target).join(args.replacement);
|
|
163
|
+
if (hasCRLF) {
|
|
164
|
+
newContent = newContent.replace(/\r?\n/g, '\r\n');
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
newContent = newContent.replace(/\r\n/g, '\n');
|
|
168
|
+
}
|
|
169
|
+
await fs.writeFile(args.path, newContent, 'utf8');
|
|
170
|
+
return JSON.stringify({
|
|
171
|
+
status: 'success',
|
|
172
|
+
action: 'edit',
|
|
173
|
+
path: args.path,
|
|
174
|
+
occurrencesReplaced: matches.length,
|
|
175
|
+
summary: `Successfully replaced all ${matches.length} occurrences in ${args.path}`
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
const targetIndex = matches[0];
|
|
179
|
+
const allLines = content.split(/\r?\n/);
|
|
180
|
+
const startLine = content.slice(0, targetIndex).split(/\r?\n/).length;
|
|
145
181
|
const endLine = startLine + removedArr.length - 1;
|
|
146
182
|
const contextBefore = allLines.slice(Math.max(0, startLine - 3), startLine - 1);
|
|
147
183
|
const contextAfter = allLines.slice(endLine, Math.min(allLines.length, endLine + 2));
|
|
148
|
-
const addedArr = args.replacement.split('\n');
|
|
149
184
|
const diffLines = [];
|
|
150
185
|
let lineCounter = startLine - contextBefore.length;
|
|
151
186
|
contextBefore.forEach((l) => diffLines.push(`${lineCounter++} ${l}`));
|
|
@@ -153,7 +188,6 @@ export const editFileTool = {
|
|
|
153
188
|
let addCounter = startLine;
|
|
154
189
|
addedArr.forEach((l) => diffLines.push(`${addCounter++} + ${l}`));
|
|
155
190
|
contextAfter.forEach((l) => diffLines.push(`${addCounter++} ${l}`));
|
|
156
|
-
const hasCRLF = oldContent.includes('\r\n');
|
|
157
191
|
let newContent = content.slice(0, targetIndex) + args.replacement + content.slice(targetIndex + target.length);
|
|
158
192
|
if (hasCRLF) {
|
|
159
193
|
newContent = newContent.replace(/\r?\n/g, '\r\n');
|
package/dist/tools/packages.js
CHANGED
|
@@ -18,6 +18,10 @@ export const installPackageTool = {
|
|
|
18
18
|
validateArgs(args) {
|
|
19
19
|
if (!args.package || typeof args.package !== 'string')
|
|
20
20
|
throw new Error('package is required');
|
|
21
|
+
const sanitized = args.package.trim();
|
|
22
|
+
if (!/^[@a-zA-Z0-9_\-./=><^~+:[\] ]+$/.test(sanitized) || /[;&|`$\n\r()]/.test(sanitized)) {
|
|
23
|
+
throw new Error(`Invalid package name: "${args.package}". Invalid characters or potential command injection detected.`);
|
|
24
|
+
}
|
|
21
25
|
},
|
|
22
26
|
async execute(args) {
|
|
23
27
|
let pkgManager = args.manager;
|
|
@@ -40,29 +44,49 @@ export const installPackageTool = {
|
|
|
40
44
|
pkgManager = 'npm';
|
|
41
45
|
}
|
|
42
46
|
}
|
|
43
|
-
let
|
|
47
|
+
let executable = 'npm';
|
|
48
|
+
let cmdArgs = [];
|
|
49
|
+
const pkgs = args.package.trim().split(/\s+/);
|
|
44
50
|
if (pkgManager === 'npm') {
|
|
45
|
-
|
|
51
|
+
executable = 'npm';
|
|
52
|
+
cmdArgs = ['install'];
|
|
53
|
+
if (args.dev)
|
|
54
|
+
cmdArgs.push('-D');
|
|
55
|
+
cmdArgs.push(...pkgs);
|
|
46
56
|
}
|
|
47
57
|
else if (pkgManager === 'yarn') {
|
|
48
|
-
|
|
58
|
+
executable = 'yarn';
|
|
59
|
+
cmdArgs = ['add'];
|
|
60
|
+
if (args.dev)
|
|
61
|
+
cmdArgs.push('-D');
|
|
62
|
+
cmdArgs.push(...pkgs);
|
|
49
63
|
}
|
|
50
64
|
else if (pkgManager === 'pnpm') {
|
|
51
|
-
|
|
65
|
+
executable = 'pnpm';
|
|
66
|
+
cmdArgs = ['add'];
|
|
67
|
+
if (args.dev)
|
|
68
|
+
cmdArgs.push('-D');
|
|
69
|
+
cmdArgs.push(...pkgs);
|
|
52
70
|
}
|
|
53
71
|
else if (pkgManager === 'pip') {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
72
|
+
executable = 'pip';
|
|
73
|
+
cmdArgs = ['install'];
|
|
74
|
+
if (process.platform !== 'win32') {
|
|
75
|
+
cmdArgs.push('--break-system-packages');
|
|
76
|
+
}
|
|
77
|
+
cmdArgs.push(...pkgs);
|
|
57
78
|
}
|
|
58
79
|
else if (pkgManager === 'cargo') {
|
|
59
|
-
|
|
80
|
+
executable = 'cargo';
|
|
81
|
+
cmdArgs = ['add', ...pkgs];
|
|
60
82
|
}
|
|
61
83
|
else {
|
|
62
|
-
|
|
84
|
+
executable = 'npm';
|
|
85
|
+
cmdArgs = ['install', ...pkgs];
|
|
63
86
|
}
|
|
87
|
+
const isWin = process.platform === 'win32';
|
|
64
88
|
return new Promise((resolve) => {
|
|
65
|
-
const proc = spawn(
|
|
89
|
+
const proc = spawn(executable, cmdArgs, { shell: isWin });
|
|
66
90
|
let output = '';
|
|
67
91
|
proc.stdout.on('data', (d) => { output += d.toString(); });
|
|
68
92
|
proc.stderr.on('data', (d) => { output += d.toString(); });
|
|
@@ -75,7 +99,7 @@ export const installPackageTool = {
|
|
|
75
99
|
}
|
|
76
100
|
});
|
|
77
101
|
proc.on('error', (err) => {
|
|
78
|
-
resolve(`Failed to run ${
|
|
102
|
+
resolve(`Failed to run ${executable} ${cmdArgs.join(' ')}: ${err.message}`);
|
|
79
103
|
});
|
|
80
104
|
});
|
|
81
105
|
}
|
package/dist/tools/search.js
CHANGED
|
@@ -11,9 +11,20 @@ const IGNORED_DIRS = new Set([
|
|
|
11
11
|
'.cache',
|
|
12
12
|
'vendor'
|
|
13
13
|
]);
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
const MAX_FILE_SIZE = 512 * 1024; // 512 KB
|
|
15
|
+
async function searchFiles(dir, query, results, maxResults = 50, visitedDirs = new Set(), depth = 0) {
|
|
16
|
+
if (results.length >= maxResults || depth > 15)
|
|
16
17
|
return;
|
|
18
|
+
let realDir;
|
|
19
|
+
try {
|
|
20
|
+
realDir = await fs.realpath(dir);
|
|
21
|
+
if (visitedDirs.has(realDir))
|
|
22
|
+
return;
|
|
23
|
+
visitedDirs.add(realDir);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
17
28
|
let entries;
|
|
18
29
|
try {
|
|
19
30
|
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
@@ -28,19 +39,27 @@ async function searchFiles(dir, query, results, maxResults = 50) {
|
|
|
28
39
|
const relPath = path.relative(process.cwd(), fullPath) || fullPath;
|
|
29
40
|
if (entry.isDirectory()) {
|
|
30
41
|
if (!IGNORED_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
|
|
31
|
-
await searchFiles(fullPath, query, results, maxResults);
|
|
42
|
+
await searchFiles(fullPath, query, results, maxResults, visitedDirs, depth + 1);
|
|
32
43
|
}
|
|
33
44
|
}
|
|
34
45
|
else if (entry.isFile()) {
|
|
35
46
|
// Skip large binary extensions
|
|
36
47
|
const ext = path.extname(entry.name).toLowerCase();
|
|
37
|
-
if (['.png', '.jpg', '.jpeg', '.gif', '.ico', '.pdf', '.zip', '.tar', '.gz', '.exe', '.dll', '.so', '.wasm'].includes(ext)) {
|
|
48
|
+
if (['.png', '.jpg', '.jpeg', '.gif', '.ico', '.pdf', '.zip', '.tar', '.gz', '.exe', '.dll', '.so', '.wasm', '.bin', '.db', '.sqlite'].includes(ext)) {
|
|
38
49
|
continue;
|
|
39
50
|
}
|
|
40
51
|
try {
|
|
52
|
+
const stat = await fs.stat(fullPath);
|
|
53
|
+
if (stat.size > MAX_FILE_SIZE) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
41
56
|
const content = await fs.readFile(fullPath, 'utf8');
|
|
57
|
+
// Skip binary files (e.g. compiled executables without extension) containing null bytes
|
|
58
|
+
if (content.includes('\0')) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
42
61
|
if (content.toLowerCase().includes(query.toLowerCase())) {
|
|
43
|
-
const lines = content.split(
|
|
62
|
+
const lines = content.split(/\r?\n/);
|
|
44
63
|
for (let i = 0; i < lines.length; i++) {
|
|
45
64
|
if (results.length >= maxResults)
|
|
46
65
|
break;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "termux-dev",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Ultra-fast, terminal-native AI coding assistant and agent built for Android Termux, Windows, macOS, and Linux.",
|
|
5
5
|
"main": "dist/cli/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
"build": "tsc && node ./scripts/copy-assets.js",
|
|
22
22
|
"start": "node ./bin/devx.js",
|
|
23
23
|
"dev": "tsc --watch",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"test:watch": "vitest",
|
|
24
26
|
"prepublishOnly": "npm run build"
|
|
25
27
|
},
|
|
26
28
|
"engines": {
|
|
@@ -61,6 +63,7 @@
|
|
|
61
63
|
"@types/marked-terminal": "^6.1.1",
|
|
62
64
|
"@types/node": "^20.0.0",
|
|
63
65
|
"@types/qrcode-terminal": "^0.12.2",
|
|
64
|
-
"typescript": "^5.4.0"
|
|
66
|
+
"typescript": "^5.4.0",
|
|
67
|
+
"vitest": "^4.1.11"
|
|
65
68
|
}
|
|
66
69
|
}
|