termux-dev 1.1.0 → 1.1.2

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/assets/banner.svg CHANGED
@@ -25,7 +25,7 @@
25
25
 
26
26
  <!-- Tagline & Badge -->
27
27
  <text x="425" y="142" font-family="system-ui, -apple-system, sans-serif" font-size="14" font-weight="600" fill="#8b949e" text-anchor="middle" letter-spacing="3">
28
- THE TERMINAL-NATIVE AI CODING AGENT <tspan fill="#00f2fe" font-weight="bold">v1.1.0</tspan>
28
+ THE TERMINAL-NATIVE AI CODING AGENT <tspan fill="#00f2fe" font-weight="bold">v1.1.2</tspan>
29
29
  </text>
30
30
 
31
31
  <!-- Top Accent Line -->
Binary file
package/dist/cli/index.js CHANGED
@@ -378,7 +378,7 @@ function drawLogo() {
378
378
  pc.cyan(' █▀▀▄ █▀▀▀ █ █ █ █'),
379
379
  pc.cyan(' █ █ █▀▀▀ ▀▄▀ ▀▄▀ '),
380
380
  pc.cyan(' █▄▄▀ █▄▄▄ ▀ ▀ ▀ '),
381
- ' ' + pc.cyan(pc.bold('v1.1.0')),
381
+ ' ' + pc.cyan(pc.bold('v1.1.2')),
382
382
  ''
383
383
  ];
