termux-dev 1.1.2 ā 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 +182 -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)
|
|
@@ -371,14 +374,15 @@ function clearTerminalScreen() {
|
|
|
371
374
|
function drawLogo() {
|
|
372
375
|
const cols = process.stdout.columns || 80;
|
|
373
376
|
clearTerminalScreen();
|
|
377
|
+
const theme = getCurrentTheme();
|
|
374
378
|
if (cols < 56) {
|
|
375
379
|
// Ultra-clean compact ASCII for small mobile screens (width: ~26 chars)
|
|
376
380
|
const logo = [
|
|
377
381
|
'',
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
' ' +
|
|
382
|
+
theme.colorFn(' āāāā āāāā ā ā ā ā'),
|
|
383
|
+
theme.colorFn(' ā ā āāāā āāā āāā '),
|
|
384
|
+
theme.colorFn(' āāāā āāāā ā ā ā '),
|
|
385
|
+
' ' + theme.boldFn('v1.2.0'),
|
|
382
386
|
''
|
|
383
387
|
];
|
|
384
388
|
for (const line of logo) {
|
|
@@ -390,10 +394,10 @@ function drawLogo() {
|
|
|
390
394
|
const indent = cols < 68 ? ' ' : ' ';
|
|
391
395
|
const logo = [
|
|
392
396
|
'',
|
|
393
|
-
indent +
|
|
394
|
-
indent +
|
|
395
|
-
indent +
|
|
396
|
-
indent +
|
|
397
|
+
indent + theme.colorFn('āāāāāāā āāāā āāāā āā āā ā ā āā āā āāāā āāāā ā ā'),
|
|
398
|
+
indent + theme.colorFn(' ā āāāā āāāā ā ā ā ā ā ā āā ā ā āāāā ā ā'),
|
|
399
|
+
indent + theme.colorFn(' ā āāāā ā āā ā ā āāāā āā āā āāāā āāāā āāā '),
|
|
400
|
+
indent + theme.boldFn('v1.2.0'),
|
|
397
401
|
''
|
|
398
402
|
];
|
|
399
403
|
for (const line of logo) {
|
|
@@ -520,6 +524,39 @@ export async function main() {
|
|
|
520
524
|
const options = program.opts();
|
|
521
525
|
let planMode = !!options.plan;
|
|
522
526
|
let config = await loadConfig();
|
|
527
|
+
if (!config.onboarded) {
|
|
528
|
+
const cols = Math.min(process.stdout.columns || 40, 42);
|
|
529
|
+
const fill = Math.max(2, cols - 16);
|
|
530
|
+
const topBorder = 'āā preview.ts ' + 'ā'.repeat(fill) + 'ā';
|
|
531
|
+
const bottomBorder = 'ā' + 'ā'.repeat(cols - 2) + 'ā';
|
|
532
|
+
const themeChoices = listThemes().map(t => {
|
|
533
|
+
const addLine = t.diffAddBg(` + 1 | const theme = "${t.id}";`.padEnd(cols - 2));
|
|
534
|
+
const remLine = t.diffRemoveBg(` - 2 | const theme = "none";`.padEnd(cols - 2));
|
|
535
|
+
const preview = `${t.colorFn(topBorder)}\n${addLine}\n${remLine}\n${t.colorFn(bottomBorder)}\n\n${pc.dim('š” You can change this anytime with /theme')}`;
|
|
536
|
+
return {
|
|
537
|
+
name: `${t.emoji} ${t.boldFn(t.name.padEnd(18))} ${pc.dim(t.desc)}`,
|
|
538
|
+
value: t.id,
|
|
539
|
+
description: preview
|
|
540
|
+
};
|
|
541
|
+
});
|
|
542
|
+
try {
|
|
543
|
+
console.clear();
|
|
544
|
+
const selected = await select({
|
|
545
|
+
message: `${pc.bold('šØ Pick a theme to personalize your workspace:')}`,
|
|
546
|
+
choices: themeChoices,
|
|
547
|
+
pageSize: 10
|
|
548
|
+
});
|
|
549
|
+
if (selected) {
|
|
550
|
+
config.theme = selected;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
catch { }
|
|
554
|
+
config.onboarded = true;
|
|
555
|
+
await saveConfig(config);
|
|
556
|
+
}
|
|
557
|
+
if (config.theme) {
|
|
558
|
+
setActiveTheme(config.theme);
|
|
559
|
+
}
|
|
523
560
|
enableDarkTheme(config.pureBlackTheme !== false);
|
|
524
561
|
let history = new History();
|
|
525
562
|
const sysPrompt = await buildSystemPrompt(planMode);
|
|
@@ -546,6 +583,7 @@ export async function main() {
|
|
|
546
583
|
const tokenStats = `Context: ${formatTokens(currentTokens)} / ${formatTokens(maxTokens)} (${usagePercent}%) ⢠${costStr}`;
|
|
547
584
|
const cols = process.stdout.columns || 80;
|
|
548
585
|
const modeName = planMode ? 'PLAN' : 'AGENT';
|
|
586
|
+
const theme = getCurrentTheme();
|
|
549
587
|
// Shorten model name if too long on narrow mobile screens
|
|
550
588
|
let displayModel = config.model;
|
|
551
589
|
if (cols < 75 && displayModel.length > 20) {
|
|
@@ -555,7 +593,7 @@ export async function main() {
|
|
|
555
593
|
displayModel = displayModel.slice(0, 17) + '...';
|
|
556
594
|
}
|
|
557
595
|
}
|
|
558
|
-
const badge =
|
|
596
|
+
const badge = theme.badgeFn(`devx | ${modeName} | ${displayModel}`);
|
|
559
597
|
if (cols < 75) {
|
|
560
598
|
// 2-line layout for mobile screens: perfectly aligned with clack box borders
|
|
561
599
|
p.intro(`${badge}\n${pc.dim('ā')} ${pc.dim(tokenStats)}`);
|
|
@@ -568,7 +606,7 @@ export async function main() {
|
|
|
568
606
|
if (autoTriggerPrompt) {
|
|
569
607
|
answer = autoTriggerPrompt;
|
|
570
608
|
autoTriggerPrompt = '';
|
|
571
|
-
console.log(
|
|
609
|
+
console.log(theme.colorFn('ā') + ' ' + pc.bold(pc.white(answer)));
|
|
572
610
|
}
|
|
573
611
|
else {
|
|
574
612
|
const inputStr = await askPrompt({
|
|
@@ -609,16 +647,44 @@ export async function main() {
|
|
|
609
647
|
if (answer.startsWith('/')) {
|
|
610
648
|
const parts = answer.split(' ');
|
|
611
649
|
let cmd = parts[0];
|
|
650
|
+
async function handleThemeSelect(config) {
|
|
651
|
+
const currentTh = getCurrentTheme();
|
|
652
|
+
const themeChoices = listThemes().map(t => ({
|
|
653
|
+
name: `${t.emoji} ${t.boldFn(t.name.padEnd(18))} ${pc.dim(t.desc)} ${t.id === currentTh.id ? pc.green('(Active)') : ''}`,
|
|
654
|
+
value: t.id,
|
|
655
|
+
description: `Apply ${t.name} color palette (${t.hex}) to banners, prompts, and actions`
|
|
656
|
+
}));
|
|
657
|
+
try {
|
|
658
|
+
const selected = await select({
|
|
659
|
+
message: `${pc.bold('šØ Select UI Theme / ŠŃŠ±ŠµŃŠøŃе ŃŠ²ŠµŃовŃŃ ŃŠµŠ¼Ń:')}`,
|
|
660
|
+
choices: themeChoices
|
|
661
|
+
});
|
|
662
|
+
if (selected) {
|
|
663
|
+
config.theme = selected;
|
|
664
|
+
await saveConfig(config);
|
|
665
|
+
const th = setActiveTheme(selected);
|
|
666
|
+
drawLogo();
|
|
667
|
+
p.log.success(th.boldFn(`šØ Theme switched to ${th.emoji} ${th.name}!`));
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
catch { }
|
|
671
|
+
}
|
|
612
672
|
async function handleSettings(config) {
|
|
613
673
|
while (true) {
|
|
614
674
|
try {
|
|
615
675
|
const maxIter = config.maxIterations || 100;
|
|
616
676
|
const maxIterLabel = maxIter >= 9999 ? 'Unlimited' : `${maxIter} steps`;
|
|
677
|
+
const currentTh = getCurrentTheme();
|
|
617
678
|
const choice = await select({
|
|
618
|
-
message: `${pc.bold('āļø Settings')} ${pc.dim(
|
|
679
|
+
message: `${pc.bold('āļø Settings')} ${pc.dim(`(devx v1.2.0 ⢠theme: ${currentTh.name})`)}`,
|
|
619
680
|
choices: [
|
|
620
681
|
{
|
|
621
|
-
name:
|
|
682
|
+
name: `šØ Color Theme: ${currentTh.emoji} ${currentTh.name}`,
|
|
683
|
+
value: 'change_theme',
|
|
684
|
+
description: `Switch UI accent colors (${currentTh.desc})`
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
name: `${config.pureBlackTheme !== false ? pc.green('š¤ Pure Black Background: ON') : pc.yellow('š¤ Pure Black Background: OFF')}`,
|
|
622
688
|
value: 'toggle_black_theme',
|
|
623
689
|
description: config.pureBlackTheme !== false
|
|
624
690
|
? 'Apply deep OLED obsidian black background (#0a0a0c) like OpenCode'
|
|
@@ -646,12 +712,12 @@ export async function main() {
|
|
|
646
712
|
: 'Disable update checking on startup (run /update manually instead)'
|
|
647
713
|
},
|
|
648
714
|
{
|
|
649
|
-
name: `š Max Agent Iterations: ${
|
|
715
|
+
name: `š Max Agent Iterations: ${currentTh.colorFn(maxIterLabel)}`,
|
|
650
716
|
value: 'change_max_iterations',
|
|
651
717
|
description: 'Limit how many tool steps (file edits, terminal commands) agent can do per request'
|
|
652
718
|
},
|
|
653
719
|
{
|
|
654
|
-
name: `${
|
|
720
|
+
name: `${currentTh.colorFn('⨠About devx')} ${pc.dim('(v1.2.0 by ApvCode)')}`,
|
|
655
721
|
value: 'about',
|
|
656
722
|
description: 'Terminal-Native AI Coding Agent created by ApvCode (https://github.com/apvcode/Termux-Dev)'
|
|
657
723
|
},
|
|
@@ -662,8 +728,13 @@ export async function main() {
|
|
|
662
728
|
}
|
|
663
729
|
]
|
|
664
730
|
});
|
|
731
|
+
if (choice === 'change_theme') {
|
|
732
|
+
await handleThemeSelect(config);
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
665
735
|
if (choice === 'about') {
|
|
666
|
-
p.note(`ā” devx v1.
|
|
736
|
+
p.note(`ā” devx v1.2.0 ā Terminal-Native AI Coding Agent\n` +
|
|
737
|
+
`šØ Theme: ${currentTh.emoji} ${currentTh.name}\n` +
|
|
667
738
|
`š¤ Author: ApvCode (https://github.com/apvcode)\n` +
|
|
668
739
|
`š Repository: https://github.com/apvcode/Termux-Dev\n` +
|
|
669
740
|
`š License: MIT License (2026)\n` +
|
|
@@ -692,13 +763,13 @@ export async function main() {
|
|
|
692
763
|
if (choice === 'toggle_memory') {
|
|
693
764
|
config.enableMemory = config.enableMemory === false ? true : false;
|
|
694
765
|
await saveConfig(config);
|
|
695
|
-
p.log.success(`Project memory: ${config.enableMemory ? pc.bold(pc.green('ON (
|
|
766
|
+
p.log.success(`Project memory bank: ${config.enableMemory !== false ? pc.bold(pc.green('ON (Persistent .devx/memory.md)')) : pc.bold(pc.yellow('OFF'))}`);
|
|
696
767
|
continue;
|
|
697
768
|
}
|
|
698
769
|
if (choice === 'toggle_check_updates') {
|
|
699
770
|
config.checkUpdates = config.checkUpdates === false ? true : false;
|
|
700
771
|
await saveConfig(config);
|
|
701
|
-
p.log.success(`Check for updates
|
|
772
|
+
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
773
|
continue;
|
|
703
774
|
}
|
|
704
775
|
if (choice === 'change_max_iterations') {
|
|
@@ -726,18 +797,34 @@ export async function main() {
|
|
|
726
797
|
process.stdin.resume();
|
|
727
798
|
return config;
|
|
728
799
|
}
|
|
729
|
-
const VALID_COMMANDS = [
|
|
800
|
+
const VALID_COMMANDS = [
|
|
801
|
+
'/new', '/reset', '/resume', '/session', '/sessions', '/history',
|
|
802
|
+
'/theme', '/themes',
|
|
803
|
+
'/settings', '/update', '/model', '/provider', '/providers',
|
|
804
|
+
'/plan', '/agent', '/image', '/serve', '/memory', '/undo',
|
|
805
|
+
'/diff', '/commit', '/status', '/compact', '/init', '/doctor',
|
|
806
|
+
'/config', '/clear', '/exit', '/quit', '/help'
|
|
807
|
+
];
|
|
730
808
|
if (!VALID_COMMANDS.includes(cmd)) {
|
|
731
809
|
const SLASH_COMMANDS = [
|
|
732
810
|
{ name: '/new - Start a new clean chat session', value: '/new' },
|
|
733
811
|
{ name: '/resume - Resume a previous chat session', value: '/resume' },
|
|
734
812
|
{ name: '/session del - Select and delete saved sessions', value: '/session del' },
|
|
813
|
+
{ name: '/theme - Switch UI color theme', value: '/theme' },
|
|
814
|
+
{ name: '/doctor - Run system & environment health diagnostics', value: '/doctor' },
|
|
735
815
|
{ name: '/settings - Configure permissions & auto-approval', value: '/settings' },
|
|
736
816
|
{ name: '/update - Check and install updates from GitHub', value: '/update' },
|
|
737
817
|
{ name: '/model - Switch model for current provider', value: '/model' },
|
|
738
818
|
{ name: '/provider - Change AI provider (Google, OpenRouter...)', value: '/provider' },
|
|
739
819
|
{ name: '/plan - Switch to PLAN mode (architect)', value: '/plan' },
|
|
740
820
|
{ name: '/agent - Switch to AGENT mode (coder)', value: '/agent' },
|
|
821
|
+
{ name: '/serve - Start local web server for web preview', value: '/serve' },
|
|
822
|
+
{ name: '/memory - View or edit project memory bank', value: '/memory' },
|
|
823
|
+
{ name: '/undo - Revert last file changes made by AI', value: '/undo' },
|
|
824
|
+
{ name: '/diff - Show git diff of modified files', value: '/diff' },
|
|
825
|
+
{ name: '/commit - AI-generated git commit message', value: '/commit' },
|
|
826
|
+
{ name: '/status - Show git repository status', value: '/status' },
|
|
827
|
+
{ name: '/compact - Compact conversation context', value: '/compact' },
|
|
741
828
|
{ name: '/config - View current configuration', value: '/config' },
|
|
742
829
|
{ name: '/clear - Clear message history', value: '/clear' },
|
|
743
830
|
{ name: '/help - Show commands overview', value: '/help' },
|
|
@@ -1070,8 +1157,8 @@ export async function main() {
|
|
|
1070
1157
|
initialValue: true
|
|
1071
1158
|
});
|
|
1072
1159
|
if (!p.isCancel(confirmed) && confirmed) {
|
|
1073
|
-
|
|
1074
|
-
|
|
1160
|
+
execFileSync('git', ['add', '-A'], { stdio: 'ignore' });
|
|
1161
|
+
execFileSync('git', ['commit', '-m', commitMsg], { stdio: 'ignore' });
|
|
1075
1162
|
p.log.success(pc.bold(pc.green(`ā
Committed: ${commitMsg}`)));
|
|
1076
1163
|
}
|
|
1077
1164
|
else {
|
|
@@ -1151,6 +1238,10 @@ export async function main() {
|
|
|
1151
1238
|
}
|
|
1152
1239
|
continue;
|
|
1153
1240
|
}
|
|
1241
|
+
if (cmd === '/doctor') {
|
|
1242
|
+
await runDoctor(config);
|
|
1243
|
+
continue;
|
|
1244
|
+
}
|
|
1154
1245
|
if (cmd === '/serve') {
|
|
1155
1246
|
const sub = parts[1]?.toLowerCase();
|
|
1156
1247
|
if (sub === 'stop') {
|
|
@@ -1165,10 +1256,7 @@ export async function main() {
|
|
|
1165
1256
|
const customPort = parseInt(parts[1], 10) || 3000;
|
|
1166
1257
|
try {
|
|
1167
1258
|
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'));
|
|
1259
|
+
await displayServerBanner(localUrl, networkUrl);
|
|
1172
1260
|
}
|
|
1173
1261
|
catch (err) {
|
|
1174
1262
|
p.log.error(`Failed to start web server: ${err.message}`);
|
|
@@ -1176,6 +1264,27 @@ export async function main() {
|
|
|
1176
1264
|
}
|
|
1177
1265
|
continue;
|
|
1178
1266
|
}
|
|
1267
|
+
if (cmd === '/theme' || cmd === '/themes') {
|
|
1268
|
+
const themeArg = answer.replace(/^\/(?:theme|themes)\s*/i, '').trim();
|
|
1269
|
+
if (themeArg) {
|
|
1270
|
+
const matched = findTheme(themeArg);
|
|
1271
|
+
if (matched) {
|
|
1272
|
+
config.theme = matched.id;
|
|
1273
|
+
await saveConfig(config);
|
|
1274
|
+
const th = setActiveTheme(matched.id);
|
|
1275
|
+
drawLogo();
|
|
1276
|
+
p.log.success(th.boldFn(`šØ Theme switched to ${th.emoji} ${th.name}!`));
|
|
1277
|
+
}
|
|
1278
|
+
else {
|
|
1279
|
+
const themes = listThemes();
|
|
1280
|
+
p.log.warn(`Unknown theme: "${themeArg}". Available themes: ${themes.map(t => `${t.emoji} ${t.id}`).join(', ')}`);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
else {
|
|
1284
|
+
await handleThemeSelect(config);
|
|
1285
|
+
}
|
|
1286
|
+
continue;
|
|
1287
|
+
}
|
|
1179
1288
|
if (cmd === '/memory') {
|
|
1180
1289
|
const sub = parts[1]?.toLowerCase();
|
|
1181
1290
|
if (sub === 'clear') {
|
|
@@ -1357,6 +1466,8 @@ export async function main() {
|
|
|
1357
1466
|
}
|
|
1358
1467
|
catch { }
|
|
1359
1468
|
}
|
|
1469
|
+
const turnStartTime = Date.now();
|
|
1470
|
+
let toolsExecutedCount = 0;
|
|
1360
1471
|
try {
|
|
1361
1472
|
for await (const event of agent.run(abortController.signal)) {
|
|
1362
1473
|
if (aborted)
|
|
@@ -1395,15 +1506,16 @@ export async function main() {
|
|
|
1395
1506
|
}
|
|
1396
1507
|
}
|
|
1397
1508
|
else if (event.type === 'tool_generating') {
|
|
1509
|
+
const theme = getCurrentTheme();
|
|
1398
1510
|
const label = formatToolGeneratingLabel(event.name, event.targetHint);
|
|
1399
1511
|
if (!spinnerActive) {
|
|
1400
1512
|
streamer.finish();
|
|
1401
1513
|
finishThinking(false);
|
|
1402
|
-
s.start(
|
|
1514
|
+
s.start(theme.boldFn(label));
|
|
1403
1515
|
spinnerActive = true;
|
|
1404
1516
|
}
|
|
1405
1517
|
else {
|
|
1406
|
-
s.message(
|
|
1518
|
+
s.message(theme.boldFn(label));
|
|
1407
1519
|
}
|
|
1408
1520
|
}
|
|
1409
1521
|
else if (event.type === 'tool_start') {
|
|
@@ -1424,6 +1536,7 @@ export async function main() {
|
|
|
1424
1536
|
}
|
|
1425
1537
|
}
|
|
1426
1538
|
else if (event.type === 'tool_end') {
|
|
1539
|
+
toolsExecutedCount++;
|
|
1427
1540
|
if (spinnerActive) {
|
|
1428
1541
|
s.stop();
|
|
1429
1542
|
spinnerActive = false;
|
|
@@ -1435,28 +1548,50 @@ export async function main() {
|
|
|
1435
1548
|
process.stdout.write(parsed.displayCard);
|
|
1436
1549
|
}
|
|
1437
1550
|
else if (parsed.action === 'edit' && parsed.diffLines) {
|
|
1551
|
+
const theme = getCurrentTheme();
|
|
1438
1552
|
const cols = Math.min(process.stdout.columns || 80, 80);
|
|
1439
|
-
const
|
|
1440
|
-
const
|
|
1441
|
-
|
|
1442
|
-
|
|
1553
|
+
const boxWidth = Math.max(30, Math.min(cols - 4, 76));
|
|
1554
|
+
const fileName = typeof parsed.path === 'string' ? parsed.path.split(/[\/\\]/).pop() || 'file' : 'file';
|
|
1555
|
+
const fillCount = Math.max(2, boxWidth - 5 - fileName.length);
|
|
1556
|
+
console.log(theme.colorFn('āā ') + pc.bold(fileName) + ' ' + theme.colorFn('ā'.repeat(fillCount) + 'ā'));
|
|
1557
|
+
const maxShown = Math.min(parsed.diffLines.length, 30);
|
|
1443
1558
|
for (let i = 0; i < maxShown; i++) {
|
|
1444
1559
|
const line = parsed.diffLines[i];
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1560
|
+
const match = line.match(/^(\d+)(\s+[+\- ]\s+)(.*)$/);
|
|
1561
|
+
if (match) {
|
|
1562
|
+
const lineNum = match[1].padStart(4, ' ');
|
|
1563
|
+
const symbol = match[2];
|
|
1564
|
+
const contentStr = match[3] || '';
|
|
1565
|
+
const prefix = symbol.includes('-') ? '-' : symbol.includes('+') ? '+' : ' ';
|
|
1566
|
+
const maxCodeLen = Math.max(10, boxWidth - 11);
|
|
1567
|
+
const paddedCode = contentStr.length > maxCodeLen
|
|
1568
|
+
? contentStr.substring(0, maxCodeLen - 1) + 'ā¦'
|
|
1569
|
+
: contentStr.padEnd(maxCodeLen, ' ');
|
|
1570
|
+
const innerRow = ` ${lineNum} ${prefix} ${paddedCode} `;
|
|
1571
|
+
if (prefix === '-') {
|
|
1572
|
+
console.log(theme.colorFn('ā') + theme.diffRemoveBg(innerRow) + theme.colorFn('ā'));
|
|
1573
|
+
}
|
|
1574
|
+
else if (prefix === '+') {
|
|
1575
|
+
console.log(theme.colorFn('ā') + theme.diffAddBg(innerRow) + theme.colorFn('ā'));
|
|
1576
|
+
}
|
|
1577
|
+
else {
|
|
1578
|
+
console.log(theme.colorFn('ā') + pc.dim(innerRow) + theme.colorFn('ā'));
|
|
1579
|
+
}
|
|
1450
1580
|
}
|
|
1451
1581
|
else {
|
|
1452
|
-
|
|
1582
|
+
const maxLen = Math.max(10, boxWidth - 4);
|
|
1583
|
+
const padded = line.length > maxLen ? line.substring(0, maxLen - 1) + 'ā¦' : line.padEnd(maxLen, ' ');
|
|
1584
|
+
console.log(theme.colorFn('ā ') + pc.white(padded) + theme.colorFn(' ā'));
|
|
1453
1585
|
}
|
|
1454
1586
|
}
|
|
1455
1587
|
if (parsed.diffLines.length > maxShown) {
|
|
1456
|
-
|
|
1588
|
+
const dots = `... +${parsed.diffLines.length - maxShown} more lines`;
|
|
1589
|
+
const maxLen = Math.max(10, boxWidth - 4);
|
|
1590
|
+
const paddedDots = dots.length > maxLen ? dots.substring(0, maxLen - 1) + 'ā¦' : dots.padEnd(maxLen, ' ');
|
|
1591
|
+
console.log(theme.colorFn('ā ') + pc.dim(paddedDots) + theme.colorFn(' ā'));
|
|
1457
1592
|
}
|
|
1458
|
-
console.log(
|
|
1459
|
-
console.log(
|
|
1593
|
+
console.log(theme.colorFn('ā' + 'ā'.repeat(boxWidth - 2) + 'ā'));
|
|
1594
|
+
console.log(theme.boldFn(` āā ${parsed.summary}`));
|
|
1460
1595
|
}
|
|
1461
1596
|
else if (parsed.summary) {
|
|
1462
1597
|
console.log(pc.green(` āā ${parsed.summary}`));
|
|
@@ -1535,6 +1670,12 @@ export async function main() {
|
|
|
1535
1670
|
process.stdout.write('\n');
|
|
1536
1671
|
}
|
|
1537
1672
|
globalSnapshotManager.finishTurn();
|
|
1673
|
+
const turnElapsedMs = Date.now() - turnStartTime;
|
|
1674
|
+
const shouldNotify = (turnElapsedMs >= 15000) || (toolsExecutedCount > 0) || (planMode && lastPlanReady !== null);
|
|
1675
|
+
if (shouldNotify && !aborted) {
|
|
1676
|
+
const secs = Math.round(turnElapsedMs / 1000);
|
|
1677
|
+
notifyDevice('devx', `Task completed in ${secs}s!`);
|
|
1678
|
+
}
|
|
1538
1679
|
// Auto-save session
|
|
1539
1680
|
await sessionManager.save(history.getMessages(), totalSessionCost, config.model, planMode);
|
|
1540
1681
|
// 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.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Ultra-fast, terminal-native AI coding assistant and agent built for Android Termux, Windows, macOS, and Linux.",
|
|
5
5
|
"main": "dist/cli/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -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
|
}
|