@shareai-lab/kode 1.0.85 → 1.0.92

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/cli.js CHANGED
@@ -3,84 +3,98 @@
3
3
  const { spawn } = require('child_process');
4
4
  const path = require('path');
5
5
 
6
- // Prefer bun if available, otherwise use node with tsx (ESM --import, fallback to --loader)
6
+ // Prefer Bun for dev; otherwise use Node + tsx (ESM --import)
7
7
  const args = process.argv.slice(2);
8
8
  const cliPath = path.join(__dirname, 'src', 'entrypoints', 'cli.tsx');
9
9
 
10
- // Try bun first
10
+ // Resolve tsx from this package directory (avoid CWD-based resolution on Windows/global installs)
11
+ let tsxImportPath = null;
12
+ let tsxCliPath = null;
13
+ try { tsxImportPath = require.resolve('tsx', { paths: [__dirname] }); } catch {}
14
+ try { tsxCliPath = require.resolve('tsx/cli', { paths: [__dirname] }); } catch {}
15
+
16
+ // Enforce minimum Node version based on package.json engines.node
17
+ try {
18
+ const req = "${enginesNode}"
19
+ const m = req.match(/(\d+)\.(\d+)\.(\d+)/)
20
+ const [maj, min, pat] = process.versions.node.split('.').map(Number)
21
+ const [rMaj, rMin, rPat] = m ? m.slice(1).map(Number) : [20, 18, 1]
22
+ const tooOld = (maj < rMaj) || (maj === rMaj && (min < rMin || (min === rMin && pat < rPat)))
23
+ if (tooOld) {
24
+ console.error(
25
+ 'Error: Node.js ' + req + ' is required. Please upgrade Node.'
26
+ );
27
+ process.exit(1)
28
+ }
29
+ } catch {}
30
+
31
+ // Try Bun first
11
32
  try {
12
33
  const { execSync } = require('child_process');
13
34
  execSync('bun --version', { stdio: 'ignore' });
14
-
15
- // Bun is available
16
35
  const child = spawn('bun', ['run', cliPath, ...args], {
17
36
  stdio: 'inherit',
18
- env: {
19
- ...process.env,
20
- YOGA_WASM_PATH: path.join(__dirname, 'yoga.wasm')
21
- }
37
+ env: { ...process.env, YOGA_WASM_PATH: path.join(__dirname, 'yoga.wasm') },
22
38
  });
23
-
24
39
  child.on('exit', (code) => process.exit(code || 0));
25
- child.on('error', () => {
26
- // Fallback to node if bun fails
27
- runWithNode();
28
- });
29
- } catch {
30
- // Bun not available, use node
31
- runWithNode();
32
- }
40
+ child.on('error', () => runWithNode());
41
+ } catch { runWithNode(); }
33
42
 
