@shareai-lab/kode 1.1.13 → 1.1.16-dev.1

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 (288) hide show
  1. package/dist/entrypoints/cli.js +59 -38
  2. package/dist/entrypoints/cli.js.map +3 -3
  3. package/dist/index.js +5 -26
  4. package/dist/package.json +4 -1
  5. package/package.json +11 -104
  6. package/dist/test/testAdapters.js +0 -88
  7. package/dist/test/testAdapters.js.map +0 -1
  8. package/src/ProjectOnboarding.tsx +0 -198
  9. package/src/Tool.ts +0 -83
  10. package/src/commands/agents.tsx +0 -3416
  11. package/src/commands/approvedTools.ts +0 -53
  12. package/src/commands/bug.tsx +0 -20
  13. package/src/commands/clear.ts +0 -43
  14. package/src/commands/compact.ts +0 -120
  15. package/src/commands/config.tsx +0 -19
  16. package/src/commands/cost.ts +0 -18
  17. package/src/commands/ctx_viz.ts +0 -209
  18. package/src/commands/doctor.ts +0 -24
  19. package/src/commands/help.tsx +0 -19
  20. package/src/commands/init.ts +0 -37
  21. package/src/commands/listen.ts +0 -42
  22. package/src/commands/login.tsx +0 -51
  23. package/src/commands/logout.tsx +0 -40
  24. package/src/commands/mcp.ts +0 -41
  25. package/src/commands/model.tsx +0 -40
  26. package/src/commands/modelstatus.tsx +0 -20
  27. package/src/commands/onboarding.tsx +0 -34
  28. package/src/commands/pr_comments.ts +0 -59
  29. package/src/commands/refreshCommands.ts +0 -54
  30. package/src/commands/release-notes.ts +0 -34
  31. package/src/commands/resume.tsx +0 -31
  32. package/src/commands/review.ts +0 -49
  33. package/src/commands/terminalSetup.ts +0 -221
  34. package/src/commands.ts +0 -139
  35. package/src/components/ApproveApiKey.tsx +0 -93
  36. package/src/components/AsciiLogo.tsx +0 -13
  37. package/src/components/AutoUpdater.tsx +0 -148
  38. package/src/components/Bug.tsx +0 -367
  39. package/src/components/Config.tsx +0 -293
  40. package/src/components/ConsoleOAuthFlow.tsx +0 -327
  41. package/src/components/Cost.tsx +0 -23
  42. package/src/components/CostThresholdDialog.tsx +0 -46
  43. package/src/components/CustomSelect/option-map.ts +0 -42
  44. package/src/components/CustomSelect/select-option.tsx +0 -78
  45. package/src/components/CustomSelect/select.tsx +0 -152
  46. package/src/components/CustomSelect/theme.ts +0 -45
  47. package/src/components/CustomSelect/use-select-state.ts +0 -414
  48. package/src/components/CustomSelect/use-select.ts +0 -35
  49. package/src/components/FallbackToolUseRejectedMessage.tsx +0 -15
  50. package/src/components/FileEditToolUpdatedMessage.tsx +0 -66
  51. package/src/components/Help.tsx +0 -215
  52. package/src/components/HighlightedCode.tsx +0 -33
  53. package/src/components/InvalidConfigDialog.tsx +0 -113
  54. package/src/components/Link.tsx +0 -32
  55. package/src/components/LogSelector.tsx +0 -86
  56. package/src/components/Logo.tsx +0 -170
  57. package/src/components/MCPServerApprovalDialog.tsx +0 -100
  58. package/src/components/MCPServerDialogCopy.tsx +0 -25
  59. package/src/components/MCPServerMultiselectDialog.tsx +0 -109
  60. package/src/components/Message.tsx +0 -221
  61. package/src/components/MessageResponse.tsx +0 -15
  62. package/src/components/MessageSelector.tsx +0 -211
  63. package/src/components/ModeIndicator.tsx +0 -88
  64. package/src/components/ModelConfig.tsx +0 -301
  65. package/src/components/ModelListManager.tsx +0 -227
  66. package/src/components/ModelSelector.tsx +0 -3387
  67. package/src/components/ModelStatusDisplay.tsx +0 -230
  68. package/src/components/Onboarding.tsx +0 -274
  69. package/src/components/PressEnterToContinue.tsx +0 -11
  70. package/src/components/PromptInput.tsx +0 -760
  71. package/src/components/SentryErrorBoundary.ts +0 -39
  72. package/src/components/Spinner.tsx +0 -129
  73. package/src/components/StickerRequestForm.tsx +0 -16
  74. package/src/components/StructuredDiff.tsx +0 -191
  75. package/src/components/TextInput.tsx +0 -259
  76. package/src/components/TodoItem.tsx +0 -47
  77. package/src/components/TokenWarning.tsx +0 -31
  78. package/src/components/ToolUseLoader.tsx +0 -40
  79. package/src/components/TrustDialog.tsx +0 -106
  80. package/src/components/binary-feedback/BinaryFeedback.tsx +0 -63
  81. package/src/components/binary-feedback/BinaryFeedbackOption.tsx +0 -111
  82. package/src/components/binary-feedback/BinaryFeedbackView.tsx +0 -172
  83. package/src/components/binary-feedback/utils.ts +0 -220
  84. package/src/components/messages/AssistantBashOutputMessage.tsx +0 -22
  85. package/src/components/messages/AssistantLocalCommandOutputMessage.tsx +0 -49
  86. package/src/components/messages/AssistantRedactedThinkingMessage.tsx +0 -19
  87. package/src/components/messages/AssistantTextMessage.tsx +0 -144
  88. package/src/components/messages/AssistantThinkingMessage.tsx +0 -40
  89. package/src/components/messages/AssistantToolUseMessage.tsx +0 -132
  90. package/src/components/messages/TaskProgressMessage.tsx +0 -32
  91. package/src/components/messages/TaskToolMessage.tsx +0 -58
  92. package/src/components/messages/UserBashInputMessage.tsx +0 -28
  93. package/src/components/messages/UserCommandMessage.tsx +0 -30
  94. package/src/components/messages/UserKodingInputMessage.tsx +0 -28
  95. package/src/components/messages/UserPromptMessage.tsx +0 -35
  96. package/src/components/messages/UserTextMessage.tsx +0 -39
  97. package/src/components/messages/UserToolResultMessage/UserToolCanceledMessage.tsx +0 -12
  98. package/src/components/messages/UserToolResultMessage/UserToolErrorMessage.tsx +0 -36
  99. package/src/components/messages/UserToolResultMessage/UserToolRejectMessage.tsx +0 -31
  100. package/src/components/messages/UserToolResultMessage/UserToolResultMessage.tsx +0 -57
  101. package/src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx +0 -35
  102. package/src/components/messages/UserToolResultMessage/utils.tsx +0 -56
  103. package/src/components/permissions/BashPermissionRequest/BashPermissionRequest.tsx +0 -121
  104. package/src/components/permissions/FallbackPermissionRequest.tsx +0 -153
  105. package/src/components/permissions/FileEditPermissionRequest/FileEditPermissionRequest.tsx +0 -182
  106. package/src/components/permissions/FileEditPermissionRequest/FileEditToolDiff.tsx +0 -77
  107. package/src/components/permissions/FileWritePermissionRequest/FileWritePermissionRequest.tsx +0 -164
  108. package/src/components/permissions/FileWritePermissionRequest/FileWriteToolDiff.tsx +0 -83
  109. package/src/components/permissions/FilesystemPermissionRequest/FilesystemPermissionRequest.tsx +0 -240
  110. package/src/components/permissions/PermissionRequest.tsx +0 -101
  111. package/src/components/permissions/PermissionRequestTitle.tsx +0 -69
  112. package/src/components/permissions/hooks.ts +0 -44
  113. package/src/components/permissions/toolUseOptions.ts +0 -59
  114. package/src/components/permissions/utils.ts +0 -23
  115. package/src/constants/betas.ts +0 -5
  116. package/src/constants/claude-asterisk-ascii-art.tsx +0 -238
  117. package/src/constants/figures.ts +0 -4
  118. package/src/constants/keys.ts +0 -3
  119. package/src/constants/macros.ts +0 -11
  120. package/src/constants/modelCapabilities.ts +0 -179
  121. package/src/constants/models.ts +0 -1025
  122. package/src/constants/oauth.ts +0 -18
  123. package/src/constants/product.ts +0 -17
  124. package/src/constants/prompts.ts +0 -168
  125. package/src/constants/releaseNotes.ts +0 -7
  126. package/src/context/PermissionContext.tsx +0 -149
  127. package/src/context.ts +0 -278
  128. package/src/cost-tracker.ts +0 -84
  129. package/src/entrypoints/cli.tsx +0 -1561
  130. package/src/entrypoints/mcp.ts +0 -175
  131. package/src/history.ts +0 -25
  132. package/src/hooks/useApiKeyVerification.ts +0 -59
  133. package/src/hooks/useArrowKeyHistory.ts +0 -55
  134. package/src/hooks/useCanUseTool.ts +0 -138
  135. package/src/hooks/useCancelRequest.ts +0 -39
  136. package/src/hooks/useDoublePress.ts +0 -41
  137. package/src/hooks/useExitOnCtrlCD.ts +0 -31
  138. package/src/hooks/useInterval.ts +0 -25
  139. package/src/hooks/useLogMessages.ts +0 -16
  140. package/src/hooks/useLogStartupTime.ts +0 -12
  141. package/src/hooks/useNotifyAfterTimeout.ts +0 -65
  142. package/src/hooks/usePermissionRequestLogging.ts +0 -44
  143. package/src/hooks/useTerminalSize.ts +0 -49
  144. package/src/hooks/useTextInput.ts +0 -317
  145. package/src/hooks/useUnifiedCompletion.ts +0 -1405
  146. package/src/index.ts +0 -34
  147. package/src/messages.ts +0 -38
  148. package/src/permissions.ts +0 -268
  149. package/src/query.ts +0 -720
  150. package/src/screens/ConfigureNpmPrefix.tsx +0 -197
  151. package/src/screens/Doctor.tsx +0 -219
  152. package/src/screens/LogList.tsx +0 -68
  153. package/src/screens/REPL.tsx +0 -813
  154. package/src/screens/ResumeConversation.tsx +0 -68
  155. package/src/services/adapters/base.ts +0 -38
  156. package/src/services/adapters/chatCompletions.ts +0 -90
  157. package/src/services/adapters/responsesAPI.ts +0 -170
  158. package/src/services/browserMocks.ts +0 -66
  159. package/src/services/claude.ts +0 -2197
  160. package/src/services/customCommands.ts +0 -704
  161. package/src/services/fileFreshness.ts +0 -377
  162. package/src/services/gpt5ConnectionTest.ts +0 -340
  163. package/src/services/mcpClient.ts +0 -564
  164. package/src/services/mcpServerApproval.tsx +0 -50
  165. package/src/services/mentionProcessor.ts +0 -273
  166. package/src/services/modelAdapterFactory.ts +0 -69
  167. package/src/services/notifier.ts +0 -40
  168. package/src/services/oauth.ts +0 -357
  169. package/src/services/openai.ts +0 -1359
  170. package/src/services/responseStateManager.ts +0 -90
  171. package/src/services/sentry.ts +0 -3
  172. package/src/services/statsig.ts +0 -172
  173. package/src/services/statsigStorage.ts +0 -86
  174. package/src/services/systemReminder.ts +0 -507
  175. package/src/services/vcr.ts +0 -161
  176. package/src/test/testAdapters.ts +0 -96
  177. package/src/tools/ArchitectTool/ArchitectTool.tsx +0 -135
  178. package/src/tools/ArchitectTool/prompt.ts +0 -15
  179. package/src/tools/AskExpertModelTool/AskExpertModelTool.tsx +0 -576
  180. package/src/tools/BashTool/BashTool.tsx +0 -243
  181. package/src/tools/BashTool/BashToolResultMessage.tsx +0 -38
  182. package/src/tools/BashTool/OutputLine.tsx +0 -49
  183. package/src/tools/BashTool/prompt.ts +0 -174
  184. package/src/tools/BashTool/utils.ts +0 -56
  185. package/src/tools/FileEditTool/FileEditTool.tsx +0 -319
  186. package/src/tools/FileEditTool/prompt.ts +0 -51
  187. package/src/tools/FileEditTool/utils.ts +0 -58
  188. package/src/tools/FileReadTool/FileReadTool.tsx +0 -404
  189. package/src/tools/FileReadTool/prompt.ts +0 -7
  190. package/src/tools/FileWriteTool/FileWriteTool.tsx +0 -301
  191. package/src/tools/FileWriteTool/prompt.ts +0 -10
  192. package/src/tools/GlobTool/GlobTool.tsx +0 -119
  193. package/src/tools/GlobTool/prompt.ts +0 -8
  194. package/src/tools/GrepTool/GrepTool.tsx +0 -147
  195. package/src/tools/GrepTool/prompt.ts +0 -11
  196. package/src/tools/MCPTool/MCPTool.tsx +0 -107
  197. package/src/tools/MCPTool/prompt.ts +0 -3
  198. package/src/tools/MemoryReadTool/MemoryReadTool.tsx +0 -127
  199. package/src/tools/MemoryReadTool/prompt.ts +0 -3
  200. package/src/tools/MemoryWriteTool/MemoryWriteTool.tsx +0 -89
  201. package/src/tools/MemoryWriteTool/prompt.ts +0 -3
  202. package/src/tools/MultiEditTool/MultiEditTool.tsx +0 -388
  203. package/src/tools/MultiEditTool/prompt.ts +0 -45
  204. package/src/tools/NotebookEditTool/NotebookEditTool.tsx +0 -298
  205. package/src/tools/NotebookEditTool/prompt.ts +0 -3
  206. package/src/tools/NotebookReadTool/NotebookReadTool.tsx +0 -258
  207. package/src/tools/NotebookReadTool/prompt.ts +0 -3
  208. package/src/tools/StickerRequestTool/StickerRequestTool.tsx +0 -107
  209. package/src/tools/StickerRequestTool/prompt.ts +0 -19
  210. package/src/tools/TaskTool/TaskTool.tsx +0 -438
  211. package/src/tools/TaskTool/constants.ts +0 -1
  212. package/src/tools/TaskTool/prompt.ts +0 -92
  213. package/src/tools/ThinkTool/ThinkTool.tsx +0 -54
  214. package/src/tools/ThinkTool/prompt.ts +0 -12
  215. package/src/tools/TodoWriteTool/TodoWriteTool.tsx +0 -313
  216. package/src/tools/TodoWriteTool/prompt.ts +0 -63
  217. package/src/tools/URLFetcherTool/URLFetcherTool.tsx +0 -178
  218. package/src/tools/URLFetcherTool/cache.ts +0 -55
  219. package/src/tools/URLFetcherTool/htmlToMarkdown.ts +0 -55
  220. package/src/tools/URLFetcherTool/prompt.ts +0 -17
  221. package/src/tools/WebSearchTool/WebSearchTool.tsx +0 -103
  222. package/src/tools/WebSearchTool/prompt.ts +0 -13
  223. package/src/tools/WebSearchTool/searchProviders.ts +0 -66
  224. package/src/tools/lsTool/lsTool.tsx +0 -272
  225. package/src/tools/lsTool/prompt.ts +0 -2
  226. package/src/tools.ts +0 -67
  227. package/src/types/PermissionMode.ts +0 -120
  228. package/src/types/RequestContext.ts +0 -72
  229. package/src/types/common.d.ts +0 -2
  230. package/src/types/conversation.ts +0 -51
  231. package/src/types/logs.ts +0 -58
  232. package/src/types/modelCapabilities.ts +0 -64
  233. package/src/types/notebook.ts +0 -87
  234. package/src/utils/Cursor.ts +0 -436
  235. package/src/utils/PersistentShell.ts +0 -552
  236. package/src/utils/advancedFuzzyMatcher.ts +0 -290
  237. package/src/utils/agentLoader.ts +0 -278
  238. package/src/utils/agentStorage.ts +0 -97
  239. package/src/utils/array.ts +0 -3
  240. package/src/utils/ask.tsx +0 -99
  241. package/src/utils/auth.ts +0 -13
  242. package/src/utils/autoCompactCore.ts +0 -223
  243. package/src/utils/autoUpdater.ts +0 -458
  244. package/src/utils/betas.ts +0 -20
  245. package/src/utils/browser.ts +0 -14
  246. package/src/utils/cleanup.ts +0 -72
  247. package/src/utils/commands.ts +0 -261
  248. package/src/utils/commonUnixCommands.ts +0 -161
  249. package/src/utils/config.ts +0 -945
  250. package/src/utils/conversationRecovery.ts +0 -55
  251. package/src/utils/debugLogger.ts +0 -1235
  252. package/src/utils/diff.ts +0 -42
  253. package/src/utils/env.ts +0 -57
  254. package/src/utils/errors.ts +0 -21
  255. package/src/utils/exampleCommands.ts +0 -109
  256. package/src/utils/execFileNoThrow.ts +0 -51
  257. package/src/utils/expertChatStorage.ts +0 -136
  258. package/src/utils/file.ts +0 -405
  259. package/src/utils/fileRecoveryCore.ts +0 -71
  260. package/src/utils/format.tsx +0 -44
  261. package/src/utils/fuzzyMatcher.ts +0 -328
  262. package/src/utils/generators.ts +0 -62
  263. package/src/utils/git.ts +0 -92
  264. package/src/utils/globalLogger.ts +0 -77
  265. package/src/utils/http.ts +0 -10
  266. package/src/utils/imagePaste.ts +0 -38
  267. package/src/utils/json.ts +0 -13
  268. package/src/utils/log.ts +0 -382
  269. package/src/utils/markdown.ts +0 -213
  270. package/src/utils/messageContextManager.ts +0 -294
  271. package/src/utils/messages.tsx +0 -945
  272. package/src/utils/model.ts +0 -914
  273. package/src/utils/permissions/filesystem.ts +0 -127
  274. package/src/utils/responseState.ts +0 -23
  275. package/src/utils/ripgrep.ts +0 -167
  276. package/src/utils/secureFile.ts +0 -564
  277. package/src/utils/sessionState.ts +0 -49
  278. package/src/utils/state.ts +0 -25
  279. package/src/utils/style.ts +0 -29
  280. package/src/utils/terminal.ts +0 -50
  281. package/src/utils/theme.ts +0 -127
  282. package/src/utils/thinking.ts +0 -144
  283. package/src/utils/todoStorage.ts +0 -431
  284. package/src/utils/tokens.ts +0 -43
  285. package/src/utils/toolExecutionController.ts +0 -163
  286. package/src/utils/unaryLogging.ts +0 -26
  287. package/src/utils/user.ts +0 -37
  288. package/src/utils/validate.ts +0 -165
