coding-tool-x 3.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.
Files changed (185) hide show
  1. package/CHANGELOG.md +599 -0
  2. package/LICENSE +21 -0
  3. package/README.md +439 -0
  4. package/bin/ctx.js +8 -0
  5. package/dist/web/assets/Analytics-DN_YsnkW.js +39 -0
  6. package/dist/web/assets/Analytics-DuYvId7u.css +1 -0
  7. package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
  8. package/dist/web/assets/ConfigTemplates-DpXIMy0p.js +1 -0
  9. package/dist/web/assets/Home-38JTUlYt.js +1 -0
  10. package/dist/web/assets/Home-CjupSEWE.css +1 -0
  11. package/dist/web/assets/PluginManager-CX2tgq2H.js +1 -0
  12. package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
  13. package/dist/web/assets/ProjectList-C1lDcsn6.js +1 -0
  14. package/dist/web/assets/ProjectList-oJIyIRkP.css +1 -0
  15. package/dist/web/assets/SessionList-C55tjV7i.css +1 -0
  16. package/dist/web/assets/SessionList-CZ7T6rVx.js +1 -0
  17. package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
  18. package/dist/web/assets/SkillManager-DLN9f79y.js +1 -0
  19. package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
  20. package/dist/web/assets/WorkspaceManager-DxlHZkpZ.js +1 -0
  21. package/dist/web/assets/icons-DRrXwWZi.js +1 -0
  22. package/dist/web/assets/index-CetESrXw.css +1 -0
  23. package/dist/web/assets/index-Cfvn-2Gb.js +2 -0
  24. package/dist/web/assets/markdown-BfC0goYb.css +10 -0
  25. package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
  26. package/dist/web/assets/naive-ui-DlpKk-8M.js +1 -0
  27. package/dist/web/assets/vendors-DMjSfzlv.js +7 -0
  28. package/dist/web/assets/vue-vendor-DET08QYg.js +45 -0
  29. package/dist/web/favicon.ico +0 -0
  30. package/dist/web/index.html +20 -0
  31. package/dist/web/logo.png +0 -0
  32. package/docs/bannel.png +0 -0
  33. package/docs/home.png +0 -0
  34. package/docs/logo.png +0 -0
  35. package/docs/model-redirection.md +251 -0
  36. package/docs/multi-channel-load-balancing.md +249 -0
  37. package/package.json +80 -0
  38. package/src/commands/channels.js +551 -0
  39. package/src/commands/cli-type.js +101 -0
  40. package/src/commands/daemon.js +365 -0
  41. package/src/commands/doctor.js +333 -0
  42. package/src/commands/export-config.js +205 -0
  43. package/src/commands/list.js +222 -0
  44. package/src/commands/logs.js +261 -0
  45. package/src/commands/plugin.js +585 -0
  46. package/src/commands/port-config.js +135 -0
  47. package/src/commands/proxy-control.js +264 -0
  48. package/src/commands/proxy.js +152 -0
  49. package/src/commands/resume.js +137 -0
  50. package/src/commands/search.js +190 -0
  51. package/src/commands/security.js +37 -0
  52. package/src/commands/stats.js +398 -0
  53. package/src/commands/switch.js +48 -0
  54. package/src/commands/toggle-proxy.js +247 -0
  55. package/src/commands/ui.js +99 -0
  56. package/src/commands/update.js +97 -0
  57. package/src/commands/workspace.js +454 -0
  58. package/src/config/default.js +69 -0
  59. package/src/config/loader.js +149 -0
  60. package/src/config/model-metadata.js +167 -0
  61. package/src/config/model-metadata.json +125 -0
  62. package/src/config/model-pricing.js +35 -0
  63. package/src/config/paths.js +190 -0
  64. package/src/index.js +680 -0
  65. package/src/plugins/constants.js +15 -0
  66. package/src/plugins/event-bus.js +54 -0
  67. package/src/plugins/manifest-validator.js +129 -0
  68. package/src/plugins/plugin-api.js +128 -0
  69. package/src/plugins/plugin-installer.js +601 -0
  70. package/src/plugins/plugin-loader.js +229 -0
  71. package/src/plugins/plugin-manager.js +170 -0
  72. package/src/plugins/registry.js +152 -0
  73. package/src/plugins/schema/plugin-manifest.json +115 -0
  74. package/src/reset-config.js +94 -0
  75. package/src/server/api/agents.js +826 -0
  76. package/src/server/api/aliases.js +36 -0
  77. package/src/server/api/channels.js +368 -0
  78. package/src/server/api/claude-hooks.js +480 -0
  79. package/src/server/api/codex-channels.js +417 -0
  80. package/src/server/api/codex-projects.js +104 -0
  81. package/src/server/api/codex-proxy.js +195 -0
  82. package/src/server/api/codex-sessions.js +483 -0
  83. package/src/server/api/codex-statistics.js +57 -0
  84. package/src/server/api/commands.js +482 -0
  85. package/src/server/api/config-export.js +212 -0
  86. package/src/server/api/config-registry.js +357 -0
  87. package/src/server/api/config-sync.js +155 -0
  88. package/src/server/api/config-templates.js +248 -0
  89. package/src/server/api/config.js +521 -0
  90. package/src/server/api/convert.js +260 -0
  91. package/src/server/api/dashboard.js +142 -0
  92. package/src/server/api/env.js +144 -0
  93. package/src/server/api/favorites.js +77 -0
  94. package/src/server/api/gemini-channels.js +366 -0
  95. package/src/server/api/gemini-projects.js +91 -0
  96. package/src/server/api/gemini-proxy.js +173 -0
  97. package/src/server/api/gemini-sessions.js +376 -0
  98. package/src/server/api/gemini-statistics.js +57 -0
  99. package/src/server/api/health-check.js +31 -0
  100. package/src/server/api/mcp.js +399 -0
  101. package/src/server/api/opencode-channels.js +419 -0
  102. package/src/server/api/opencode-projects.js +99 -0
  103. package/src/server/api/opencode-proxy.js +207 -0
  104. package/src/server/api/opencode-sessions.js +327 -0
  105. package/src/server/api/opencode-statistics.js +57 -0
  106. package/src/server/api/plugins.js +463 -0
  107. package/src/server/api/pm2-autostart.js +269 -0
  108. package/src/server/api/projects.js +124 -0
  109. package/src/server/api/prompts.js +279 -0
  110. package/src/server/api/proxy.js +306 -0
  111. package/src/server/api/security.js +53 -0
  112. package/src/server/api/sessions.js +514 -0
  113. package/src/server/api/settings.js +142 -0
  114. package/src/server/api/skills.js +570 -0
  115. package/src/server/api/statistics.js +238 -0
  116. package/src/server/api/ui-config.js +64 -0
  117. package/src/server/api/workspaces.js +456 -0
  118. package/src/server/codex-proxy-server.js +681 -0
  119. package/src/server/dev-server.js +26 -0
  120. package/src/server/gemini-proxy-server.js +610 -0
  121. package/src/server/index.js +422 -0
  122. package/src/server/opencode-proxy-server.js +4771 -0
  123. package/src/server/proxy-server.js +669 -0
  124. package/src/server/services/agents-service.js +1137 -0
  125. package/src/server/services/alias.js +71 -0
  126. package/src/server/services/channel-health.js +234 -0
  127. package/src/server/services/channel-scheduler.js +240 -0
  128. package/src/server/services/channels.js +447 -0
  129. package/src/server/services/codex-channels.js +705 -0
  130. package/src/server/services/codex-config.js +90 -0
  131. package/src/server/services/codex-parser.js +322 -0
  132. package/src/server/services/codex-sessions.js +936 -0
  133. package/src/server/services/codex-settings-manager.js +619 -0
  134. package/src/server/services/codex-speed-test-template.json +24 -0
  135. package/src/server/services/codex-statistics-service.js +161 -0
  136. package/src/server/services/commands-service.js +574 -0
  137. package/src/server/services/config-export-service.js +1165 -0
  138. package/src/server/services/config-registry-service.js +828 -0
  139. package/src/server/services/config-sync-manager.js +941 -0
  140. package/src/server/services/config-sync-service.js +504 -0
  141. package/src/server/services/config-templates-service.js +913 -0
  142. package/src/server/services/enhanced-cache.js +196 -0
  143. package/src/server/services/env-checker.js +409 -0
  144. package/src/server/services/env-manager.js +436 -0
  145. package/src/server/services/favorites.js +165 -0
  146. package/src/server/services/format-converter.js +620 -0
  147. package/src/server/services/gemini-channels.js +459 -0
  148. package/src/server/services/gemini-config.js +73 -0
  149. package/src/server/services/gemini-sessions.js +689 -0
  150. package/src/server/services/gemini-settings-manager.js +263 -0
  151. package/src/server/services/gemini-statistics-service.js +157 -0
  152. package/src/server/services/health-check.js +85 -0
  153. package/src/server/services/mcp-client.js +790 -0
  154. package/src/server/services/mcp-service.js +1732 -0
  155. package/src/server/services/model-detector.js +1245 -0
  156. package/src/server/services/network-access.js +80 -0
  157. package/src/server/services/opencode-channels.js +366 -0
  158. package/src/server/services/opencode-gateway-adapters.js +1168 -0
  159. package/src/server/services/opencode-gateway-converter.js +639 -0
  160. package/src/server/services/opencode-sessions.js +931 -0
  161. package/src/server/services/opencode-settings-manager.js +478 -0
  162. package/src/server/services/opencode-statistics-service.js +161 -0
  163. package/src/server/services/plugins-service.js +1268 -0
  164. package/src/server/services/prompts-service.js +534 -0
  165. package/src/server/services/proxy-runtime.js +79 -0
  166. package/src/server/services/repo-scanner-base.js +708 -0
  167. package/src/server/services/request-logger.js +130 -0
  168. package/src/server/services/response-decoder.js +21 -0
  169. package/src/server/services/security-config.js +131 -0
  170. package/src/server/services/session-cache.js +127 -0
  171. package/src/server/services/session-converter.js +577 -0
  172. package/src/server/services/sessions.js +900 -0
  173. package/src/server/services/settings-manager.js +163 -0
  174. package/src/server/services/skill-service.js +1482 -0
  175. package/src/server/services/speed-test.js +1146 -0
  176. package/src/server/services/statistics-service.js +1043 -0
  177. package/src/server/services/ui-config.js +132 -0
  178. package/src/server/services/workspace-service.js +830 -0
  179. package/src/server/utils/pricing.js +73 -0
  180. package/src/server/websocket-server.js +513 -0
  181. package/src/ui/menu.js +139 -0
  182. package/src/ui/prompts.js +100 -0
  183. package/src/utils/format.js +43 -0
  184. package/src/utils/port-helper.js +108 -0
  185. package/src/utils/session.js +240 -0
