termux-dev 1.1.2 ā 1.2.1
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 +191 -41
- package/dist/cli/prompt.js +34 -3
- package/dist/cli/server.js +56 -1
- package/dist/cli/theme.js +140 -0
- package/dist/core/notify.js +40 -0
- package/dist/core/snapshot.js +2 -6
- package/dist/prompts/builder.js +2 -1
- package/dist/tools/fs.js +21 -9
- package/dist/tools/index.js +2 -2
- package/dist/tools/server.js +4 -3
- package/package.json +4 -2
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.
|
|
28
|
+
THE TERMINAL-NATIVE AI CODING AGENT <tspan fill="#00f2fe" font-weight="bold">v1.2.0</tspan>
|
|
29
29
|
</text>
|
|
30
30
|
|
|
31
31
|
<!-- Top Accent Line -->
|
package/assets/preview.png
CHANGED
|
Binary file
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
import https from 'https';
|
|
4
|
+
import pc from 'picocolors';
|
|
5
|
+
import { getCurrentTheme } from './theme.js';
|
|
6
|
+
import { isTermux } from '../core/notify.js';
|
|
7
|
+
function checkCmd(cmd) {
|
|
8
|
+
try {
|
|
9
|
+
const out = execSync(cmd, { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' }).trim();
|
|
10
|
+
return out.split('\n')[0];
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
async function pingUrl(url) {
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
const req = https.get(url, { timeout: 3000 }, (res) => {
|
|
19
|
+
resolve(res.statusCode !== undefined);
|
|
20
|
+
});
|
|
21
|
+
req.on('error', () => resolve(false));
|
|
22
|
+
req.on('timeout', () => {
|
|
23
|
+
req.destroy();
|
|
24
|
+
resolve(false);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export async function runDoctor(config) {
|
|
29
|
+
const theme = getCurrentTheme();
|
|
30
|
+
console.log('\n' + theme.boldFn('𩺠devx Doctor ā System & Environment Health Diagnostics'));
|
|
31
|
+
console.log(pc.dim('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
|
32
|
+
const envChecks = [];
|
|
33
|
+
const toolChecks = [];
|
|
34
|
+
const keyChecks = [];
|
|
35
|
+
const netChecks = [];
|
|
36
|
+
// 1. Environment Checks
|
|
37
|
+
const nodeVer = process.version;
|
|
38
|
+
const nodeMajor = parseInt(nodeVer.replace(/^v/, '').split('.')[0], 10);
|
|
39
|
+
envChecks.push({
|
|
40
|
+
name: 'Node.js Runtime',
|
|
41
|
+
status: nodeMajor >= 20 ? 'ok' : 'warn',
|
|
42
|
+
details: `${nodeVer} (Recommended: >= 20.0)`
|
|
43
|
+
});
|
|
44
|
+
const termux = isTermux();
|
|
45
|
+
envChecks.push({
|
|
46
|
+
name: 'Platform / Environment',
|
|
47
|
+
status: 'ok',
|
|
48
|
+
details: termux ? 'Android (Termux ARM64)' : `${os.platform()} ${os.arch()} (${os.release()})`
|
|
49
|
+
});
|
|
50
|
+
const cwd = process.cwd();
|
|
51
|
+
envChecks.push({
|
|
52
|
+
name: 'Working Directory',
|
|
53
|
+
status: 'ok',
|
|
54
|
+
details: cwd
|
|
55
|
+
});
|
|
56
|
+
// 2. Essential Tools
|
|
57
|
+
const gitVer = checkCmd('git --version');
|
|
58
|
+
toolChecks.push({
|
|
59
|
+
name: 'Git VCS',
|
|
60
|
+
status: gitVer ? 'ok' : 'warn',
|
|
61
|
+
details: gitVer || 'Not installed (some git features disabled)'
|
|
62
|
+
});
|
|
63
|
+
const npmVer = checkCmd('npm --version');
|
|
64
|
+
toolChecks.push({
|
|
65
|
+
name: 'NPM Package Manager',
|
|
66
|
+
status: npmVer ? 'ok' : 'warn',
|
|
67
|
+
details: npmVer ? `v${npmVer}` : 'Not installed'
|
|
68
|
+
});
|
|
69
|
+
const pythonVer = checkCmd('python --version') || checkCmd('python3 --version');
|
|
70
|
+
toolChecks.push({
|
|
71
|
+
name: 'Python Runtime',
|
|
72
|
+
status: pythonVer ? 'ok' : 'warn',
|
|
73
|
+
details: pythonVer || 'Optional (not found)'
|
|
74
|
+
});
|
|
75
|
+
const cCompiler = checkCmd('clang --version') || checkCmd('gcc --version');
|
|
76
|
+
toolChecks.push({
|
|
77
|
+
name: 'C/C++ Compiler',
|
|
78
|
+
status: cCompiler ? 'ok' : 'warn',
|
|
79
|
+
details: cCompiler ? cCompiler.split(' ')[0] : 'Optional (not found)'
|
|
80
|
+
});
|
|
81
|
+
// 3. AI Providers & Keys
|
|
82
|
+
const activeProvider = config.provider || 'openrouter';
|
|
83
|
+
const hasActiveKey = !!config.apiKey;
|
|
84
|
+
keyChecks.push({
|
|
85
|
+
name: `Active Provider (${activeProvider})`,
|
|
86
|
+
status: hasActiveKey ? 'ok' : 'fail',
|
|
87
|
+
details: hasActiveKey ? `Configured (Model: ${config.model})` : 'Missing API Key! Set via /provider or ~/.devxrc.json'
|
|
88
|
+
});
|
|
89
|
+
const envOpenRouter = !!process.env.OPENROUTER_API_KEY;
|
|
90
|
+
const envOpenAI = !!process.env.OPENAI_API_KEY;
|
|
91
|
+
const envAnthropic = !!process.env.ANTHROPIC_API_KEY;
|
|
92
|
+
const envGoogle = !!process.env.GEMINI_API_KEY;
|
|
93
|
+
keyChecks.push({
|
|
94
|
+
name: 'OpenRouter Key',
|
|
95
|
+
status: envOpenRouter || config.provider === 'openrouter' && hasActiveKey ? 'ok' : 'warn',
|
|
96
|
+
details: envOpenRouter || (config.provider === 'openrouter' && hasActiveKey) ? 'Configured' : 'Not set'
|
|
97
|
+
});
|
|
98
|
+
keyChecks.push({
|
|
99
|
+
name: 'OpenAI / Gemini / Anthropic Keys',
|
|
100
|
+
status: envOpenAI || envAnthropic || envGoogle ? 'ok' : 'warn',
|
|
101
|
+
details: [envOpenAI && 'OpenAI', envAnthropic && 'Anthropic', envGoogle && 'Gemini'].filter(Boolean).join(', ') || 'Not set in env (using default provider)'
|
|
102
|
+
});
|
|
103
|
+
// 4. Network Connectivity
|
|
104
|
+
const openRouterOnline = await pingUrl('https://openrouter.ai');
|
|
105
|
+
netChecks.push({
|
|
106
|
+
name: 'OpenRouter API Connectivity',
|
|
107
|
+
status: openRouterOnline ? 'ok' : 'warn',
|
|
108
|
+
details: openRouterOnline ? 'Online (HTTP 200/reachable)' : 'Unreachable (check VPN/DNS/WiFi)'
|
|
109
|
+
});
|
|
110
|
+
const githubOnline = await pingUrl('https://api.github.com');
|
|
111
|
+
netChecks.push({
|
|
112
|
+
name: 'GitHub API (for updater)',
|
|
113
|
+
status: githubOnline ? 'ok' : 'warn',
|
|
114
|
+
details: githubOnline ? 'Online (reachable)' : 'Unreachable'
|
|
115
|
+
});
|
|
116
|
+
// Helper to render section
|
|
117
|
+
const renderSection = (title, items) => {
|
|
118
|
+
console.log('\n' + theme.colorFn(pc.bold(`⢠${title}`)));
|
|
119
|
+
for (const item of items) {
|
|
120
|
+
let icon = pc.green('ā');
|
|
121
|
+
if (item.status === 'warn')
|
|
122
|
+
icon = pc.yellow('ā²');
|
|
123
|
+
if (item.status === 'fail')
|
|
124
|
+
icon = pc.red('ā');
|
|
125
|
+
console.log(` ${icon} ${pc.bold(item.name)}: ${pc.dim(item.details)}`);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
renderSection('Environment & OS', envChecks);
|
|
129
|
+
renderSection('Developer Tools', toolChecks);
|
|
130
|
+
renderSection('AI Configuration & Keys', keyChecks);
|
|
131
|
+
renderSection('Network Connectivity', netChecks);
|
|
132
|
+
console.log('\n' + pc.dim('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
|
133
|
+
const hasFailures = [...envChecks, ...toolChecks, ...keyChecks, ...netChecks].some(i => i.status === 'fail');
|
|
134
|
+
if (hasFailures) {
|
|
135
|
+
console.log(pc.red(pc.bold('ā Doctor found critical issues that might prevent devx from functioning properly.')));
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
console.log(theme.boldFn('š Everything looks healthy! devx is fully configured and ready for vibe-coding.'));
|
|
139
|
+
}
|
|
140
|
+
console.log();
|
|
141
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import os from 'os';
|
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import fs from 'fs/promises';
|
|
8
8
|
import fsSync from 'fs';
|
|
9
|
-
import { execSync } from 'child_process';
|
|
9
|
+
import { execSync, execFileSync } from 'child_process';
|
|
10
10
|
import { History } from '../core/history.js';
|
|
11
11
|
import { Agent } from '../core/loop.js';
|
|
12
12
|
import { buildSystemPrompt } from '../prompts/builder.js';
|
|
@@ -15,7 +15,9 @@ import { getTools, lastPlanReady, resetPlanReady } from '../tools/index.js';
|
|
|
15
15
|
import { CLIConsoleGuard } from '../permissions/guard.js';
|
|
16
16
|
import { globalSnapshotManager } from '../core/snapshot.js';
|
|
17
17
|
import { MemoryManager } from '../core/memory.js';
|
|
18
|
-
import { startServer, stopServer } from './server.js';
|
|
18
|
+
import { startServer, stopServer, displayServerBanner } from './server.js';
|
|
19
|
+
import { notifyDevice } from '../core/notify.js';
|
|
20
|
+
import { runDoctor } from './doctor.js';
|
|
19
21
|
import { ALL_PROVIDERS } from './providers.js';
|
|
20
22
|
import { getModelContextLimit } from '../core/models.js';
|
|
21
23
|
import { SessionManager } from '../core/session.js';
|
|
@@ -24,6 +26,7 @@ import { SmoothStreamer } from './smooth.js';
|
|
|
24
26
|
import { askPrompt } from './prompt.js';
|
|
25
27
|
import { resolveAtMentions } from './files.js';
|
|
26
28
|
import { runStartupUpdateCheck, checkForUpdates, performSelfUpdate } from './updater.js';
|
|
29
|
+
import { setActiveTheme, getCurrentTheme, listThemes, findTheme } from './theme.js';
|
|
27
30
|
const CONFIG_PATH = path.join(os.homedir(), '.devxrc.json');
|
|
28
31
|
function maskApiKey(key) {
|
|
29
32
|
if (!key)
|
|
@@ -348,6 +351,10 @@ function resetTerminalTheme() {
|
|
|
348
351
|
}
|
|
349
352
|
let activeAbortHandler = null;
|
|
350
353
|
process.on('exit', () => {
|
|
354
|
+
if (process.stdin.isTTY) {
|
|
355
|
+
process.stdin.setRawMode(false);
|
|
356
|
+
}
|
|
357
|
+
process.stdout.write('\x1B[?25h');
|
|
351
358
|
resetTerminalTheme();
|
|
352
359
|
});
|
|
353
360
|
process.on('SIGINT', () => {
|
|
@@ -355,6 +362,11 @@ process.on('SIGINT', () => {
|
|
|
355
362
|
activeAbortHandler();
|
|
356
363
|
}
|
|
357
364
|
else {
|
|
365
|
+
if (process.stdin.isTTY) {
|
|
366
|
+
process.stdin.setRawMode(false);
|
|
367
|
+
process.stdin.pause();
|
|
368
|
+
}
|
|
369
|
+
process.stdout.write('\x1B[?25h'); // restore cursor
|
|
358
370
|
resetTerminalTheme();
|
|
359
371
|
process.exit(0);
|
|
360
372
|
}
|
|
@@ -371,14 +383,15 @@ function clearTerminalScreen() {
|
|
|
371
383
|
function drawLogo() {
|
|
372
384
|
const cols = process.stdout.columns || 80;
|
|
373
385
|
clearTerminalScreen();
|
|
386
|
+
const theme = getCurrentTheme();
|
|
374
387
|
if (cols < 56) {
|
|
375
388
|
// Ultra-clean compact ASCII for small mobile screens (width: ~26 chars)
|
|
376
389
|
const logo = [
|
|
377
390
|
'',
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
' ' +
|
|
391
|
+
theme.colorFn(' āāāā āāāā ā ā ā ā'),
|
|
392
|
+
theme.colorFn(' ā ā āāāā āāā āāā '),
|
|
393
|
+
theme.colorFn(' āāāā āāāā ā ā ā '),
|
|
394
|
+
' ' + theme.boldFn('v1.2.0'),
|
|
382
395
|
''
|
|
383
396
|
];
|
|
384
397
|
for (const line of logo) {
|
|
@@ -390,10 +403,10 @@ function drawLogo() {
|
|
|
390
403
|
const indent = cols < 68 ? ' ' : ' ';
|
|
391
404
|
const logo = [
|
|
392
405
|
'',
|
|
393
|
-
indent +
|
|
394
|
-
indent +
|
|
395
|
-
indent +
|
|
396
|
-
indent +
|
|
406
|
+
indent + theme.colorFn('āāāāāāā āāāā āāāā āā āā ā ā āā āā āāāā āāāā ā ā'),
|
|
407
|
+
indent + theme.colorFn(' ā āāāā āāāā ā ā ā ā ā ā āā ā ā āāāā ā ā'),
|
|
408
|
+
indent + theme.colorFn(' ā āāāā ā āā ā ā āāāā āā āā āāāā āāāā āāā '),
|
|
409
|
+
indent + theme.boldFn('v1.2.0'),
|
|
397
410
|
''
|
|
398
411
|
];
|
|
399
412
|
for (const line of logo) {
|
|
@@ -520,6 +533,39 @@ export async function main() {
|
|
|
520
533
|
const options = program.opts();
|
|
521
534
|
let planMode = !!options.plan;
|
|
522
535
|
let config = await loadConfig();
|
|
536
|
+
if (!config.onboarded) {
|
|
537
|
+
const cols = Math.min(process.stdout.columns || 40, 42);
|
|
538
|
+
const fill = Math.max(2, cols - 16);
|
|
539
|
+
const topBorder = 'āā preview.ts ' + 'ā'.repeat(fill) + 'ā';
|
|
540
|
+
const bottomBorder = 'ā' + 'ā'.repeat(cols - 2) + 'ā';
|
|
541
|
+
const themeChoices = listThemes().map(t => {
|
|
542
|
+
const addLine = t.diffAddBg(` + 1 | const theme = "${t.id}";`.padEnd(cols - 2));
|
|
543
|
+
const remLine = t.diffRemoveBg(` - 2 | const theme = "none";`.padEnd(cols - 2));
|
|
544
|
+
const preview = `${t.colorFn(topBorder)}\n${addLine}\n${remLine}\n${t.colorFn(bottomBorder)}\n\n${pc.dim('š” You can change this anytime with /theme')}`;
|
|
545
|
+
return {
|
|
546
|
+
name: `${t.emoji} ${t.boldFn(t.name.padEnd(18))} ${pc.dim(t.desc)}`,
|
|
547
|
+
value: t.id,
|
|
548
|
+
description: preview
|
|
549
|
+
};
|
|
550
|
+
});
|
|
551
|
+
try {
|
|
552
|
+
clearTerminalScreen();
|
|
553
|
+
const selected = await search({
|
|
554
|
+
message: `${pc.bold('šØ Pick a theme to personalize your workspace:')}`,
|
|
555
|
+
source: async () => themeChoices,
|
|
556
|
+
pageSize: 6
|
|
557
|
+
});
|
|
558
|
+
if (selected) {
|
|
559
|
+
config.theme = selected;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
catch { }
|
|
563
|
+
config.onboarded = true;
|
|
564
|
+
await saveConfig(config);
|
|
565
|
+
}
|
|
566
|
+
if (config.theme) {
|
|
567
|
+
setActiveTheme(config.theme);
|
|
568
|
+
}
|
|
523
569
|
enableDarkTheme(config.pureBlackTheme !== false);
|
|
524
570
|
let history = new History();
|
|
525
571
|
const sysPrompt = await buildSystemPrompt(planMode);
|
|
@@ -546,6 +592,7 @@ export async function main() {
|
|
|
546
592
|
const tokenStats = `Context: ${formatTokens(currentTokens)} / ${formatTokens(maxTokens)} (${usagePercent}%) ⢠${costStr}`;
|
|
547
593
|
const cols = process.stdout.columns || 80;
|
|
548
594
|
const modeName = planMode ? 'PLAN' : 'AGENT';
|
|
595
|
+
const theme = getCurrentTheme();
|
|
549
596
|
// Shorten model name if too long on narrow mobile screens
|
|
550
597
|
let displayModel = config.model;
|
|
551
598
|
if (cols < 75 && displayModel.length > 20) {
|
|
@@ -555,7 +602,7 @@ export async function main() {
|
|
|
555
602
|
displayModel = displayModel.slice(0, 17) + '...';
|
|
556
603
|
}
|
|
557
604
|
}
|
|
558
|
-
const badge =
|
|
605
|
+
const badge = theme.badgeFn(`devx | ${modeName} | ${displayModel}`);
|
|
559
606
|
if (cols < 75) {
|
|
560
607
|
// 2-line layout for mobile screens: perfectly aligned with clack box borders
|
|
561
608
|
p.intro(`${badge}\n${pc.dim('ā')} ${pc.dim(tokenStats)}`);
|
|
@@ -568,7 +615,7 @@ export async function main() {
|
|
|
568
615
|
if (autoTriggerPrompt) {
|
|
569
616
|
answer = autoTriggerPrompt;
|
|
570
617
|
autoTriggerPrompt = '';
|
|
571
|
-
console.log(
|
|
618
|
+
console.log(theme.colorFn('ā') + ' ' + pc.bold(pc.white(answer)));
|
|
572
619
|
}
|
|
573
620
|
else {
|
|
574
621
|
const inputStr = await askPrompt({
|
|
@@ -609,16 +656,44 @@ export async function main() {
|
|
|
609
656
|
if (answer.startsWith('/')) {
|
|
610
657
|
const parts = answer.split(' ');
|
|
611
658
|
let cmd = parts[0];
|
|
659
|
+
async function handleThemeSelect(config) {
|
|
660
|
+
const currentTh = getCurrentTheme();
|
|
661
|
+
const themeChoices = listThemes().map(t => ({
|
|
662
|
+
name: `${t.emoji} ${t.boldFn(t.name.padEnd(18))} ${pc.dim(t.desc)} ${t.id === currentTh.id ? pc.green('(Active)') : ''}`,
|
|
663
|
+
value: t.id,
|
|
664
|
+
description: `Apply ${t.name} color palette (${t.hex}) to banners, prompts, and actions`
|
|
665
|
+
}));
|
|
666
|
+
try {
|
|
667
|
+
const selected = await select({
|
|
668
|
+
message: `${pc.bold('šØ Select UI Theme / ŠŃŠ±ŠµŃŠøŃе ŃŠ²ŠµŃовŃŃ ŃŠµŠ¼Ń:')}`,
|
|
669
|
+
choices: themeChoices
|
|
670
|
+
});
|
|
671
|
+
if (selected) {
|
|
672
|
+
config.theme = selected;
|
|
673
|
+
await saveConfig(config);
|
|
674
|
+
const th = setActiveTheme(selected);
|
|
675
|
+
drawLogo();
|
|
676
|
+
p.log.success(th.boldFn(`šØ Theme switched to ${th.emoji} ${th.name}!`));
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
catch { }
|
|
680
|
+
}
|
|
612
681
|
async function handleSettings(config) {
|
|
613
682
|
while (true) {
|
|
614
683
|
try {
|
|
615
684
|
const maxIter = config.maxIterations || 100;
|
|
616
685
|
const maxIterLabel = maxIter >= 9999 ? 'Unlimited' : `${maxIter} steps`;
|
|
686
|
+
const currentTh = getCurrentTheme();
|
|
617
687
|
const choice = await select({
|
|
618
|
-
message: `${pc.bold('āļø Settings')} ${pc.dim(
|
|
688
|
+
message: `${pc.bold('āļø Settings')} ${pc.dim(`(devx v1.2.0 ⢠theme: ${currentTh.name})`)}`,
|
|
619
689
|
choices: [
|
|
620
690
|
{
|
|
621
|
-
name:
|
|
691
|
+
name: `šØ Color Theme: ${currentTh.emoji} ${currentTh.name}`,
|
|
692
|
+
value: 'change_theme',
|
|
693
|
+
description: `Switch UI accent colors (${currentTh.desc})`
|
|
694
|
+
},
|
|
695
|
+
{
|
|
696
|
+
name: `${config.pureBlackTheme !== false ? pc.green('š¤ Pure Black Background: ON') : pc.yellow('š¤ Pure Black Background: OFF')}`,
|
|
622
697
|
value: 'toggle_black_theme',
|
|
623
698
|
description: config.pureBlackTheme !== false
|
|
624
699
|
? 'Apply deep OLED obsidian black background (#0a0a0c) like OpenCode'
|
|
@@ -646,12 +721,12 @@ export async function main() {
|
|
|
646
721
|
: 'Disable update checking on startup (run /update manually instead)'
|
|
647
722
|
},
|
|
648
723
|
{
|
|
649
|
-
name: `š Max Agent Iterations: ${
|
|
724
|
+
name: `š Max Agent Iterations: ${currentTh.colorFn(maxIterLabel)}`,
|
|
650
725
|
value: 'change_max_iterations',
|
|
651
726
|
description: 'Limit how many tool steps (file edits, terminal commands) agent can do per request'
|
|
652
727
|
},
|
|
653
728
|
{
|
|
654
|
-
name: `${
|
|
729
|
+
name: `${currentTh.colorFn('⨠About devx')} ${pc.dim('(v1.2.0 by ApvCode)')}`,
|
|
655
730
|
value: 'about',
|
|
656
731
|
description: 'Terminal-Native AI Coding Agent created by ApvCode (https://github.com/apvcode/Termux-Dev)'
|
|
657
732
|
},
|
|
@@ -662,8 +737,13 @@ export async function main() {
|
|
|
662
737
|
}
|
|
663
738
|
]
|
|
664
739
|
});
|
|
740
|
+
if (choice === 'change_theme') {
|
|
741
|
+
await handleThemeSelect(config);
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
665
744
|
if (choice === 'about') {
|
|
666
|
-
p.note(`ā” devx v1.
|
|
745
|
+
p.note(`ā” devx v1.2.0 ā Terminal-Native AI Coding Agent\n` +
|
|
746
|
+
`šØ Theme: ${currentTh.emoji} ${currentTh.name}\n` +
|
|
667
747
|
`š¤ Author: ApvCode (https://github.com/apvcode)\n` +
|
|
668
748
|
`š Repository: https://github.com/apvcode/Termux-Dev\n` +
|
|
669
749
|
`š License: MIT License (2026)\n` +
|
|
@@ -692,13 +772,13 @@ export async function main() {
|
|
|
692
772
|
if (choice === 'toggle_memory') {
|
|
693
773
|
config.enableMemory = config.enableMemory === false ? true : false;
|
|
694
774
|
await saveConfig(config);
|
|
695
|
-
p.log.success(`Project memory: ${config.enableMemory ? pc.bold(pc.green('ON (
|
|
775
|
+
p.log.success(`Project memory bank: ${config.enableMemory !== false ? pc.bold(pc.green('ON (Persistent .devx/memory.md)')) : pc.bold(pc.yellow('OFF'))}`);
|
|
696
776
|
continue;
|
|
697
777
|
}
|
|
698
778
|
if (choice === 'toggle_check_updates') {
|
|
699
779
|
config.checkUpdates = config.checkUpdates === false ? true : false;
|
|
700
780
|
await saveConfig(config);
|
|
701
|
-
p.log.success(`Check for updates
|
|
781
|
+
p.log.success(`Check for updates: ${config.checkUpdates !== false ? pc.bold(pc.green('ON (Checked on startup)')) : pc.bold(pc.yellow('OFF (Manual only)'))}`);
|
|
702
782
|
continue;
|
|
703
783
|
}
|
|
704
784
|
if (choice === 'change_max_iterations') {
|
|
@@ -726,18 +806,34 @@ export async function main() {
|
|
|
726
806
|
process.stdin.resume();
|
|
727
807
|
return config;
|
|
728
808
|
}
|
|
729
|
-
const VALID_COMMANDS = [
|
|
809
|
+
const VALID_COMMANDS = [
|
|
810
|
+
'/new', '/reset', '/resume', '/session', '/sessions', '/history',
|
|
811
|
+
'/theme', '/themes',
|
|
812
|
+
'/settings', '/update', '/model', '/provider', '/providers',
|
|
813
|
+
'/plan', '/agent', '/image', '/serve', '/memory', '/undo',
|
|
814
|
+
'/diff', '/commit', '/status', '/compact', '/init', '/doctor',
|
|
815
|
+
'/config', '/clear', '/exit', '/quit', '/help'
|
|
816
|
+
];
|
|
730
817
|
if (!VALID_COMMANDS.includes(cmd)) {
|
|
731
818
|
const SLASH_COMMANDS = [
|
|
732
819
|
{ name: '/new - Start a new clean chat session', value: '/new' },
|
|
733
820
|
{ name: '/resume - Resume a previous chat session', value: '/resume' },
|
|
734
821
|
{ name: '/session del - Select and delete saved sessions', value: '/session del' },
|
|
822
|
+
{ name: '/theme - Switch UI color theme', value: '/theme' },
|
|
823
|
+
{ name: '/doctor - Run system & environment health diagnostics', value: '/doctor' },
|
|
735
824
|
{ name: '/settings - Configure permissions & auto-approval', value: '/settings' },
|
|
736
825
|
{ name: '/update - Check and install updates from GitHub', value: '/update' },
|
|
737
826
|
{ name: '/model - Switch model for current provider', value: '/model' },
|
|
738
827
|
{ name: '/provider - Change AI provider (Google, OpenRouter...)', value: '/provider' },
|
|
739
828
|
{ name: '/plan - Switch to PLAN mode (architect)', value: '/plan' },
|
|
740
829
|
{ name: '/agent - Switch to AGENT mode (coder)', value: '/agent' },
|
|
830
|
+
{ name: '/serve - Start local web server for web preview', value: '/serve' },
|
|
831
|
+
{ name: '/memory - View or edit project memory bank', value: '/memory' },
|
|
832
|
+
{ name: '/undo - Revert last file changes made by AI', value: '/undo' },
|
|
833
|
+
{ name: '/diff - Show git diff of modified files', value: '/diff' },
|
|
834
|
+
{ name: '/commit - AI-generated git commit message', value: '/commit' },
|
|
835
|
+
{ name: '/status - Show git repository status', value: '/status' },
|
|
836
|
+
{ name: '/compact - Compact conversation context', value: '/compact' },
|
|
741
837
|
{ name: '/config - View current configuration', value: '/config' },
|
|
742
838
|
{ name: '/clear - Clear message history', value: '/clear' },
|
|
743
839
|
{ name: '/help - Show commands overview', value: '/help' },
|
|
@@ -1070,8 +1166,8 @@ export async function main() {
|
|
|
1070
1166
|
initialValue: true
|
|
1071
1167
|
});
|
|
1072
1168
|
if (!p.isCancel(confirmed) && confirmed) {
|
|
1073
|
-
|
|
1074
|
-
|
|
1169
|
+
execFileSync('git', ['add', '-A'], { stdio: 'ignore' });
|
|
1170
|
+
execFileSync('git', ['commit', '-m', commitMsg], { stdio: 'ignore' });
|
|
1075
1171
|
p.log.success(pc.bold(pc.green(`ā
Committed: ${commitMsg}`)));
|
|
1076
1172
|
}
|
|
1077
1173
|
else {
|
|
@@ -1151,6 +1247,10 @@ export async function main() {
|
|
|
1151
1247
|
}
|
|
1152
1248
|
continue;
|
|
1153
1249
|
}
|
|
1250
|
+
if (cmd === '/doctor') {
|
|
1251
|
+
await runDoctor(config);
|
|
1252
|
+
continue;
|
|
1253
|
+
}
|
|
1154
1254
|
if (cmd === '/serve') {
|
|
1155
1255
|
const sub = parts[1]?.toLowerCase();
|
|
1156
1256
|
if (sub === 'stop') {
|
|
@@ -1165,10 +1265,7 @@ export async function main() {
|
|
|
1165
1265
|
const customPort = parseInt(parts[1], 10) || 3000;
|
|
1166
1266
|
try {
|
|
1167
1267
|
const { port, localUrl, networkUrl } = await startServer(customPort);
|
|
1168
|
-
|
|
1169
|
-
console.log(pc.cyan(` ⢠Local: ${localUrl}`));
|
|
1170
|
-
console.log(pc.cyan(` ⢠Network: ${networkUrl}`));
|
|
1171
|
-
console.log(pc.dim(' (Use /serve stop to stop the server)\n'));
|
|
1268
|
+
await displayServerBanner(localUrl, networkUrl);
|
|
1172
1269
|
}
|
|
1173
1270
|
catch (err) {
|
|
1174
1271
|
p.log.error(`Failed to start web server: ${err.message}`);
|
|
@@ -1176,6 +1273,27 @@ export async function main() {
|
|
|
1176
1273
|
}
|
|
1177
1274
|
continue;
|
|
1178
1275
|
}
|
|
1276
|
+
if (cmd === '/theme' || cmd === '/themes') {
|
|
1277
|
+
const themeArg = answer.replace(/^\/(?:theme|themes)\s*/i, '').trim();
|
|
1278
|
+
if (themeArg) {
|
|
1279
|
+
const matched = findTheme(themeArg);
|
|
1280
|
+
if (matched) {
|
|
1281
|
+
config.theme = matched.id;
|
|
1282
|
+
await saveConfig(config);
|
|
1283
|
+
const th = setActiveTheme(matched.id);
|
|
1284
|
+
drawLogo();
|
|
1285
|
+
p.log.success(th.boldFn(`šØ Theme switched to ${th.emoji} ${th.name}!`));
|
|
1286
|
+
}
|
|
1287
|
+
else {
|
|
1288
|
+
const themes = listThemes();
|
|
1289
|
+
p.log.warn(`Unknown theme: "${themeArg}". Available themes: ${themes.map(t => `${t.emoji} ${t.id}`).join(', ')}`);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
else {
|
|
1293
|
+
await handleThemeSelect(config);
|
|
1294
|
+
}
|
|
1295
|
+
continue;
|
|
1296
|
+
}
|
|
1179
1297
|
if (cmd === '/memory') {
|
|
1180
1298
|
const sub = parts[1]?.toLowerCase();
|
|
1181
1299
|
if (sub === 'clear') {
|
|
@@ -1357,6 +1475,8 @@ export async function main() {
|
|
|
1357
1475
|
}
|
|
1358
1476
|
catch { }
|
|
1359
1477
|
}
|
|
1478
|
+
const turnStartTime = Date.now();
|
|
1479
|
+
let toolsExecutedCount = 0;
|
|
1360
1480
|
try {
|
|
1361
1481
|
for await (const event of agent.run(abortController.signal)) {
|
|
1362
1482
|
if (aborted)
|
|
@@ -1395,15 +1515,16 @@ export async function main() {
|
|
|
1395
1515
|
}
|
|
1396
1516
|
}
|
|
1397
1517
|
else if (event.type === 'tool_generating') {
|
|
1518
|
+
const theme = getCurrentTheme();
|
|
1398
1519
|
const label = formatToolGeneratingLabel(event.name, event.targetHint);
|
|
1399
1520
|
if (!spinnerActive) {
|
|
1400
1521
|
streamer.finish();
|
|
1401
1522
|
finishThinking(false);
|
|
1402
|
-
s.start(
|
|
1523
|
+
s.start(theme.boldFn(label));
|
|
1403
1524
|
spinnerActive = true;
|
|
1404
1525
|
}
|
|
1405
1526
|
else {
|
|
1406
|
-
s.message(
|
|
1527
|
+
s.message(theme.boldFn(label));
|
|
1407
1528
|
}
|
|
1408
1529
|
}
|
|
1409
1530
|
else if (event.type === 'tool_start') {
|
|
@@ -1424,6 +1545,7 @@ export async function main() {
|
|
|
1424
1545
|
}
|
|
1425
1546
|
}
|
|
1426
1547
|
else if (event.type === 'tool_end') {
|
|
1548
|
+
toolsExecutedCount++;
|
|
1427
1549
|
if (spinnerActive) {
|
|
1428
1550
|
s.stop();
|
|
1429
1551
|
spinnerActive = false;
|
|
@@ -1435,28 +1557,50 @@ export async function main() {
|
|
|
1435
1557
|
process.stdout.write(parsed.displayCard);
|
|
1436
1558
|
}
|
|
1437
1559
|
else if (parsed.action === 'edit' && parsed.diffLines) {
|
|
1560
|
+
const theme = getCurrentTheme();
|
|
1438
1561
|
const cols = Math.min(process.stdout.columns || 80, 80);
|
|
1439
|
-
const
|
|
1440
|
-
const
|
|
1441
|
-
|
|
1442
|
-
|
|
1562
|
+
const boxWidth = Math.max(30, Math.min(cols - 4, 76));
|
|
1563
|
+
const fileName = typeof parsed.path === 'string' ? parsed.path.split(/[\/\\]/).pop() || 'file' : 'file';
|
|
1564
|
+
const fillCount = Math.max(2, boxWidth - 5 - fileName.length);
|
|
1565
|
+
console.log(theme.colorFn('āā ') + pc.bold(fileName) + ' ' + theme.colorFn('ā'.repeat(fillCount) + 'ā'));
|
|
1566
|
+
const maxShown = Math.min(parsed.diffLines.length, 30);
|
|
1443
1567
|
for (let i = 0; i < maxShown; i++) {
|
|
1444
1568
|
const line = parsed.diffLines[i];
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1569
|
+
const match = line.match(/^(\d+)(\s+[+\- ]\s+)(.*)$/);
|
|
1570
|
+
if (match) {
|
|
1571
|
+
const lineNum = match[1].padStart(4, ' ');
|
|
1572
|
+
const symbol = match[2];
|
|
1573
|
+
const contentStr = match[3] || '';
|
|
1574
|
+
const prefix = symbol.includes('-') ? '-' : symbol.includes('+') ? '+' : ' ';
|
|
1575
|
+
const maxCodeLen = Math.max(10, boxWidth - 11);
|
|
1576
|
+
const paddedCode = contentStr.length > maxCodeLen
|
|
1577
|
+
? contentStr.substring(0, maxCodeLen - 1) + 'ā¦'
|
|
1578
|
+
: contentStr.padEnd(maxCodeLen, ' ');
|
|
1579
|
+
const innerRow = ` ${lineNum} ${prefix} ${paddedCode} `;
|
|
1580
|
+
if (prefix === '-') {
|
|
1581
|
+
console.log(theme.colorFn('ā') + theme.diffRemoveBg(innerRow) + theme.colorFn('ā'));
|
|
1582
|
+
}
|
|
1583
|
+
else if (prefix === '+') {
|
|
1584
|
+
console.log(theme.colorFn('ā') + theme.diffAddBg(innerRow) + theme.colorFn('ā'));
|
|
1585
|
+
}
|
|
1586
|
+
else {
|
|
1587
|
+
console.log(theme.colorFn('ā') + pc.dim(innerRow) + theme.colorFn('ā'));
|
|
1588
|
+
}
|
|
1450
1589
|
}
|
|
1451
1590
|
else {
|
|
1452
|
-
|
|
1591
|
+
const maxLen = Math.max(10, boxWidth - 4);
|
|
1592
|
+
const padded = line.length > maxLen ? line.substring(0, maxLen - 1) + 'ā¦' : line.padEnd(maxLen, ' ');
|
|
1593
|
+
console.log(theme.colorFn('ā ') + pc.white(padded) + theme.colorFn(' ā'));
|
|
1453
1594
|
}
|
|
1454
1595
|
}
|
|
1455
1596
|
if (parsed.diffLines.length > maxShown) {
|
|
1456
|
-
|
|
1597
|
+
const dots = `... +${parsed.diffLines.length - maxShown} more lines`;
|
|
1598
|
+
const maxLen = Math.max(10, boxWidth - 4);
|
|
1599
|
+
const paddedDots = dots.length > maxLen ? dots.substring(0, maxLen - 1) + 'ā¦' : dots.padEnd(maxLen, ' ');
|
|
1600
|
+
console.log(theme.colorFn('ā ') + pc.dim(paddedDots) + theme.colorFn(' ā'));
|
|
1457
1601
|
}
|
|
1458
|
-
console.log(
|
|
1459
|
-
console.log(
|
|
1602
|
+
console.log(theme.colorFn('ā' + 'ā'.repeat(boxWidth - 2) + 'ā'));
|
|
1603
|
+
console.log(theme.boldFn(` āā ${parsed.summary}`));
|
|
1460
1604
|
}
|
|
1461
1605
|
else if (parsed.summary) {
|
|
1462
1606
|
console.log(pc.green(` āā ${parsed.summary}`));
|
|
@@ -1535,6 +1679,12 @@ export async function main() {
|
|
|
1535
1679
|
process.stdout.write('\n');
|
|
1536
1680
|
}
|
|
1537
1681
|
globalSnapshotManager.finishTurn();
|
|
1682
|
+
const turnElapsedMs = Date.now() - turnStartTime;
|
|
1683
|
+
const shouldNotify = (turnElapsedMs >= 15000) || (toolsExecutedCount > 0) || (planMode && lastPlanReady !== null);
|
|
1684
|
+
if (shouldNotify && !aborted) {
|
|
1685
|
+
const secs = Math.round(turnElapsedMs / 1000);
|
|
1686
|
+
notifyDevice('devx', `Task completed in ${secs}s!`);
|
|
1687
|
+
}
|
|
1538
1688
|
// Auto-save session
|
|
1539
1689
|
await sessionManager.save(history.getMessages(), totalSessionCost, config.model, planMode);
|
|
1540
1690
|
// Check if plan was finalized in PLAN mode
|
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,6 +4,9 @@ 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',
|
|
@@ -136,7 +139,7 @@ function renderDirectoryHtml(dirPath, relPath, files, port) {
|
|
|
136
139
|
${parentLink}
|
|
137
140
|
${items || '<li style="padding: 20px; text-align: center; color: #6e7681;">No visible files in this directory</li>'}
|
|
138
141
|
</ul>
|
|
139
|
-
<div class="footer">devx v1.
|
|
142
|
+
<div class="footer">devx v1.2.0 • Terminal-Native AI Assistant</div>
|
|
140
143
|
</div>
|
|
141
144
|
</body>
|
|
142
145
|
</html>`;
|
|
@@ -160,6 +163,13 @@ export async function startServer(preferredPort = 3000) {
|
|
|
160
163
|
res.end('Forbidden');
|
|
161
164
|
return;
|
|
162
165
|
}
|
|
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');
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
163
173
|
// 1. Root / Directory Request Handling
|
|
164
174
|
if (fsSync.existsSync(targetPath)) {
|
|
165
175
|
const stat = await fs.stat(targetPath);
|
|
@@ -232,8 +242,14 @@ export async function startServer(preferredPort = 3000) {
|
|
|
232
242
|
res.end(`Internal Server Error: ${err.message}`);
|
|
233
243
|
}
|
|
234
244
|
});
|
|
245
|
+
let retryCount = 0;
|
|
235
246
|
server.on('error', (err) => {
|
|
236
247
|
if (err.code === 'EADDRINUSE') {
|
|
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
|
+
}
|
|
237
253
|
activePort++;
|
|
238
254
|
server.listen(activePort, '0.0.0.0');
|
|
239
255
|
}
|
|
@@ -250,6 +266,7 @@ export async function startServer(preferredPort = 3000) {
|
|
|
250
266
|
if (process.env.PREFIX?.includes('com.termux')) {
|
|
251
267
|
try {
|
|
252
268
|
const opener = spawn('termux-open-url', [localUrl], { stdio: 'ignore', detached: true });
|
|
269
|
+
opener.on('error', () => { });
|
|
253
270
|
opener.unref();
|
|
254
271
|
}
|
|
255
272
|
catch { }
|
|
@@ -258,3 +275,41 @@ export async function startServer(preferredPort = 3000) {
|
|
|
258
275
|
});
|
|
259
276
|
});
|
|
260
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
|
+
}
|
|
@@ -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/tools/fs.js
CHANGED
|
@@ -63,13 +63,20 @@ export const writeFileTool = {
|
|
|
63
63
|
}
|
|
64
64
|
catch { }
|
|
65
65
|
await fs.writeFile(args.path, args.content, 'utf8');
|
|
66
|
-
const
|
|
66
|
+
const contentLines = args.content.split('\n');
|
|
67
|
+
const newLines = contentLines.length;
|
|
68
|
+
const diffLines = [];
|
|
69
|
+
const showLines = Math.min(contentLines.length, 6);
|
|
70
|
+
for (let i = 0; i < showLines; i++) {
|
|
71
|
+
diffLines.push(`${i + 1} + ${contentLines[i]}`);
|
|
72
|
+
}
|
|
67
73
|
if (!existed) {
|
|
68
74
|
return JSON.stringify({
|
|
69
75
|
status: 'success',
|
|
70
|
-
action: '
|
|
76
|
+
action: 'edit',
|
|
71
77
|
path: args.path,
|
|
72
78
|
addedCount: newLines,
|
|
79
|
+
diffLines,
|
|
73
80
|
summary: `Successfully created ${args.path} (+${newLines} lines)`
|
|
74
81
|
});
|
|
75
82
|
}
|
|
@@ -78,9 +85,10 @@ export const writeFileTool = {
|
|
|
78
85
|
const diffStr = diff >= 0 ? `+${diff}` : `${diff}`;
|
|
79
86
|
return JSON.stringify({
|
|
80
87
|
status: 'success',
|
|
81
|
-
action: '
|
|
88
|
+
action: 'edit',
|
|
82
89
|
path: args.path,
|
|
83
90
|
addedCount: newLines,
|
|
91
|
+
diffLines,
|
|
84
92
|
summary: `Successfully updated ${args.path} (${diffStr} lines, ${newLines} total)`
|
|
85
93
|
});
|
|
86
94
|
}
|
|
@@ -131,16 +139,20 @@ export const editFileTool = {
|
|
|
131
139
|
}
|
|
132
140
|
}
|
|
133
141
|
const targetIndex = content.indexOf(target);
|
|
142
|
+
const allLines = content.split('\n');
|
|
134
143
|
const startLine = content.slice(0, targetIndex).split('\n').length;
|
|
135
144
|
const removedArr = target.split('\n');
|
|
145
|
+
const endLine = startLine + removedArr.length - 1;
|
|
146
|
+
const contextBefore = allLines.slice(Math.max(0, startLine - 3), startLine - 1);
|
|
147
|
+
const contextAfter = allLines.slice(endLine, Math.min(allLines.length, endLine + 2));
|
|
136
148
|
const addedArr = args.replacement.split('\n');
|
|
137
149
|
const diffLines = [];
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
});
|
|
150
|
+
let lineCounter = startLine - contextBefore.length;
|
|
151
|
+
contextBefore.forEach((l) => diffLines.push(`${lineCounter++} ${l}`));
|
|
152
|
+
removedArr.forEach((l) => diffLines.push(`${lineCounter++} - ${l}`));
|
|
153
|
+
let addCounter = startLine;
|
|
154
|
+
addedArr.forEach((l) => diffLines.push(`${addCounter++} + ${l}`));
|
|
155
|
+
contextAfter.forEach((l) => diffLines.push(`${addCounter++} ${l}`));
|
|
144
156
|
const newContent = content.replace(target, args.replacement);
|
|
145
157
|
await fs.writeFile(args.path, newContent, 'utf8');
|
|
146
158
|
return JSON.stringify({
|
package/dist/tools/index.js
CHANGED
|
@@ -10,10 +10,10 @@ import { planReadyTool, lastPlanReady, resetPlanReady } from './plan.js';
|
|
|
10
10
|
import { todoListTool, currentTodoList, resetTodoList } from './todo.js';
|
|
11
11
|
import { servePreviewTool } from './server.js';
|
|
12
12
|
export function getTools(planMode) {
|
|
13
|
-
const baseTools = [readFileTool, listDirTool, searchTool, askQuestionsTool, webSearchTool, fetchUrlTool, saveMemoryTool, planReadyTool, todoListTool
|
|
13
|
+
const baseTools = [readFileTool, listDirTool, searchTool, askQuestionsTool, webSearchTool, fetchUrlTool, saveMemoryTool, planReadyTool, todoListTool];
|
|
14
14
|
if (planMode) {
|
|
15
15
|
return baseTools;
|
|
16
16
|
}
|
|
17
|
-
return [...baseTools, writeFileTool, editFileTool, mkdirTool, bashTool, diagnoseCodeTool, installPackageTool];
|
|
17
|
+
return [...baseTools, writeFileTool, editFileTool, mkdirTool, bashTool, diagnoseCodeTool, installPackageTool, servePreviewTool];
|
|
18
18
|
}
|
|
19
19
|
export { webSearchTool, fetchUrlTool, diagnoseCodeTool, installPackageTool, saveMemoryTool, planReadyTool, lastPlanReady, resetPlanReady, todoListTool, currentTodoList, resetTodoList, servePreviewTool };
|
package/dist/tools/server.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { startServer, stopServer, isServerRunning, getServerPort } from '../cli/server.js';
|
|
1
|
+
import { startServer, stopServer, isServerRunning, getServerPort, displayServerBanner } from '../cli/server.js';
|
|
2
2
|
export const servePreviewTool = {
|
|
3
3
|
name: 'serve_preview',
|
|
4
4
|
definition: {
|
|
5
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.',
|
|
6
|
+
description: 'Start or stop the built-in local live web preview server to view HTML/web apps in the browser with instant QR-code.',
|
|
7
7
|
parameters: {
|
|
8
8
|
type: 'object',
|
|
9
9
|
properties: {
|
|
@@ -37,7 +37,8 @@ export const servePreviewTool = {
|
|
|
37
37
|
const preferredPort = args.port || 3000;
|
|
38
38
|
try {
|
|
39
39
|
const { port, localUrl, networkUrl } = await startServer(preferredPort);
|
|
40
|
-
|
|
40
|
+
await displayServerBanner(localUrl, networkUrl);
|
|
41
|
+
return `Web Server is now live!\n⢠Local URL: ${localUrl}\n⢠Network URL: ${networkUrl}\nOpened in browser automatically with mobile QR-code displayed in terminal.`;
|
|
41
42
|
}
|
|
42
43
|
catch (err) {
|
|
43
44
|
throw new Error(`Failed to start web server: ${err.message}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "termux-dev",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
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",
|
|
@@ -54,11 +54,13 @@
|
|
|
54
54
|
"commander": "^12.0.0",
|
|
55
55
|
"marked": "^15.0.12",
|
|
56
56
|
"marked-terminal": "^7.3.0",
|
|
57
|
-
"picocolors": "^1.1.1"
|
|
57
|
+
"picocolors": "^1.1.1",
|
|
58
|
+
"qrcode-terminal": "^0.12.0"
|
|
58
59
|
},
|
|
59
60
|
"devDependencies": {
|
|
60
61
|
"@types/marked-terminal": "^6.1.1",
|
|
61
62
|
"@types/node": "^20.0.0",
|
|
63
|
+
"@types/qrcode-terminal": "^0.12.2",
|
|
62
64
|
"typescript": "^5.4.0"
|
|
63
65
|
}
|
|
64
66
|
}
|