@@ -1,552 +0,0 @@
1
- import * as fs from 'fs'
2
- import { homedir } from 'os'
3
- import { existsSync } from 'fs'
4
- import shellquote from 'shell-quote'
5
- import { spawn, execSync, type ChildProcess } from 'child_process'
6
- import { isAbsolute, resolve, join } from 'path'
7
- import { logError } from './log'
8
- import * as os from 'os'
9
- import { logEvent } from '../services/statsig'
10
- import { PRODUCT_COMMAND } from '../constants/product'
11
-
12
- type ExecResult = {
13
- stdout: string
14
- stderr: string
15
- code: number
16
- interrupted: boolean
17
- }
18
- type QueuedCommand = {
19
- command: string
20
- abortSignal?: AbortSignal
21
- timeout?: number
22
- resolve: (result: ExecResult) => void
23
- reject: (error: Error) => void
24
- }
25
-
26
- const TEMPFILE_PREFIX = os.tmpdir() + `/${PRODUCT_COMMAND}-`
27
- const DEFAULT_TIMEOUT = 30 * 60 * 1000
28
- const SIGTERM_CODE = 143 // Standard exit code for SIGTERM
29
- const FILE_SUFFIXES = {
30
- STATUS: '-status',
31
- STDOUT: '-stdout',
32
- STDERR: '-stderr',
33
- CWD: '-cwd',
34
- }
35
- const SHELL_CONFIGS: Record<string, string> = {
36
- '/bin/bash': '.bashrc',
37
- '/bin/zsh': '.zshrc',
38
- }
39
-
40
- type DetectedShell = {
41
- bin: string
42
- args: string[]
43
- type: 'posix' | 'msys' | 'wsl'
44
- }
45
-
46
- function quoteForBash(str: string): string {
47
- return `'${str.replace(/'/g, "'\\''")}'`
48
- }
49
-
50
- function toBashPath(pathStr: string, type: 'posix' | 'msys' | 'wsl'): string {
51
- // Already POSIX absolute path
52
- if (pathStr.startsWith('/')) return pathStr
53
- if (type === 'posix') return pathStr
54
-
55
- // Normalize backslashes
56
- const normalized = pathStr.replace(/\\/g, '/').replace(/\\\\/g, '/')
57
- const driveMatch = /^[A-Za-z]:/.exec(normalized)
58
- if (driveMatch) {
59
- const drive = normalized[0].toLowerCase()
60
- const rest = normalized.slice(2)
61
- if (type === 'msys') {
62
- return `/` + drive + (rest.startsWith('/') ? rest : `/${rest}`)
63
- }
64
- // wsl
65
- return `/mnt/` + drive + (rest.startsWith('/') ? rest : `/${rest}`)
66
- }
67
- // Relative path: just convert slashes
68
- return normalized
69
- }
70
-
71
- function fileExists(p: string | undefined): p is string {
72
- return !!p && existsSync(p)
73
- }
74
-
75
- // Robust PATH splitter for Windows and POSIX
76
- function splitPathEntries(pathEnv: string, platform: NodeJS.Platform): string[] {
77
- if (!pathEnv) return []
78
-
79
- // POSIX: ':' is the separator
80
- if (platform !== 'win32') {
81
- return pathEnv
82
- .split(':')
83
- .map(s => s.trim().replace(/^"|"$/g, ''))
84
- .filter(Boolean)
85
- }
86
-
87
- // Windows: primarily ';', but some environments may use ':'
88
- // We must not split drive letters like 'C:\\' or 'D:foo\\bar'
89
- const entries: string[] = []
90
- let current = ''
91
- const pushCurrent = () => {
92
- const cleaned = current.trim().replace(/^"|"$/g, '')
93
- if (cleaned) entries.push(cleaned)
94
- current = ''
95
- }
96
-
97
- for (let i = 0; i < pathEnv.length; i++) {
98
- const ch = pathEnv[i]
99
-
100
- if (ch === ';') {
101
- pushCurrent()
102
- continue
103
- }
104
-
105
- if (ch === ':') {
106
- const segmentLength = current.length
107
- const firstChar = current[0]
108
- const isDriveLetterPrefix = segmentLength === 1 && /[A-Za-z]/.test(firstChar || '')
109
- // Treat ':' as separator only if it's NOT the drive letter colon
110
- if (!isDriveLetterPrefix) {
111
- pushCurrent()
112
- continue
113
- }
114
- }
115
-
116
- current += ch
117
- }
118
-
119
- // Flush the final segment
120
- pushCurrent()
121
-
122
- return entries
123
- }
124
-
125
- function detectShell(): DetectedShell {
126
- const isWin = process.platform === 'win32'
127
- if (!isWin) {
128
- const bin = process.env.SHELL || '/bin/bash'
129
- return { bin, args: ['-l'], type: 'posix' }
130
- }
131
-
132
- // 1) Respect SHELL if it points to a bash.exe that exists
133
- if (process.env.SHELL && /bash\.exe$/i.test(process.env.SHELL) && existsSync(process.env.SHELL)) {
134
- return { bin: process.env.SHELL, args: ['-l'], type: 'msys' }
135
- }
136
-
137
- // 1.1) Explicit override
138
- if (process.env.KODE_BASH && existsSync(process.env.KODE_BASH)) {
139
- return { bin: process.env.KODE_BASH, args: ['-l'], type: 'msys' }
140
- }
141
-
142
- // 2) Common Git Bash/MSYS2 locations
143
- const programFiles = [
144
- process.env['ProgramFiles'],
145
- process.env['ProgramFiles(x86)'],
146
- process.env['ProgramW6432'],
147
- ].filter(Boolean) as string[]
148
-
149
- const localAppData = process.env['LocalAppData']
150
-
151
- const candidates: string[] = []
152
- for (const base of programFiles) {
153
- candidates.push(
154
- join(base, 'Git', 'bin', 'bash.exe'),
155
- join(base, 'Git', 'usr', 'bin', 'bash.exe'),
156
- )
157
- }
158
- if (localAppData) {
159
- candidates.push(
160
- join(localAppData, 'Programs', 'Git', 'bin', 'bash.exe'),
161
- join(localAppData, 'Programs', 'Git', 'usr', 'bin', 'bash.exe'),
162
- )
163
- }
164
- // MSYS2 default
165
- candidates.push('C:/msys64/usr/bin/bash.exe')
166
-
167
- for (const c of candidates) {
168
- if (existsSync(c)) {
169
- return { bin: c, args: ['-l'], type: 'msys' }
170
- }
171
- }
172
-
173
- // 2.1) Search in PATH for bash.exe
174
- const pathEnv = process.env.PATH || process.env.Path || process.env.path || ''
175
- const pathEntries = splitPathEntries(pathEnv, process.platform)
176
- for (const p of pathEntries) {
177
- const candidate = join(p, 'bash.exe')
178
- if (existsSync(candidate)) {
179
- return { bin: candidate, args: ['-l'], type: 'msys' }
180
- }
181
- }
182
-
183
- // 3) WSL
184
- try {
185
- // Quick probe to ensure WSL+bash exists
186
- execSync('wsl.exe -e bash -lc "echo KODE_OK"', { stdio: 'ignore', timeout: 1500 })
187
- return { bin: 'wsl.exe', args: ['-e', 'bash', '-l'], type: 'wsl' }
188
- } catch {}
189
-
190
- // 4) Last resort: meaningful error
191
- const hint = [
192
- '无法找到可用的 bash。请安装 Git for Windows 或启用 WSL。',
193
- '推荐安装 Git: https://git-scm.com/download/win',
194
- '或启用 WSL 并安装 Ubuntu: https://learn.microsoft.com/windows/wsl/install',
195
- ].join('\n')
196
- throw new Error(hint)
197
- }
198
-
199
- export class PersistentShell {
200
- private commandQueue: QueuedCommand[] = []
201
- private isExecuting: boolean = false
202
- private shell: ChildProcess
203
- private isAlive: boolean = true
204
- private commandInterrupted: boolean = false
205
- private statusFile: string
206
- private stdoutFile: string
207
- private stderrFile: string
208
- private cwdFile: string
209
- private cwd: string
210
- private binShell: string
211
- private shellArgs: string[]
212
- private shellType: 'posix' | 'msys' | 'wsl'
213
- private statusFileBashPath: string
214
- private stdoutFileBashPath: string
215
- private stderrFileBashPath: string
216
- private cwdFileBashPath: string
217
-
218
- constructor(cwd: string) {
219
- const { bin, args, type } = detectShell()
220
- this.binShell = bin
221
- this.shellArgs = args
222
- this.shellType = type
223
-
224
- this.shell = spawn(this.binShell, this.shellArgs, {
225
- stdio: ['pipe', 'pipe', 'pipe'],
226
- cwd,
227
- env: {
228
- ...process.env,
229
- GIT_EDITOR: 'true',
230
- },
231
- })
232
-
233
- this.cwd = cwd
234
-
235
- this.shell.on('exit', (code, signal) => {
236
- if (code) {
237
- // TODO: It would be nice to alert the user that shell crashed
238
- logError(`Shell exited with code ${code} and signal ${signal}`)
239
- logEvent('persistent_shell_exit', {
240
- code: code?.toString() || 'null',
241
- signal: signal || 'null',
242
- })
243
- }
244
- for (const file of [
245
- this.statusFile,
246
- this.stdoutFile,
247
- this.stderrFile,
248
- this.cwdFile,
249
- ]) {
250
- if (fs.existsSync(file)) {
251
- fs.unlinkSync(file)
252
- }
253
- }
254
- this.isAlive = false
255
- })
256
-
257
- const id = Math.floor(Math.random() * 0x10000)
258
- .toString(16)
259
- .padStart(4, '0')
260
-
261
- this.statusFile = TEMPFILE_PREFIX + id + FILE_SUFFIXES.STATUS
262
- this.stdoutFile = TEMPFILE_PREFIX + id + FILE_SUFFIXES.STDOUT
263
- this.stderrFile = TEMPFILE_PREFIX + id + FILE_SUFFIXES.STDERR
264
- this.cwdFile = TEMPFILE_PREFIX + id + FILE_SUFFIXES.CWD
265
- for (const file of [this.statusFile, this.stdoutFile, this.stderrFile]) {
266
- fs.writeFileSync(file, '')
267
- }
268
- // Initialize CWD file with initial directory
269
- fs.writeFileSync(this.cwdFile, cwd)
270
-
271
- // Compute bash-visible paths for redirections
272
- this.statusFileBashPath = toBashPath(this.statusFile, this.shellType)
273
- this.stdoutFileBashPath = toBashPath(this.stdoutFile, this.shellType)
274
- this.stderrFileBashPath = toBashPath(this.stderrFile, this.shellType)
275
- this.cwdFileBashPath = toBashPath(this.cwdFile, this.shellType)
276
-
277
- // Source ~/.bashrc when available (works for bash on POSIX/MSYS/WSL)
278
- this.sendToShell('[ -f ~/.bashrc ] && source ~/.bashrc || true')
279
- }
280
-
281
- private static instance: PersistentShell | null = null
282
-
283
- static restart() {
284
- if (PersistentShell.instance) {
285
- PersistentShell.instance.close()
286
- PersistentShell.instance = null
287
- }
288
- }
289
-
290
- static getInstance(): PersistentShell {
291
- if (!PersistentShell.instance || !PersistentShell.instance.isAlive) {
292
- PersistentShell.instance = new PersistentShell(process.cwd())
293
- }
294
- return PersistentShell.instance
295
- }
296
-
297
- killChildren() {
298
- const parentPid = this.shell.pid
299
- try {
300
- const childPids = execSync(`pgrep -P ${parentPid}`)
301
- .toString()
302
- .trim()
303
- .split('\n')
304
- .filter(Boolean) // Filter out empty strings
305
-
306
- if (childPids.length > 0) {
307
- logEvent('persistent_shell_command_interrupted', {
308
- numChildProcesses: childPids.length.toString(),
309
- })
310
- }
311
-
312
- childPids.forEach(pid => {
313
- try {
314
- process.kill(Number(pid), 'SIGTERM')
315
- } catch (error) {
316
- logError(`Failed to kill process ${pid}: ${error}`)
317
- logEvent('persistent_shell_kill_process_error', {
318
- error: (error as Error).message.substring(0, 10),
319
- })
320
- }
321
- })
322
- } catch {
323
- // pgrep returns non-zero when no processes are found - this is expected
324
- } finally {
325
- this.commandInterrupted = true
326
- }
327
- }
328
-
329
- private async processQueue() {
330
- /**
331
- * Processes commands from the queue one at a time.
332
- * Concurrency invariants:
333
- * - Only one instance runs at a time (controlled by isExecuting)
334
- * - Is the only caller of updateCwd() in the system
335
- * - Calls updateCwd() after each command completes
336
- * - Ensures commands execute serially via the queue
337
- * - Handles interruption via abortSignal by calling killChildren()
338
- * - Cleans up abortSignal listeners after command completion or interruption
339
- */
340
- if (this.isExecuting || this.commandQueue.length === 0) return
341
-
342
- this.isExecuting = true
343
- const { command, abortSignal, timeout, resolve, reject } =
344
- this.commandQueue.shift()!
345
-
346
- const killChildren = () => this.killChildren()
347
- if (abortSignal) {
348
- abortSignal.addEventListener('abort', killChildren)
349
- }
350
-
351
- try {
352
- const result = await this.exec_(command, timeout)
353
-
354
- // No need to update cwd - it's handled in exec_ via the CWD file
355
-
356
- resolve(result)
357
- } catch (error) {
358
- logEvent('persistent_shell_command_error', {
359
- error: (error as Error).message.substring(0, 10),
360
- })
361
- reject(error as Error)
362
- } finally {
363
- this.isExecuting = false
364
- if (abortSignal) {
365
- abortSignal.removeEventListener('abort', killChildren)
366
- }
367
- // Process next command in queue
368
- this.processQueue()
369
- }
370
- }
371
-
372
- async exec(
373
- command: string,
374
- abortSignal?: AbortSignal,
375
- timeout?: number,
376
- ): Promise<ExecResult> {
377
- return new Promise((resolve, reject) => {
378
- this.commandQueue.push({ command, abortSignal, timeout, resolve, reject })
379
- this.processQueue()
380
- })
381
- }
382
-
383
- private async exec_(command: string, timeout?: number): Promise<ExecResult> {
384
- /**
385
- * Direct command execution without going through the queue.
386
- * Concurrency invariants:
387
- * - Not safe for concurrent calls (uses shared files)
388
- * - Called only when queue is idle
389
- * - Relies on file-based IPC to handle shell interaction
390
- * - Does not modify the command queue state
391
- * - Tracks interruption state via commandInterrupted flag
392
- * - Resets interruption state at start of new command
393
- * - Reports interruption status in result object
394
- *
395
- * Exit Code & CWD Handling:
396
- * - Executes command and immediately captures its exit code into a shell variable
397
- * - Updates the CWD file with the working directory after capturing exit code
398
- * - Writes the preserved exit code to the status file as the final step
399
- * - This sequence eliminates race conditions between exit code capture and CWD updates
400
- * - The pwd() method reads the CWD file directly for current directory info
401
- */
402
- const quotedCommand = shellquote.quote([command])
403
-
404
- // Check the syntax of the command
405
- try {
406
- if (this.shellType === 'wsl') {
407
- execSync(`wsl.exe -e bash -n -c ${quotedCommand}`, {
408
- stdio: 'ignore',
409
- timeout: 1000,
410
- })
411
- } else {
412
- execSync(`${this.binShell} -n -c ${quotedCommand}`, {
413
- stdio: 'ignore',
414
- timeout: 1000,
415
- })
416
- }
417
- } catch (stderr) {
418
- // If there's a syntax error, return an error and log it
419
- const errorStr =
420
- typeof stderr === 'string' ? stderr : String(stderr || '')
421
- logEvent('persistent_shell_syntax_error', {
422
- error: errorStr.substring(0, 10),
423
- })
424
- return Promise.resolve({
425
- stdout: '',
426
- stderr: errorStr,
427
- code: 128,
428
- interrupted: false,
429
- })
430
- }
431
-
432
- const commandTimeout = timeout || DEFAULT_TIMEOUT
433
- // Reset interrupted state for new command
434
- this.commandInterrupted = false
435
- return new Promise<ExecResult>(resolve => {
436
- // Truncate output files
437
- fs.writeFileSync(this.stdoutFile, '')
438
- fs.writeFileSync(this.stderrFile, '')
439
- fs.writeFileSync(this.statusFile, '')
440
- // Break up the command sequence for clarity using an array of commands
441
- const commandParts = []
442
-
443
- // 1. Execute the main command with redirections
444
- commandParts.push(
445
- `eval ${quotedCommand} < /dev/null > ${quoteForBash(this.stdoutFileBashPath)} 2> ${quoteForBash(this.stderrFileBashPath)}`,
446
- )
447
-
448
- // 2. Capture exit code immediately after command execution to avoid losing it
449
- commandParts.push(`EXEC_EXIT_CODE=$?`)
450
-
451
- // 3. Update CWD file
452
- commandParts.push(`pwd > ${quoteForBash(this.cwdFileBashPath)}`)
453
-
454
- // 4. Write the preserved exit code to status file to avoid race with pwd
455
- commandParts.push(`echo $EXEC_EXIT_CODE > ${quoteForBash(this.statusFileBashPath)}`)
456
-
457
- // Send the combined commands as a single operation to maintain atomicity
458
- this.sendToShell(commandParts.join('\n'))
459
-
460
- // Check for command completion or timeout
461
- const start = Date.now()
462
- const checkCompletion = setInterval(() => {
463
- try {
464
- let statusFileSize = 0
465
- if (fs.existsSync(this.statusFile)) {
466
- statusFileSize = fs.statSync(this.statusFile).size
467
- }
468
-
469
- if (
470
- statusFileSize > 0 ||
471
- Date.now() - start > commandTimeout ||
472
- this.commandInterrupted
473
- ) {
474
- clearInterval(checkCompletion)
475
- const stdout = fs.existsSync(this.stdoutFile)
476
- ? fs.readFileSync(this.stdoutFile, 'utf8')
477
- : ''
478
- let stderr = fs.existsSync(this.stderrFile)
479
- ? fs.readFileSync(this.stderrFile, 'utf8')
480
- : ''
481
- let code: number
482
- if (statusFileSize) {
483
- code = Number(fs.readFileSync(this.statusFile, 'utf8'))
484
- } else {
485
- // Timeout occurred - kill any running processes
486
- this.killChildren()
487
- code = SIGTERM_CODE
488
- stderr += (stderr ? '\n' : '') + 'Command execution timed out'
489
- logEvent('persistent_shell_command_timeout', {
490
- command: command.substring(0, 10),
491
- timeout: commandTimeout.toString(),
492
- })
493
- }
494
- resolve({
495
- stdout,
496
- stderr,
497
- code,
498
- interrupted: this.commandInterrupted,
499
- })
500
- }
501
- } catch {
502
- // Ignore file system errors during polling - they are expected
503
- // as we check for completion before files exist
504
- }
505
- }, 10) // increasing this will introduce latency
506
- })
507
- }
508
-
509
- private sendToShell(command: string) {
510
- try {
511
- this.shell!.stdin!.write(command + '\n')
512
- } catch (error) {
513
- const errorString =
514
- error instanceof Error
515
- ? error.message
516
- : String(error || 'Unknown error')
517
- logError(`Error in sendToShell: ${errorString}`)
518
- logEvent('persistent_shell_write_error', {
519
- error: errorString.substring(0, 100),
520
- command: command.substring(0, 30),
521
- })
522
- throw error
523
- }
524
- }
525
-
526
- pwd(): string {
527
- try {
528
- const newCwd = fs.readFileSync(this.cwdFile, 'utf8').trim()
529
- if (newCwd) {
530
- this.cwd = newCwd
531
- }
532
- } catch (error) {
533
- logError(`Shell pwd error ${error}`)
534
- }
535
- // Always return the cached value
536
- return this.cwd
537
- }
538
-
539
- async setCwd(cwd: string) {
540
- const resolved = isAbsolute(cwd) ? cwd : resolve(process.cwd(), cwd)
541
- if (!existsSync(resolved)) {
542
- throw new Error(`Path "${resolved}" does not exist`)
543
- }
544
- const bashPath = toBashPath(resolved, this.shellType)
545
- await this.exec(`cd ${quoteForBash(bashPath)}`)
546
- }
547
-
548
- close(): void {
549
- this.shell!.stdin!.end()
550
- this.shell.kill()
551
- }
552
- }