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 +1 -1
- package/assets/preview.png +0 -0
- package/dist/cli/index.js +35 -8
- package/dist/cli/server.js +163 -40
- package/dist/cli/updater.js +67 -50
- package/dist/core/loop.js +1 -1
- package/dist/providers/openai.js +23 -1
- package/dist/tools/index.js +3 -2
- package/dist/tools/server.js +46 -0
- package/package.json +1 -1
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.
|
|
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 -->
|
package/assets/preview.png
CHANGED
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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(
|
|
1402
|
+
s.start(pc.bold(pc.cyan(label)));
|
|
1375
1403
|
spinnerActive = true;
|
|
1376
1404
|
}
|
|
1377
1405
|
else {
|
|
1378
|
-
|
|
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') {
|
package/dist/cli/server.js
CHANGED
|
@@ -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
|
-
|
|
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 • ${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> • 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 • 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
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
if (
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
87
|
-
if (
|
|
88
|
-
const
|
|
89
|
-
if (fsSync.existsSync(
|
|
90
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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
|
|
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
|
}
|
package/dist/cli/updater.js
CHANGED
|
@@ -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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
166
|
-
console.log(pc.
|
|
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;
|
package/dist/providers/openai.js
CHANGED
|
@@ -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
|
}
|
package/dist/tools/index.js
CHANGED
|
@@ -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