@@ -0,0 +1,456 @@
1
+ // 工作区 API 路由
2
+ const express = require('express');
3
+ const router = express.Router();
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { execFileSync } = require('child_process');
7
+ const workspaceService = require('../services/workspace-service');
8
+
9
+ function normalizeBranchName(branchName) {
10
+ if (typeof branchName !== 'string') {
11
+ return '';
12
+ }
13
+ return branchName.trim();
14
+ }
15
+
16
+ function validateBranchName(branchName) {
17
+ const normalized = normalizeBranchName(branchName);
18
+ if (!normalized) {
19
+ return { valid: false, normalized, message: '分支名不能为空' };
20
+ }
21
+ if (normalized.length > 255) {
22
+ return { valid: false, normalized, message: '分支名长度不能超过 255 个字符' };
23
+ }
24
+
25
+ try {
26
+ execFileSync('git', ['check-ref-format', '--branch', normalized], {
27
+ stdio: 'ignore'
28
+ });
29
+ return { valid: true, normalized };
30
+ } catch (error) {
31
+ return { valid: false, normalized, message: `非法分支名: ${normalized}` };
32
+ }
33
+ }
34
+
35
+ /**
36
+ * GET /api/workspaces
37
+ * 获取所有工作区列表
38
+ */
39
+ router.get('/', (req, res) => {
40
+ try {
41
+ const workspaces = workspaceService.listWorkspaces();
42
+ res.json({
43
+ success: true,
44
+ data: workspaces
45
+ });
46
+ } catch (error) {
47
+ res.status(500).json({
48
+ success: false,
49
+ message: error.message
50
+ });
51
+ }
52
+ });
53
+
54
+ /**
55
+ * GET /api/workspaces/read-file
56
+ * 读取工作区中的文件内容(仅限 CLAUDE.md 等配置文件)
57
+ * Query: path=文件路径
58
+ */
59
+ router.get('/read-file', (req, res) => {
60
+ try {
61
+ const filePath = req.query.path;
62
+
63
+ if (!filePath) {
64
+ return res.status(400).json({
65
+ success: false,
66
+ message: '文件路径不能为空'
67
+ });
68
+ }
69
+
70
+ // 安全检查:只允许读取特定类型的文件
71
+ const allowedFiles = ['CLAUDE.md', 'AGENTS.md', 'GEMINI.md', '.ctx-config.json'];
72
+ const fileName = path.basename(filePath);
73
+
74
+ if (!allowedFiles.includes(fileName)) {
75
+ return res.status(403).json({
76
+ success: false,
77
+ message: '不允许读取该文件'
78
+ });
79
+ }
80
+
81
+ if (!fs.existsSync(filePath)) {
82
+ return res.status(404).json({
83
+ success: false,
84
+ message: '文件不存在'
85
+ });
86
+ }
87
+
88
+ const content = fs.readFileSync(filePath, 'utf8');
89
+
90
+ res.json({
91
+ success: true,
92
+ content: content
93
+ });
94
+ } catch (error) {
95
+ res.status(500).json({
96
+ success: false,
97
+ message: error.message
98
+ });
99
+ }
100
+ });
101
+
102
+ /**
103
+ * GET /api/workspaces/check-git/:projectPath
104
+ * 检查项目是否是 git 仓库并获取 worktrees
105
+ */
106
+ router.get('/check-git/*', (req, res) => {
107
+ try {
108
+ const projectPath = req.params[0];
109
+
110
+ if (!projectPath) {
111
+ return res.status(400).json({
112
+ success: false,
113
+ message: '项目路径不能为空'
114
+ });
115
+ }
116
+
117
+ const isGit = workspaceService.isGitRepo(projectPath);
118
+ const worktrees = isGit ? workspaceService.getGitWorktrees(projectPath) : [];
119
+
120
+ res.json({
121
+ success: true,
122
+ data: {
123
+ isGitRepo: isGit,
124
+ worktrees
125
+ }
126
+ });
127
+ } catch (error) {
128
+ res.status(500).json({
129
+ success: false,
130
+ message: error.message
131
+ });
132
+ }
133
+ });
134
+
135
+ /**
136
+ * GET /api/workspaces/available-projects
137
+ * 获取所有渠道(Claude/Codex/Gemini)的项目并集
138
+ */
139
+ router.get('/available-projects', async (req, res) => {
140
+ try {
141
+ const projects = await workspaceService.getAllAvailableProjects();
142
+ res.json({
143
+ success: true,
144
+ data: projects
145
+ });
146
+ } catch (error) {
147
+ res.status(500).json({
148
+ success: false,
149
+ message: error.message
150
+ });
151
+ }
152
+ });
153
+
154
+ /**
155
+ * GET /api/workspaces/:id
156
+ * 获取单个工作区详情
157
+ */
158
+ // 注意:此路由需要放在所有静态子路由之后,避免把 /available-projects 等路径当成 id
159
+ router.get('/:id', (req, res, next) => {
160
+ try {
161
+ // 兜底:即便路由顺序被改动,也避免把保留路径当成工作区 ID
162
+ const reservedIds = new Set(['available-projects', 'read-file']);
163
+ if (reservedIds.has(req.params.id)) {
164
+ return next();
165
+ }
166
+
167
+ const workspace = workspaceService.getWorkspace(req.params.id);
168
+ if (!workspace) {
169
+ return res.status(404).json({
170
+ success: false,
171
+ message: '工作区不存在'
172
+ });
173
+ }
174
+ res.json({
175
+ success: true,
176
+ data: workspace
177
+ });
178
+ } catch (error) {
179
+ res.status(500).json({
180
+ success: false,
181
+ message: error.message
182
+ });
183
+ }
184
+ });
185
+
186
+ /**
187
+ * POST /api/workspaces
188
+ * 创建新工作区
189
+ * Body: {
190
+ * name: string,
191
+ * description?: string,
192
+ * baseDir?: string,
193
+ * projects: [{
194
+ * sourcePath: string,
195
+ * name?: string,
196
+ * createWorktree?: boolean,
197
+ * branch?: string
198
+ * }]
199
+ * }
200
+ */
201
+ router.post('/', (req, res) => {
202
+ try {
203
+ const { name, description, baseDir, projects, configTemplateId } = req.body;
204
+
205
+ if (!name || !name.trim()) {
206
+ return res.status(400).json({
207
+ success: false,
208
+ message: '工作区名称不能为空'
209
+ });
210
+ }
211
+
212
+ if (!projects || !Array.isArray(projects) || projects.length === 0) {
213
+ return res.status(400).json({
214
+ success: false,
215
+ message: '至少需要选择一个项目'
216
+ });
217
+ }
218
+
219
+ // 验证项目配置
220
+ for (const proj of projects) {
221
+ if (!proj.sourcePath || !proj.sourcePath.trim()) {
222
+ return res.status(400).json({
223
+ success: false,
224
+ message: '项目源路径不能为空'
225
+ });
226
+ }
227
+
228
+ if (proj.createWorktree && (!proj.branch || !proj.branch.trim())) {
229
+ return res.status(400).json({
230
+ success: false,
231
+ message: '创建 worktree 时必须指定分支名'
232
+ });
233
+ }
234
+
235
+ const normalizedBranch = normalizeBranchName(proj.branch);
236
+ if (normalizedBranch) {
237
+ const branchValidation = validateBranchName(normalizedBranch);
238
+ if (!branchValidation.valid) {
239
+ return res.status(400).json({
240
+ success: false,
241
+ message: branchValidation.message
242
+ });
243
+ }
244
+ proj.branch = branchValidation.normalized;
245
+ }
246
+
247
+ const normalizedBaseBranch = normalizeBranchName(proj.baseBranch);
248
+ if (normalizedBaseBranch) {
249
+ const baseBranchValidation = validateBranchName(normalizedBaseBranch);
250
+ if (!baseBranchValidation.valid) {
251
+ return res.status(400).json({
252
+ success: false,
253
+ message: `基础分支不合法: ${baseBranchValidation.normalized}`
254
+ });
255
+ }
256
+ proj.baseBranch = baseBranchValidation.normalized;
257
+ }
258
+ }
259
+
260
+ const workspace = workspaceService.createWorkspace({
261
+ name,
262
+ description,
263
+ baseDir,
264
+ projects,
265
+ configTemplateId
266
+ });
267
+
268
+ res.json({
269
+ success: true,
270
+ message: '工作区创建成功',
271
+ data: workspace
272
+ });
273
+ } catch (error) {
274
+ res.status(500).json({
275
+ success: false,
276
+ message: error.message
277
+ });
278
+ }
279
+ });
280
+
281
+ /**
282
+ * DELETE /api/workspaces/:id
283
+ * 删除工作区
284
+ * Query: removeFiles=true/false
285
+ */
286
+ router.delete('/:id', (req, res) => {
287
+ try {
288
+ const { id } = req.params;
289
+ const removeFiles = req.query.removeFiles === 'true';
290
+
291
+ workspaceService.deleteWorkspace(id, removeFiles);
292
+
293
+ res.json({
294
+ success: true,
295
+ message: '工作区删除成功'
296
+ });
297
+ } catch (error) {
298
+ res.status(500).json({
299
+ success: false,
300
+ message: error.message
301
+ });
302
+ }
303
+ });
304
+
305
+ /**
306
+ * PUT /api/workspaces/:id/last-used
307
+ * 更新工作区最后使用时间
308
+ */
309
+ router.put('/:id/last-used', (req, res) => {
310
+ try {
311
+ workspaceService.updateWorkspaceLastUsed(req.params.id);
312
+ res.json({
313
+ success: true,
314
+ message: '更新成功'
315
+ });
316
+ } catch (error) {
317
+ res.status(500).json({
318
+ success: false,
319
+ message: error.message
320
+ });
321
+ }
322
+ });
323
+
324
+ /**
325
+ * POST /api/workspaces/:id/projects
326
+ * 向工作区添加项目
327
+ * Body: {
328
+ * sourcePath: string,
329
+ * name?: string,
330
+ * createWorktree?: boolean,
331
+ * branch?: string,
332
+ * baseBranch?: string
333
+ * }
334
+ */
335
+ router.post('/:id/projects', (req, res) => {
336
+ try {
337
+ const { id } = req.params;
338
+ const { sourcePath, name, createWorktree, branch, baseBranch } = req.body;
339
+ const normalizedBranch = normalizeBranchName(branch);
340
+ const normalizedBaseBranch = normalizeBranchName(baseBranch);
341
+
342
+ if (!sourcePath || !sourcePath.trim()) {
343
+ return res.status(400).json({
344
+ success: false,
345
+ message: '项目源路径不能为空'
346
+ });
347
+ }
348
+
349
+ if (createWorktree && !normalizedBranch) {
350
+ return res.status(400).json({
351
+ success: false,
352
+ message: '创建 worktree 时必须指定分支名'
353
+ });
354
+ }
355
+
356
+ if (normalizedBranch) {
357
+ const branchValidation = validateBranchName(normalizedBranch);
358
+ if (!branchValidation.valid) {
359
+ return res.status(400).json({
360
+ success: false,
361
+ message: branchValidation.message
362
+ });
363
+ }
364
+ }
365
+
366
+ if (normalizedBaseBranch) {
367
+ const baseBranchValidation = validateBranchName(normalizedBaseBranch);
368
+ if (!baseBranchValidation.valid) {
369
+ return res.status(400).json({
370
+ success: false,
371
+ message: `基础分支不合法: ${baseBranchValidation.normalized}`
372
+ });
373
+ }
374
+ }
375
+
376
+ const workspace = workspaceService.addProjectToWorkspace(id, {
377
+ sourcePath,
378
+ name,
379
+ createWorktree,
380
+ branch: normalizedBranch || branch,
381
+ baseBranch: normalizedBaseBranch || baseBranch
382
+ });
383
+
384
+ res.json({
385
+ success: true,
386
+ message: '项目添加成功',
387
+ data: workspace
388
+ });
389
+ } catch (error) {
390
+ res.status(500).json({
391
+ success: false,
392
+ message: error.message
393
+ });
394
+ }
395
+ });
396
+
397
+ /**
398
+ * DELETE /api/workspaces/:id/projects/:projectName
399
+ * 从工作区移除项目
400
+ * Query: removeWorktrees=true/false
401
+ */
402
+ router.delete('/:id/projects/:projectName', (req, res) => {
403
+ try {
404
+ const { id, projectName } = req.params;
405
+ const removeWorktrees = req.query.removeWorktrees === 'true';
406
+
407
+ const workspace = workspaceService.removeProjectFromWorkspace(
408
+ id,
409
+ projectName,
410
+ removeWorktrees
411
+ );
412
+
413
+ res.json({
414
+ success: true,
415
+ message: '项目移除成功',
416
+ data: workspace
417
+ });
418
+ } catch (error) {
419
+ res.status(500).json({
420
+ success: false,
421
+ message: error.message
422
+ });
423
+ }
424
+ });
425
+
426
+ /**
427
+ * POST /api/workspaces/:id/launch
428
+ * 获取在工作区启动 CLI 工具的命令
429
+ * Body: { tool: 'claude' | 'codex' | 'gemini', projectName?: string }
430
+ */
431
+ router.post('/:id/launch', (req, res) => {
432
+ try {
433
+ const { id } = req.params;
434
+ const { tool, projectName } = req.body;
435
+
436
+ if (!tool) {
437
+ return res.status(400).json({
438
+ success: false,
439
+ message: '请指定要启动的工具'
440
+ });
441
+ }
442
+
443
+ const launchInfo = workspaceService.getLaunchCommand(id, tool, projectName);
444
+ res.json({
445
+ success: true,
446
+ data: launchInfo
447
+ });
448
+ } catch (error) {
449
+ res.status(500).json({
450
+ success: false,
451
+ message: error.message
452
+ });
453
+ }
454
+ });
455
+
456
+ module.exports = router;