@lightharu/krouter 1.8.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/LICENSE +679 -0
- package/README.md +238 -0
- package/dist-web/assets/index-CM4-0adf.css +1 -0
- package/dist-web/assets/index-DCslvfUR.js +139 -0
- package/dist-web/favicon.svg +9 -0
- package/dist-web/icon.svg +9 -0
- package/dist-web/index.html +19 -0
- package/out-server/main/kiroAuthSync.js +249 -0
- package/out-server/main/kproxy/certManager.js +262 -0
- package/out-server/main/kproxy/index.js +254 -0
- package/out-server/main/kproxy/mitmProxy.js +475 -0
- package/out-server/main/kproxy/types.js +23 -0
- package/out-server/main/proxy/accountPool.js +543 -0
- package/out-server/main/proxy/clientConfig.js +596 -0
- package/out-server/main/proxy/index.js +25 -0
- package/out-server/main/proxy/kiroApi.js +1996 -0
- package/out-server/main/proxy/logger.js +407 -0
- package/out-server/main/proxy/modelCatalog.js +75 -0
- package/out-server/main/proxy/promptCacheTracker.js +301 -0
- package/out-server/main/proxy/proxyServer.js +3543 -0
- package/out-server/main/proxy/selfSignedCert.js +179 -0
- package/out-server/main/proxy/systemProxy.js +250 -0
- package/out-server/main/proxy/tokenCounter.js +164 -0
- package/out-server/main/proxy/toolNameRegistry.js +57 -0
- package/out-server/main/proxy/translator.js +1084 -0
- package/out-server/main/proxy/types.js +3 -0
- package/out-server/main/registration/browser-identity.js +184 -0
- package/out-server/main/registration/chainProxy.js +349 -0
- package/out-server/main/registration/config.js +58 -0
- package/out-server/main/registration/email-service.js +801 -0
- package/out-server/main/registration/fingerprint.js +352 -0
- package/out-server/main/registration/http-utils.js +148 -0
- package/out-server/main/registration/jwe.js +74 -0
- package/out-server/main/registration/names.js +142 -0
- package/out-server/main/registration/proton-mail-window.js +339 -0
- package/out-server/main/registration/registrar.js +1715 -0
- package/out-server/main/registration/tlsClientPool.js +70 -0
- package/out-server/main/registration/xxtea.js +161 -0
- package/out-server/main/runtimePaths.js +19 -0
- package/out-server/main/utils/redact.js +95 -0
- package/out-server/server/index.js +1272 -0
- package/out-server/server/services/accountExtras.js +105 -0
- package/out-server/server/services/accountProfileHydration.js +95 -0
- package/out-server/server/services/authFlows.js +509 -0
- package/out-server/server/services/dashboardTunnel.js +315 -0
- package/out-server/server/services/diagnostics.js +326 -0
- package/out-server/server/services/kiroAccounts.js +431 -0
- package/out-server/server/services/kiroSettings.js +260 -0
- package/out-server/server/services/kproxyRuntime.js +264 -0
- package/out-server/server/services/localKiroCredentials.js +320 -0
- package/out-server/server/services/machineIdRuntime.js +327 -0
- package/out-server/server/services/protonBrowserRuntime.js +724 -0
- package/out-server/server/services/proxyRuntime.js +523 -0
- package/out-server/server/services/registrationRuntime.js +106 -0
- package/out-server/server/store.js +266 -0
- package/package.json +113 -0
- package/resources/tls-client-xgo-1.14.0-windows-amd64.dll +0 -0
- package/scripts/kiro-manager-cli.cjs +3 -0
- package/scripts/krouter-cli.cjs +509 -0
- package/src/renderer/src/assets/krouter-logo.svg +11 -0
- package/src/renderer/src/assets/krouter-mark.svg +9 -0
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto')
|
|
4
|
+
const fs = require('fs')
|
|
5
|
+
const os = require('os')
|
|
6
|
+
const path = require('path')
|
|
7
|
+
const readline = require('readline/promises')
|
|
8
|
+
const { spawn } = require('child_process')
|
|
9
|
+
const { stdin: input, stdout: output } = require('process')
|
|
10
|
+
|
|
11
|
+
const PACKAGE_ROOT = path.resolve(__dirname, '..')
|
|
12
|
+
const DATA_DIR = path.resolve(process.env.KROUTER_DATA_DIR || process.env.KIRO_WEB_DATA_DIR || path.join(os.homedir(), '.krouter'))
|
|
13
|
+
const ENV_FILE = path.join(DATA_DIR, '.env')
|
|
14
|
+
const PID_FILE = path.join(DATA_DIR, 'server.pid')
|
|
15
|
+
const SERVER_OUT = path.join(DATA_DIR, 'server.out.log')
|
|
16
|
+
const SERVER_ERR = path.join(DATA_DIR, 'server.err.log')
|
|
17
|
+
const SERVER_ENTRY = path.join(PACKAGE_ROOT, 'out-server', 'server', 'index.js')
|
|
18
|
+
const STATIC_ENTRY = path.join(PACKAGE_ROOT, 'dist-web', 'index.html')
|
|
19
|
+
const DEFAULT_PORT = process.env.PORT || '4010'
|
|
20
|
+
const API_BASE = (process.env.KROUTER_API_BASE || process.env.KAM_API_BASE || `http://127.0.0.1:${DEFAULT_PORT}`).replace(/\/$/, '')
|
|
21
|
+
const DASHBOARD_URL = (
|
|
22
|
+
process.env.KROUTER_DASHBOARD_URL ||
|
|
23
|
+
process.env.KAM_DASHBOARD_URL ||
|
|
24
|
+
process.env.PUBLIC_DASHBOARD_URL ||
|
|
25
|
+
process.env.DASHBOARD_URL ||
|
|
26
|
+
API_BASE
|
|
27
|
+
).replace(/\/$/, '')
|
|
28
|
+
const invokedName = path.basename(process.argv[1] || 'krouter')
|
|
29
|
+
const COMMAND_NAME = /^(krouter-cli|kiro-manager-cli)\.cjs$/i.test(invokedName) ? 'krouter' : invokedName
|
|
30
|
+
const USE_COLOR = process.stdout.isTTY && !process.env.NO_COLOR
|
|
31
|
+
const COLORS = {
|
|
32
|
+
reset: USE_COLOR ? '\x1b[0m' : '',
|
|
33
|
+
green: USE_COLOR ? '\x1b[32m' : '',
|
|
34
|
+
red: USE_COLOR ? '\x1b[31m' : '',
|
|
35
|
+
yellow: USE_COLOR ? '\x1b[33m' : '',
|
|
36
|
+
cyan: USE_COLOR ? '\x1b[36m' : '',
|
|
37
|
+
bold: USE_COLOR ? '\x1b[1m' : '',
|
|
38
|
+
dim: USE_COLOR ? '\x1b[2m' : ''
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let cookie = ''
|
|
42
|
+
|
|
43
|
+
function ensureDir(dir) {
|
|
44
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function randomSecret() {
|
|
48
|
+
return crypto.randomBytes(32).toString('base64url')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseEnvFile(file) {
|
|
52
|
+
if (!fs.existsSync(file)) return {}
|
|
53
|
+
const env = {}
|
|
54
|
+
for (const line of fs.readFileSync(file, 'utf8').split(/\r?\n/)) {
|
|
55
|
+
const trimmed = line.trim()
|
|
56
|
+
if (!trimmed || trimmed.startsWith('#') || !trimmed.includes('=')) continue
|
|
57
|
+
const index = trimmed.indexOf('=')
|
|
58
|
+
env[trimmed.slice(0, index).trim()] = trimmed.slice(index + 1).trim().replace(/^['"]|['"]$/g, '')
|
|
59
|
+
}
|
|
60
|
+
return env
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function writeEnvFile(file, env) {
|
|
64
|
+
const body = Object.entries(env)
|
|
65
|
+
.map(([key, value]) => `${key}=${String(value).replace(/\r?\n/g, '')}`)
|
|
66
|
+
.join('\n')
|
|
67
|
+
fs.writeFileSync(file, `${body}\n`, 'utf8')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function ensureRuntimeEnv() {
|
|
71
|
+
ensureDir(DATA_DIR)
|
|
72
|
+
const env = parseEnvFile(ENV_FILE)
|
|
73
|
+
if (!env.SESSION_SECRET) env.SESSION_SECRET = randomSecret()
|
|
74
|
+
if (!env.APP_ENCRYPTION_KEY) env.APP_ENCRYPTION_KEY = randomSecret()
|
|
75
|
+
if (!env.KIRO_WEB_DATA_DIR) env.KIRO_WEB_DATA_DIR = DATA_DIR
|
|
76
|
+
if (!env.KIRO_RUNTIME_DATA_DIR) env.KIRO_RUNTIME_DATA_DIR = DATA_DIR
|
|
77
|
+
writeEnvFile(ENV_FILE, env)
|
|
78
|
+
return env
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function readEnvFile() {
|
|
82
|
+
const candidates = [
|
|
83
|
+
process.env.KROUTER_ENV_FILE,
|
|
84
|
+
process.env.KAM_ENV_FILE,
|
|
85
|
+
ENV_FILE,
|
|
86
|
+
path.join(process.cwd(), 'shared', '.env.web'),
|
|
87
|
+
path.join(process.cwd(), '..', 'shared', '.env.web'),
|
|
88
|
+
path.join(process.cwd(), '..', '..', 'shared', '.env.web'),
|
|
89
|
+
path.join(process.cwd(), '.env.web'),
|
|
90
|
+
path.join(process.cwd(), '.env')
|
|
91
|
+
].filter(Boolean)
|
|
92
|
+
|
|
93
|
+
for (const file of candidates) {
|
|
94
|
+
const env = parseEnvFile(file)
|
|
95
|
+
if (Object.keys(env).length > 0) return env
|
|
96
|
+
}
|
|
97
|
+
return {}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function request(pathname, options = {}) {
|
|
101
|
+
const headers = {
|
|
102
|
+
...(options.body ? { 'Content-Type': 'application/json' } : {}),
|
|
103
|
+
...(cookie ? { Cookie: cookie } : {}),
|
|
104
|
+
...(options.headers || {})
|
|
105
|
+
}
|
|
106
|
+
const response = await fetch(`${API_BASE}${pathname}`, { ...options, headers })
|
|
107
|
+
const setCookie = response.headers.get('set-cookie')
|
|
108
|
+
if (setCookie) cookie = setCookie.split(';')[0]
|
|
109
|
+
const text = await response.text()
|
|
110
|
+
let data = null
|
|
111
|
+
if (text) {
|
|
112
|
+
try {
|
|
113
|
+
data = JSON.parse(text)
|
|
114
|
+
} catch {
|
|
115
|
+
data = { message: text }
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (!response.ok) {
|
|
119
|
+
throw new Error(data?.error || data?.message || response.statusText)
|
|
120
|
+
}
|
|
121
|
+
return data
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function getHealth() {
|
|
125
|
+
try {
|
|
126
|
+
return await request('/healthz')
|
|
127
|
+
} catch {
|
|
128
|
+
return null
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function isRemoteApiBase() {
|
|
133
|
+
return Boolean(process.env.KROUTER_API_BASE || process.env.KAM_API_BASE)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isPidRunning(pid) {
|
|
137
|
+
if (!pid || Number.isNaN(Number(pid))) return false
|
|
138
|
+
try {
|
|
139
|
+
process.kill(Number(pid), 0)
|
|
140
|
+
return true
|
|
141
|
+
} catch {
|
|
142
|
+
return false
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function readPid() {
|
|
147
|
+
try {
|
|
148
|
+
return Number(fs.readFileSync(PID_FILE, 'utf8').trim())
|
|
149
|
+
} catch {
|
|
150
|
+
return 0
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function sleep(ms) {
|
|
155
|
+
await new Promise((resolve) => setTimeout(resolve, ms))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function waitForHealth(timeoutMs = 15000) {
|
|
159
|
+
const startedAt = Date.now()
|
|
160
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
161
|
+
const health = await getHealth()
|
|
162
|
+
if (health?.ok) return health
|
|
163
|
+
await sleep(500)
|
|
164
|
+
}
|
|
165
|
+
return null
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function ensureServer() {
|
|
169
|
+
const existing = await getHealth()
|
|
170
|
+
if (existing?.ok) return existing
|
|
171
|
+
|
|
172
|
+
if (isRemoteApiBase()) {
|
|
173
|
+
throw new Error(`Khong ket noi duoc Krouter backend tai ${API_BASE}`)
|
|
174
|
+
}
|
|
175
|
+
if (!fs.existsSync(SERVER_ENTRY)) {
|
|
176
|
+
throw new Error(`Thieu backend build: ${SERVER_ENTRY}. Cai lai package hoac chay npm run build:fullstack.`)
|
|
177
|
+
}
|
|
178
|
+
if (!fs.existsSync(STATIC_ENTRY)) {
|
|
179
|
+
throw new Error(`Thieu web build: ${STATIC_ENTRY}. Cai lai package hoac chay npm run build:fullstack.`)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const pid = readPid()
|
|
183
|
+
if (isPidRunning(pid)) {
|
|
184
|
+
const health = await waitForHealth(5000)
|
|
185
|
+
if (health?.ok) return health
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const runtimeEnv = ensureRuntimeEnv()
|
|
189
|
+
const out = fs.openSync(SERVER_OUT, 'a')
|
|
190
|
+
const err = fs.openSync(SERVER_ERR, 'a')
|
|
191
|
+
const child = spawn(process.execPath, [SERVER_ENTRY], {
|
|
192
|
+
cwd: PACKAGE_ROOT,
|
|
193
|
+
detached: true,
|
|
194
|
+
stdio: ['ignore', out, err],
|
|
195
|
+
env: {
|
|
196
|
+
...process.env,
|
|
197
|
+
...runtimeEnv,
|
|
198
|
+
PORT: DEFAULT_PORT,
|
|
199
|
+
HOST: process.env.HOST || '127.0.0.1',
|
|
200
|
+
SERVE_STATIC: process.env.SERVE_STATIC || 'true',
|
|
201
|
+
KROUTER_SERVER_MODE: process.env.KROUTER_SERVER_MODE || 'fullstack',
|
|
202
|
+
KROUTER_DASHBOARD_URL: DASHBOARD_URL
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
fs.writeFileSync(PID_FILE, String(child.pid), 'utf8')
|
|
206
|
+
child.unref()
|
|
207
|
+
|
|
208
|
+
const health = await waitForHealth()
|
|
209
|
+
if (!health?.ok) {
|
|
210
|
+
throw new Error(`Krouter backend khoi dong chua thanh cong. Xem log: ${SERVER_ERR}`)
|
|
211
|
+
}
|
|
212
|
+
return health
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function openBrowser(url) {
|
|
216
|
+
if (process.env.KROUTER_NO_OPEN || process.env.NO_OPEN) return
|
|
217
|
+
const platform = process.platform
|
|
218
|
+
const command = platform === 'win32' ? 'cmd'
|
|
219
|
+
: platform === 'darwin' ? 'open'
|
|
220
|
+
: 'xdg-open'
|
|
221
|
+
const args = platform === 'win32' ? ['/c', 'start', '', url] : [url]
|
|
222
|
+
try {
|
|
223
|
+
spawn(command, args, { detached: true, stdio: 'ignore' }).unref()
|
|
224
|
+
} catch {
|
|
225
|
+
// Opening the browser is best effort only.
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function login() {
|
|
230
|
+
const fileEnv = readEnvFile()
|
|
231
|
+
const session = await request('/api/auth/session').catch(() => null)
|
|
232
|
+
if (session?.authenticated) return
|
|
233
|
+
if (session?.setupRequired) {
|
|
234
|
+
throw new Error(`Krouter chua duoc setup. Chay: ${COMMAND_NAME} setup`)
|
|
235
|
+
}
|
|
236
|
+
const email = process.env.KROUTER_ADMIN_EMAIL || process.env.KAM_ADMIN_EMAIL || process.env.ADMIN_EMAIL || fileEnv.KROUTER_ADMIN_EMAIL || fileEnv.KAM_ADMIN_EMAIL || fileEnv.ADMIN_EMAIL || 'admin@krouter.local'
|
|
237
|
+
const password = process.env.KROUTER_ADMIN_PASSWORD || process.env.KAM_ADMIN_PASSWORD || process.env.ADMIN_PASSWORD || fileEnv.KROUTER_ADMIN_PASSWORD || fileEnv.KAM_ADMIN_PASSWORD || fileEnv.ADMIN_PASSWORD
|
|
238
|
+
if (!password) {
|
|
239
|
+
throw new Error(`Thieu mat khau admin. Mo dashboard ${DASHBOARD_URL} hoac dat KROUTER_ADMIN_PASSWORD.`)
|
|
240
|
+
}
|
|
241
|
+
await request('/api/auth/login', {
|
|
242
|
+
method: 'POST',
|
|
243
|
+
body: JSON.stringify({ email, password })
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function getSetupStatus() {
|
|
248
|
+
return request('/api/auth/setup/status')
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function setupKrouter() {
|
|
252
|
+
const status = await getSetupStatus()
|
|
253
|
+
if (!status.setupRequired) {
|
|
254
|
+
console.log(`${COLORS.green}Krouter da duoc setup.${COLORS.reset}`)
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const rl = readline.createInterface({ input, output })
|
|
259
|
+
try {
|
|
260
|
+
boxedTitle('Krouter setup', 'Tao mat khau admin lan dau')
|
|
261
|
+
console.log(`${COLORS.bold}1.${COLORS.reset} Krouter tao mat khau random`)
|
|
262
|
+
console.log(`${COLORS.bold}2.${COLORS.reset} Tu dat mat khau`)
|
|
263
|
+
const choice = await ask(rl, '\nChon: ', '1')
|
|
264
|
+
let body
|
|
265
|
+
if (choice === '2') {
|
|
266
|
+
const password = await ask(rl, 'Mat khau moi (toi thieu 8 ky tu): ')
|
|
267
|
+
const confirm = await ask(rl, 'Nhap lai mat khau: ')
|
|
268
|
+
if (password !== confirm) throw new Error('Hai mat khau khong khop')
|
|
269
|
+
body = { mode: 'custom', password }
|
|
270
|
+
} else {
|
|
271
|
+
body = { mode: 'random' }
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const result = await request('/api/auth/setup', {
|
|
275
|
+
method: 'POST',
|
|
276
|
+
body: JSON.stringify(body)
|
|
277
|
+
})
|
|
278
|
+
console.log(`${COLORS.green}Setup thanh cong.${COLORS.reset}`)
|
|
279
|
+
if (result.generatedPassword) {
|
|
280
|
+
console.log(line('Mat khau', `${COLORS.yellow}${result.generatedPassword}${COLORS.reset}`))
|
|
281
|
+
console.log(`${COLORS.yellow}Luu mat khau nay ngay bay gio, Krouter chi hien thi mot lan.${COLORS.reset}`)
|
|
282
|
+
}
|
|
283
|
+
} finally {
|
|
284
|
+
rl.close()
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function ipc(method, args = []) {
|
|
289
|
+
return request('/api/ipc', {
|
|
290
|
+
method: 'POST',
|
|
291
|
+
body: JSON.stringify({ method, args })
|
|
292
|
+
})
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function activeDashboardUrl(tunnel) {
|
|
296
|
+
return tunnel?.running && tunnel.publicUrl ? tunnel.publicUrl.replace(/\/$/, '') : DASHBOARD_URL
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function statusLabel(value) {
|
|
300
|
+
return value ? `${COLORS.green}ON${COLORS.reset}` : `${COLORS.red}OFF${COLORS.reset}`
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function line(label, value) {
|
|
304
|
+
const padded = `${label}:`.padEnd(16, ' ')
|
|
305
|
+
return ` ${COLORS.dim}${padded}${COLORS.reset}${value || '-'}`
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function horizontal(width = 66) {
|
|
309
|
+
return '+'.padEnd(width - 1, '-') + '+'
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function boxedTitle(title, subtitle) {
|
|
313
|
+
const width = 66
|
|
314
|
+
console.log(horizontal(width))
|
|
315
|
+
console.log(`| ${COLORS.bold}${title}${COLORS.reset}`.padEnd(width - 1, ' ') + '|')
|
|
316
|
+
console.log(`| ${COLORS.dim}${subtitle}${COLORS.reset}`.padEnd(width - 1, ' ') + '|')
|
|
317
|
+
console.log(horizontal(width))
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function getTunnelStatus() {
|
|
321
|
+
return ipc('dashboardTunnelGetStatus')
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function printLinks() {
|
|
325
|
+
const tunnel = await getTunnelStatus()
|
|
326
|
+
console.log(line('Web local', `${COLORS.cyan}${DASHBOARD_URL}${COLORS.reset}`))
|
|
327
|
+
console.log(line('Tunnel', `${statusLabel(Boolean(tunnel.running))}${tunnel.publicUrl ? ` ${COLORS.green}${tunnel.publicUrl}${COLORS.reset}` : ''}`))
|
|
328
|
+
console.log(line('Dung link nay', `${COLORS.green}${activeDashboardUrl(tunnel)}${COLORS.reset}`))
|
|
329
|
+
if (tunnel.error) console.log(line('Loi tunnel', `${COLORS.red}${tunnel.error}${COLORS.reset}`))
|
|
330
|
+
return tunnel
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function printBasicStart() {
|
|
334
|
+
const session = await request('/api/auth/session').catch(() => null)
|
|
335
|
+
boxedTitle('Krouter', 'Dashboard web va API proxy')
|
|
336
|
+
console.log(line('Backend', API_BASE))
|
|
337
|
+
console.log(line('Dashboard', `${COLORS.green}${DASHBOARD_URL}${COLORS.reset}`))
|
|
338
|
+
console.log(line('Data', DATA_DIR))
|
|
339
|
+
if (session?.setupRequired) {
|
|
340
|
+
console.log(line('Setup', `${COLORS.yellow}${COMMAND_NAME} setup${COLORS.reset}`))
|
|
341
|
+
}
|
|
342
|
+
console.log(horizontal())
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function printStatus() {
|
|
346
|
+
const [health, tunnel] = await Promise.all([getHealth(), getTunnelStatus()])
|
|
347
|
+
boxedTitle('Krouter', 'Dashboard web va tunnel')
|
|
348
|
+
console.log(line('Backend', `${API_BASE}${health ? ` (${health.mode || 'ok'})` : ''}`))
|
|
349
|
+
console.log(line('Web local', `${COLORS.cyan}${DASHBOARD_URL}${COLORS.reset}`))
|
|
350
|
+
console.log(line('Data', DATA_DIR))
|
|
351
|
+
console.log(line('Tunnel', `${statusLabel(Boolean(tunnel.running))}${tunnel.publicUrl ? ` ${COLORS.green}${tunnel.publicUrl}${COLORS.reset}` : ''}`))
|
|
352
|
+
console.log(line('Tro ve', tunnel.localUrl))
|
|
353
|
+
if (tunnel.publicUrl) console.log(line('Web public', `${COLORS.green}${tunnel.publicUrl}${COLORS.reset}`))
|
|
354
|
+
if (tunnel.error) console.log(line('Loi', `${COLORS.red}${tunnel.error}${COLORS.reset}`))
|
|
355
|
+
console.log(horizontal())
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function startTunnel(localUrl) {
|
|
359
|
+
const result = await ipc('dashboardTunnelStart', [{ localUrl: localUrl || DASHBOARD_URL }])
|
|
360
|
+
const status = result.status
|
|
361
|
+
if (!result.success && result.error) {
|
|
362
|
+
console.log(`${COLORS.red}Loi tunnel:${COLORS.reset} ${result.error}`)
|
|
363
|
+
} else if (status.publicUrl) {
|
|
364
|
+
console.log(`${COLORS.green}Link tunnel:${COLORS.reset} ${status.publicUrl}`)
|
|
365
|
+
} else {
|
|
366
|
+
console.log(`${COLORS.yellow}Dang bat tunnel.${COLORS.reset} Kiem tra: ${COMMAND_NAME} status`)
|
|
367
|
+
}
|
|
368
|
+
return status
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function stopTunnel() {
|
|
372
|
+
const result = await ipc('dashboardTunnelStop')
|
|
373
|
+
console.log(result.success ? `${COLORS.green}Da tat tunnel.${COLORS.reset}` : `${COLORS.red}Tat tunnel loi:${COLORS.reset} ${result.error || result.status?.error || 'unknown error'}`)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function restartTunnel(localUrl) {
|
|
377
|
+
await stopTunnel()
|
|
378
|
+
await startTunnel(localUrl)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async function stopServer() {
|
|
382
|
+
const pid = readPid()
|
|
383
|
+
if (!pid || !isPidRunning(pid)) {
|
|
384
|
+
console.log(`${COLORS.yellow}Krouter backend khong chay theo pid file.${COLORS.reset}`)
|
|
385
|
+
return
|
|
386
|
+
}
|
|
387
|
+
process.kill(pid)
|
|
388
|
+
console.log(`${COLORS.green}Da gui lenh tat backend.${COLORS.reset}`)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function waitForEnter(rl) {
|
|
392
|
+
await ask(rl, `\n${COLORS.dim}Nhan Enter de tiep tuc...${COLORS.reset}`, '')
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async function ask(rl, prompt, fallback = '') {
|
|
396
|
+
try {
|
|
397
|
+
return String(await rl.question(prompt)).trim()
|
|
398
|
+
} catch (error) {
|
|
399
|
+
const message = error?.message || ''
|
|
400
|
+
const code = error?.code || ''
|
|
401
|
+
if (code === 'ERR_USE_AFTER_CLOSE' || /readline was closed/i.test(message)) return fallback
|
|
402
|
+
throw error
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async function menu() {
|
|
407
|
+
const rl = readline.createInterface({ input, output })
|
|
408
|
+
try {
|
|
409
|
+
while (true) {
|
|
410
|
+
if (process.stdout.isTTY) console.clear()
|
|
411
|
+
await printStatus()
|
|
412
|
+
console.log('')
|
|
413
|
+
console.log(`${COLORS.bold}1.${COLORS.reset} Lay link truy cap`)
|
|
414
|
+
console.log(`${COLORS.bold}2.${COLORS.reset} Bat tunnel public`)
|
|
415
|
+
console.log(`${COLORS.bold}3.${COLORS.reset} Tao lai tunnel public`)
|
|
416
|
+
console.log(`${COLORS.bold}4.${COLORS.reset} Tat tunnel`)
|
|
417
|
+
console.log(`${COLORS.bold}5.${COLORS.reset} Mo dashboard`)
|
|
418
|
+
console.log(`${COLORS.bold}0.${COLORS.reset} Thoat`)
|
|
419
|
+
const choice = await ask(rl, '\nChon: ', '0')
|
|
420
|
+
try {
|
|
421
|
+
if (choice === '1') {
|
|
422
|
+
console.log('')
|
|
423
|
+
await printLinks()
|
|
424
|
+
await waitForEnter(rl)
|
|
425
|
+
} else if (choice === '2') {
|
|
426
|
+
await startTunnel()
|
|
427
|
+
await waitForEnter(rl)
|
|
428
|
+
} else if (choice === '3') {
|
|
429
|
+
await restartTunnel()
|
|
430
|
+
await waitForEnter(rl)
|
|
431
|
+
} else if (choice === '4') {
|
|
432
|
+
await stopTunnel()
|
|
433
|
+
await waitForEnter(rl)
|
|
434
|
+
} else if (choice === '5') {
|
|
435
|
+
openBrowser(DASHBOARD_URL)
|
|
436
|
+
await waitForEnter(rl)
|
|
437
|
+
} else if (choice === '0' || /^q/i.test(choice)) {
|
|
438
|
+
return
|
|
439
|
+
}
|
|
440
|
+
} catch (error) {
|
|
441
|
+
console.error(error.message || error)
|
|
442
|
+
await waitForEnter(rl)
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
} finally {
|
|
446
|
+
rl.close()
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function usage() {
|
|
451
|
+
console.log('Huong dan:')
|
|
452
|
+
console.log(` npm install -g @lightharu/krouter`)
|
|
453
|
+
console.log(` ${COMMAND_NAME}`)
|
|
454
|
+
console.log(` ${COMMAND_NAME} start`)
|
|
455
|
+
console.log(` ${COMMAND_NAME} setup`)
|
|
456
|
+
console.log(` ${COMMAND_NAME} status`)
|
|
457
|
+
console.log(` ${COMMAND_NAME} links`)
|
|
458
|
+
console.log(` ${COMMAND_NAME} tunnel start [local-url]`)
|
|
459
|
+
console.log(` ${COMMAND_NAME} tunnel restart [local-url]`)
|
|
460
|
+
console.log(` ${COMMAND_NAME} tunnel stop`)
|
|
461
|
+
console.log(` ${COMMAND_NAME} stop`)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async function main() {
|
|
465
|
+
const [command, subcommand, ...rest] = process.argv.slice(2)
|
|
466
|
+
if (command === 'help' || command === '--help' || command === '-h') {
|
|
467
|
+
usage()
|
|
468
|
+
return
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
await ensureServer()
|
|
472
|
+
|
|
473
|
+
if (command === 'start') {
|
|
474
|
+
await printBasicStart()
|
|
475
|
+
openBrowser(DASHBOARD_URL)
|
|
476
|
+
return
|
|
477
|
+
}
|
|
478
|
+
if (command === 'stop') return stopServer()
|
|
479
|
+
if (command === 'setup') return setupKrouter()
|
|
480
|
+
|
|
481
|
+
if (!command || command === 'menu') {
|
|
482
|
+
const session = await request('/api/auth/session').catch(() => null)
|
|
483
|
+
if (session?.setupRequired) await setupKrouter()
|
|
484
|
+
try {
|
|
485
|
+
await login()
|
|
486
|
+
openBrowser(DASHBOARD_URL)
|
|
487
|
+
return menu()
|
|
488
|
+
} catch (error) {
|
|
489
|
+
await printBasicStart()
|
|
490
|
+
console.log(`${COLORS.yellow}${error.message || error}${COLORS.reset}`)
|
|
491
|
+
openBrowser(DASHBOARD_URL)
|
|
492
|
+
return
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
await login()
|
|
497
|
+
if (command === 'status') return printStatus()
|
|
498
|
+
if (command === 'links' || command === 'url' || command === 'link') return printLinks()
|
|
499
|
+
if (command === 'tunnel' && subcommand === 'start') return startTunnel(rest[0])
|
|
500
|
+
if (command === 'tunnel' && subcommand === 'restart') return restartTunnel(rest[0])
|
|
501
|
+
if (command === 'tunnel' && subcommand === 'stop') return stopTunnel()
|
|
502
|
+
if (command === 'tunnel' && (!subcommand || subcommand === 'status')) return printStatus()
|
|
503
|
+
usage()
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
main().catch(error => {
|
|
507
|
+
console.error(error.message || error)
|
|
508
|
+
process.exit(1)
|
|
509
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<svg width="360" height="96" viewBox="0 0 360 96" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Krouter">
|
|
2
|
+
<rect x="8" y="8" width="80" height="80" rx="22" fill="#0B1220"/>
|
|
3
|
+
<path d="M28 68V28h10v16.8L54.2 28H67L49.8 45.5 68.5 68H55.4L42.9 52.7 38 57.7V68H28Z" fill="#FFFFFF"/>
|
|
4
|
+
<path d="M63 28h5v5h-5v-5Z" fill="#22C55E"/>
|
|
5
|
+
<path d="M69 34h5v5h-5v-5Z" fill="#38BDF8"/>
|
|
6
|
+
<path d="M63 40h5v5h-5v-5Z" fill="#F59E0B"/>
|
|
7
|
+
<path d="M62.5 32.5 49.5 45.5M68.5 36.5 52 52M62.5 42.5 55.5 56" stroke="#94A3B8" stroke-width="2" stroke-linecap="round"/>
|
|
8
|
+
<circle cx="48" cy="48" r="5" fill="#38BDF8"/>
|
|
9
|
+
<text x="108" y="57" fill="#111827" font-family="Inter, Segoe UI, Arial, sans-serif" font-size="38" font-weight="800">Krouter</text>
|
|
10
|
+
<text x="110" y="76" fill="#64748B" font-family="Inter, Segoe UI, Arial, sans-serif" font-size="14" font-weight="600">Kiro quota router</text>
|
|
11
|
+
</svg>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Krouter">
|
|
2
|
+
<rect x="8" y="8" width="80" height="80" rx="22" fill="#0B1220"/>
|
|
3
|
+
<path d="M28 68V28h10v16.8L54.2 28H67L49.8 45.5 68.5 68H55.4L42.9 52.7 38 57.7V68H28Z" fill="#FFFFFF"/>
|
|
4
|
+
<path d="M63 28h5v5h-5v-5Z" fill="#22C55E"/>
|
|
5
|
+
<path d="M69 34h5v5h-5v-5Z" fill="#38BDF8"/>
|
|
6
|
+
<path d="M63 40h5v5h-5v-5Z" fill="#F59E0B"/>
|
|
7
|
+
<path d="M62.5 32.5 49.5 45.5M68.5 36.5 52 52M62.5 42.5 55.5 56" stroke="#94A3B8" stroke-width="2" stroke-linecap="round"/>
|
|
8
|
+
<circle cx="48" cy="48" r="5" fill="#38BDF8"/>
|
|
9
|
+
</svg>
|