384
384
  for (const line of logo) {
@@ -393,7 +393,7 @@ function drawLogo() {
393
393
  indent + pc.cyan('▀▀▀█▀▀▀ █▀▀▀ █▀▀█ █▄ ▄█ █ █ ▀▄ ▄▀ █▀▀▄ █▀▀▀ █ █'),
394
394
  indent + pc.cyan(' █ █▀▀▀ █▄▄▀ █ █ █ █ █ █ ▀▀ █ █ █▀▀▀ █ █'),
395
395
  indent + pc.cyan(' █ █▄▄▄ █ ▀▄ █ █ ▀▄▄▀ ▄▀ ▀▄ █▄▄▀ █▄▄▄ ▀▄▀ '),
396
- indent + pc.cyan(pc.bold('v1.1.0')),
396
+ indent + pc.cyan(pc.bold('v1.1.2')),
397
397
  ''
398
398
  ];
399
399
  for (const line of logo) {
@@ -415,6 +415,33 @@ function formatSessionTime(timestamp) {
415
415
  const days = Math.floor(hours / 24);
416
416
  return `${days}d ago`;
417
417
  }
418
+ function formatToolGeneratingLabel(name, targetHint) {
419
+ const t = name.toLowerCase();
420
+ const target = targetHint ? ` ${targetHint}` : '';
421
+ if (t === 'read_file' || t === 'read')
422
+ return `→ Read${target || '...'}`;
423
+ if (t === 'write_file' || t === 'write')
424
+ return `→ Write${target || '...'}`;
425
+ if (t === 'edit_file' || t === 'patch')
426
+ return `→ Edit${target || '...'}`;
427
+ if (t === 'delete_file' || t === 'remove_file' || t === 'rm')
428
+ return `→ Delete${target || '...'}`;
429
+ if (t === 'bash' || t === 'run_command' || t === 'exec')
430
+ return `→ Run:${target || '...'}`;
431
+ if (t === 'search' || t === 'grep_search')
432
+ return `→ Search${target || '...'}`;
433
+ if (t === 'list_dir' || t === 'ls')
434
+ return `→ List${target || '...'}`;
435
+ if (t === 'todo_list' || t === 'update_todos')
436
+ return `→ Update Plan & Tasks...`;
437
+ if (t === 'plan_ready')
438
+ return `→ Finalize Plan...`;
439
+ if (t === 'diagnose_code')
440
+ return `→ Diagnosing code...`;
441
+ if (t === 'ask_questions')
442
+ return `→ Clarifying questions...`;
443
+ return `→ ${name}${target}...`;
444
+ }
418
445
  async function handleSessionDelete() {
419
446
  while (true) {
420
447
  const sessions = await SessionManager.listSessions();
@@ -588,7 +615,7 @@ export async function main() {
588
615
  const maxIter = config.maxIterations || 100;
589
616
  const maxIterLabel = maxIter >= 9999 ? 'Unlimited' : `${maxIter} steps`;
590
617
  const choice = await select({
591
- message: `${pc.bold('⚙️ Settings')} ${pc.dim('(devx v1.1.0 • by ApvCode)')}`,
618
+ message: `${pc.bold('⚙️ Settings')} ${pc.dim('(devx v1.1.2 • by ApvCode)')}`,
592
619
  choices: [
593
620
  {
594
621
  name: `${config.pureBlackTheme !== false ? pc.green('🎨 Pure Black Background: ON') : pc.yellow('🎨 Pure Black Background: OFF')}`,
@@ -624,7 +651,7 @@ export async function main() {
624
651
  description: 'Limit how many tool steps (file edits, terminal commands) agent can do per request'
625
652
  },
626
653
  {
627
- name: `${pc.cyan('✨ About devx')} ${pc.dim('(v1.1.0 by ApvCode)')}`,
654
+ name: `${pc.cyan('✨ About devx')} ${pc.dim('(v1.1.2 by ApvCode)')}`,
628
655
  value: 'about',
629
656
  description: 'Terminal-Native AI Coding Agent created by ApvCode (https://github.com/apvcode/Termux-Dev)'
630
657
  },
@@ -636,7 +663,7 @@ export async function main() {
636
663
  ]
637
664
  });
638
665
  if (choice === 'about') {
639
- p.note(`⚡ devx v1.1.0 — Terminal-Native AI Coding Agent\n` +
666
+ p.note(`⚡ devx v1.1.2 — Terminal-Native AI Coding Agent\n` +
640
667
  `👤 Author: ApvCode (https://github.com/apvcode)\n` +
641
668
  `🌟 Repository: https://github.com/apvcode/Termux-Dev\n` +
642
669
  `📜 License: MIT License (2026)\n` +
@@ -1368,15 +1395,15 @@ export async function main() {
1368
1395
  }
1369
1396
  }
1370
1397
  else if (event.type === 'tool_generating') {
1398
+ const label = formatToolGeneratingLabel(event.name, event.targetHint);
1371
1399
  if (!spinnerActive) {
1372
1400
  streamer.finish();
1373
1401
  finishThinking(false);
1374
- s.start(pc.cyan(`⚡ Calling ${event.name}...`));
1402
+ s.start(pc.bold(pc.cyan(label)));
1375
1403
  spinnerActive = true;
1376
1404
  }
1377
1405
  else {
1378
- const chars = event.bytes > 1000 ? `${(event.bytes / 1000).toFixed(1)}k chars` : `${event.bytes} chars`;
1379
- s.message(pc.cyan(`⚡ Generating ${event.name} (${chars})...`));
1406
+ s.message(pc.bold(pc.cyan(label)));
1380
1407
  }
1381
1408
  }
1382
1409
  else if (event.type === 'tool_start') {
@@ -10,6 +10,8 @@ const MIME_TYPES = {
10
10
  '.css': 'text/css; charset=utf-8',
11
11
  '.js': 'application/javascript; charset=utf-8',
12
12
  '.mjs': 'application/javascript; charset=utf-8',
13
+ '.ts': 'text/plain; charset=utf-8',
14
+ '.tsx': 'text/plain; charset=utf-8',
13
15
  '.json': 'application/json; charset=utf-8',
14
16
  '.png': 'image/png',
15
17
  '.jpg': 'image/jpeg',
@@ -22,6 +24,8 @@ const MIME_TYPES = {
22
24
  '.mp3': 'audio/mpeg',
23
25
  '.ogg': 'audio/ogg',
24
26
  '.wasm': 'application/wasm',
27
+ '.pdf': 'application/pdf',
28
+ '.md': 'text/markdown; charset=utf-8',
25
29
  '.txt': 'text/plain; charset=utf-8'
26
30
  };
27
31
  let activeServer = null;
@@ -45,12 +49,98 @@ export function getServerPort() {
45
49
  }
46
50
  export function stopServer() {
47
51
  if (activeServer) {
48
- activeServer.close();
52
+ try {
53
+ activeServer.close();
54
+ }
55
+ catch { }
49
56
  activeServer = null;
50
57
  return true;
51
58
  }
52
59
  return false;
53
60
  }
61
+ function renderDirectoryHtml(dirPath, relPath, files, port) {
62
+ const currentRel = relPath === '/' ? '' : relPath;
63
+ const items = files
64
+ .filter(f => !f.name.startsWith('.') && f.name !== 'node_modules')
65
+ .sort((a, b) => {
66
+ if (a.isDirectory() && !b.isDirectory())
67
+ return -1;
68
+ if (!a.isDirectory() && b.isDirectory())
69
+ return 1;
70
+ return a.name.localeCompare(b.name);
71
+ })
72
+ .map(f => {
73
+ const isDir = f.isDirectory();
74
+ const href = `${currentRel}/${encodeURIComponent(f.name)}${isDir ? '/' : ''}`;
75
+ const ext = path.extname(f.name).toLowerCase();
76
+ let icon = isDir ? '📁' : '📄';
77
+ let badge = '';
78
+ if (ext === '.html' || ext === '.htm') {
79
+ icon = '🌐';
80
+ badge = '<span class="badge">HTML</span>';
81
+ }
82
+ else if (['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp'].includes(ext)) {
83
+ icon = '🖼️';
84
+ }
85
+ else if (['.js', '.ts', '.tsx', '.jsx', '.json'].includes(ext)) {
86
+ icon = '⚡';
87
+ }
88
+ else if (ext === '.css') {
89
+ icon = '🎨';
90
+ }
91
+ return `
92
+ <li>
93
+ <a class="file-item" href="${href}">
94
+ <span class="icon">${icon}</span>
95
+ <span class="name">${f.name}</span>
96
+ ${badge}
97
+ <span class="type">${isDir ? 'Directory' : ext || 'File'}</span>
98
+ </a>
99
+ </li>
100
+ `;
101
+ })
102
+ .join('');
103
+ const parentLink = currentRel && currentRel !== '/'
104
+ ? `<li><a class="file-item" href="${path.dirname(currentRel) === '/' ? '/' : path.dirname(currentRel) + '/'}"><span class="icon">⬆️</span><span class="name">.. (Parent Directory)</span></a></li>`
105
+ : '';
106
+ return `<!DOCTYPE html>
107
+ <html lang="en">
108
+ <head>
109
+ <meta charset="utf-8"/>
110
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
111
+ <title>devx Live Preview &bull; ${relPath}</title>
112
+ <style>
113
+ * { box-sizing: border-box; }
114
+ body { background: #0a0a0c; color: #e1e4e8; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, monospace; margin: 0; padding: 24px 16px; }
115
+ .card { background: #111116; border: 1px solid #22222c; border-radius: 12px; max-width: 800px; margin: 0 auto; padding: 24px; box-shadow: 0 8px 30px rgba(0,0,0,0.6); }
116
+ h1 { color: #00f2fe; margin: 0 0 8px 0; font-size: 22px; display: flex; align-items: center; gap: 8px; }
117
+ .info { color: #8b949e; font-size: 14px; margin-bottom: 20px; word-break: break-all; }
118
+ .info code { background: #1a1a24; padding: 2px 6px; border-radius: 4px; color: #f0f6fc; }
119
+ ul { list-style: none; padding: 0; margin: 0; }
120
+ .file-item { display: flex; align-items: center; padding: 10px 14px; border-bottom: 1px solid #1a1a22; text-decoration: none; color: #f0f6fc; border-radius: 6px; transition: all 0.15s; }
121
+ .file-item:hover { background: #1a1a26; transform: translateX(3px); }
122
+ .icon { margin-right: 12px; font-size: 18px; }
123
+ .name { flex: 1; font-weight: 500; font-size: 14px; word-break: break-all; }
124
+ .type { color: #6e7681; font-size: 12px; margin-left: 12px; }
125
+ .badge { background: rgba(0, 242, 254, 0.15); color: #00f2fe; border: 1px solid rgba(0, 242, 254, 0.3); padding: 2px 8px; border-radius: 12px; font-size: 11px; margin-left: 8px; font-weight: bold; }
126
+ .footer { margin-top: 24px; text-align: center; color: #484f58; font-size: 12px; }
127
+ </style>
128
+ </head>
129
+ <body>
130
+ <div class="card">
131
+ <h1>⚡ devx Live Preview</h1>
132
+ <div class="info">
133
+ Path: <code>${relPath}</code> &bull; Port: <code>${port}</code>
134
+ </div>
135
+ <ul>
136
+ ${parentLink}
137
+ ${items || '<li style="padding: 20px; text-align: center; color: #6e7681;">No visible files in this directory</li>'}
138
+ </ul>
139
+ <div class="footer">devx v1.1.2 &bull; Terminal-Native AI Assistant</div>
140
+ </div>
141
+ </body>
142
+ </html>`;
143
+ }
54
144
  export async function startServer(preferredPort = 3000) {
55
145
  if (activeServer) {
56
146
  stopServer();
@@ -59,75 +149,108 @@ export async function startServer(preferredPort = 3000) {
59
149
  return new Promise((resolve, reject) => {
60
150
  const server = http.createServer(async (req, res) => {
61
151
  try {
62
- let reqPath = decodeURIComponent(req.url?.split('?')[0] || '/');
63
- if (reqPath === '/') {
64
- reqPath = '/index.html';
65
- }
66
- const filePath = path.join(process.cwd(), reqPath);
152
+ let rawPath = req.url?.split('?')[0] || '/';
153
+ let reqPath = decodeURIComponent(rawPath);
154
+ const cwd = process.cwd();
155
+ let targetPath = path.resolve(cwd, '.' + reqPath);
67
156
  // Security check: ensure path is within cwd
68
- if (!filePath.startsWith(process.cwd())) {
157
+ const rel = path.relative(cwd, targetPath);
158
+ if (rel.startsWith('..') || path.isAbsolute(rel)) {
69
159
  res.writeHead(403, { 'Content-Type': 'text/plain' });
70
160
  res.end('Forbidden');
71
161
  return;
72
162
  }
73
- if (!fsSync.existsSync(filePath)) {
74
- // If html file requested without extension
75
- const withHtml = `${filePath}.html`;
76
- if (fsSync.existsSync(withHtml)) {
77
- const content = await fs.readFile(withHtml);
78
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
79
- res.end(content);
163
+ // 1. Root / Directory Request Handling
164
+ if (fsSync.existsSync(targetPath)) {
165
+ const stat = await fs.stat(targetPath);
166
+ if (stat.isDirectory()) {
167
+ // Check for index.html in directory
168
+ const possibleIndexes = [
169
+ path.join(targetPath, 'index.html'),
170
+ path.join(targetPath, 'index.htm'),
171
+ path.join(targetPath, 'public/index.html'),
172
+ path.join(targetPath, 'dist/index.html'),
173
+ path.join(targetPath, 'build/index.html')
174
+ ];
175
+ for (const idxPath of possibleIndexes) {
176
+ if (fsSync.existsSync(idxPath)) {
177
+ const content = await fs.readFile(idxPath);
178
+ res.writeHead(200, {
179
+ 'Content-Type': 'text/html; charset=utf-8',
180
+ 'Access-Control-Allow-Origin': '*'
181
+ });
182
+ res.end(content);
183
+ return;
184
+ }
185
+ }
186
+ // If no index.html, render sleek directory explorer
187
+ const dirents = await fs.readdir(targetPath, { withFileTypes: true });
188
+ const dirHtml = renderDirectoryHtml(targetPath, reqPath, dirents, activePort);
189
+ res.writeHead(200, {
190
+ 'Content-Type': 'text/html; charset=utf-8',
191
+ 'Access-Control-Allow-Origin': '*'
192
+ });
193
+ res.end(dirHtml);
80
194
  return;
81
195
  }
82
- res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
83
- res.end(`404 Not Found: ${reqPath}`);
84
- return;
85
196
  }
86
- const stat = await fs.stat(filePath);
87
- if (stat.isDirectory()) {
88
- const indexPath = path.join(filePath, 'index.html');
89
- if (fsSync.existsSync(indexPath)) {
90
- const content = await fs.readFile(indexPath);
91
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
92
- res.end(content);
93
- return;
197
+ // 2. Fallback check for missing .html extension
198
+ if (!fsSync.existsSync(targetPath)) {
199
+ const withHtml = `${targetPath}.html`;
200
+ if (fsSync.existsSync(withHtml)) {
201
+ targetPath = withHtml;
94
202
  }
95
- res.writeHead(403, { 'Content-Type': 'text/plain' });
96
- res.end('Directory listing disabled');
203
+ }
204
+ // 3. Serve File
205
+ if (fsSync.existsSync(targetPath)) {
206
+ const ext = path.extname(targetPath).toLowerCase();
207
+ const contentType = MIME_TYPES[ext] || 'application/octet-stream';
208
+ const content = await fs.readFile(targetPath);
209
+ res.writeHead(200, {
210
+ 'Content-Type': contentType,
211
+ 'Access-Control-Allow-Origin': '*'
212
+ });
213
+ res.end(content);
214
+ return;
215
+ }
216
+ // 4. SPA Fallback: check if root index.html exists
217
+ const rootIndex = path.join(cwd, 'index.html');
218
+ if (fsSync.existsSync(rootIndex)) {
219
+ const content = await fs.readFile(rootIndex);
220
+ res.writeHead(200, {
221
+ 'Content-Type': 'text/html; charset=utf-8',
222
+ 'Access-Control-Allow-Origin': '*'
223
+ });
224
+ res.end(content);
97
225
  return;
98
226
  }
99
- const ext = path.extname(filePath).toLowerCase();
100
- const contentType = MIME_TYPES[ext] || 'application/octet-stream';
101
- const content = await fs.readFile(filePath);
102
- res.writeHead(200, {
103
- 'Content-Type': contentType,
104
- 'Access-Control-Allow-Origin': '*'
105
- });
106
- res.end(content);
227
+ res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
228
+ res.end(`404 Not Found: ${reqPath}`);
107
229
  }
108
230
  catch (err) {
109
- res.writeHead(500, { 'Content-Type': 'text/plain' });
231
+ res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
110
232
  res.end(`Internal Server Error: ${err.message}`);
111
233
  }
112
234
  });
113
235
  server.on('error', (err) => {
114
236
  if (err.code === 'EADDRINUSE') {
115
- server.listen(activePort + 1);
116
237
  activePort++;
238
+ server.listen(activePort, '0.0.0.0');
117
239
  }
118
240
  else {
119
241
  reject(err);
120
242
  }
121
243
  });
122
- server.listen(activePort, () => {
244
+ server.listen(activePort, '0.0.0.0', () => {
123
245
  activeServer = server;
124
246
  const localIp = getLocalIp();
125
247
  const localUrl = `http://localhost:${activePort}`;
126
248
  const networkUrl = `http://${localIp}:${activePort}`;
127
- // If in Android Termux, try to open in browser
249
+ // If on Android Termux, attempt to open browser
128
250
  if (process.env.PREFIX?.includes('com.termux')) {
129
251
  try {
130
- spawn('termux-open-url', [localUrl], { stdio: 'ignore' });
252
+ const opener = spawn('termux-open-url', [localUrl], { stdio: 'ignore', detached: true });
253
+ opener.unref();
131
254
  }
132
255
  catch { }
133
256
  }
@@ -102,75 +102,92 @@ export async function checkForUpdates(timeoutMs = 10000) {
102
102
  };
103
103
  }
104
104
  }
105
+ async function isGitRepository(dir) {
106
+ try {
107
+ await fs.stat(path.join(dir, '.git'));
108
+ return true;
109
+ }
110
+ catch {
111
+ return false;
112
+ }
113
+ }
105
114
  export async function performSelfUpdate(latestVersion) {
106
115
  const __filename = fileURLToPath(import.meta.url);
107
116
  const projectRoot = path.resolve(path.dirname(__filename), '../../');
117
+ const isGit = await isGitRepository(projectRoot);
108
118
  console.log('\n' + pc.bold(pc.cyan('─── 🚀 Starting devx Update ──────────────────────────')));
109
- const steps = [
110
- {
111
- title: '📦 Pulling latest updates from GitHub repository...',
112
- action: () => {
113
- try {
114
- execSync('git pull origin main --quiet', { cwd: projectRoot, stdio: 'pipe' });
115
- }
116
- catch {
117
- // If not git clone or detached, try global git install or fetch
118
- try {
119
- execSync('git pull --quiet', { cwd: projectRoot, stdio: 'pipe' });
120
- }
121
- catch {
122
- execSync(`npm install -g git+${GITHUB_REPO_URL}.git --quiet`, { stdio: 'pipe' });
123
- }
124
- }
125
- }
126
- },
127
- {
128
- title: '🔨 Building and compiling TypeScript sources...',
129
- action: () => {
130
- execSync('npm install --quiet', { cwd: projectRoot, stdio: 'pipe' });
131
- execSync('npm run build --quiet', { cwd: projectRoot, stdio: 'pipe' });
132
- }
133
- },
134
- {
135
- title: '🔑 Ensuring binary execution permissions & linking...',
136
- action: () => {
137
- try {
138
- execSync('chmod +x bin/* 2>/dev/null || true', { cwd: projectRoot, stdio: 'pipe' });
139
- }
140
- catch { }
141
- try {
142
- execSync('npm link --quiet', { cwd: projectRoot, stdio: 'pipe' });
143
- }
144
- catch { }
145
- }
146
- }
147
- ];
148
- for (let i = 0; i < steps.length; i++) {
149
- const step = steps[i];
150
- process.stdout.write(` ${pc.cyan('⚡')} [${i + 1}/${steps.length}] ${pc.white(step.title)}\n`);
151
- // Animation spinner for each step
119
+ if (!isGit) {
120
+ // 1. Installed via global npm package: termux-dev
121
+ process.stdout.write(` ${pc.cyan('⚡')} [1/1] ${pc.white(`Updating termux-dev via npm (v${latestVersion})...`)}\n`);
152
122
  const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
153
123
  let fIdx = 0;
154
124
  const interval = setInterval(() => {
155
- process.stdout.write(`\r ${pc.cyan(frames[fIdx++ % frames.length])} ${pc.dim(step.title)} `);
125
+ process.stdout.write(`\r ${pc.cyan(frames[fIdx++ % frames.length])} ${pc.dim(`npm install -g termux-dev@latest`)} `);
156
126
  }, 80);
157
127
  try {
158
- await new Promise(resolve => setTimeout(resolve, 300));
159
- step.action();
128
+ execSync('npm install -g termux-dev@latest', { stdio: 'pipe' });
160
129
  clearInterval(interval);
161
- process.stdout.write(`\r ${pc.green('✔')} ${pc.bold(pc.white(step.title))}\n`);
130
+ process.stdout.write(`\r ${pc.green('✔')} ${pc.bold(pc.white(`Successfully updated termux-dev to v${latestVersion} via npm!`))}\n`);
162
131
  }
163
132
  catch (err) {
164
133
  clearInterval(interval);
165
- process.stdout.write(`\r ${pc.red('✖')} ${pc.red(step.title)} - ${err.message}\n`);
166
- console.log(pc.red(`\nUpdate failed during step ${i + 1}: ${err.message}`));
134
+ process.stdout.write(`\r ${pc.red('✖')} ${pc.red(`Update failed: ${err.message}`)}\n`);
135
+ console.log(pc.yellow(`\n💡 To update manually, run: ${pc.bold('npm install -g termux-dev')}`));
167
136
  return false;
168
137
  }
169
138
  }
139
+ else {
140
+ // 2. Installed via git clone (Developer mode)
141
+ const steps = [
142
+ {
143
+ title: '📦 Pulling latest updates from GitHub repository...',
144
+ action: () => {
145
+ execSync('git pull origin main', { cwd: projectRoot, stdio: 'pipe' });
146
+ }
147
+ },
148
+ {
149
+ title: '🔨 Building and compiling TypeScript sources...',
150
+ action: () => {
151
+ execSync('npm install', { cwd: projectRoot, stdio: 'pipe' });
152
+ execSync('npm run build', { cwd: projectRoot, stdio: 'pipe' });
153
+ }
154
+ },
155
+ {
156
+ title: '🔑 Ensuring binary execution permissions & linking...',
157
+ action: () => {
158
+ try {
159
+ execSync('npm link', { cwd: projectRoot, stdio: 'pipe' });
160
+ }
161
+ catch { }
162
+ }
163
+ }
164
+ ];
165
+ for (let i = 0; i < steps.length; i++) {
166
+ const step = steps[i];
167
+ process.stdout.write(` ${pc.cyan('⚡')} [${i + 1}/${steps.length}] ${pc.white(step.title)}\n`);
168
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
169
+ let fIdx = 0;
170
+ const interval = setInterval(() => {
171
+ process.stdout.write(`\r ${pc.cyan(frames[fIdx++ % frames.length])} ${pc.dim(step.title)} `);
172
+ }, 80);
173
+ try {
174
+ await new Promise(resolve => setTimeout(resolve, 200));
175
+ step.action();
176
+ clearInterval(interval);
177
+ process.stdout.write(`\r ${pc.green('✔')} ${pc.bold(pc.white(step.title))}\n`);
178
+ }
179
+ catch (err) {
180
+ clearInterval(interval);
181
+ process.stdout.write(`\r ${pc.red('✖')} ${pc.red(step.title)} - ${err.message}\n`);
182
+ console.log(pc.red(`\nUpdate failed during step ${i + 1}: ${err.message}`));
183
+ console.log(pc.yellow(`\n💡 To update manually, run: ${pc.bold('npm install -g termux-dev')}`));
184
+ return false;
185
+ }
186
+ }
187
+ }
170
188
  console.log(pc.bold(pc.green(`\n✅ Successfully updated to v${latestVersion}! Restarting devx...\n`)));
171
189
  console.log(pc.bold(pc.cyan('──────────────────────────────────────────────────────\n')));
172
190
  await new Promise(r => setTimeout(r, 1000));
173
- // Spawn new devx process and exit current
174
191
  try {
175
192
  const child = spawn(process.argv[0], process.argv.slice(1), {
176
193
  stdio: 'inherit',
package/dist/core/loop.js CHANGED
@@ -88,7 +88,7 @@ export class Agent {
88
88
  yield { type: 'text_delta', delta: chunk.delta };
89
89
  }
90
90
  else if (chunk.type === 'tool_generating') {
91
- yield { type: 'tool_generating', name: chunk.name, bytes: chunk.bytes };
91
+ yield { type: 'tool_generating', name: chunk.name, bytes: chunk.bytes, targetHint: chunk.targetHint };
92
92
  }
93
93
  else if (chunk.type === 'done') {
94
94
  response = chunk.response;
@@ -261,10 +261,32 @@ export class OpenAIProvider {
261
261
  if (tc.function?.arguments)
262
262
  existing.argsStr += tc.function.arguments;
263
263
  toolMap.set(idx, existing);
264
+ // Extract live target hint (e.g. path or command being generated)
265
+ let targetHint = undefined;
266
+ const args = existing.argsStr;
267
+ if (args) {
268
+ const pathMatch = args.match(/"(?:path|filePath|targetFile)"\s*:\s*"([^"\\]*(?:\\.[^"\\]*)*)"/);
269
+ if (pathMatch && pathMatch[1]) {
270
+ targetHint = pathMatch[1];
271
+ }
272
+ else {
273
+ const cmdMatch = args.match(/"(?:command|cmd)"\s*:\s*"([^"\\]*(?:\\.[^"\\]*)*)"/);
274
+ if (cmdMatch && cmdMatch[1]) {
275
+ targetHint = cmdMatch[1].length > 30 ? cmdMatch[1].slice(0, 27) + '...' : cmdMatch[1];
276
+ }
277
+ else {
278
+ const qMatch = args.match(/"(?:query|pattern)"\s*:\s*"([^"\\]*(?:\\.[^"\\]*)*)"/);
279
+ if (qMatch && qMatch[1]) {
280
+ targetHint = `"${qMatch[1]}"`;
281
+ }
282
+ }
283
+ }
284
+ }
264
285
  yield {
265
286
  type: 'tool_generating',
266
287
  name: existing.name || 'tool',
267
- bytes: existing.argsStr.length
288
+ bytes: existing.argsStr.length,
289
+ targetHint
268
290
  };
269
291
  }
270
292
  }
@@ -8,11 +8,12 @@ import { installPackageTool } from './packages.js';
8
8
  import { saveMemoryTool } from '../core/memory.js';
9
9
  import { planReadyTool, lastPlanReady, resetPlanReady } from './plan.js';
10
10
  import { todoListTool, currentTodoList, resetTodoList } from './todo.js';
11
+ import { servePreviewTool } from './server.js';
11
12
  export function getTools(planMode) {
12
- const baseTools = [readFileTool, listDirTool, searchTool, askQuestionsTool, webSearchTool, fetchUrlTool, saveMemoryTool, planReadyTool, todoListTool];
13
+ const baseTools = [readFileTool, listDirTool, searchTool, askQuestionsTool, webSearchTool, fetchUrlTool, saveMemoryTool, planReadyTool, todoListTool, servePreviewTool];
13
14
  if (planMode) {
14
15
  return baseTools;
15
16
  }
16
17
  return [...baseTools, writeFileTool, editFileTool, mkdirTool, bashTool, diagnoseCodeTool, installPackageTool];
17
18
  }
18
- export { webSearchTool, fetchUrlTool, diagnoseCodeTool, installPackageTool, saveMemoryTool, planReadyTool, lastPlanReady, resetPlanReady, todoListTool, currentTodoList, resetTodoList };
19
+ export { webSearchTool, fetchUrlTool, diagnoseCodeTool, installPackageTool, saveMemoryTool, planReadyTool, lastPlanReady, resetPlanReady, todoListTool, currentTodoList, resetTodoList, servePreviewTool };
@@ -0,0 +1,46 @@
1
+ import { startServer, stopServer, isServerRunning, getServerPort } from '../cli/server.js';
2
+ export const servePreviewTool = {
3
+ name: 'serve_preview',
4
+ definition: {
5
+ name: 'serve_preview',
6
+ description: 'Start or stop the built-in local live web preview server to view HTML/web apps in the browser.',
7
+ parameters: {
8
+ type: 'object',
9
+ properties: {
10
+ action: {
11
+ type: 'string',
12
+ enum: ['start', 'stop', 'status'],
13
+ description: 'Action to perform: start the server, stop it, or check status'
14
+ },
15
+ port: {
16
+ type: 'number',
17
+ description: 'Optional preferred port number (defaults to 3000)'
18
+ }
19
+ },
20
+ required: ['action']
21
+ }
22
+ },
23
+ validateArgs: (args) => {
24
+ if (!args || !args.action) {
25
+ throw new Error('action is required');
26
+ }
27
+ },
28
+ execute: async (args) => {
29
+ if (args.action === 'stop') {
30
+ const stopped = stopServer();
31
+ return stopped ? 'Web preview server stopped.' : 'No web preview server was running.';
32
+ }
33
+ if (args.action === 'status') {
34
+ const running = isServerRunning();
35
+ return running ? `Web server is running on port ${getServerPort()}.` : 'Web server is not running.';
36
+ }
37
+ const preferredPort = args.port || 3000;
38
+ try {
39
+ const { port, localUrl, networkUrl } = await startServer(preferredPort);
40
+ return `Web Server is now live!\n• Local URL: ${localUrl}\n• Network URL: ${networkUrl}\nOpened in browser automatically.`;
41
+ }
42
+ catch (err) {
43
+ throw new Error(`Failed to start web server: ${err.message}`);
44
+ }
45
+ }
46
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "termux-dev",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
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",