@shareai-lab/kode 1.0.81 → 1.0.83

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.
@@ -37,6 +37,165 @@ const SHELL_CONFIGS: Record<string, string> = {
37
37
  '/bin/zsh': '.zshrc',
38
38
  }
39
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
+
40
199
  export class PersistentShell {
41
200
  private commandQueue: QueuedCommand[] = []
42
201
  private isExecuting: boolean = false
@@ -49,10 +208,20 @@ export class PersistentShell {
49
208
  private cwdFile: string
50
209
  private cwd: string
51
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
52
217
 
53
218
  constructor(cwd: string) {
54
- this.binShell = process.env.SHELL || '/bin/bash'
55
- this.shell = spawn(this.binShell, ['-l'], {
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, {
56
225
  stdio: ['pipe', 'pipe', 'pipe'],
57
226
  cwd,
58
227
  env: {
@@ -98,13 +267,15 @@ export class PersistentShell {
98
267
  }
99
268
  // Initialize CWD file with initial directory
100
269
  fs.writeFileSync(this.cwdFile, cwd)
101
- const configFile = SHELL_CONFIGS[this.binShell]
102
- if (configFile) {
103
- const configFilePath = join(homedir(), configFile)
104
- if (existsSync(configFilePath)) {
105
- this.sendToShell(`source ${configFilePath}`)
106
- }
107
- }
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')
108
279
  }
109
280
 
110
281
  private static instance: PersistentShell | null = null
@@ -232,10 +403,17 @@ export class PersistentShell {
232
403
 
233
404
  // Check the syntax of the command
234
405
  try {
235
- execSync(`${this.binShell} -n -c ${quotedCommand}`, {
236
- stdio: 'ignore',
237
- timeout: 1000,
238
- })
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
+ }
239
417
  } catch (stderr) {
240
418
  // If there's a syntax error, return an error and log it
241
419
  const errorStr =
@@ -264,17 +442,17 @@ export class PersistentShell {
264
442
 
265
443
  // 1. Execute the main command with redirections
266
444
  commandParts.push(
267
- `eval ${quotedCommand} < /dev/null > ${this.stdoutFile} 2> ${this.stderrFile}`,
445
+ `eval ${quotedCommand} < /dev/null > ${quoteForBash(this.stdoutFileBashPath)} 2> ${quoteForBash(this.stderrFileBashPath)}`,
268
446
  )
269
447
 
270
448
  // 2. Capture exit code immediately after command execution to avoid losing it
271
449
  commandParts.push(`EXEC_EXIT_CODE=$?`)
272
450
 
273
451
  // 3. Update CWD file
274
- commandParts.push(`pwd > ${this.cwdFile}`)
452
+ commandParts.push(`pwd > ${quoteForBash(this.cwdFileBashPath)}`)
275
453
 
276
454
  // 4. Write the preserved exit code to status file to avoid race with pwd
277
- commandParts.push(`echo $EXEC_EXIT_CODE > ${this.statusFile}`)
455
+ commandParts.push(`echo $EXEC_EXIT_CODE > ${quoteForBash(this.statusFileBashPath)}`)
278
456
 
279
457
  // Send the combined commands as a single operation to maintain atomicity
280
458
  this.sendToShell(commandParts.join('\n'))
@@ -363,7 +541,8 @@ export class PersistentShell {
363
541
  if (!existsSync(resolved)) {
364
542
  throw new Error(`Path "${resolved}" does not exist`)
365
543
  }
366
- await this.exec(`cd ${resolved}`)
544
+ const bashPath = toBashPath(resolved, this.shellType)
545
+ await this.exec(`cd ${quoteForBash(bashPath)}`)
367
546
  }
368
547
 
369
548
  close(): void {
@@ -3,6 +3,7 @@ import { join } from 'path'
3
3
  import { homedir } from 'os'
4
4
  import { randomUUID } from 'crypto'
5
5
  import chalk from 'chalk'
6
+ import envPaths from 'env-paths'
6
7
  import { PRODUCT_COMMAND } from '../constants/product'
7
8
  import { SESSION_ID } from './log'
8
9
  import type { Message } from '../types/conversation'
package/src/utils/log.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { existsSync, mkdirSync } from 'fs'
2
2
  import { dirname, join } from 'path'
3
- import { homedir } from 'os'
4
3
  import { writeFileSync, readFileSync } from 'fs'
5
4
  import { captureException } from '../services/sentry'
6
5
  import { randomUUID } from 'crypto'
6
+ import envPaths from 'env-paths'
7
7
  import { promises as fsPromises } from 'fs'
8
8
  import type { LogOption, SerializedMessage } from '../types/logs'
9
9
  import { MACRO } from '../constants/macros'
@@ -16,18 +16,17 @@ const MAX_IN_MEMORY_ERRORS = 100 // Limit to prevent memory issues
16
16
 
17
17
  export const SESSION_ID = randomUUID()
18
18
 
19
- // 统一使用 ~/.kode 目录
20
- const KODE_DIR = join(homedir(), '.kode')
19
+ const paths = envPaths(PRODUCT_COMMAND)
21
20
 
22
21
  function getProjectDir(cwd: string): string {
23
22
  return cwd.replace(/[^a-zA-Z0-9]/g, '-')
24
23
  }
25
24
 
26
25
  export const CACHE_PATHS = {
27
- errors: () => join(KODE_DIR, getProjectDir(process.cwd()), 'errors'),
28
- messages: () => join(KODE_DIR, getProjectDir(process.cwd()), 'messages'),
26
+ errors: () => join(paths.cache, getProjectDir(process.cwd()), 'errors'),
27
+ messages: () => join(paths.cache, getProjectDir(process.cwd()), 'messages'),
29
28
  mcpLogs: (serverName: string) =>
30
- join(KODE_DIR, getProjectDir(process.cwd()), `mcp-logs-${serverName}`),
29
+ join(paths.cache, getProjectDir(process.cwd()), `mcp-logs-${serverName}`),
31
30
  }
32
31
 
33
32
  export function dateToFilename(date: Date): string {