termux-dev 1.4.19 → 1.6.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.
@@ -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 glob
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);
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') ||
@@ -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 === 'data: [DONE]')
224
- continue;
225
- if (trimmed.startsWith('data: ')) {
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(trimmed.slice(6));
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
- const targetIndex = content.indexOf(target);
142
- const allLines = content.split('\n');
143
- const startLine = content.slice(0, targetIndex).split('\n').length;
144
- const removedArr = target.split('\n');
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');
@@ -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 cmd = '';
47
+ let executable = 'npm';
48
+ let cmdArgs = [];
49
+ const pkgs = args.package.trim().split(/\s+/);
44
50
  if (pkgManager === 'npm') {
45
- cmd = `npm install ${args.dev ? '-D ' : ''}${args.package}`;
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
- cmd = `yarn add ${args.dev ? '-D ' : ''}${args.package}`;
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
- cmd = `pnpm add ${args.dev ? '-D ' : ''}${args.package}`;
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
- const isUnix = process.platform !== 'win32';
55
- const extraFlag = isUnix ? ' --break-system-packages' : '';
56
- cmd = `pip install ${args.package}${extraFlag}`;
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
- cmd = `cargo add ${args.package}`;
80
+ executable = 'cargo';
81
+ cmdArgs = ['add', ...pkgs];
60
82
  }
61
83
  else {
62
- cmd = `npm install ${args.package}`;
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(cmd, { shell: true });
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 ${cmd}: ${err.message}`);
102
+ resolve(`Failed to run ${executable} ${cmdArgs.join(' ')}: ${err.message}`);
79
103
  });
80
104
  });
81
105
  }
@@ -11,9 +11,20 @@ const IGNORED_DIRS = new Set([
11
11
  '.cache',
12
12
  'vendor'
13
13
  ]);
14
- async function searchFiles(dir, query, results, maxResults = 50) {
15
- if (results.length >= maxResults)
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('\n');
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.4.19",
3
+ "version": "1.6.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",
@@ -15,12 +15,16 @@
15
15
  "files": [
16
16
  "bin",
17
17
  "dist",
18
- "assets"
18
+ "assets/banner.svg",
19
+ "assets/preview.svg",
20
+ "assets/preview.png"
19
21
  ],
20
22
  "scripts": {
21
23
  "build": "tsc && node ./scripts/copy-assets.js",
22
24
  "start": "node ./bin/devx.js",
23
25
  "dev": "tsc --watch",
26
+ "test": "vitest run",
27
+ "test:watch": "vitest",
24
28
  "prepublishOnly": "npm run build"
25
29
  },
26
30
  "engines": {
@@ -61,6 +65,7 @@
61
65
  "@types/marked-terminal": "^6.1.1",
62
66
  "@types/node": "^20.0.0",
63
67
  "@types/qrcode-terminal": "^0.12.2",
64
- "typescript": "^5.4.0"
68
+ "typescript": "^5.4.0",
69
+ "vitest": "^4.1.11"
65
70
  }
66
71
  }
Binary file