termux-dev 1.1.1 → 1.2.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/assets/banner.svg +1 -1
- package/assets/preview.png +0 -0
- package/dist/cli/doctor.js +141 -0
- package/dist/cli/index.js +210 -42
- package/dist/cli/prompt.js +34 -3
- package/dist/cli/server.js +218 -40
- package/dist/cli/theme.js +140 -0
- package/dist/core/loop.js +1 -1
- package/dist/core/notify.js +40 -0
- package/dist/core/snapshot.js +2 -6
- package/dist/prompts/builder.js +2 -1
- package/dist/providers/openai.js +23 -1
- package/dist/tools/fs.js +21 -9
- package/dist/tools/index.js +3 -2
- package/dist/tools/server.js +47 -0
- package/package.json +4 -2
package/dist/cli/prompt.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import pc from 'picocolors';
|
|
2
2
|
import { scanProjectFiles } from './files.js';
|
|
3
3
|
import { saveClipboardImage, processPastedFilePath } from './clipboard.js';
|
|
4
|
+
import { getCurrentTheme, listThemes } from './theme.js';
|
|
4
5
|
export const SLASH_COMMANDS = [
|
|
5
6
|
{ cmd: '/new', desc: 'Start a new clean chat session' },
|
|
6
7
|
{ cmd: '/resume', desc: 'Resume a previous chat session' },
|
|
7
8
|
{ cmd: '/session', desc: 'Show active session ID, stats, and info' },
|
|
8
9
|
{ cmd: '/session del', desc: 'Select and delete saved sessions' },
|
|
10
|
+
{ cmd: '/theme', desc: 'Switch UI theme (Cyan, Purple, Matrix, Amber, etc.)' },
|
|
11
|
+
{ cmd: '/doctor', desc: 'Run system & environment health diagnostics' },
|
|
9
12
|
{ cmd: '/settings', desc: 'Configure permissions & auto-approval' },
|
|
10
13
|
{ cmd: '/update', desc: 'Check and install updates from GitHub' },
|
|
11
14
|
{ cmd: '/model', desc: 'Switch model for current provider' },
|
|
@@ -47,8 +50,9 @@ export function askPrompt(opts = {}) {
|
|
|
47
50
|
const pastes = [];
|
|
48
51
|
const imageAttachments = [];
|
|
49
52
|
const usedImageNames = new Set();
|
|
53
|
+
const theme = getCurrentTheme();
|
|
50
54
|
// Header printed once
|
|
51
|
-
console.log(
|
|
55
|
+
console.log(theme.colorFn('◆') + ' ' + pc.bold(msg));
|
|
52
56
|
if (process.stdin.isTTY) {
|
|
53
57
|
process.stdin.setRawMode(true);
|
|
54
58
|
}
|
|
@@ -56,6 +60,33 @@ export function askPrompt(opts = {}) {
|
|
|
56
60
|
process.stdout.write('\x1b[?2004h');
|
|
57
61
|
}
|
|
58
62
|
function getDropdownItems() {
|
|
63
|
+
if (input.startsWith('/theme ') || input.startsWith('/themes ') || input === '/theme') {
|
|
64
|
+
const afterCmd = input.replace(/^\/(?:theme|themes)\s*/i, '').trim().toLowerCase();
|
|
65
|
+
const themes = listThemes();
|
|
66
|
+
const matched = themes.filter(t => !afterCmd ||
|
|
67
|
+
t.id.toLowerCase().startsWith(afterCmd) ||
|
|
68
|
+
t.name.toLowerCase().includes(afterCmd));
|
|
69
|
+
const list = [];
|
|
70
|
+
if (!afterCmd || '/theme'.startsWith(input.trim().toLowerCase())) {
|
|
71
|
+
list.push({
|
|
72
|
+
label: '/theme',
|
|
73
|
+
desc: 'Interactive UI theme picker menu',
|
|
74
|
+
replacement: '/theme',
|
|
75
|
+
replaceStart: 0,
|
|
76
|
+
replaceLen: input.length
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
for (const t of matched) {
|
|
80
|
+
list.push({
|
|
81
|
+
label: `/theme ${t.id}`,
|
|
82
|
+
desc: `${t.emoji} ${t.name} (${t.desc})`,
|
|
83
|
+
replacement: `/theme ${t.id}`,
|
|
84
|
+
replaceStart: 0,
|
|
85
|
+
replaceLen: input.length
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return list;
|
|
89
|
+
}
|
|
59
90
|
if (input.startsWith('/')) {
|
|
60
91
|
const q = input.trim().toLowerCase();
|
|
61
92
|
const filtered = SLASH_COMMANDS.filter(c => c.cmd.toLowerCase().startsWith(q) || q === '/');
|
|
@@ -146,9 +177,9 @@ export function askPrompt(opts = {}) {
|
|
|
146
177
|
const labelStr = item.label.length > 20 ? item.label.slice(0, 19) + '…' : item.label.padEnd(20);
|
|
147
178
|
const maxDescLen = Math.max(6, boxWidth - 25);
|
|
148
179
|
const descStr = item.desc.length > maxDescLen ? item.desc.slice(0, maxDescLen - 3) + '...' : item.desc.padEnd(maxDescLen);
|
|
149
|
-
let row = ` ${isSelected ?
|
|
180
|
+
let row = ` ${isSelected ? theme.colorFn('›') : ' '} ${isSelected ? theme.boldFn(labelStr) : pc.white(labelStr)} ${pc.gray(descStr)} `;
|
|
150
181
|
if (isSelected) {
|
|
151
|
-
row =
|
|
182
|
+
row = theme.badgeFn(`› ${labelStr} ${descStr}`);
|
|
152
183
|
}
|
|
153
184
|
dropdownLines.push(pc.dim('│') + ' ' + pc.dim('│') + row + pc.dim('│'));
|
|
154
185
|
}
|
package/dist/cli/server.js
CHANGED
|
@@ -4,12 +4,17 @@ import fsSync from 'fs';
|
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import os from 'os';
|
|
6
6
|
import { spawn } from 'child_process';
|
|
7
|
+
import pc from 'picocolors';
|
|
8
|
+
import qrcode from 'qrcode-terminal';
|
|
9
|
+
import { getTheme } from './theme.js';
|
|
7
10
|
const MIME_TYPES = {
|
|
8
11
|
'.html': 'text/html; charset=utf-8',
|
|
9
12
|
'.htm': 'text/html; charset=utf-8',
|
|
10
13
|
'.css': 'text/css; charset=utf-8',
|
|
11
14
|
'.js': 'application/javascript; charset=utf-8',
|
|
12
15
|
'.mjs': 'application/javascript; charset=utf-8',
|
|
16
|
+
'.ts': 'text/plain; charset=utf-8',
|
|
17
|
+
'.tsx': 'text/plain; charset=utf-8',
|
|
13
18
|
'.json': 'application/json; charset=utf-8',
|
|
14
19
|
'.png': 'image/png',
|
|
15
20
|
'.jpg': 'image/jpeg',
|
|
@@ -22,6 +27,8 @@ const MIME_TYPES = {
|
|
|
22
27
|
'.mp3': 'audio/mpeg',
|
|
23
28
|
'.ogg': 'audio/ogg',
|
|
24
29
|
'.wasm': 'application/wasm',
|
|
30
|
+
'.pdf': 'application/pdf',
|
|
31
|
+
'.md': 'text/markdown; charset=utf-8',
|
|
25
32
|
'.txt': 'text/plain; charset=utf-8'
|
|
26
33
|
};
|
|
27
34
|
let activeServer = null;
|
|
@@ -45,12 +52,98 @@ export function getServerPort() {
|
|
|
45
52
|
}
|
|
46
53
|
export function stopServer() {
|
|
47
54
|
if (activeServer) {
|
|
48
|
-
|
|
55
|
+
try {
|
|
56
|
+
activeServer.close();
|
|
57
|
+
}
|
|
58
|
+
catch { }
|
|
49
59
|
activeServer = null;
|
|
50
60
|
return true;
|
|
51
61
|
}
|
|
52
62
|
return false;
|
|
53
63
|
}
|
|
64
|
+
function renderDirectoryHtml(dirPath, relPath, files, port) {
|
|
65
|
+
const currentRel = relPath === '/' ? '' : relPath;
|
|
66
|
+
const items = files
|
|
67
|
+
.filter(f => !f.name.startsWith('.') && f.name !== 'node_modules')
|
|
68
|
+
.sort((a, b) => {
|
|
69
|
+
if (a.isDirectory() && !b.isDirectory())
|
|
70
|
+
return -1;
|
|
71
|
+
if (!a.isDirectory() && b.isDirectory())
|
|
72
|
+
return 1;
|
|
73
|
+
return a.name.localeCompare(b.name);
|
|
74
|
+
})
|
|
75
|
+
.map(f => {
|
|
76
|
+
const isDir = f.isDirectory();
|
|
77
|
+
const href = `${currentRel}/${encodeURIComponent(f.name)}${isDir ? '/' : ''}`;
|
|
78
|
+
const ext = path.extname(f.name).toLowerCase();
|
|
79
|
+
let icon = isDir ? '📁' : '📄';
|
|
80
|
+
let badge = '';
|
|
81
|
+
if (ext === '.html' || ext === '.htm') {
|
|
82
|
+
icon = '🌐';
|
|
83
|
+
badge = '<span class="badge">HTML</span>';
|
|
84
|
+
}
|
|
85
|
+
else if (['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp'].includes(ext)) {
|
|
86
|
+
icon = '🖼️';
|
|
87
|
+
}
|
|
88
|
+
else if (['.js', '.ts', '.tsx', '.jsx', '.json'].includes(ext)) {
|
|
89
|
+
icon = '⚡';
|
|
90
|
+
}
|
|
91
|
+
else if (ext === '.css') {
|
|
92
|
+
icon = '🎨';
|
|
93
|
+
}
|
|
94
|
+
return `
|
|
95
|
+
<li>
|
|
96
|
+
<a class="file-item" href="${href}">
|
|
97
|
+
<span class="icon">${icon}</span>
|
|
98
|
+
<span class="name">${f.name}</span>
|
|
99
|
+
${badge}
|
|
100
|
+
<span class="type">${isDir ? 'Directory' : ext || 'File'}</span>
|
|
101
|
+
</a>
|
|
102
|
+
</li>
|
|
103
|
+
`;
|
|
104
|
+
})
|
|
105
|
+
.join('');
|
|
106
|
+
const parentLink = currentRel && currentRel !== '/'
|
|
107
|
+
? `<li><a class="file-item" href="${path.dirname(currentRel) === '/' ? '/' : path.dirname(currentRel) + '/'}"><span class="icon">⬆️</span><span class="name">.. (Parent Directory)</span></a></li>`
|
|
108
|
+
: '';
|
|
109
|
+
return `<!DOCTYPE html>
|
|
110
|
+
<html lang="en">
|
|
111
|
+
<head>
|
|
112
|
+
<meta charset="utf-8"/>
|
|
113
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
114
|
+
<title>devx Live Preview • ${relPath}</title>
|
|
115
|
+
<style>
|
|
116
|
+
* { box-sizing: border-box; }
|
|
117
|
+
body { background: #0a0a0c; color: #e1e4e8; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, monospace; margin: 0; padding: 24px 16px; }
|
|
118
|
+
.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); }
|
|
119
|
+
h1 { color: #00f2fe; margin: 0 0 8px 0; font-size: 22px; display: flex; align-items: center; gap: 8px; }
|
|
120
|
+
.info { color: #8b949e; font-size: 14px; margin-bottom: 20px; word-break: break-all; }
|
|
121
|
+
.info code { background: #1a1a24; padding: 2px 6px; border-radius: 4px; color: #f0f6fc; }
|
|
122
|
+
ul { list-style: none; padding: 0; margin: 0; }
|
|
123
|
+
.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; }
|
|
124
|
+
.file-item:hover { background: #1a1a26; transform: translateX(3px); }
|
|
125
|
+
.icon { margin-right: 12px; font-size: 18px; }
|
|
126
|
+
.name { flex: 1; font-weight: 500; font-size: 14px; word-break: break-all; }
|
|
127
|
+
.type { color: #6e7681; font-size: 12px; margin-left: 12px; }
|
|
128
|
+
.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; }
|
|
129
|
+
.footer { margin-top: 24px; text-align: center; color: #484f58; font-size: 12px; }
|
|
130
|
+
</style>
|
|
131
|
+
</head>
|
|
132
|
+
<body>
|
|
133
|
+
<div class="card">
|
|
134
|
+
<h1>⚡ devx Live Preview</h1>
|
|
135
|
+
<div class="info">
|
|
136
|
+
Path: <code>${relPath}</code> • Port: <code>${port}</code>
|
|
137
|
+
</div>
|
|
138
|
+
<ul>
|
|
139
|
+
${parentLink}
|
|
140
|
+
${items || '<li style="padding: 20px; text-align: center; color: #6e7681;">No visible files in this directory</li>'}
|
|
141
|
+
</ul>
|
|
142
|
+
<div class="footer">devx v1.2.0 • Terminal-Native AI Assistant</div>
|
|
143
|
+
</div>
|
|
144
|
+
</body>
|
|
145
|
+
</html>`;
|
|
146
|
+
}
|
|
54
147
|
export async function startServer(preferredPort = 3000) {
|
|
55
148
|
if (activeServer) {
|
|
56
149
|
stopServer();
|
|
@@ -59,75 +152,122 @@ export async function startServer(preferredPort = 3000) {
|
|
|
59
152
|
return new Promise((resolve, reject) => {
|
|
60
153
|
const server = http.createServer(async (req, res) => {
|
|
61
154
|
try {
|
|
62
|
-
let
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const filePath = path.join(process.cwd(), reqPath);
|
|
155
|
+
let rawPath = req.url?.split('?')[0] || '/';
|
|
156
|
+
let reqPath = decodeURIComponent(rawPath);
|
|
157
|
+
const cwd = process.cwd();
|
|
158
|
+
let targetPath = path.resolve(cwd, '.' + reqPath);
|
|
67
159
|
// Security check: ensure path is within cwd
|
|
68
|
-
|
|
160
|
+
const rel = path.relative(cwd, targetPath);
|
|
161
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
69
162
|
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
70
163
|
res.end('Forbidden');
|
|
71
164
|
return;
|
|
72
165
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
79
|
-
res.end(content);
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
83
|
-
res.end(`404 Not Found: ${reqPath}`);
|
|
166
|
+
// Security check: block dotfiles, hidden directories (.env, .git, etc.) and node_modules
|
|
167
|
+
const segments = reqPath.split(/[\/\\]/).filter(Boolean);
|
|
168
|
+
if (segments.some(seg => (seg.startsWith('.') && seg !== '.') || seg === 'node_modules')) {
|
|
169
|
+
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
170
|
+
res.end('Forbidden');
|
|
84
171
|
return;
|
|
85
172
|
}
|
|
86
|
-
|
|
87
|
-
if (
|
|
88
|
-
const
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
173
|
+
// 1. Root / Directory Request Handling
|
|
174
|
+
if (fsSync.existsSync(targetPath)) {
|
|
175
|
+
const stat = await fs.stat(targetPath);
|
|
176
|
+
if (stat.isDirectory()) {
|
|
177
|
+
// Check for index.html in directory
|
|
178
|
+
const possibleIndexes = [
|
|
179
|
+
path.join(targetPath, 'index.html'),
|
|
180
|
+
path.join(targetPath, 'index.htm'),
|
|
181
|
+
path.join(targetPath, 'public/index.html'),
|
|
182
|
+
path.join(targetPath, 'dist/index.html'),
|
|
183
|
+
path.join(targetPath, 'build/index.html')
|
|
184
|
+
];
|
|
185
|
+
for (const idxPath of possibleIndexes) {
|
|
186
|
+
if (fsSync.existsSync(idxPath)) {
|
|
187
|
+
const content = await fs.readFile(idxPath);
|
|
188
|
+
res.writeHead(200, {
|
|
189
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
190
|
+
'Access-Control-Allow-Origin': '*'
|
|
191
|
+
});
|
|
192
|
+
res.end(content);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// If no index.html, render sleek directory explorer
|
|
197
|
+
const dirents = await fs.readdir(targetPath, { withFileTypes: true });
|
|
198
|
+
const dirHtml = renderDirectoryHtml(targetPath, reqPath, dirents, activePort);
|
|
199
|
+
res.writeHead(200, {
|
|
200
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
201
|
+
'Access-Control-Allow-Origin': '*'
|
|
202
|
+
});
|
|
203
|
+
res.end(dirHtml);
|
|
93
204
|
return;
|
|
94
205
|
}
|
|
95
|
-
|
|
96
|
-
|
|
206
|
+
}
|
|
207
|
+
// 2. Fallback check for missing .html extension
|
|
208
|
+
if (!fsSync.existsSync(targetPath)) {
|
|
209
|
+
const withHtml = `${targetPath}.html`;
|
|
210
|
+
if (fsSync.existsSync(withHtml)) {
|
|
211
|
+
targetPath = withHtml;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// 3. Serve File
|
|
215
|
+
if (fsSync.existsSync(targetPath)) {
|
|
216
|
+
const ext = path.extname(targetPath).toLowerCase();
|
|
217
|
+
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
|
|
218
|
+
const content = await fs.readFile(targetPath);
|
|
219
|
+
res.writeHead(200, {
|
|
220
|
+
'Content-Type': contentType,
|
|
221
|
+
'Access-Control-Allow-Origin': '*'
|
|
222
|
+
});
|
|
223
|
+
res.end(content);
|
|
97
224
|
return;
|
|
98
225
|
}
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
226
|
+
// 4. SPA Fallback: check if root index.html exists
|
|
227
|
+
const rootIndex = path.join(cwd, 'index.html');
|
|
228
|
+
if (fsSync.existsSync(rootIndex)) {
|
|
229
|
+
const content = await fs.readFile(rootIndex);
|
|
230
|
+
res.writeHead(200, {
|
|
231
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
232
|
+
'Access-Control-Allow-Origin': '*'
|
|
233
|
+
});
|
|
234
|
+
res.end(content);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
238
|
+
res.end(`404 Not Found: ${reqPath}`);
|
|
107
239
|
}
|
|
108
240
|
catch (err) {
|
|
109
|
-
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
241
|
+
res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
110
242
|
res.end(`Internal Server Error: ${err.message}`);
|
|
111
243
|
}
|
|
112
244
|
});
|
|
245
|
+
let retryCount = 0;
|
|
113
246
|
server.on('error', (err) => {
|
|
114
247
|
if (err.code === 'EADDRINUSE') {
|
|
115
|
-
|
|
248
|
+
retryCount++;
|
|
249
|
+
if (retryCount > 20) {
|
|
250
|
+
reject(new Error(`Could not find an open port after 20 attempts (started at ${preferredPort})`));
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
116
253
|
activePort++;
|
|
254
|
+
server.listen(activePort, '0.0.0.0');
|
|
117
255
|
}
|
|
118
256
|
else {
|
|
119
257
|
reject(err);
|
|
120
258
|
}
|
|
121
259
|
});
|
|
122
|
-
server.listen(activePort, () => {
|
|
260
|
+
server.listen(activePort, '0.0.0.0', () => {
|
|
123
261
|
activeServer = server;
|
|
124
262
|
const localIp = getLocalIp();
|
|
125
263
|
const localUrl = `http://localhost:${activePort}`;
|
|
126
264
|
const networkUrl = `http://${localIp}:${activePort}`;
|
|
127
|
-
// If
|
|
265
|
+
// If on Android Termux, attempt to open browser
|
|
128
266
|
if (process.env.PREFIX?.includes('com.termux')) {
|
|
129
267
|
try {
|
|
130
|
-
spawn('termux-open-url', [localUrl], { stdio: 'ignore' });
|
|
268
|
+
const opener = spawn('termux-open-url', [localUrl], { stdio: 'ignore', detached: true });
|
|
269
|
+
opener.on('error', () => { });
|
|
270
|
+
opener.unref();
|
|
131
271
|
}
|
|
132
272
|
catch { }
|
|
133
273
|
}
|
|
@@ -135,3 +275,41 @@ export async function startServer(preferredPort = 3000) {
|
|
|
135
275
|
});
|
|
136
276
|
});
|
|
137
277
|
}
|
|
278
|
+
export function getQrCodeString(text) {
|
|
279
|
+
return new Promise((resolve) => {
|
|
280
|
+
try {
|
|
281
|
+
qrcode.generate(text, { small: true }, (qr) => {
|
|
282
|
+
resolve(qr);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
resolve('');
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
export async function displayServerBanner(localUrl, networkUrl) {
|
|
291
|
+
const th = getTheme();
|
|
292
|
+
const qr = await getQrCodeString(networkUrl);
|
|
293
|
+
const cols = Math.min(process.stdout.columns || 80, 80);
|
|
294
|
+
const cardWidth = Math.max(36, Math.min(cols - 4, 66));
|
|
295
|
+
const innerWidth = cardWidth - 2;
|
|
296
|
+
const title = ' 🌐 Live Web Preview ';
|
|
297
|
+
const topFill = Math.max(2, cardWidth - 3 - title.length);
|
|
298
|
+
console.log('\n' + th.colorFn('┌─') + pc.bold(title) + th.colorFn('─'.repeat(topFill) + '┐'));
|
|
299
|
+
const printRow = (content) => {
|
|
300
|
+
const visibleLength = content.replace(/\u001b\[[0-9;]*m/g, '').length;
|
|
301
|
+
const padding = Math.max(0, innerWidth - visibleLength);
|
|
302
|
+
console.log(th.colorFn('│') + content + ' '.repeat(padding) + th.colorFn('│'));
|
|
303
|
+
};
|
|
304
|
+
printRow(` ${pc.bold('Local:')} ${pc.cyan(localUrl)}`);
|
|
305
|
+
printRow(` ${pc.bold('Network:')} ${pc.green(networkUrl)}`);
|
|
306
|
+
printRow(' ');
|
|
307
|
+
printRow(` ${pc.bold('📱 Mobile QR:')}`);
|
|
308
|
+
if (qr) {
|
|
309
|
+
const qrLines = qr.trim().split('\n');
|
|
310
|
+
for (const ql of qrLines) {
|
|
311
|
+
printRow(` ${ql}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
console.log(th.colorFn('└' + '─'.repeat(innerWidth) + '┘\n'));
|
|
315
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
export const THEMES = {
|
|
3
|
+
cyan: {
|
|
4
|
+
id: 'cyan',
|
|
5
|
+
name: 'Cyan Cyber',
|
|
6
|
+
desc: 'Electric neon cyan & obsidian (Default)',
|
|
7
|
+
emoji: '⚡',
|
|
8
|
+
colorFn: (s) => pc.cyan(s),
|
|
9
|
+
boldFn: (s) => pc.bold(pc.cyan(s)),
|
|
10
|
+
accentFn: (s) => pc.blue(s),
|
|
11
|
+
badgeFn: (s) => pc.bgCyan(pc.black(` ${s} `)),
|
|
12
|
+
diffAddBg: (s) => pc.bgCyan(pc.black(s)),
|
|
13
|
+
diffRemoveBg: (s) => pc.bgBlue(pc.white(s)),
|
|
14
|
+
hex: '#00f2fe'
|
|
15
|
+
},
|
|
16
|
+
purple: {
|
|
17
|
+
id: 'purple',
|
|
18
|
+
name: 'Synthwave Purple',
|
|
19
|
+
desc: 'Vibrant neon magenta & violet retro',
|
|
20
|
+
emoji: '🟣',
|
|
21
|
+
colorFn: (s) => pc.magenta(s),
|
|
22
|
+
boldFn: (s) => pc.bold(pc.magenta(s)),
|
|
23
|
+
accentFn: (s) => pc.blue(s),
|
|
24
|
+
badgeFn: (s) => pc.bgMagenta(pc.black(` ${s} `)),
|
|
25
|
+
diffAddBg: (s) => pc.bgMagenta(pc.black(s)),
|
|
26
|
+
diffRemoveBg: (s) => pc.bgBlue(pc.white(s)),
|
|
27
|
+
hex: '#d946ef'
|
|
28
|
+
},
|
|
29
|
+
matrix: {
|
|
30
|
+
id: 'matrix',
|
|
31
|
+
name: 'Matrix Hacker',
|
|
32
|
+
desc: 'Classic bright phosphor green terminal',
|
|
33
|
+
emoji: '🟢',
|
|
34
|
+
colorFn: (s) => pc.green(s),
|
|
35
|
+
boldFn: (s) => pc.bold(pc.green(s)),
|
|
36
|
+
accentFn: (s) => pc.cyan(s),
|
|
37
|
+
badgeFn: (s) => pc.bgGreen(pc.black(` ${s} `)),
|
|
38
|
+
diffAddBg: (s) => pc.bgGreen(pc.black(s)),
|
|
39
|
+
diffRemoveBg: (s) => pc.bgRed(pc.white(s)),
|
|
40
|
+
hex: '#22c55e'
|
|
41
|
+
},
|
|
42
|
+
amber: {
|
|
43
|
+
id: 'amber',
|
|
44
|
+
name: 'Solar Amber',
|
|
45
|
+
desc: 'Warm vintage CRT amber gold',
|
|
46
|
+
emoji: '🟡',
|
|
47
|
+
colorFn: (s) => pc.yellow(s),
|
|
48
|
+
boldFn: (s) => pc.bold(pc.yellow(s)),
|
|
49
|
+
accentFn: (s) => pc.red(s),
|
|
50
|
+
badgeFn: (s) => pc.bgYellow(pc.black(` ${s} `)),
|
|
51
|
+
diffAddBg: (s) => pc.bgYellow(pc.black(s)),
|
|
52
|
+
diffRemoveBg: (s) => pc.bgRed(pc.white(s)),
|
|
53
|
+
hex: '#f59e0b'
|
|
54
|
+
},
|
|
55
|
+
crimson: {
|
|
56
|
+
id: 'crimson',
|
|
57
|
+
name: 'Ruby Crimson',
|
|
58
|
+
desc: 'Aggressive cyberpunk scarlet red',
|
|
59
|
+
emoji: '🔴',
|
|
60
|
+
colorFn: (s) => pc.red(s),
|
|
61
|
+
boldFn: (s) => pc.bold(pc.red(s)),
|
|
62
|
+
accentFn: (s) => pc.magenta(s),
|
|
63
|
+
badgeFn: (s) => pc.bgRed(pc.white(` ${s} `)),
|
|
64
|
+
diffAddBg: (s) => pc.bgRed(pc.white(s)),
|
|
65
|
+
diffRemoveBg: (s) => pc.bgMagenta(pc.white(s)),
|
|
66
|
+
hex: '#ef4444'
|
|
67
|
+
},
|
|
68
|
+
monochrome: {
|
|
69
|
+
id: 'monochrome',
|
|
70
|
+
name: 'Pure Monochrome',
|
|
71
|
+
desc: 'Crisp minimal pure white & gray',
|
|
72
|
+
emoji: '⚪',
|
|
73
|
+
colorFn: (s) => pc.white(s),
|
|
74
|
+
boldFn: (s) => pc.bold(pc.white(s)),
|
|
75
|
+
accentFn: (s) => pc.dim(s),
|
|
76
|
+
badgeFn: (s) => pc.bgWhite(pc.black(` ${s} `)),
|
|
77
|
+
diffAddBg: (s) => pc.bgWhite(pc.black(s)),
|
|
78
|
+
diffRemoveBg: (s) => pc.bgBlack(pc.white(s)),
|
|
79
|
+
hex: '#ffffff'
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const THEME_ALIASES = {
|
|
83
|
+
'1': 'cyan',
|
|
84
|
+
'2': 'purple',
|
|
85
|
+
'3': 'matrix',
|
|
86
|
+
'4': 'amber',
|
|
87
|
+
'5': 'crimson',
|
|
88
|
+
'6': 'monochrome',
|
|
89
|
+
'blue': 'cyan',
|
|
90
|
+
'neon': 'cyan',
|
|
91
|
+
'cyber': 'cyan',
|
|
92
|
+
'magenta': 'purple',
|
|
93
|
+
'violet': 'purple',
|
|
94
|
+
'pink': 'purple',
|
|
95
|
+
'synthwave': 'purple',
|
|
96
|
+
'green': 'matrix',
|
|
97
|
+
'hacker': 'matrix',
|
|
98
|
+
'terminal': 'matrix',
|
|
99
|
+
'yellow': 'amber',
|
|
100
|
+
'gold': 'amber',
|
|
101
|
+
'solar': 'amber',
|
|
102
|
+
'orange': 'amber',
|
|
103
|
+
'red': 'crimson',
|
|
104
|
+
'ruby': 'crimson',
|
|
105
|
+
'cyberpunk': 'crimson',
|
|
106
|
+
'white': 'monochrome',
|
|
107
|
+
'mono': 'monochrome',
|
|
108
|
+
'gray': 'monochrome',
|
|
109
|
+
'grey': 'monochrome'
|
|
110
|
+
};
|
|
111
|
+
let activeThemeId = 'cyan';
|
|
112
|
+
export function getTheme(themeId) {
|
|
113
|
+
const id = (themeId || activeThemeId).toLowerCase().trim();
|
|
114
|
+
return THEMES[id] || (THEME_ALIASES[id] && THEMES[THEME_ALIASES[id]]) || THEMES.cyan;
|
|
115
|
+
}
|
|
116
|
+
export function findTheme(query) {
|
|
117
|
+
const q = (query || '').toLowerCase().trim();
|
|
118
|
+
if (!q)
|
|
119
|
+
return null;
|
|
120
|
+
if (THEMES[q])
|
|
121
|
+
return THEMES[q];
|
|
122
|
+
if (THEME_ALIASES[q] && THEMES[THEME_ALIASES[q]])
|
|
123
|
+
return THEMES[THEME_ALIASES[q]];
|
|
124
|
+
const all = listThemes();
|
|
125
|
+
const found = all.find(t => t.id.toLowerCase() === q ||
|
|
126
|
+
t.id.toLowerCase().startsWith(q) ||
|
|
127
|
+
t.name.toLowerCase().includes(q));
|
|
128
|
+
return found || null;
|
|
129
|
+
}
|
|
130
|
+
export function setActiveTheme(themeId) {
|
|
131
|
+
const theme = getTheme(themeId);
|
|
132
|
+
activeThemeId = theme.id;
|
|
133
|
+
return theme;
|
|
134
|
+
}
|
|
135
|
+
export function getCurrentTheme() {
|
|
136
|
+
return THEMES[activeThemeId] || THEMES.cyan;
|
|
137
|
+
}
|
|
138
|
+
export function listThemes() {
|
|
139
|
+
return Object.values(THEMES);
|
|
140
|
+
}
|
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;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
export function isTermux() {
|
|
3
|
+
return !!process.env.PREFIX?.includes('com.termux');
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Sends a notification and haptic vibration to the device.
|
|
7
|
+
* On Termux: uses termux-notification and termux-vibrate via termux-api.
|
|
8
|
+
* On Desktop: emits terminal bell \u0007.
|
|
9
|
+
*/
|
|
10
|
+
export function notifyDevice(title, message, options = { vibrate: true, sound: true }) {
|
|
11
|
+
if (isTermux()) {
|
|
12
|
+
try {
|
|
13
|
+
// 1. Android notification via Termux API
|
|
14
|
+
const notif = spawn('termux-notification', [
|
|
15
|
+
'--title', title,
|
|
16
|
+
'--content', message,
|
|
17
|
+
'--id', 'devx_task_notif',
|
|
18
|
+
'--priority', 'high'
|
|
19
|
+
], { stdio: 'ignore', detached: true });
|
|
20
|
+
notif.on('error', () => { });
|
|
21
|
+
notif.unref();
|
|
22
|
+
// 2. Haptic vibration (150ms)
|
|
23
|
+
if (options.vibrate !== false) {
|
|
24
|
+
const vib = spawn('termux-vibrate', ['-d', '150'], { stdio: 'ignore', detached: true });
|
|
25
|
+
vib.on('error', () => { });
|
|
26
|
+
vib.unref();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch { }
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
// Desktop terminal bell
|
|
33
|
+
if (options.sound !== false) {
|
|
34
|
+
try {
|
|
35
|
+
process.stdout.write('\u0007');
|
|
36
|
+
}
|
|
37
|
+
catch { }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
package/dist/core/snapshot.js
CHANGED
|
@@ -36,12 +36,8 @@ export class SnapshotManager {
|
|
|
36
36
|
});
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
-
catch {
|
|
40
|
-
|
|
41
|
-
filePath: resolved,
|
|
42
|
-
existed: false,
|
|
43
|
-
content: null
|
|
44
|
-
});
|
|
39
|
+
catch (err) {
|
|
40
|
+
throw new Error(`Cannot safely snapshot file ${resolved} before edit: ${err.message}`);
|
|
45
41
|
}
|
|
46
42
|
}
|
|
47
43
|
finishTurn() {
|
package/dist/prompts/builder.js
CHANGED
|
@@ -41,8 +41,9 @@ export async function buildSystemPrompt(planMode) {
|
|
|
41
41
|
prompt += `- Whenever you modify or create files, use the 'diagnose_code' tool to check for any syntax or type errors.\n`;
|
|
42
42
|
prompt += `- If any error is found, automatically fix it with 'edit_file' until all diagnostics pass cleanly.\n`;
|
|
43
43
|
prompt += `- If you need dependencies, use 'install_package' to install them cleanly.\n`;
|
|
44
|
+
prompt += `- Whenever you create or modify web applications, sites, HTML/CSS/JS, canvas games, or React/Vite frontend apps, automatically call the 'serve_preview' tool with action='start' to start the local preview server and display the mobile QR-code for the user!\n`;
|
|
44
45
|
prompt += `- Use 'save_memory' to remember important architectural decisions, user preferences, or project rules.\n`;
|
|
45
|
-
prompt +=
|
|
46
|
+
prompt += `- Always explain your actions briefly before using tools.\n`;
|
|
46
47
|
}
|
|
47
48
|
// Load Project Memory Bank
|
|
48
49
|
try {
|
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
|
}
|