34
43
  function runWithNode() {
35
- const nodeVersion = process.versions.node.split('.').map(Number)
36
- const [major, minor] = nodeVersion
37
- const supportsImport = major > 20 || (major === 20 && minor >= 6) || major >= 21
38
-
39
44
  const baseArgs = ['--no-warnings=ExperimentalWarning', '--enable-source-maps']
40
45
 
41
- const tryWithLoader = () => {
42
- const child = spawn(process.execPath, [...baseArgs, '--loader', 'tsx', cliPath, ...args], {
46
+ const tryWithImport = () => {
47
+ let importArg = 'tsx'
48
+ try {
49
+ if (tsxImportPath) {
50
+ const { pathToFileURL } = require('node:url')
51
+ importArg = pathToFileURL(tsxImportPath).href
52
+ }
53
+ } catch {}
54
+ const child = spawn(process.execPath, [...baseArgs, '--import', importArg, cliPath, ...args], {
43
55
  stdio: 'inherit',
44
56
  env: { ...process.env, YOGA_WASM_PATH: path.join(__dirname, 'yoga.wasm') },
45
57
  })
46
- child.on('error', () => tryWithTsxBinary())
58
+ child.on('error', () => failWithTsxHint())
47
59
  child.on('exit', code => {
48
- if (code && code !== 0) return tryWithTsxBinary()
60
+ if (code && code !== 0) return failWithTsxHint()
49
61
  process.exit(code || 0)
50
62
  })
51
63
  }
52
64
 
53
65
  const tryWithTsxBinary = () => {
54
- const tsxPath = path.join(__dirname, 'node_modules', '.bin', 'tsx')
55
- const child = spawn(tsxPath, [cliPath, ...args], {
56
- stdio: 'inherit',
57
- env: { ...process.env, YOGA_WASM_PATH: path.join(__dirname, 'yoga.wasm') },
58
- })
59
- child.on('error', err => {
60
- if (err.code === 'ENOENT') {
61
- console.error('')
62
- console.error('Error: tsx is required but not found.')
63
- console.error('Please run: npm install')
64
- process.exit(1)
65
- } else {
66
- console.error('Failed to start Kode:', err.message)
67
- process.exit(1)
68
- }
69
- })
70
- child.on('exit', code => process.exit(code || 0))
71
- }
72
-
73
- if (supportsImport) {
74
- const child = spawn(process.execPath, [...baseArgs, '--import', 'tsx', cliPath, ...args], {
66
+ if (!tsxCliPath) return failWithTsxHint()
67
+ const child = spawn(process.execPath, [tsxCliPath, cliPath, ...args], {
75
68
  stdio: 'inherit',
76
69
  env: { ...process.env, YOGA_WASM_PATH: path.join(__dirname, 'yoga.wasm') },
77
70
  })
78
- child.on('error', () => tryWithLoader())
71
+ child.on('error', () => failWithTsxHint())
79
72
  child.on('exit', code => {
80
- if (code && code !== 0) return tryWithLoader()
73
+ if (code && code !== 0) return failWithTsxHint()
81
74
  process.exit(code || 0)
82
75
  })
76
+ }
77
+
78
+ function failWithTsxHint() {
79
+ console.error('')
80
+ console.error('Failed to launch with Node + tsx.')
81
+ console.error('Try reinstalling: npm i -g @shareai-lab/kode (ensures bundled tsx).')
82
+ console.error('Or install Bun and run: bun run kode')
83
+ process.exit(1)
84
+ }
85
+
86
+ // Windows prefers tsx CLI due to top-level await in CJS transforms
87
+ if (process.platform === 'win32') {
88
+ tryWithTsxBinary()
83
89
  } else {
84
- tryWithLoader()
90
+ // Non-Windows: try import-based first, then fallback
91
+ let attempted = false
92
+ const child = spawn(process.execPath, [...baseArgs, '--eval', ''], { stdio: 'ignore' })
93
+ child.on('error', () => {})
94
+ child.on('exit', () => {
95
+ if (attempted) return
96
+ attempted = true
97
+ tryWithImport()
98
+ })
85
99
  }
86
100
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shareai-lab/kode",
3
- "version": "1.0.85",
3
+ "version": "1.0.92",
4
4
  "bin": {
5
5
  "kode": "cli.js",
6
6
  "kwa": "cli.js",
@@ -1,56 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { execSync } = require('child_process');
4
- const fs = require('fs');
5
- const path = require('path');
3
+ // This postinstall is intentionally minimal and cross-platform safe.
4
+ // npm/pnpm/yarn already create shims from package.json "bin" fields.
5
+ // We avoid attempting to create symlinks or relying on platform-specific tools like `which`/`where`.
6
6
 
7
- const primaryCommand = 'kode';
8
- const alternativeCommands = ['kwa', 'kd'];
9
-
10
- function commandExists(cmd) {
7
+ function postinstallNotice() {
8
+ // Only print informational hints; never fail install.
11
9
  try {
12
- execSync(`which ${cmd}`, { stdio: 'ignore' });
13
- return true;
14
- } catch {
15
- return false;
16
- }
17
- }
18
-
19
- function setupCommand() {
20
- // Check if primary command exists
21
- if (!commandExists(primaryCommand)) {
22
- console.log(`✅ '${primaryCommand}' command is available and has been set up.`);
23
- return;
24
- }
25
-
26
- console.log(`⚠️ '${primaryCommand}' command already exists on your system.`);
27
-
28
- // Find an available alternative
29
- for (const alt of alternativeCommands) {
30
- if (!commandExists(alt)) {
31
- // Create alternative command
32
- const binPath = path.join(__dirname, '..', 'cli.js');
33
- const altBinPath = path.join(__dirname, '..', '..', '..', '.bin', alt);
34
-
35
- try {
36
- fs.symlinkSync(binPath, altBinPath);
37
- console.log(`✅ Created alternative command '${alt}' instead.`);
38
- console.log(` You can run the tool using: ${alt}`);
39
- return;
40
- } catch (err) {
41
- // Continue to next alternative
42
- }
43
- }
44
- }
45
-
46
- console.log(`
47
- ⚠️ All common command names are taken. You can still run the tool using:
48
- - npx @shareai-lab/kode
49
- - Or create your own alias: alias myai='npx @shareai-lab/kode'
50
- `);
10
+ console.log('✅ @shareai-lab/kode installed. Commands available: kode, kwa, kd');
11
+ console.log(' If shell cannot find them, try reloading your terminal or reinstall globally:');
12
+ console.log(' npm i -g @shareai-lab/kode (or use: npx @shareai-lab/kode)');
13
+ } catch {}
51
14
  }
52
15
 
53
- // Only run in postinstall, not in development
54
16
  if (process.env.npm_lifecycle_event === 'postinstall') {
55
- setupCommand();
56
- }
17
+ postinstallNotice();
18
+ }
@@ -462,7 +462,16 @@ async function openInEditor(filePath: string): Promise<void> {
462
462
  const projectDir = process.cwd()
463
463
  const homeDir = os.homedir()
464
464
 
465
- if (!resolvedPath.startsWith(projectDir) && !resolvedPath.startsWith(homeDir)) {
465
+ const isSub = (base: string, target: string) => {
466
+ const path = require('path')
467
+ const rel = path.relative(path.resolve(base), path.resolve(target))
468
+ if (!rel || rel === '') return true
469
+ if (rel.startsWith('..')) return false
470
+ if (path.isAbsolute(rel)) return false
471
+ return true
472
+ }
473
+
474
+ if (!isSub(projectDir, resolvedPath) && !isSub(homeDir, resolvedPath)) {
466
475
  throw new Error('Access denied: File path outside allowed directories')
467
476
  }
468
477
 
@@ -7,7 +7,7 @@ export function AsciiLogo(): React.ReactNode {
7
7
  const theme = getTheme()
8
8
  return (
9
9
  <Box flexDirection="column" alignItems="flex-start">
10
- <Text color={theme.claude}>{ASCII_LOGO}</Text>
10
+ <Text color={theme.kode}>{ASCII_LOGO}</Text>
11
11
  </Box>
12
12
  )
13
13
  }
@@ -45,13 +45,13 @@ export function SelectOption({
45
45
  paddingRight: 1,
46
46
  }),
47
47
  focusIndicator: () => ({
48
- color: appTheme.claude,
48
+ color: appTheme.kode,
49
49
  }),
50
50
  label: ({ isFocused, isSelected }: { isFocused: boolean; isSelected: boolean }) => ({
51
51
  color: isSelected
52
52
  ? appTheme.success
53
53
  : isFocused
54
- ? appTheme.claude
54
+ ? appTheme.kode
55
55
  : appTheme.text,
56
56
  bold: isSelected,
57
57
  }),
@@ -66,7 +66,7 @@ export function Help({
66
66
 
67
67
  return (
68
68
  <Box flexDirection="column" padding={1}>
69
- <Text bold color={theme.claude}>
69
+ <Text bold color={theme.kode}>
70
70
  {`${PRODUCT_NAME} v${MACRO.VERSION}`}
71
71
  </Text>
72
72
 
@@ -150,7 +150,7 @@ export function Help({
150
150
  <Box flexDirection="column">
151
151
  {customCommands.map((cmd, i) => (
152
152
  <Box key={i} marginLeft={1}>
153
- <Text bold color={theme.claude}>{`/${cmd.name}`}</Text>
153
+ <Text bold color={theme.kode}>{`/${cmd.name}`}</Text>
154
154
  <Text> - {cmd.description}</Text>
155
155
  {cmd.aliases && cmd.aliases.length > 0 && (
156
156
  <Text color={theme.secondaryText}>
@@ -13,9 +13,13 @@ export const MIN_LOGO_WIDTH = 50
13
13
  export function Logo({
14
14
  mcpClients,
15
15
  isDefaultModel = false,
16
+ updateBannerVersion,
17
+ updateBannerCommands,
16
18
  }: {
17
19
  mcpClients: WrappedClient[]
18
20
  isDefaultModel?: boolean
21
+ updateBannerVersion?: string | null
22
+ updateBannerCommands?: string[] | null
19
23
  }): React.ReactNode {
20
24
  const width = Math.max(MIN_LOGO_WIDTH, getCwd().length + 12)
21
25
  const theme = getTheme()
@@ -35,15 +39,36 @@ export function Logo({
35
39
  return (
36
40
  <Box flexDirection="column">
37
41
  <Box
38
- borderColor={theme.claude}
42
+ borderColor={theme.kode}
39
43
  borderStyle="round"
40
44
  flexDirection="column"
41
45
  gap={1}
42
46
  paddingLeft={1}
47
+ marginRight={2}
43
48
  width={width}
44
49
  >
50
+ {updateBannerVersion ? (
51
+ <Box flexDirection="column">
52
+ <Text color="yellow">New version available: {updateBannerVersion}</Text>
53
+ <Text>Run the following command to update:</Text>
54
+ <Text>
55
+ {' '}
56
+ {updateBannerCommands?.[0] ?? 'bun add -g @shareai-lab/kode@latest'}
57
+ </Text>
58
+ <Text>Or:</Text>
59
+ <Text>
60
+ {' '}
61
+ {updateBannerCommands?.[1] ?? 'npm install -g @shareai-lab/kode@latest'}
62
+ </Text>
63
+ {process.platform !== 'win32' && (
64
+ <Text dimColor>
65
+ Note: you may need to prefix with "sudo" on macOS/Linux.
66
+ </Text>
67
+ )}
68
+ </Box>
69
+ ) : null}
45
70
  <Text>
46
- <Text color={theme.claude}>✻</Text> Welcome to{' '}
71
+ <Text color={theme.kode}>✻</Text> Welcome to{' '}
47
72
  <Text bold>{PRODUCT_NAME}</Text> <Text>research preview!</Text>
48
73
  </Text>
49
74
  {/* <AsciiLogo /> */}
@@ -44,7 +44,7 @@ export function ModelStatusDisplay({ onClose }: Props): React.ReactNode {
44
44
  <Box key={pointer} flexDirection="column" marginBottom={1}>
45
45
  <Text>
46
46
  🎯{' '}
47
- <Text bold color={theme.claude}>
47
+ <Text bold color={theme.kode}>
48
48
  {pointer.toUpperCase()}
49
49
  </Text>{' '}
50
50
  → {model.name}
@@ -76,7 +76,7 @@ export function ModelStatusDisplay({ onClose }: Props): React.ReactNode {
76
76
  <Box key={pointer} flexDirection="column" marginBottom={1}>
77
77
  <Text>
78
78
  🎯{' '}
79
- <Text bold color={theme.claude}>
79
+ <Text bold color={theme.kode}>
80
80
  {pointer.toUpperCase()}
81
81
  </Text>{' '}
82
82
  → <Text color={theme.error}>❌ Not configured</Text>
@@ -89,7 +89,7 @@ export function ModelStatusDisplay({ onClose }: Props): React.ReactNode {
89
89
  <Box key={pointer} flexDirection="column" marginBottom={1}>
90
90
  <Text>
91
91
  🎯{' '}
92
- <Text bold color={theme.claude}>
92
+ <Text bold color={theme.kode}>
93
93
  {pointer.toUpperCase()}
94
94
  </Text>{' '}
95
95
  →{' '}
@@ -260,13 +260,13 @@ export function WelcomeBox(): React.ReactNode {
260
260
  const theme = getTheme()
261
261
  return (
262
262
  <Box
263
- borderColor={theme.claude}
263
+ borderColor={theme.kode}
264
264
  borderStyle="round"
265
265
  paddingX={1}
266
266
  width={MIN_LOGO_WIDTH}
267
267
  >
268
268
  <Text>
269
- <Text color={theme.claude}>✻</Text> Welcome to{' '}
269
+ <Text color={theme.kode}>✻</Text> Welcome to{' '}
270
270
  <Text bold>{PRODUCT_NAME}</Text> research preview!
271
271
  </Text>
272
272
  </Box>
@@ -596,7 +596,7 @@ function PromptInput({
596
596
  mode === 'bash'
597
597
  ? theme.bashBorder
598
598
  : mode === 'koding'
599
- ? theme.koding
599
+ ? theme.noting
600
600
  : theme.secondaryBorder
601
601
  }
602
602
  borderDimColor
@@ -614,7 +614,7 @@ function PromptInput({
614
614
  {mode === 'bash' ? (
615
615
  <Text color={theme.bashBorder}>&nbsp;!&nbsp;</Text>
616
616
  ) : mode === 'koding' ? (
617
- <Text color={theme.koding}>&nbsp;#&nbsp;</Text>
617
+ <Text color={theme.noting}>&nbsp;#&nbsp;</Text>
618
618
  ) : (
619
619
  <Text color={isLoading ? theme.secondaryText : undefined}>
620
620
  &nbsp;&gt;&nbsp;
@@ -668,7 +668,7 @@ function PromptInput({
668
668
  ! for bash mode
669
669
  </Text>
670
670
  <Text
671
- color={mode === 'koding' ? theme.koding : undefined}
671
+ color={mode === 'koding' ? theme.noting : undefined}
672
672
  dimColor={mode !== 'koding'}
673
673
  >
674
674
  · # for AGENTS.md
@@ -96,9 +96,9 @@ export function Spinner(): React.ReactNode {
96
96
  return (
97
97
  <Box flexDirection="row" marginTop={1}>
98
98
  <Box flexWrap="nowrap" height={1} width={2}>
99
- <Text color={getTheme().claude}>{frames[frame]}</Text>
99
+ <Text color={getTheme().kode}>{frames[frame]}</Text>
100
100
  </Box>
101
- <Text color={getTheme().claude}>{message.current}… </Text>
101
+ <Text color={getTheme().kode}>{message.current}… </Text>
102
102
  <Text color={getTheme().secondaryText}>
103
103
  ({elapsedTime}s · <Text bold>esc</Text> to interrupt)
104
104
  </Text>
@@ -123,7 +123,7 @@ export function SimpleSpinner(): React.ReactNode {
123
123
 
124
124
  return (
125
125
  <Box flexWrap="nowrap" height={1} width={2}>
126
- <Text color={getTheme().claude}>{frames[frame]}</Text>
126
+ <Text color={getTheme().kode}>{frames[frame]}</Text>
127
127
  </Box>
128
128
  )
129
129
  }
@@ -14,7 +14,7 @@ export function TaskProgressMessage({ agentType, status, toolCount }: Props) {
14
14
  return (
15
15
  <Box flexDirection="column" marginTop={1}>
16
16
  <Box flexDirection="row">
17
- <Text color={theme.claude}>⎯ </Text>
17
+ <Text color={theme.kode}>⎯ </Text>
18
18
  <Text color={theme.text} bold>
19
19
  [{agentType}]
20
20
  </Text>
@@ -29,4 +29,4 @@ export function TaskProgressMessage({ agentType, status, toolCount }: Props) {
29
29
  )}
30
30
  </Box>
31
31
  )
32
- }
32
+ }
@@ -20,7 +20,7 @@ export function UserKodingInputMessage({
20
20
  return (
21
21
  <Box flexDirection="column" marginTop={addMargin ? 1 : 0} width="100%">
22
22
  <Box>
23
- <Text color={getTheme().koding}>#</Text>
23
+ <Text color={getTheme().noting}>#</Text>
24
24
  <Text color={getTheme().secondaryText}> {input}</Text>
25
25
  </Box>
26
26
  </Box>
@@ -3,6 +3,23 @@ import { initSentry } from '../services/sentry'
3
3
  import { PRODUCT_COMMAND, PRODUCT_NAME } from '../constants/product'
4
4
  initSentry() // Initialize Sentry as early as possible
5
5
 
6
+ // Ensure YOGA_WASM_PATH is set for Ink across run modes (wrapper/dev)
7
+ // Resolve yoga.wasm relative to this file when missing using ESM-friendly APIs
8
+ try {
9
+ if (!process.env.YOGA_WASM_PATH) {
10
+ const { fileURLToPath } = await import('node:url')
11
+ const { dirname, join } = await import('node:path')
12
+ const { existsSync } = await import('node:fs')
13
+ const __filename = fileURLToPath(import.meta.url)
14
+ const __dirname = dirname(__filename)
15
+ const devCandidate = join(__dirname, '../../yoga.wasm')
16
+ // Prefer dev path; wrapper already sets env for normal runs
17
+ process.env.YOGA_WASM_PATH = existsSync(devCandidate)
18
+ ? devCandidate
19
+ : process.env.YOGA_WASM_PATH
20
+ }
21
+ } catch {}
22
+
6
23
  // XXX: Without this line (and the Object.keys, even though it seems like it does nothing!),
7
24
  // there is a bug in Bun only on Win32 that causes this import to be removed, even though
8
25
  // its use is solely because of its side-effects.
@@ -74,8 +91,11 @@ import {
74
91
  getLatestVersion,
75
92
  installGlobalPackage,
76
93
  assertMinVersion,
94
+ getUpdateCommandSuggestions,
77
95
  } from '../utils/autoUpdater'
96
+ import { gt } from 'semver'
78
97
  import { CACHE_PATHS } from '../utils/log'
98
+ // import { checkAndNotifyUpdate } from '../utils/autoUpdater'
79
99
  import { PersistentShell } from '../utils/PersistentShell'
80
100
  import { GATE_USE_EXTERNAL_UPDATER } from '../constants/betas'
81
101
  import { clearTerminal } from '../utils/terminal'
@@ -290,6 +310,8 @@ async function main() {
290
310
  }
291
311
  }
292
312
 
313
+ // Disabled background notifier to avoid mid-screen logs during REPL
314
+
293
315
  let inputPrompt = ''
294
316
  let renderContext: RenderOptions | undefined = {
295
317
  exitOnCtrlC: false,
@@ -417,6 +439,18 @@ ${commandList}`,
417
439
  } else {
418
440
  const isDefaultModel = await isDefaultSlowAndCapableModel()
419
441
 
442
+ // Prefetch update info before first render to place banner at top
443
+ const updateInfo = await (async () => {
444
+ try {
445
+ const latest = await getLatestVersion()
446
+ if (latest && gt(latest, MACRO.VERSION)) {
447
+ const cmds = await getUpdateCommandSuggestions()
448
+ return { version: latest as string, commands: cmds as string[] }
449
+ }
450
+ } catch {}
451
+ return { version: null as string | null, commands: null as string[] | null }
452
+ })()
453
+
420
454
  render(
421
455
  <REPL
422
456
  commands={commands}
@@ -429,6 +463,8 @@ ${commandList}`,
429
463
  safeMode={safe}
430
464
  mcpClients={mcpClients}
431
465
  isDefaultModel={isDefaultModel}
466
+ initialUpdateVersion={updateInfo.version}
467
+ initialUpdateCommands={updateInfo.commands}
432
468
  />,
433
469
  renderContext,
434
470
  )
@@ -1098,11 +1134,11 @@ ${commandList}`,
1098
1134
  <Box
1099
1135
  flexDirection="column"
1100
1136
  borderStyle="round"
1101
- borderColor={theme.claude}
1137
+ borderColor={theme.kode}
1102
1138
  padding={1}
1103
1139
  width={'100%'}
1104
1140
  >
1105
- <Text bold color={theme.claude}>
1141
+ <Text bold color={theme.kode}>
1106
1142
  Import MCP Servers from Claude Desktop
1107
1143
  </Text>
1108
1144
 
@@ -1219,16 +1255,8 @@ ${commandList}`,
1219
1255
  // claude update
1220
1256
  program
1221
1257
  .command('update')
1222
- .description('Check for updates and install if available')
1258
+ .description('Show manual upgrade commands (no auto-install)')
1223
1259
  .action(async () => {
1224
- const useExternalUpdater = await checkGate(GATE_USE_EXTERNAL_UPDATER)
1225
- if (useExternalUpdater) {
1226
- // The external updater intercepts calls to "claude update", which means if we have received
1227
- // this command at all, the extenral updater isn't installed on this machine.
1228
- console.log(`This version of ${PRODUCT_NAME} is no longer supported.`)
1229
- process.exit(0)
1230
- }
1231
-
1232
1260
  logEvent('tengu_update_check', {})
1233
1261
  console.log(`Current version: ${MACRO.VERSION}`)
1234
1262
  console.log('Checking for updates...')
@@ -1246,30 +1274,12 @@ ${commandList}`,
1246
1274
  }
1247
1275
 
1248
1276
  console.log(`New version available: ${latestVersion}`)
1249
- console.log('Installing update...')
1250
-
1251
- const status = await installGlobalPackage()
1252
-
1253
- switch (status) {
1254
- case 'success':
1255
- console.log(`Successfully updated to version ${latestVersion}`)
1256
- break
1257
- case 'no_permissions':
1258
- console.error('Error: Insufficient permissions to install update')
1259
- console.error('Try running with sudo or fix npm permissions')
1260
- process.exit(1)
1261
- break
1262
- case 'install_failed':
1263
- console.error('Error: Failed to install update')
1264
- process.exit(1)
1265
- break
1266
- case 'in_progress':
1267
- console.error(
1268
- 'Error: Another instance is currently performing an update',
1269
- )
1270
- console.error('Please wait and try again later')
1271
- process.exit(1)
1272
- break
1277
+ const { getUpdateCommandSuggestions } = await import('../utils/autoUpdater')
1278
+ const cmds = await getUpdateCommandSuggestions()
1279
+ console.log('\nRun one of the following commands to update:')
1280
+ for (const c of cmds) console.log(` ${c}`)
1281
+ if (process.platform !== 'win32') {
1282
+ console.log('\nNote: you may need to prefix with "sudo" on macOS/Linux.')
1273
1283
  }
1274
1284
  process.exit(0)
1275
1285
  })
@@ -1501,9 +1511,23 @@ process.on('exit', () => {
1501
1511
  PersistentShell.getInstance().close()
1502
1512
  })
1503
1513
 
1504
- process.on('SIGINT', () => {
1505
- console.log('SIGINT')
1506
- process.exit(0)
1514
+ function gracefulExit(code = 0) {
1515
+ try { resetCursor() } catch {}
1516
+ try { PersistentShell.getInstance().close() } catch {}
1517
+ process.exit(code)
1518
+ }
1519
+
1520
+ process.on('SIGINT', () => gracefulExit(0))
1521
+ process.on('SIGTERM', () => gracefulExit(0))
1522
+ // Windows CTRL+BREAK
1523
+ process.on('SIGBREAK', () => gracefulExit(0))
1524
+ process.on('unhandledRejection', err => {
1525
+ console.error('Unhandled rejection:', err)
1526
+ gracefulExit(1)
1527
+ })
1528
+ process.on('uncaughtException', err => {
1529
+ console.error('Uncaught exception:', err)
1530
+ gracefulExit(1)
1507
1531
  })
1508
1532
 
1509
1533
  function resetCursor() {
@@ -44,6 +44,7 @@ import type { WrappedClient } from '../services/mcpClient'
44
44
  import type { Tool } from '../Tool'
45
45
  import { AutoUpdaterResult } from '../utils/autoUpdater'
46
46
  import { getGlobalConfig, saveGlobalConfig } from '../utils/config'
47
+ import { MACRO } from '../constants/macros'
47
48
  import { logEvent } from '../services/statsig'
48
49
  import { getNextAvailableLogForkNumber } from '../utils/log'
49
50
  import {
@@ -87,6 +88,9 @@ type Props = {
87
88
  mcpClients?: WrappedClient[]
88
89
  // Flag to indicate if current model is default
89
90
  isDefaultModel?: boolean
91
+ // Update banner info passed from CLI before first render
92
+ initialUpdateVersion?: string | null
93
+ initialUpdateCommands?: string[] | null
90
94
  }
91
95
 
92
96
  export type BinaryFeedbackContext = {
@@ -108,6 +112,8 @@ export function REPL({
108
112
  initialMessages,
109
113
  mcpClients = [],
110
114
  isDefaultModel = true,
115
+ initialUpdateVersion,
116
+ initialUpdateCommands,
111
117
  }: Props): React.ReactNode {
112
118
  // TODO: probably shouldn't re-read config from file synchronously on every keystroke
113
119
  const verbose = verboseFromCLI ?? getGlobalConfig().verbose
@@ -149,6 +155,10 @@ export function REPL({
149
155
 
150
156
  const [binaryFeedbackContext, setBinaryFeedbackContext] =
151
157
  useState<BinaryFeedbackContext | null>(null)
158
+ // New version banner: passed in from CLI to guarantee top placement
159
+ const updateAvailableVersion = initialUpdateVersion ?? null
160
+ const updateCommands = initialUpdateCommands ?? null
161
+ // No separate Static for banner; it renders inside Logo
152
162
 
153
163
  const getBinaryFeedbackResponse = useCallback(
154
164
  (
@@ -209,6 +219,8 @@ export function REPL({
209
219
  }
210
220
  }, [messages, showCostDialog, haveShownCostDialog])
211
221
 
222
+ // Update banner is provided by CLI at startup; no async check here.
223
+
212
224
  const canUseTool = useCanUseTool(setToolUseConfirm)
213
225
 
214
226
  async function onInit() {
@@ -478,7 +490,12 @@ export function REPL({
478
490
  type: 'static',
479
491
  jsx: (
480
492
  <Box flexDirection="column" key={`logo${forkNumber}`}>
481
- <Logo mcpClients={mcpClients} isDefaultModel={isDefaultModel} />
493
+ <Logo
494
+ mcpClients={mcpClients}
495
+ isDefaultModel={isDefaultModel}
496
+ updateBannerVersion={updateAvailableVersion}
497
+ updateBannerCommands={updateCommands}
498
+ />
482
499
  <ProjectOnboarding workspaceDir={getOriginalCwd()} />
483
500
  </Box>
484
501
  ),
@@ -602,6 +619,7 @@ export function REPL({
602
619
 
603
620
  return (
604
621
  <PermissionProvider isBypassPermissionsModeAvailable={!safeMode}>
622
+ {/* Update banner now renders inside Logo for stable placement */}
605
623
  <ModeIndicator />
606
624
  <React.Fragment key={`static-messages-${forkNumber}`}>
607
625
  <Static
@@ -518,7 +518,7 @@ export async function getCompletionWithProfile(
518
518
  messageCount: opts.messages?.length || 0,
519
519
  streamMode: opts.stream,
520
520
  timestamp: new Date().toISOString(),
521
- modelProfileName: modelProfile?.modelName,
521
+ modelProfileModelName: modelProfile?.modelName,
522
522
  modelProfileName: modelProfile?.name,
523
523
  })
524
524
 
@@ -107,7 +107,8 @@ export function logEvent(
107
107
  }
108
108
 
109
109
  export const checkGate = memoize(async (gateName: string): Promise<boolean> => {
110
- return true
110
+ // Default to disabled gates when Statsig is not active
111
+ return false
111
112
  // if (env.isCI || process.env.NODE_ENV === 'test') {
112
113
  // return false
113
114
  // }
@@ -120,7 +121,7 @@ export const checkGate = memoize(async (gateName: string): Promise<boolean> => {
120
121
  })
121
122
 
122
123
  export const useStatsigGate = (gateName: string, defaultValue = false) => {
123
- return true
124
+ return false
124
125
  // const [gateValue, setGateValue] = React.useState(defaultValue)
125
126
  // React.useEffect(() => {
126
127
  // checkGate(gateName).then(setGateValue)
@@ -263,22 +263,16 @@ export async function startAgentWatcher(onChange?: () => void): Promise<void> {
263
263
  * Stop watching agent configuration directories
264
264
  */
265
265
  export async function stopAgentWatcher(): Promise<void> {
266
- const closePromises = watchers.map(watcher =>
267
- new Promise<void>((resolve) => {
266
+ // FSWatcher.close() is synchronous and does not accept a callback on Node 18/20
267
+ try {
268
+ for (const watcher of watchers) {
268
269
  try {
269
- watcher.close((err) => {
270
- if (err) {
271
- console.error('Failed to close file watcher:', err)
272
- }
273
- resolve()
274
- })
275
- } catch (error) {
276
- console.error('Error closing watcher:', error)
277
- resolve()
270
+ watcher.close()
271
+ } catch (err) {
272
+ console.error('Failed to close file watcher:', err)
278
273
  }
279
- })
280
- )
281
-
282
- await Promise.allSettled(closePromises)
283
- watchers = []
284
- }
274
+ }
275
+ } finally {
276
+ watchers = []
277
+ }
278
+ }
@@ -12,13 +12,16 @@ import {
12
12
  } from 'fs'
13
13
  import { platform } from 'process'
14
14
  import { execFileNoThrow } from './execFileNoThrow'
15
+ import { spawn } from 'child_process'
15
16
  import { logError } from './log'
16
17
  import { accessSync } from 'fs'
17
18
  import { CLAUDE_BASE_DIR } from './env'
18
19
  import { logEvent, getDynamicConfig } from '../services/statsig'
19
- import { lt } from 'semver'
20
+ import { lt, gt } from 'semver'
20
21
  import { MACRO } from '../constants/macros'
21
22
  import { PRODUCT_COMMAND, PRODUCT_NAME } from '../constants/product'
23
+ import { getGlobalConfig, saveGlobalConfig, isAutoUpdaterDisabled } from './config'
24
+ import { env } from './env'
22
25
  export type InstallStatus =
23
26
  | 'success'
24
27
  | 'no_permissions'
@@ -49,14 +52,12 @@ export async function assertMinVersion(): Promise<void> {
49
52
  versionConfig.minVersion &&
50
53
  lt(MACRO.VERSION, versionConfig.minVersion)
51
54
  ) {
55
+ const suggestions = await getUpdateCommandSuggestions()
56
+ const cmdLines = suggestions.map(c => ` ${c}`).join('\n')
52
57
  console.error(`
53
- It looks like your version of ${PRODUCT_NAME} (${MACRO.VERSION}) needs an update.
54
- A newer version (${versionConfig.minVersion} or higher) is required to continue.
55
-
56
- To update, please run:
57
- ${PRODUCT_COMMAND} update
58
-
59
- This will ensure you have access to the latest features and improvements.
58
+ 您的 ${PRODUCT_NAME} 版本 (${MACRO.VERSION}) 过低,需要升级到 ${versionConfig.minVersion} 或更高版本。
59
+ 请手动执行以下任一命令进行升级:
60
+ ${cmdLines}
60
61
  `)
61
62
  process.exit(1)
62
63
  }
@@ -267,21 +268,48 @@ export function getPermissionsCommand(npmPrefix: string): string {
267
268
  }
268
269
 
269
270
  export async function getLatestVersion(): Promise<string | null> {
270
- const abortController = new AbortController()
271
- setTimeout(() => abortController.abort(), 5000)
272
-
273
- const result = await execFileNoThrow(
274
- 'npm',
275
- ['view', MACRO.PACKAGE_URL, 'version'],
276
- abortController.signal,
277
- )
278
- if (result.code !== 0) {
271
+ // 1) Try npm CLI (fast when available)
272
+ try {
273
+ const abortController = new AbortController()
274
+ setTimeout(() => abortController.abort(), 5000)
275
+ const result = await execFileNoThrow(
276
+ 'npm',
277
+ ['view', MACRO.PACKAGE_URL, 'version'],
278
+ abortController.signal,
279
+ )
280
+ if (result.code === 0) {
281
+ const v = result.stdout.trim()
282
+ if (v) return v
283
+ }
284
+ } catch {}
285
+
286
+ // 2) Fallback: fetch npm registry (works in Bun/Node without npm)
287
+ try {
288
+ const controller = new AbortController()
289
+ const timer = setTimeout(() => controller.abort(), 5000)
290
+ const res = await fetch(
291
+ `https://registry.npmjs.org/${encodeURIComponent(MACRO.PACKAGE_URL)}`,
292
+ {
293
+ method: 'GET',
294
+ headers: {
295
+ Accept: 'application/vnd.npm.install-v1+json',
296
+ 'User-Agent': `${PRODUCT_NAME}/${MACRO.VERSION}`,
297
+ },
298
+ signal: controller.signal,
299
+ },
300
+ )
301
+ clearTimeout(timer)
302
+ if (!res.ok) return null
303
+ const json: any = await res.json().catch(() => null)
304
+ const latest = json && json['dist-tags'] && json['dist-tags'].latest
305
+ return typeof latest === 'string' ? latest : null
306
+ } catch {
279
307
  return null
280
308
  }
281
- return result.stdout.trim()
282
309
  }
283
310
 
284
311
  export async function installGlobalPackage(): Promise<InstallStatus> {
312
+ // Detect preferred package manager and install accordingly
285
313
  if (!acquireLock()) {
286
314
  logError('Another process is currently installing an update')
287
315
  // Log the lock contention to statsig
@@ -293,26 +321,138 @@ export async function installGlobalPackage(): Promise<InstallStatus> {
293
321
  }
294
322
 
295
323
  try {
296
- const { hasPermissions } = await checkNpmPermissions()
297
- if (!hasPermissions) {
298
- return 'no_permissions'
324
+ const manager = await detectPackageManager()
325
+ if (manager === 'npm') {
326
+ const { hasPermissions } = await checkNpmPermissions()
327
+ if (!hasPermissions) {
328
+ return 'no_permissions'
329
+ }
330
+ // Stream实时输出,减少用户等待感
331
+ const code = await runStreaming('npm', ['install', '-g', MACRO.PACKAGE_URL])
332
+ if (code !== 0) {
333
+ logError(`Failed to install new version via npm (exit ${code})`)
334
+ return 'install_failed'
335
+ }
336
+ return 'success'
299
337
  }
300
338
 
301
- const installResult = await execFileNoThrow('npm', [
302
- 'install',
303
- '-g',
304
- MACRO.PACKAGE_URL,
305
- ])
306
- if (installResult.code !== 0) {
307
- logError(
308
- `Failed to install new version of claude: ${installResult.stdout} ${installResult.stderr}`,
309
- )
310
- return 'install_failed'
339
+ if (manager === 'bun') {
340
+ const code = await runStreaming('bun', ['add', '-g', `${MACRO.PACKAGE_URL}@latest`])
341
+ if (code !== 0) {
342
+ logError(`Failed to install new version via bun (exit ${code})`)
343
+ return 'install_failed'
344
+ }
345
+ return 'success'
311
346
  }
312
347
 
348
+ // Fallback to npm if unknown
349
+ const { hasPermissions } = await checkNpmPermissions()
350
+ if (!hasPermissions) return 'no_permissions'
351
+ const code = await runStreaming('npm', ['install', '-g', MACRO.PACKAGE_URL])
352
+ if (code !== 0) return 'install_failed'
313
353
  return 'success'
314
354
  } finally {
315
355
  // Ensure we always release the lock
316
356
  releaseLock()
317
357
  }
318
358
  }
359
+
360
+ export type PackageManager = 'npm' | 'bun'
361
+
362
+ export async function detectPackageManager(): Promise<PackageManager> {
363
+ // Respect explicit override if provided later via config/env (future-proof)
364
+ try {
365
+ // Heuristic 1: npm available and global root resolvable
366
+ const npmRoot = await execFileNoThrow('npm', ['-g', 'root'])
367
+ if (npmRoot.code === 0 && npmRoot.stdout.trim()) {
368
+ return 'npm'
369
+ }
370
+ } catch {}
371
+
372
+ try {
373
+ // Heuristic 2: running on a system with bun and installed path hints bun
374
+ const bunVer = await execFileNoThrow('bun', ['--version'])
375
+ if (bunVer.code === 0) {
376
+ // BUN_INSTALL defaults to ~/.bun; if our package lives under that tree, prefer bun
377
+ // If npm not detected but bun is available, choose bun
378
+ return 'bun'
379
+ }
380
+ } catch {}
381
+
382
+ // Default to npm when uncertain
383
+ return 'npm'
384
+ }
385
+
386
+ function runStreaming(cmd: string, args: string[]): Promise<number> {
387
+ return new Promise(resolve => {
388
+ // 打印正在使用的包管理器与命令,增强透明度
389
+ try {
390
+ // eslint-disable-next-line no-console
391
+ console.log(`> ${cmd} ${args.join(' ')}`)
392
+ } catch {}
393
+
394
+ const child = spawn(cmd, args, {
395
+ stdio: 'inherit',
396
+ env: process.env,
397
+ })
398
+ child.on('close', code => resolve(code ?? 0))
399
+ child.on('error', () => resolve(1))
400
+ })
401
+ }
402
+
403
+ /**
404
+ * Generate human-friendly update commands for the detected package manager.
405
+ * Also includes an alternative manager command as fallback for users.
406
+ */
407
+ export async function getUpdateCommandSuggestions(): Promise<string[]> {
408
+ // Prefer Bun first, then npm (consistent, simple UX). Include @latest.
409
+ return [
410
+ `bun add -g ${MACRO.PACKAGE_URL}@latest`,
411
+ `npm install -g ${MACRO.PACKAGE_URL}@latest`,
412
+ ]
413
+ }
414
+
415
+ /**
416
+ * Non-blocking update notifier (daily)
417
+ * - Respects CI and disabled auto-updater
418
+ * - Uses env.hasInternetAccess() to quickly skip offline cases
419
+ * - Stores last check timestamp + last suggested version in global config
420
+ */
421
+ export async function checkAndNotifyUpdate(): Promise<void> {
422
+ try {
423
+ if (process.env.NODE_ENV === 'test') return
424
+ if (await isAutoUpdaterDisabled()) return
425
+ if (await env.getIsDocker()) return
426
+ if (!(await env.hasInternetAccess())) return
427
+
428
+ const config: any = getGlobalConfig()
429
+ const now = Date.now()
430
+ const DAY_MS = 24 * 60 * 60 * 1000
431
+ const lastCheck = Number(config.lastUpdateCheckAt || 0)
432
+ if (lastCheck && now - lastCheck < DAY_MS) return
433
+
434
+ const latest = await getLatestVersion()
435
+ if (!latest) {
436
+ // Still record the check to avoid spamming
437
+ saveGlobalConfig({ ...config, lastUpdateCheckAt: now })
438
+ return
439
+ }
440
+
441
+ if (gt(latest, MACRO.VERSION)) {
442
+ // Update stored state and print a low-noise hint
443
+ saveGlobalConfig({
444
+ ...config,
445
+ lastUpdateCheckAt: now,
446
+ lastSuggestedVersion: latest,
447
+ })
448
+ const suggestions = await getUpdateCommandSuggestions()
449
+ const first = suggestions[0]
450
+ console.log(`New version available: ${latest}. Recommended: ${first}`)
451
+ } else {
452
+ saveGlobalConfig({ ...config, lastUpdateCheckAt: now })
453
+ }
454
+ } catch (error) {
455
+ // Never block or throw; just log and move on
456
+ logError(`update-notify: ${error}`)
457
+ }
458
+ }
@@ -173,6 +173,8 @@ export type GlobalConfig = {
173
173
  modelProfiles?: ModelProfile[] // Model configuration list
174
174
  modelPointers?: ModelPointers // Model pointer system
175
175
  defaultModelName?: string // Default model
176
+ // Update notifications
177
+ lastDismissedUpdateVersion?: string
176
178
  }
177
179
 
178
180
  export const DEFAULT_GLOBAL_CONFIG: GlobalConfig = {
@@ -196,6 +198,7 @@ export const DEFAULT_GLOBAL_CONFIG: GlobalConfig = {
196
198
  reasoning: '',
197
199
  quick: '',
198
200
  },
201
+ lastDismissedUpdateVersion: undefined,
199
202
  }
200
203
 
201
204
  export const GLOBAL_CONFIG_KEYS = [
package/src/utils/file.ts CHANGED
@@ -98,12 +98,15 @@ export function isInDirectory(
98
98
  : normalizedCwd + sep
99
99
 
100
100
  // Join with a base directory to make them absolute-like for comparison
101
- // Using 'dummy' as base to avoid any actual file system dependencies
102
101
  const fullPath = resolvePath(cwd(), normalizedCwd, normalizedPath)
103
102
  const fullCwd = resolvePath(cwd(), normalizedCwd)
104
103
 
105
- // Check if the path starts with the cwd
106
- return fullPath.startsWith(fullCwd)
104
+ // Robust subpath check using path.relative (case-insensitive on Windows)
105
+ const rel = relative(fullCwd, fullPath)
106
+ if (!rel || rel === '') return true
107
+ if (rel.startsWith('..')) return false
108
+ if (isAbsolute(rel)) return false
109
+ return true
107
110
  }
108
111
 
109
112
  export function readTextContent(
@@ -1,4 +1,4 @@
1
- import { isAbsolute, resolve } from 'path'
1
+ import { isAbsolute, resolve, relative, sep } from 'path'
2
2
  import { getCwd, getOriginalCwd } from '../state'
3
3
 
4
4
  // In-memory storage for file permissions that resets each session
@@ -12,7 +12,26 @@ const writeFileAllowedDirectories: Set<string> = new Set()
12
12
  * @returns Absolute path
13
13
  */
14
14
  export function toAbsolutePath(path: string): string {
15
- return isAbsolute(path) ? resolve(path) : resolve(getCwd(), path)
15
+ const abs = isAbsolute(path) ? resolve(path) : resolve(getCwd(), path)
16
+ return normalizeForCompare(abs)
17
+ }
18
+
19
+ function normalizeForCompare(p: string): string {
20
+ // Normalize separators and resolve .. and . segments
21
+ const norm = resolve(p)
22
+ // On Windows, comparisons should be case-insensitive
23
+ return process.platform === 'win32' ? norm.toLowerCase() : norm
24
+ }
25
+
26
+ function isSubpath(base: string, target: string): boolean {
27
+ const rel = relative(base, target)
28
+ // If different drive letters on Windows, relative returns the target path
29
+ if (!rel || rel === '') return true
30
+ // Not a subpath if it goes up to parent
31
+ if (rel.startsWith('..')) return false
32
+ // Not a subpath if absolute
33
+ if (isAbsolute(rel)) return false
34
+ return true
16
35
  }
17
36
 
18
37
  /**
@@ -22,7 +41,8 @@ export function toAbsolutePath(path: string): string {
22
41
  */
23
42
  export function pathInOriginalCwd(path: string): boolean {
24
43
  const absolutePath = toAbsolutePath(path)
25
- return absolutePath.startsWith(toAbsolutePath(getOriginalCwd()))
44
+ const base = toAbsolutePath(getOriginalCwd())
45
+ return isSubpath(base, absolutePath)
26
46
  }
27
47
 
28
48
  /**
@@ -32,12 +52,8 @@ export function pathInOriginalCwd(path: string): boolean {
32
52
  */
33
53
  export function hasReadPermission(directory: string): boolean {
34
54
  const absolutePath = toAbsolutePath(directory)
35
-
36
55
  for (const allowedPath of readFileAllowedDirectories) {
37
- // Permission exists for this directory or a path prefix
38
- if (absolutePath.startsWith(allowedPath)) {
39
- return true
40
- }
56
+ if (isSubpath(allowedPath, absolutePath)) return true
41
57
  }
42
58
  return false
43
59
  }
@@ -49,12 +65,8 @@ export function hasReadPermission(directory: string): boolean {
49
65
  */
50
66
  export function hasWritePermission(directory: string): boolean {
51
67
  const absolutePath = toAbsolutePath(directory)
52
-
53
68
  for (const allowedPath of writeFileAllowedDirectories) {
54
- // Permission exists for this directory or a path prefix
55
- if (absolutePath.startsWith(allowedPath)) {
56
- return true
57
- }
69
+ if (isSubpath(allowedPath, absolutePath)) return true
58
70
  }
59
71
  return false
60
72
  }
@@ -65,10 +77,9 @@ export function hasWritePermission(directory: string): boolean {
65
77
  */
66
78
  function saveReadPermission(directory: string): void {
67
79
  const absolutePath = toAbsolutePath(directory)
68
-
69
- // Clean up any existing subdirectories of this path
70
- for (const allowedPath of readFileAllowedDirectories) {
71
- if (allowedPath.startsWith(absolutePath)) {
80
+ // Remove any existing subpaths contained by this new path
81
+ for (const allowedPath of Array.from(readFileAllowedDirectories)) {
82
+ if (isSubpath(absolutePath, allowedPath)) {
72
83
  readFileAllowedDirectories.delete(allowedPath)
73
84
  }
74
85
  }
@@ -92,10 +103,8 @@ export function grantReadPermissionForOriginalDir(): void {
92
103
  */
93
104
  function saveWritePermission(directory: string): void {
94
105
  const absolutePath = toAbsolutePath(directory)
95
-
96
- // Clean up any existing subdirectories of this path
97
- for (const allowedPath of writeFileAllowedDirectories) {
98
- if (allowedPath.startsWith(absolutePath)) {
106
+ for (const allowedPath of Array.from(writeFileAllowedDirectories)) {
107
+ if (isSubpath(absolutePath, allowedPath)) {
99
108
  writeFileAllowedDirectories.delete(allowedPath)
100
109
  }
101
110
  }
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync, unlinkSync, renameSync } from 'node:fs'
2
- import { join, dirname, normalize, resolve, extname } from 'node:path'
2
+ import { join, dirname, normalize, resolve, extname, relative, isAbsolute } from 'node:path'
3
3
  import { homedir } from 'node:os'
4
4
 
5
5
  /**
@@ -98,7 +98,12 @@ export class SecureFileService {
98
98
 
99
99
  // 检查是否在允许的基础路径中
100
100
  const isInAllowedPath = Array.from(this.allowedBasePaths).some(basePath => {
101
- return absolutePath.startsWith(basePath)
101
+ const base = resolve(basePath)
102
+ const rel = relative(base, absolutePath)
103
+ if (!rel || rel === '') return true
104
+ if (rel.startsWith('..')) return false
105
+ if (isAbsolute(rel)) return false
106
+ return true
102
107
  })
103
108
 
104
109
  if (!isInAllowedPath) {
@@ -556,4 +561,4 @@ export class SecureFileService {
556
561
  }
557
562
 
558
563
  // 导出单例实例
559
- export const secureFileService = SecureFileService.getInstance()
564
+ export const secureFileService = SecureFileService.getInstance()
@@ -2,18 +2,16 @@ import { getGlobalConfig } from './config'
2
2
 
3
3
  export interface Theme {
4
4
  bashBorder: string
5
- claude: string
6
- koding: string
5
+ kode: string
6
+ noting: string
7
7
  permission: string
8
8
  secondaryBorder: string
9
9
  text: string
10
10
  secondaryText: string
11
11
  suggestion: string
12
- // Semantic colors
13
12
  success: string
14
13
  error: string
15
14
  warning: string
16
- // UI colors
17
15
  primary: string
18
16
  secondary: string
19
17
  diff: {
@@ -25,9 +23,9 @@ export interface Theme {
25
23
  }
26
24
 
27
25
  const lightTheme: Theme = {
28
- bashBorder: '#ff0087',
29
- claude: '#7aff59ff',
30
- koding: '#9dff00ff',
26
+ bashBorder: '#FF6E57',
27
+ kode: '#FFC233',
28
+ noting: '#222222',
31
29
  permission: '#e9c61aff',
32
30
  secondaryBorder: '#999',
33
31
  text: '#000',
@@ -47,31 +45,31 @@ const lightTheme: Theme = {
47
45
  }
48
46
 
49
47
  const lightDaltonizedTheme: Theme = {
50
- bashBorder: '#0066cc', // Blue instead of pink for better contrast
51
- claude: '#5f97cd', // Orange adjusted for deuteranopia
52
- koding: '#0000ff',
53
- permission: '#3366ff', // Brighter blue for better visibility
48
+ bashBorder: '#FF6E57',
49
+ kode: '#FFC233',
50
+ noting: '#222222',
51
+ permission: '#3366ff',
54
52
  secondaryBorder: '#999',
55
53
  text: '#000',
56
54
  secondaryText: '#666',
57
55
  suggestion: '#3366ff',
58
- success: '#006699', // Blue instead of green
59
- error: '#cc0000', // Pure red for better distinction
60
- warning: '#ff9900', // Orange adjusted for deuteranopia
56
+ success: '#006699',
57
+ error: '#cc0000',
58
+ warning: '#ff9900',
61
59
  primary: '#000',
62
60
  secondary: '#666',
63
61
  diff: {
64
- added: '#99ccff', // Light blue instead of green
65
- removed: '#ffcccc', // Light red for better contrast
62
+ added: '#99ccff',
63
+ removed: '#ffcccc',
66
64
  addedDimmed: '#d1e7fd',
67
65
  removedDimmed: '#ffe9e9',
68
66
  },
69
67
  }
70
68
 
71
69
  const darkTheme: Theme = {
72
- bashBorder: '#fd5db1',
73
- claude: '#5f97cd',
74
- koding: '#0000ff',
70
+ bashBorder: '#FF6E57',
71
+ kode: '#FFC233',
72
+ noting: '#222222',
75
73
  permission: '#b1b9f9',
76
74
  secondaryBorder: '#888',
77
75
  text: '#fff',
@@ -91,32 +89,28 @@ const darkTheme: Theme = {
91
89
  }
92
90
 
93
91
  const darkDaltonizedTheme: Theme = {
94
- bashBorder: '#3399ff', // Bright blue instead of pink
95
- claude: '#5f97cd', // Orange adjusted for deuteranopia
96
- koding: '#0000ff',
97
- permission: '#99ccff', // Light blue for better contrast
92
+ bashBorder: '#FF6E57',
93
+ kode: '#FFC233',
94
+ noting: '#222222',
95
+ permission: '#99ccff',
98
96
  secondaryBorder: '#888',
99
97
  text: '#fff',
100
98
  secondaryText: '#999',
101
99
  suggestion: '#99ccff',
102
- success: '#3399ff', // Bright blue instead of green
103
- error: '#ff6666', // Bright red for better visibility
104
- warning: '#ffcc00', // Yellow-orange for deuteranopia
100
+ success: '#3399ff',
101
+ error: '#ff6666',
102
+ warning: '#ffcc00',
105
103
  primary: '#fff',
106
104
  secondary: '#999',
107
105
  diff: {
108
- added: '#004466', // Dark blue instead of green
109
- removed: '#660000', // Dark red for better contrast
106
+ added: '#004466',
107
+ removed: '#660000',
110
108
  addedDimmed: '#3e515b',
111
109
  removedDimmed: '#3e2c2c',
112
110
  },
113
111
  }
114
112
 
115
- export type ThemeNames =
116
- | 'dark'
117
- | 'light'
118
- | 'light-daltonized'
119
- | 'dark-daltonized'
113
+ export type ThemeNames = 'dark' | 'light' | 'light-daltonized' | 'dark-daltonized'
120
114
 
121
115
  export function getTheme(overrideTheme?: ThemeNames): Theme {
122
116
  const config = getGlobalConfig()