@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.
Files changed (61) hide show
  1. package/LICENSE +679 -0
  2. package/README.md +238 -0
  3. package/dist-web/assets/index-CM4-0adf.css +1 -0
  4. package/dist-web/assets/index-DCslvfUR.js +139 -0
  5. package/dist-web/favicon.svg +9 -0
  6. package/dist-web/icon.svg +9 -0
  7. package/dist-web/index.html +19 -0
  8. package/out-server/main/kiroAuthSync.js +249 -0
  9. package/out-server/main/kproxy/certManager.js +262 -0
  10. package/out-server/main/kproxy/index.js +254 -0
  11. package/out-server/main/kproxy/mitmProxy.js +475 -0
  12. package/out-server/main/kproxy/types.js +23 -0
  13. package/out-server/main/proxy/accountPool.js +543 -0
  14. package/out-server/main/proxy/clientConfig.js +596 -0
  15. package/out-server/main/proxy/index.js +25 -0
  16. package/out-server/main/proxy/kiroApi.js +1996 -0
  17. package/out-server/main/proxy/logger.js +407 -0
  18. package/out-server/main/proxy/modelCatalog.js +75 -0
  19. package/out-server/main/proxy/promptCacheTracker.js +301 -0
  20. package/out-server/main/proxy/proxyServer.js +3543 -0
  21. package/out-server/main/proxy/selfSignedCert.js +179 -0
  22. package/out-server/main/proxy/systemProxy.js +250 -0
  23. package/out-server/main/proxy/tokenCounter.js +164 -0
  24. package/out-server/main/proxy/toolNameRegistry.js +57 -0
  25. package/out-server/main/proxy/translator.js +1084 -0
  26. package/out-server/main/proxy/types.js +3 -0
  27. package/out-server/main/registration/browser-identity.js +184 -0
  28. package/out-server/main/registration/chainProxy.js +349 -0
  29. package/out-server/main/registration/config.js +58 -0
  30. package/out-server/main/registration/email-service.js +801 -0
  31. package/out-server/main/registration/fingerprint.js +352 -0
  32. package/out-server/main/registration/http-utils.js +148 -0
  33. package/out-server/main/registration/jwe.js +74 -0
  34. package/out-server/main/registration/names.js +142 -0
  35. package/out-server/main/registration/proton-mail-window.js +339 -0
  36. package/out-server/main/registration/registrar.js +1715 -0
  37. package/out-server/main/registration/tlsClientPool.js +70 -0
  38. package/out-server/main/registration/xxtea.js +161 -0
  39. package/out-server/main/runtimePaths.js +19 -0
  40. package/out-server/main/utils/redact.js +95 -0
  41. package/out-server/server/index.js +1272 -0
  42. package/out-server/server/services/accountExtras.js +105 -0
  43. package/out-server/server/services/accountProfileHydration.js +95 -0
  44. package/out-server/server/services/authFlows.js +509 -0
  45. package/out-server/server/services/dashboardTunnel.js +315 -0
  46. package/out-server/server/services/diagnostics.js +326 -0
  47. package/out-server/server/services/kiroAccounts.js +431 -0
  48. package/out-server/server/services/kiroSettings.js +260 -0
  49. package/out-server/server/services/kproxyRuntime.js +264 -0
  50. package/out-server/server/services/localKiroCredentials.js +320 -0
  51. package/out-server/server/services/machineIdRuntime.js +327 -0
  52. package/out-server/server/services/protonBrowserRuntime.js +724 -0
  53. package/out-server/server/services/proxyRuntime.js +523 -0
  54. package/out-server/server/services/registrationRuntime.js +106 -0
  55. package/out-server/server/store.js +266 -0
  56. package/package.json +113 -0
  57. package/resources/tls-client-xgo-1.14.0-windows-amd64.dll +0 -0
  58. package/scripts/kiro-manager-cli.cjs +3 -0
  59. package/scripts/krouter-cli.cjs +509 -0
  60. package/src/renderer/src/assets/krouter-logo.svg +11 -0
  61. 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>