@darksol/terminal 0.10.0 → 0.11.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/README.md +111 -1
- package/package.json +4 -1
- package/src/browser/actions.js +58 -0
- package/src/cli.js +136 -0
- package/src/config/keys.js +12 -2
- package/src/daemon/index.js +225 -0
- package/src/daemon/manager.js +148 -0
- package/src/daemon/pid.js +80 -0
- package/src/services/browser.js +659 -0
- package/src/services/telegram.js +570 -0
- package/src/web/commands.js +135 -1
- package/src/web/server.js +21 -2
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ A unified CLI for market intel, trading, AI-powered analysis, on-chain oracle, c
|
|
|
15
15
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
|
16
16
|
[](https://nodejs.org/)
|
|
17
17
|
|
|
18
|
-
- Current release: **0.
|
|
18
|
+
- Current release: **0.11.0**
|
|
19
19
|
- Changelog: `CHANGELOG.md`
|
|
20
20
|
|
|
21
21
|
## Install
|
|
@@ -81,6 +81,27 @@ darksol serve
|
|
|
81
81
|
|
|
82
82
|
# Start agent signer for OpenClaw
|
|
83
83
|
darksol agent start main
|
|
84
|
+
|
|
85
|
+
# Telegram bot — AI chat through Telegram
|
|
86
|
+
darksol telegram setup
|
|
87
|
+
darksol telegram start
|
|
88
|
+
darksol telegram status
|
|
89
|
+
darksol telegram send 123456789 "Hello from DARKSOL"
|
|
90
|
+
|
|
91
|
+
# Background daemon — manage persistent services
|
|
92
|
+
darksol daemon start
|
|
93
|
+
darksol daemon status
|
|
94
|
+
darksol daemon stop
|
|
95
|
+
|
|
96
|
+
# Browser automation (requires: npm i playwright-core)
|
|
97
|
+
darksol browser launch --headed
|
|
98
|
+
darksol browser navigate https://app.uniswap.org
|
|
99
|
+
darksol browser screenshot swap-page.png
|
|
100
|
+
darksol browser click "#swap-button"
|
|
101
|
+
darksol browser type "#amount-input" "1.0"
|
|
102
|
+
darksol browser eval "document.title"
|
|
103
|
+
darksol browser close
|
|
104
|
+
darksol browser install
|
|
84
105
|
```
|
|
85
106
|
|
|
86
107
|
## `darksol serve` (Web Terminal UX)
|
|
@@ -147,11 +168,100 @@ ai <prompt> # chat with trading assistant
|
|
|
147
168
|
| `cards` | Crypto → prepaid Visa/MC cards | Service fees |
|
|
148
169
|
| `builders` | ERC-8021 builder directory + leaderboard | Free |
|
|
149
170
|
| `facilitator` | x402 payment verification & settlement | Free |
|
|
171
|
+
| `telegram` | Telegram bot — AI chat via Telegram Bot API | Provider dependent |
|
|
172
|
+
| `daemon` | Background service daemon (manages TG, browser, etc.) | Free |
|
|
173
|
+
| `browser` | Playwright-powered browser automation | Free |
|
|
150
174
|
| `serve` | Local interactive web terminal (xterm.js) | Free |
|
|
151
175
|
| `config` | Terminal configuration | Free |
|
|
152
176
|
|
|
153
177
|
---
|
|
154
178
|
|
|
179
|
+
## 📱 Telegram Bot
|
|
180
|
+
|
|
181
|
+
Turn your terminal into a Telegram AI agent. Same brain (LLM + soul + memory), different mouth.
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
# Guided setup — walks you through BotFather
|
|
185
|
+
darksol telegram setup
|
|
186
|
+
|
|
187
|
+
# Start the bot (foreground, or managed by daemon)
|
|
188
|
+
darksol telegram start
|
|
189
|
+
|
|
190
|
+
# Check bot status
|
|
191
|
+
darksol telegram status
|
|
192
|
+
|
|
193
|
+
# Send a direct message
|
|
194
|
+
darksol telegram send <chat_id> "Hello from DARKSOL"
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**Setup walkthrough:**
|
|
198
|
+
1. Open Telegram → search `@BotFather` → send `/newbot`
|
|
199
|
+
2. Follow BotFather's prompts to name your bot
|
|
200
|
+
3. Copy the bot token
|
|
201
|
+
4. Run `darksol telegram setup` → paste token → auto-validates via `getMe`
|
|
202
|
+
5. Token encrypted and stored in your key vault
|
|
203
|
+
6. `darksol telegram start` → bot goes live
|
|
204
|
+
|
|
205
|
+
**Features:**
|
|
206
|
+
- Per-chat session memory (remembers conversation context)
|
|
207
|
+
- Soul system prompt (your agent's personality carries over)
|
|
208
|
+
- Built-in commands: `/start`, `/help`, `/status`
|
|
209
|
+
- Typing indicators while LLM processes
|
|
210
|
+
- Rate limiting (1 req/sec per chat)
|
|
211
|
+
- 429 auto-retry for Telegram API limits
|
|
212
|
+
- Daemon-aware: runs foreground solo, or as a managed service
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## 🖥️ Background Daemon
|
|
217
|
+
|
|
218
|
+
One process to rule them all. Manages persistent services (Telegram bot, browser, future channels).
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
darksol daemon start # Detached background process
|
|
222
|
+
darksol daemon status # PID, uptime, active services
|
|
223
|
+
darksol daemon stop # Graceful shutdown
|
|
224
|
+
darksol daemon restart # Stop + start
|
|
225
|
+
darksol daemon start --port 9999 # Custom health port
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**Health endpoint:** `http://localhost:18792/health` — returns uptime, version, active services list.
|
|
229
|
+
|
|
230
|
+
**Service registry:** Services (Telegram, browser, etc.) register with the daemon for managed lifecycle. Start once, everything runs.
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## 🌐 Browser Automation
|
|
235
|
+
|
|
236
|
+
Playwright-powered browser control — automate dApps, scrape data, take screenshots.
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
# Install browser binary (one-time)
|
|
240
|
+
darksol browser install
|
|
241
|
+
|
|
242
|
+
# Launch and control
|
|
243
|
+
darksol browser launch --headed --type chromium
|
|
244
|
+
darksol browser navigate https://app.uniswap.org
|
|
245
|
+
darksol browser screenshot swap-page.png
|
|
246
|
+
darksol browser click "#connect-wallet"
|
|
247
|
+
darksol browser type "#search" "AERO"
|
|
248
|
+
darksol browser eval "document.title"
|
|
249
|
+
darksol browser status
|
|
250
|
+
darksol browser close
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**Requires:** `npm install playwright-core` (optional dependency — only needed if you use browser features).
|
|
254
|
+
|
|
255
|
+
**Features:**
|
|
256
|
+
- Chromium, Firefox, or WebKit
|
|
257
|
+
- Headless (default) or headed mode
|
|
258
|
+
- Named profiles with persistent cookies/sessions (`~/.darksol/browser/profiles/`)
|
|
259
|
+
- IPC via named pipes — CLI commands talk to a running browser instance
|
|
260
|
+
- Web shell integration (`browser` command in `darksol serve`)
|
|
261
|
+
- Daemon-managed when running
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
155
265
|
## 🔐 Secure Agent Signer
|
|
156
266
|
|
|
157
267
|
**The killer feature.** A PK-isolated signing proxy for AI agents (OpenClaw, etc.).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@darksol/terminal",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "DARKSOL Terminal — unified CLI for all DARKSOL services. Market intel, trading, oracle, casino, and more.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -46,6 +46,9 @@
|
|
|
46
46
|
"update-notifier": "^7.3.1",
|
|
47
47
|
"ws": "^8.19.0"
|
|
48
48
|
},
|
|
49
|
+
"optionalDependencies": {
|
|
50
|
+
"playwright-core": "^1.52.0"
|
|
51
|
+
},
|
|
49
52
|
"engines": {
|
|
50
53
|
"node": ">=18.0.0"
|
|
51
54
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { sendBrowserCommand } from '../services/browser.js';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_TIMEOUT = 30_000;
|
|
4
|
+
|
|
5
|
+
export async function waitForPage(target, opts = {}) {
|
|
6
|
+
const expression = JSON.stringify(target);
|
|
7
|
+
return sendBrowserCommand('eval', {
|
|
8
|
+
expression: `
|
|
9
|
+
new Promise((resolve) => {
|
|
10
|
+
const target = ${expression};
|
|
11
|
+
const timeout = ${Number(opts.timeout || DEFAULT_TIMEOUT)};
|
|
12
|
+
const start = Date.now();
|
|
13
|
+
const check = () => {
|
|
14
|
+
const matches = typeof target === 'string'
|
|
15
|
+
? window.location.href.includes(target)
|
|
16
|
+
: true;
|
|
17
|
+
if (matches) return resolve({ ok: true, url: window.location.href });
|
|
18
|
+
if (Date.now() - start > timeout) return resolve({ ok: false, url: window.location.href });
|
|
19
|
+
setTimeout(check, 250);
|
|
20
|
+
};
|
|
21
|
+
check();
|
|
22
|
+
})
|
|
23
|
+
`,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function fillForm(fields = [], opts = {}) {
|
|
28
|
+
for (const field of fields) {
|
|
29
|
+
await sendBrowserCommand('type', {
|
|
30
|
+
selector: field.selector,
|
|
31
|
+
text: field.value,
|
|
32
|
+
timeout: opts.timeout || DEFAULT_TIMEOUT,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function runLoginFlow(flow = {}) {
|
|
39
|
+
if (flow.url) {
|
|
40
|
+
await sendBrowserCommand('navigate', {
|
|
41
|
+
url: flow.url,
|
|
42
|
+
timeout: flow.timeout || DEFAULT_TIMEOUT,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
if (Array.isArray(flow.fields) && flow.fields.length) {
|
|
46
|
+
await fillForm(flow.fields, flow);
|
|
47
|
+
}
|
|
48
|
+
if (flow.submitSelector) {
|
|
49
|
+
await sendBrowserCommand('click', {
|
|
50
|
+
selector: flow.submitSelector,
|
|
51
|
+
timeout: flow.timeout || DEFAULT_TIMEOUT,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
if (flow.waitFor) {
|
|
55
|
+
await waitForPage(flow.waitFor, flow);
|
|
56
|
+
}
|
|
57
|
+
return sendBrowserCommand('status');
|
|
58
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -22,6 +22,17 @@ import { cardsCatalog, cardsOrder, cardsStatus } from './services/cards.js';
|
|
|
22
22
|
import { facilitatorHealth, facilitatorVerify, facilitatorSettle } from './services/facilitator.js';
|
|
23
23
|
import { buildersLeaderboard, buildersLookup, buildersFeed } from './services/builders.js';
|
|
24
24
|
import { createScript, listScripts, runScript, showScript, editScript, deleteScript, cloneScript, listTemplates } from './scripts/engine.js';
|
|
25
|
+
import {
|
|
26
|
+
launchBrowserCommand,
|
|
27
|
+
navigateBrowserCommand,
|
|
28
|
+
browserScreenshotCommand,
|
|
29
|
+
browserClickCommand,
|
|
30
|
+
browserTypeCommand,
|
|
31
|
+
browserEvalCommand,
|
|
32
|
+
browserCloseCommand,
|
|
33
|
+
showBrowserStatus,
|
|
34
|
+
installPlaywrightBrowsers,
|
|
35
|
+
} from './services/browser.js';
|
|
25
36
|
import { showTradingTips, showScriptTips, showNetworkReference, showQuickStart, showWalletSummary, showTokenInfo, showTxResult } from './utils/helpers.js';
|
|
26
37
|
import { addKey, removeKey, listKeys } from './config/keys.js';
|
|
27
38
|
import { parseIntent, startChat, adviseStrategy, analyzeToken, executeIntent } from './llm/intent.js';
|
|
@@ -31,6 +42,8 @@ import { runSetupWizard } from './setup/wizard.js';
|
|
|
31
42
|
import { displaySoul, hasSoul, resetSoul, runSoulSetup } from './soul/index.js';
|
|
32
43
|
import { clearMemories, exportMemories, getRecentMemories, searchMemories } from './memory/index.js';
|
|
33
44
|
import { getAgentStatus, planAgentGoal, runAgentTask } from './agent/index.js';
|
|
45
|
+
import { daemonStart, daemonStop, daemonStatus, daemonRestart } from './daemon/index.js';
|
|
46
|
+
import { telegramSetup, telegramStartForeground, telegramStopCommand, telegramStatusCommand, telegramSendCommand } from './services/telegram.js';
|
|
34
47
|
import { createRequire } from 'module';
|
|
35
48
|
import { resolve } from 'path';
|
|
36
49
|
import { getConfiguredModel, getProviderDefaultModel } from './llm/models.js';
|
|
@@ -608,6 +621,64 @@ export function cli(argv) {
|
|
|
608
621
|
.option('--no-open', 'Don\'t auto-open browser')
|
|
609
622
|
.action((opts) => startWebShell(opts));
|
|
610
623
|
|
|
624
|
+
const browser = program
|
|
625
|
+
.command('browser')
|
|
626
|
+
.description('Playwright-powered browser automation');
|
|
627
|
+
|
|
628
|
+
browser
|
|
629
|
+
.command('launch')
|
|
630
|
+
.description('Launch a browser instance and keep it running')
|
|
631
|
+
.option('--headed', 'Launch with a visible browser window')
|
|
632
|
+
.option('--type <browser>', 'Browser type', 'chromium')
|
|
633
|
+
.option('--profile <name>', 'Browser profile name', 'default')
|
|
634
|
+
.action((opts) => launchBrowserCommand(opts));
|
|
635
|
+
|
|
636
|
+
browser
|
|
637
|
+
.command('navigate <url>')
|
|
638
|
+
.description('Navigate the active page to a URL')
|
|
639
|
+
.action((url) => navigateBrowserCommand(url));
|
|
640
|
+
|
|
641
|
+
browser
|
|
642
|
+
.command('screenshot [filename]')
|
|
643
|
+
.description('Capture a screenshot of the active page')
|
|
644
|
+
.action((filename) => browserScreenshotCommand(filename));
|
|
645
|
+
|
|
646
|
+
browser
|
|
647
|
+
.command('click <selector>')
|
|
648
|
+
.description('Click an element on the active page')
|
|
649
|
+
.action((selector) => browserClickCommand(selector));
|
|
650
|
+
|
|
651
|
+
browser
|
|
652
|
+
.command('type <selector> <text>')
|
|
653
|
+
.description('Type text into an element on the active page')
|
|
654
|
+
.action((selector, text) => browserTypeCommand(selector, text));
|
|
655
|
+
|
|
656
|
+
browser
|
|
657
|
+
.command('eval <js>')
|
|
658
|
+
.description('Evaluate JavaScript in the active page')
|
|
659
|
+
.action((js) => browserEvalCommand(js));
|
|
660
|
+
|
|
661
|
+
browser
|
|
662
|
+
.command('close')
|
|
663
|
+
.description('Close the running browser service')
|
|
664
|
+
.action(() => browserCloseCommand());
|
|
665
|
+
|
|
666
|
+
browser
|
|
667
|
+
.command('status')
|
|
668
|
+
.description('Show current browser state')
|
|
669
|
+
.action(() => showBrowserStatus());
|
|
670
|
+
|
|
671
|
+
browser
|
|
672
|
+
.command('install')
|
|
673
|
+
.description('Install a Playwright browser binary after user confirmation')
|
|
674
|
+
.action(async () => {
|
|
675
|
+
try {
|
|
676
|
+
await installPlaywrightBrowsers();
|
|
677
|
+
} catch (err) {
|
|
678
|
+
error(err.message);
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
|
|
611
682
|
// ═══════════════════════════════════════
|
|
612
683
|
// PORTFOLIO SHORTCUT
|
|
613
684
|
// ═══════════════════════════════════════
|
|
@@ -1031,6 +1102,68 @@ export function cli(argv) {
|
|
|
1031
1102
|
.description('Uninstall a skill')
|
|
1032
1103
|
.action((name) => uninstallSkill(name));
|
|
1033
1104
|
|
|
1105
|
+
// ═══════════════════════════════════════
|
|
1106
|
+
// DAEMON COMMANDS
|
|
1107
|
+
// ═══════════════════════════════════════
|
|
1108
|
+
const daemon = program
|
|
1109
|
+
.command('daemon')
|
|
1110
|
+
.description('Background daemon - manage persistent services');
|
|
1111
|
+
|
|
1112
|
+
daemon
|
|
1113
|
+
.command('start')
|
|
1114
|
+
.description('Start the background daemon')
|
|
1115
|
+
.option('-p, --port <port>', 'Health server port', '18792')
|
|
1116
|
+
.action((opts) => daemonStart(opts));
|
|
1117
|
+
|
|
1118
|
+
daemon
|
|
1119
|
+
.command('stop')
|
|
1120
|
+
.description('Stop the background daemon')
|
|
1121
|
+
.action(() => daemonStop());
|
|
1122
|
+
|
|
1123
|
+
daemon
|
|
1124
|
+
.command('status')
|
|
1125
|
+
.description('Show daemon status and health')
|
|
1126
|
+
.option('-p, --port <port>', 'Health server port', '18792')
|
|
1127
|
+
.action((opts) => daemonStatus(opts));
|
|
1128
|
+
|
|
1129
|
+
daemon
|
|
1130
|
+
.command('restart')
|
|
1131
|
+
.description('Restart the daemon')
|
|
1132
|
+
.option('-p, --port <port>', 'Health server port', '18792')
|
|
1133
|
+
.action((opts) => daemonRestart(opts));
|
|
1134
|
+
|
|
1135
|
+
// ═══════════════════════════════════════
|
|
1136
|
+
// TELEGRAM COMMANDS
|
|
1137
|
+
// ═══════════════════════════════════════
|
|
1138
|
+
const telegram = program
|
|
1139
|
+
.command('telegram')
|
|
1140
|
+
.description('Telegram bot - AI chat via Telegram');
|
|
1141
|
+
|
|
1142
|
+
telegram
|
|
1143
|
+
.command('setup')
|
|
1144
|
+
.description('Interactive Telegram bot setup with BotFather')
|
|
1145
|
+
.action(() => telegramSetup());
|
|
1146
|
+
|
|
1147
|
+
telegram
|
|
1148
|
+
.command('start')
|
|
1149
|
+
.description('Start the Telegram bot (foreground)')
|
|
1150
|
+
.action(() => telegramStartForeground());
|
|
1151
|
+
|
|
1152
|
+
telegram
|
|
1153
|
+
.command('stop')
|
|
1154
|
+
.description('Stop the Telegram bot')
|
|
1155
|
+
.action(() => telegramStopCommand());
|
|
1156
|
+
|
|
1157
|
+
telegram
|
|
1158
|
+
.command('status')
|
|
1159
|
+
.description('Show bot info and connection state')
|
|
1160
|
+
.action(() => telegramStatusCommand());
|
|
1161
|
+
|
|
1162
|
+
telegram
|
|
1163
|
+
.command('send <chatId> <message...>')
|
|
1164
|
+
.description('Send a direct message to a chat')
|
|
1165
|
+
.action((chatId, message) => telegramSendCommand(chatId, message));
|
|
1166
|
+
|
|
1034
1167
|
// ═══════════════════════════════════════
|
|
1035
1168
|
// TIPS & REFERENCE COMMANDS
|
|
1036
1169
|
// ═══════════════════════════════════════
|
|
@@ -1602,6 +1735,9 @@ function showCommandList() {
|
|
|
1602
1735
|
['mail', 'AgentMail - email for your agent'],
|
|
1603
1736
|
['facilitator', 'x402 payment facilitator'],
|
|
1604
1737
|
['skills', 'Agent skill directory'],
|
|
1738
|
+
['browser', 'Playwright browser automation'],
|
|
1739
|
+
['daemon', 'Background service daemon'],
|
|
1740
|
+
['telegram', 'Telegram bot - AI chat'],
|
|
1605
1741
|
['serve', 'Launch web terminal in browser'],
|
|
1606
1742
|
['setup', 'Re-run setup wizard'],
|
|
1607
1743
|
['config', 'Terminal configuration'],
|
package/src/config/keys.js
CHANGED
|
@@ -178,6 +178,16 @@ export const SERVICES = {
|
|
|
178
178
|
docsUrl: 'https://console.agentmail.to',
|
|
179
179
|
validate: (key) => key.startsWith('am_'),
|
|
180
180
|
},
|
|
181
|
+
|
|
182
|
+
// Messaging
|
|
183
|
+
telegram: {
|
|
184
|
+
name: 'Telegram Bot',
|
|
185
|
+
category: 'messaging',
|
|
186
|
+
description: 'Telegram bot token — AI chat via Telegram',
|
|
187
|
+
envVar: 'TELEGRAM_BOT_TOKEN',
|
|
188
|
+
docsUrl: 'https://core.telegram.org/bots#botfather',
|
|
189
|
+
validate: (key) => /^\d+:.+$/.test(key),
|
|
190
|
+
},
|
|
181
191
|
paraswap: {
|
|
182
192
|
name: 'ParaSwap',
|
|
183
193
|
category: 'trading',
|
|
@@ -319,8 +329,8 @@ export function listKeys() {
|
|
|
319
329
|
|
|
320
330
|
showSection('API KEY VAULT');
|
|
321
331
|
|
|
322
|
-
const categories = ['llm', 'data', 'rpc', 'trading', 'email'];
|
|
323
|
-
const catNames = { llm: '🧠 LLM PROVIDERS', data: '📊 DATA PROVIDERS', rpc: '🌐 RPC PROVIDERS', trading: '📈 TRADING', email: '📧 EMAIL' };
|
|
332
|
+
const categories = ['llm', 'data', 'rpc', 'trading', 'email', 'messaging'];
|
|
333
|
+
const catNames = { llm: '🧠 LLM PROVIDERS', data: '📊 DATA PROVIDERS', rpc: '🌐 RPC PROVIDERS', trading: '📈 TRADING', email: '📧 EMAIL', messaging: '💬 MESSAGING' };
|
|
324
334
|
|
|
325
335
|
for (const cat of categories) {
|
|
326
336
|
console.log('');
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { spawn, execSync } from 'child_process';
|
|
2
|
+
import { createServer } from 'http';
|
|
3
|
+
import { appendFileSync, mkdirSync, existsSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { homedir } from 'os';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { writePid, readPid, removePid, getDaemonStatus, DARKSOL_DIR } from './pid.js';
|
|
8
|
+
import { getAllServiceStatus, stopAllServices, listServices } from './manager.js';
|
|
9
|
+
import { theme } from '../ui/theme.js';
|
|
10
|
+
import { spinner, kvDisplay, success, error, warn, info } from '../ui/components.js';
|
|
11
|
+
import { showSection } from '../ui/banner.js';
|
|
12
|
+
import { createRequire } from 'module';
|
|
13
|
+
import fetch from 'node-fetch';
|
|
14
|
+
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
const { version: PKG_VERSION } = require('../../package.json');
|
|
17
|
+
|
|
18
|
+
const LOGS_DIR = join(DARKSOL_DIR, 'logs');
|
|
19
|
+
const LOG_FILE = join(LOGS_DIR, 'daemon.log');
|
|
20
|
+
const DEFAULT_PORT = 18792;
|
|
21
|
+
|
|
22
|
+
function ensureLogsDir() {
|
|
23
|
+
if (!existsSync(LOGS_DIR)) mkdirSync(LOGS_DIR, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function daemonLog(msg) {
|
|
27
|
+
ensureLogsDir();
|
|
28
|
+
const ts = new Date().toISOString();
|
|
29
|
+
appendFileSync(LOG_FILE, `[${ts}] ${msg}\n`, 'utf8');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Start the daemon as a detached background process.
|
|
34
|
+
* @param {object} [opts]
|
|
35
|
+
* @param {number} [opts.port]
|
|
36
|
+
*/
|
|
37
|
+
export async function daemonStart(opts = {}) {
|
|
38
|
+
const status = getDaemonStatus();
|
|
39
|
+
if (status.running) {
|
|
40
|
+
warn(`Daemon already running (PID ${status.pid})`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const port = parseInt(opts.port, 10) || DEFAULT_PORT;
|
|
45
|
+
const entryScript = fileURLToPath(new URL('./index.js', import.meta.url));
|
|
46
|
+
|
|
47
|
+
const child = spawn(process.execPath, [entryScript, '--daemon-run', String(port)], {
|
|
48
|
+
detached: true,
|
|
49
|
+
stdio: 'ignore',
|
|
50
|
+
env: { ...process.env, DARKSOL_DAEMON: '1' },
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
child.unref();
|
|
54
|
+
|
|
55
|
+
if (child.pid) {
|
|
56
|
+
writePid(child.pid);
|
|
57
|
+
success(`Daemon started (PID ${child.pid}, port ${port})`);
|
|
58
|
+
info(`Logs: ${LOG_FILE}`);
|
|
59
|
+
info(`Health: http://localhost:${port}/health`);
|
|
60
|
+
} else {
|
|
61
|
+
error('Failed to start daemon process');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Stop the running daemon.
|
|
67
|
+
*/
|
|
68
|
+
export async function daemonStop() {
|
|
69
|
+
const status = getDaemonStatus();
|
|
70
|
+
if (!status.running) {
|
|
71
|
+
warn('Daemon is not running');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const spin = spinner('Stopping daemon...').start();
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
if (process.platform === 'win32') {
|
|
79
|
+
execSync(`taskkill /PID ${status.pid} /F /T`, { stdio: 'ignore' });
|
|
80
|
+
} else {
|
|
81
|
+
process.kill(status.pid, 'SIGTERM');
|
|
82
|
+
}
|
|
83
|
+
removePid();
|
|
84
|
+
spin.succeed(`Daemon stopped (PID ${status.pid})`);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
removePid();
|
|
87
|
+
spin.fail('Daemon stop had issues');
|
|
88
|
+
warn(err.message);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Show daemon status — process check + health endpoint query.
|
|
94
|
+
* @param {object} [opts]
|
|
95
|
+
* @param {number} [opts.port]
|
|
96
|
+
*/
|
|
97
|
+
export async function daemonStatus(opts = {}) {
|
|
98
|
+
const port = parseInt(opts.port, 10) || DEFAULT_PORT;
|
|
99
|
+
const status = getDaemonStatus();
|
|
100
|
+
|
|
101
|
+
showSection('DAEMON STATUS');
|
|
102
|
+
|
|
103
|
+
if (!status.running) {
|
|
104
|
+
kvDisplay([
|
|
105
|
+
['Process', theme.dim('not running')],
|
|
106
|
+
['PID File', theme.dim('none')],
|
|
107
|
+
]);
|
|
108
|
+
console.log('');
|
|
109
|
+
info('Start with: darksol daemon start');
|
|
110
|
+
console.log('');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Process is alive — try health endpoint
|
|
115
|
+
let health = null;
|
|
116
|
+
try {
|
|
117
|
+
const res = await fetch(`http://localhost:${port}/health`, { timeout: 3000 });
|
|
118
|
+
if (res.ok) health = await res.json();
|
|
119
|
+
} catch {
|
|
120
|
+
// health endpoint unreachable
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const pairs = [
|
|
124
|
+
['Process', theme.success(`running (PID ${status.pid})`)],
|
|
125
|
+
['Port', String(port)],
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
if (health) {
|
|
129
|
+
pairs.push(['Uptime', `${Math.round(health.uptime)}s`]);
|
|
130
|
+
pairs.push(['Version', health.version || PKG_VERSION]);
|
|
131
|
+
pairs.push(['Services', health.services?.length ? health.services.join(', ') : theme.dim('none')]);
|
|
132
|
+
} else {
|
|
133
|
+
pairs.push(['Health', theme.warning('unreachable')]);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
pairs.push(['Log', LOG_FILE]);
|
|
137
|
+
kvDisplay(pairs);
|
|
138
|
+
console.log('');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Restart the daemon (stop + start).
|
|
143
|
+
* @param {object} [opts]
|
|
144
|
+
*/
|
|
145
|
+
export async function daemonRestart(opts = {}) {
|
|
146
|
+
const status = getDaemonStatus();
|
|
147
|
+
if (status.running) {
|
|
148
|
+
await daemonStop();
|
|
149
|
+
// Brief pause to let the OS release the port
|
|
150
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
151
|
+
}
|
|
152
|
+
await daemonStart(opts);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ─────────────────────────────────────
|
|
156
|
+
// DAEMON PROCESS ENTRY (run by child)
|
|
157
|
+
// ─────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Run the actual daemon process (HTTP health server + service management).
|
|
161
|
+
* Called when the script is executed directly with --daemon-run.
|
|
162
|
+
* @param {number} port
|
|
163
|
+
*/
|
|
164
|
+
export async function runDaemonProcess(port) {
|
|
165
|
+
const startTime = Date.now();
|
|
166
|
+
|
|
167
|
+
daemonLog(`Daemon starting on port ${port} (PID ${process.pid})`);
|
|
168
|
+
|
|
169
|
+
const server = createServer((req, res) => {
|
|
170
|
+
if (req.url === '/health' && req.method === 'GET') {
|
|
171
|
+
const uptimeSec = (Date.now() - startTime) / 1000;
|
|
172
|
+
const services = getAllServiceStatus();
|
|
173
|
+
const body = JSON.stringify({
|
|
174
|
+
status: 'ok',
|
|
175
|
+
pid: process.pid,
|
|
176
|
+
uptime: uptimeSec,
|
|
177
|
+
version: PKG_VERSION,
|
|
178
|
+
services: services.map((s) => s.name),
|
|
179
|
+
serviceDetails: services,
|
|
180
|
+
});
|
|
181
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
182
|
+
res.end(body);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
187
|
+
res.end(JSON.stringify({ error: 'not found' }));
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
server.listen(port, '127.0.0.1', () => {
|
|
191
|
+
daemonLog(`Health server listening on http://127.0.0.1:${port}/health`);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Graceful shutdown
|
|
195
|
+
const shutdown = async () => {
|
|
196
|
+
daemonLog('Shutting down daemon...');
|
|
197
|
+
await stopAllServices();
|
|
198
|
+
server.close();
|
|
199
|
+
removePid();
|
|
200
|
+
daemonLog('Daemon stopped');
|
|
201
|
+
process.exit(0);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
process.on('SIGTERM', shutdown);
|
|
205
|
+
process.on('SIGINT', shutdown);
|
|
206
|
+
process.on('uncaughtException', (err) => {
|
|
207
|
+
daemonLog(`Uncaught exception: ${err.message}`);
|
|
208
|
+
});
|
|
209
|
+
process.on('unhandledRejection', (err) => {
|
|
210
|
+
daemonLog(`Unhandled rejection: ${err}`);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ─────────────────────────────────────
|
|
215
|
+
// SELF-EXECUTION: when run directly as daemon child process
|
|
216
|
+
// ─────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
const args = process.argv.slice(2);
|
|
219
|
+
if (args[0] === '--daemon-run') {
|
|
220
|
+
const port = parseInt(args[1], 10) || DEFAULT_PORT;
|
|
221
|
+
writePid(process.pid);
|
|
222
|
+
runDaemonProcess(port);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export { DEFAULT_PORT, LOG_FILE };
|