feat: upgrade to React 19 and Ink 6, fix Windows compatibility
- Upgrade React from 18.3.1 to 19.1.1 - Upgrade Ink from 5.2.1 to 6.2.3 - Fix Windows spawn EINVAL error by removing --tsconfig-raw - Fix top-level await issues in cli.tsx - Update build scripts for better Windows support - Version bump to 1.1.12
This commit is contained in:
parent
8ae7cb47ce
commit
487aef295d
10
package.json
10
package.json
@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@shareai-lab/kode",
|
||||
"version": "1.0.82",
|
||||
"version": "1.1.12",
|
||||
"bin": {
|
||||
"kode": "cli.js",
|
||||
"kwa": "cli.js",
|
||||
"kd": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
"node": ">=20.18.1"
|
||||
},
|
||||
"main": "cli.js",
|
||||
"author": "ShareAI-lab <ai-lab@foxmail.com>",
|
||||
@ -54,7 +54,7 @@
|
||||
"@modelcontextprotocol/sdk": "^1.15.1",
|
||||
"@statsig/js-client": "^3.18.2",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react": "^19.1.12",
|
||||
"ansi-escapes": "^7.0.0",
|
||||
"chalk": "^5.4.1",
|
||||
"cli-highlight": "^2.1.11",
|
||||
@ -68,7 +68,7 @@
|
||||
"glob": "^11.0.3",
|
||||
"gray-matter": "^4.0.3",
|
||||
"highlight.js": "^11.11.1",
|
||||
"ink": "^5.2.1",
|
||||
"ink": "^6.2.3",
|
||||
"ink-link": "^4.1.0",
|
||||
"ink-select-input": "^6.2.0",
|
||||
"ink-text-input": "^6.0.0",
|
||||
@ -79,7 +79,7 @@
|
||||
"node-fetch": "^3.3.2",
|
||||
"node-html-parser": "^7.0.1",
|
||||
"openai": "^4.104.0",
|
||||
"react": "18.3.1",
|
||||
"react": "^19.1.1",
|
||||
"semver": "^7.7.2",
|
||||
"shell-quote": "^1.8.3",
|
||||
"spawn-rx": "^5.1.2",
|
||||
|
||||
@ -12,8 +12,9 @@ async function build() {
|
||||
rmSync(file, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
// No dist artifacts; wrapper-only build
|
||||
|
||||
// Create the CLI wrapper
|
||||
// Create the CLI wrapper (exactly as in the referenced PR)
|
||||
const wrapper = `#!/usr/bin/env node
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
@ -48,34 +49,50 @@ try {
|
||||
}
|
||||
|
||||
function runWithNode() {
|
||||
// Use local tsx installation
|
||||
const tsxPath = path.join(__dirname, 'node_modules', '.bin', 'tsx');
|
||||
// Use local tsx installation; if missing, try PATH-resolved tsx
|
||||
const binDir = path.join(__dirname, 'node_modules', '.bin')
|
||||
const tsxPath = process.platform === 'win32'
|
||||
? path.join(binDir, 'tsx.cmd')
|
||||
: path.join(binDir, 'tsx')
|
||||
|
||||
const runPathTsx = () => {
|
||||
const child2 = spawn('tsx', [cliPath, ...args], {
|
||||
stdio: 'inherit',
|
||||
shell: process.platform === 'win32',
|
||||
env: {
|
||||
...process.env,
|
||||
YOGA_WASM_PATH: path.join(__dirname, 'yoga.wasm'),
|
||||
TSX_TSCONFIG_PATH: process.platform === 'win32' ? 'noop' : undefined
|
||||
},
|
||||
})
|
||||
child2.on('error', () => {
|
||||
console.error('\\nError: tsx is required but not found.')
|
||||
console.error('Please install tsx globally: npm install -g tsx')
|
||||
process.exit(1)
|
||||
})
|
||||
child2.on('exit', (code2) => process.exit(code2 || 0))
|
||||
}
|
||||
|
||||
const child = spawn(tsxPath, [cliPath, ...args], {
|
||||
stdio: 'inherit',
|
||||
env: {
|
||||
...process.env,
|
||||
YOGA_WASM_PATH: path.join(__dirname, 'yoga.wasm')
|
||||
}
|
||||
});
|
||||
shell: process.platform === 'win32',
|
||||
env: {
|
||||
...process.env,
|
||||
YOGA_WASM_PATH: path.join(__dirname, 'yoga.wasm'),
|
||||
TSX_TSCONFIG_PATH: process.platform === 'win32' ? 'noop' : undefined
|
||||
},
|
||||
})
|
||||
|
||||
child.on('error', (err) => {
|
||||
if (err.code === 'ENOENT') {
|
||||
console.error('\\nError: tsx is required but not found.');
|
||||
console.error('Please run: npm install');
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.error('Failed to start Kode:', err.message);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
child.on('exit', (code) => process.exit(code || 0));
|
||||
child.on('error', () => runPathTsx())
|
||||
child.on('exit', (code) => {
|
||||
if (code && code !== 0) return runPathTsx()
|
||||
process.exit(code || 0)
|
||||
})
|
||||
}
|
||||
`;
|
||||
|
||||
writeFileSync('cli.js', wrapper);
|
||||
chmodSync('cli.js', 0o755);
|
||||
|
||||
// Create .npmrc
|
||||
const npmrc = `# Ensure tsx is installed
|
||||
auto-install-peers=true
|
||||
@ -100,4 +117,4 @@ if (import.meta.main) {
|
||||
build();
|
||||
}
|
||||
|
||||
export { build };
|
||||
export { build };
|
||||
|
||||
@ -1,56 +1,18 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
// This postinstall is intentionally minimal and cross-platform safe.
|
||||
// npm/pnpm/yarn already create shims from package.json "bin" fields.
|
||||
// We avoid attempting to create symlinks or relying on platform-specific tools like `which`/`where`.
|
||||
|
||||
const primaryCommand = 'kode';
|
||||
const alternativeCommands = ['kwa', 'kd'];
|
||||
|
||||
function commandExists(cmd) {
|
||||
function postinstallNotice() {
|
||||
// Only print informational hints; never fail install.
|
||||
try {
|
||||
execSync(`which ${cmd}`, { stdio: 'ignore' });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
console.log('✅ @shareai-lab/kode installed. Commands available: kode, kwa, kd');
|
||||
console.log(' If shell cannot find them, try reloading your terminal or reinstall globally:');
|
||||
console.log(' npm i -g @shareai-lab/kode (or use: npx @shareai-lab/kode)');
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function setupCommand() {
|
||||
// Check if primary command exists
|
||||
if (!commandExists(primaryCommand)) {
|
||||
console.log(`✅ '${primaryCommand}' command is available and has been set up.`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`⚠️ '${primaryCommand}' command already exists on your system.`);
|
||||
|
||||
// Find an available alternative
|
||||
for (const alt of alternativeCommands) {
|
||||
if (!commandExists(alt)) {
|
||||
// Create alternative command
|
||||
const binPath = path.join(__dirname, '..', 'cli.js');
|
||||
const altBinPath = path.join(__dirname, '..', '..', '..', '.bin', alt);
|
||||
|
||||
try {
|
||||
fs.symlinkSync(binPath, altBinPath);
|
||||
console.log(`✅ Created alternative command '${alt}' instead.`);
|
||||
console.log(` You can run the tool using: ${alt}`);
|
||||
return;
|
||||
} catch (err) {
|
||||
// Continue to next alternative
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`
|
||||
⚠️ All common command names are taken. You can still run the tool using:
|
||||
- npx @shareai-lab/kode
|
||||
- Or create your own alias: alias myai='npx @shareai-lab/kode'
|
||||
`);
|
||||
}
|
||||
|
||||
// Only run in postinstall, not in development
|
||||
if (process.env.npm_lifecycle_event === 'postinstall') {
|
||||
setupCommand();
|
||||
}
|
||||
postinstallNotice();
|
||||
}
|
||||
|
||||
@ -39,4 +39,4 @@ console.log(` Version: ${pkg.version}`);
|
||||
console.log(` Main: ${pkg.main}`);
|
||||
console.log(` Bin: kode -> ${pkg.bin.kode}`);
|
||||
console.log('\n🚀 Ready to publish!');
|
||||
console.log(' Run: npm publish');
|
||||
console.log(' Run: npm publish');
|
||||
|
||||
@ -462,7 +462,16 @@ async function openInEditor(filePath: string): Promise<void> {
|
||||
const projectDir = process.cwd()
|
||||
const homeDir = os.homedir()
|
||||
|
||||
if (!resolvedPath.startsWith(projectDir) && !resolvedPath.startsWith(homeDir)) {
|
||||
const isSub = (base: string, target: string) => {
|
||||
const path = require('path')
|
||||
const rel = path.relative(path.resolve(base), path.resolve(target))
|
||||
if (!rel || rel === '') return true
|
||||
if (rel.startsWith('..')) return false
|
||||
if (path.isAbsolute(rel)) return false
|
||||
return true
|
||||
}
|
||||
|
||||
if (!isSub(projectDir, resolvedPath) && !isSub(homeDir, resolvedPath)) {
|
||||
throw new Error('Access denied: File path outside allowed directories')
|
||||
}
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ export function AsciiLogo(): React.ReactNode {
|
||||
const theme = getTheme()
|
||||
return (
|
||||
<Box flexDirection="column" alignItems="flex-start">
|
||||
<Text color={theme.claude}>{ASCII_LOGO}</Text>
|
||||
<Text color={theme.kode}>{ASCII_LOGO}</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@ -45,13 +45,13 @@ export function SelectOption({
|
||||
paddingRight: 1,
|
||||
}),
|
||||
focusIndicator: () => ({
|
||||
color: appTheme.claude,
|
||||
color: appTheme.kode,
|
||||
}),
|
||||
label: ({ isFocused, isSelected }: { isFocused: boolean; isSelected: boolean }) => ({
|
||||
color: isSelected
|
||||
? appTheme.success
|
||||
: isFocused
|
||||
? appTheme.claude
|
||||
? appTheme.kode
|
||||
: appTheme.text,
|
||||
bold: isSelected,
|
||||
}),
|
||||
|
||||
@ -66,7 +66,7 @@ export function Help({
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" padding={1}>
|
||||
<Text bold color={theme.claude}>
|
||||
<Text bold color={theme.kode}>
|
||||
{`${PRODUCT_NAME} v${MACRO.VERSION}`}
|
||||
</Text>
|
||||
|
||||
@ -150,7 +150,7 @@ export function Help({
|
||||
<Box flexDirection="column">
|
||||
{customCommands.map((cmd, i) => (
|
||||
<Box key={i} marginLeft={1}>
|
||||
<Text bold color={theme.claude}>{`/${cmd.name}`}</Text>
|
||||
<Text bold color={theme.kode}>{`/${cmd.name}`}</Text>
|
||||
<Text> - {cmd.description}</Text>
|
||||
{cmd.aliases && cmd.aliases.length > 0 && (
|
||||
<Text color={theme.secondaryText}>
|
||||
|
||||
@ -13,9 +13,13 @@ export const MIN_LOGO_WIDTH = 50
|
||||
export function Logo({
|
||||
mcpClients,
|
||||
isDefaultModel = false,
|
||||
updateBannerVersion,
|
||||
updateBannerCommands,
|
||||
}: {
|
||||
mcpClients: WrappedClient[]
|
||||
isDefaultModel?: boolean
|
||||
updateBannerVersion?: string | null
|
||||
updateBannerCommands?: string[] | null
|
||||
}): React.ReactNode {
|
||||
const width = Math.max(MIN_LOGO_WIDTH, getCwd().length + 12)
|
||||
const theme = getTheme()
|
||||
@ -35,15 +39,36 @@ export function Logo({
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Box
|
||||
borderColor={theme.claude}
|
||||
borderColor={theme.kode}
|
||||
borderStyle="round"
|
||||
flexDirection="column"
|
||||
gap={1}
|
||||
paddingLeft={1}
|
||||
marginRight={2}
|
||||
width={width}
|
||||
>
|
||||
{updateBannerVersion ? (
|
||||
<Box flexDirection="column">
|
||||
<Text color="yellow">New version available: {updateBannerVersion}</Text>
|
||||
<Text>Run the following command to update:</Text>
|
||||
<Text>
|
||||
{' '}
|
||||
{updateBannerCommands?.[0] ?? 'bun add -g @shareai-lab/kode@latest'}
|
||||
</Text>
|
||||
<Text>Or:</Text>
|
||||
<Text>
|
||||
{' '}
|
||||
{updateBannerCommands?.[1] ?? 'npm install -g @shareai-lab/kode@latest'}
|
||||
</Text>
|
||||
{process.platform !== 'win32' && (
|
||||
<Text dimColor>
|
||||
Note: you may need to prefix with "sudo" on macOS/Linux.
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
) : null}
|
||||
<Text>
|
||||
<Text color={theme.claude}>✻</Text> Welcome to{' '}
|
||||
<Text color={theme.kode}>✻</Text> Welcome to{' '}
|
||||
<Text bold>{PRODUCT_NAME}</Text> <Text>research preview!</Text>
|
||||
</Text>
|
||||
{/* <AsciiLogo /> */}
|
||||
|
||||
@ -44,7 +44,7 @@ export function ModelStatusDisplay({ onClose }: Props): React.ReactNode {
|
||||
<Box key={pointer} flexDirection="column" marginBottom={1}>
|
||||
<Text>
|
||||
🎯{' '}
|
||||
<Text bold color={theme.claude}>
|
||||
<Text bold color={theme.kode}>
|
||||
{pointer.toUpperCase()}
|
||||
</Text>{' '}
|
||||
→ {model.name}
|
||||
@ -76,7 +76,7 @@ export function ModelStatusDisplay({ onClose }: Props): React.ReactNode {
|
||||
<Box key={pointer} flexDirection="column" marginBottom={1}>
|
||||
<Text>
|
||||
🎯{' '}
|
||||
<Text bold color={theme.claude}>
|
||||
<Text bold color={theme.kode}>
|
||||
{pointer.toUpperCase()}
|
||||
</Text>{' '}
|
||||
→ <Text color={theme.error}>❌ Not configured</Text>
|
||||
@ -89,7 +89,7 @@ export function ModelStatusDisplay({ onClose }: Props): React.ReactNode {
|
||||
<Box key={pointer} flexDirection="column" marginBottom={1}>
|
||||
<Text>
|
||||
🎯{' '}
|
||||
<Text bold color={theme.claude}>
|
||||
<Text bold color={theme.kode}>
|
||||
{pointer.toUpperCase()}
|
||||
</Text>{' '}
|
||||
→{' '}
|
||||
|
||||
@ -260,13 +260,13 @@ export function WelcomeBox(): React.ReactNode {
|
||||
const theme = getTheme()
|
||||
return (
|
||||
<Box
|
||||
borderColor={theme.claude}
|
||||
borderColor={theme.kode}
|
||||
borderStyle="round"
|
||||
paddingX={1}
|
||||
width={MIN_LOGO_WIDTH}
|
||||
>
|
||||
<Text>
|
||||
<Text color={theme.claude}>✻</Text> Welcome to{' '}
|
||||
<Text color={theme.kode}>✻</Text> Welcome to{' '}
|
||||
<Text bold>{PRODUCT_NAME}</Text> research preview!
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
@ -596,7 +596,7 @@ function PromptInput({
|
||||
mode === 'bash'
|
||||
? theme.bashBorder
|
||||
: mode === 'koding'
|
||||
? theme.koding
|
||||
? theme.noting
|
||||
: theme.secondaryBorder
|
||||
}
|
||||
borderDimColor
|
||||
@ -614,7 +614,7 @@ function PromptInput({
|
||||
{mode === 'bash' ? (
|
||||
<Text color={theme.bashBorder}> ! </Text>
|
||||
) : mode === 'koding' ? (
|
||||
<Text color={theme.koding}> # </Text>
|
||||
<Text color={theme.noting}> # </Text>
|
||||
) : (
|
||||
<Text color={isLoading ? theme.secondaryText : undefined}>
|
||||
>
|
||||
@ -668,7 +668,7 @@ function PromptInput({
|
||||
! for bash mode
|
||||
</Text>
|
||||
<Text
|
||||
color={mode === 'koding' ? theme.koding : undefined}
|
||||
color={mode === 'koding' ? theme.noting : undefined}
|
||||
dimColor={mode !== 'koding'}
|
||||
>
|
||||
· # for AGENTS.md
|
||||
|
||||
@ -96,9 +96,9 @@ export function Spinner(): React.ReactNode {
|
||||
return (
|
||||
<Box flexDirection="row" marginTop={1}>
|
||||
<Box flexWrap="nowrap" height={1} width={2}>
|
||||
<Text color={getTheme().claude}>{frames[frame]}</Text>
|
||||
<Text color={getTheme().kode}>{frames[frame]}</Text>
|
||||
</Box>
|
||||
<Text color={getTheme().claude}>{message.current}… </Text>
|
||||
<Text color={getTheme().kode}>{message.current}… </Text>
|
||||
<Text color={getTheme().secondaryText}>
|
||||
({elapsedTime}s · <Text bold>esc</Text> to interrupt)
|
||||
</Text>
|
||||
@ -123,7 +123,7 @@ export function SimpleSpinner(): React.ReactNode {
|
||||
|
||||
return (
|
||||
<Box flexWrap="nowrap" height={1} width={2}>
|
||||
<Text color={getTheme().claude}>{frames[frame]}</Text>
|
||||
<Text color={getTheme().kode}>{frames[frame]}</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ export function TaskProgressMessage({ agentType, status, toolCount }: Props) {
|
||||
return (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Box flexDirection="row">
|
||||
<Text color={theme.claude}>⎯ </Text>
|
||||
<Text color={theme.kode}>⎯ </Text>
|
||||
<Text color={theme.text} bold>
|
||||
[{agentType}]
|
||||
</Text>
|
||||
@ -29,4 +29,4 @@ export function TaskProgressMessage({ agentType, status, toolCount }: Props) {
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@ export function UserKodingInputMessage({
|
||||
return (
|
||||
<Box flexDirection="column" marginTop={addMargin ? 1 : 0} width="100%">
|
||||
<Box>
|
||||
<Text color={getTheme().koding}>#</Text>
|
||||
<Text color={getTheme().noting}>#</Text>
|
||||
<Text color={getTheme().secondaryText}> {input}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@ -1,8 +1,25 @@
|
||||
#!/usr/bin/env -S node --no-warnings=ExperimentalWarning --enable-source-maps
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { initSentry } from '../services/sentry'
|
||||
import { PRODUCT_COMMAND, PRODUCT_NAME } from '../constants/product'
|
||||
initSentry() // Initialize Sentry as early as possible
|
||||
|
||||
// Ensure YOGA_WASM_PATH is set for Ink across run modes (wrapper/dev)
|
||||
// Resolve yoga.wasm relative to this file when missing using ESM-friendly APIs
|
||||
try {
|
||||
if (!process.env.YOGA_WASM_PATH) {
|
||||
const { existsSync: fsExistsSync } = require('fs')
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
const devCandidate = join(__dirname, '../../yoga.wasm')
|
||||
// Prefer dev path; wrapper already sets env for normal runs
|
||||
process.env.YOGA_WASM_PATH = fsExistsSync(devCandidate)
|
||||
? devCandidate
|
||||
: process.env.YOGA_WASM_PATH
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// XXX: Without this line (and the Object.keys, even though it seems like it does nothing!),
|
||||
// there is a bug in Bun only on Win32 that causes this import to be removed, even though
|
||||
// its use is solely because of its side-effects.
|
||||
@ -74,8 +91,11 @@ import {
|
||||
getLatestVersion,
|
||||
installGlobalPackage,
|
||||
assertMinVersion,
|
||||
getUpdateCommandSuggestions,
|
||||
} from '../utils/autoUpdater'
|
||||
import { gt } from 'semver'
|
||||
import { CACHE_PATHS } from '../utils/log'
|
||||
// import { checkAndNotifyUpdate } from '../utils/autoUpdater'
|
||||
import { PersistentShell } from '../utils/PersistentShell'
|
||||
import { GATE_USE_EXTERNAL_UPDATER } from '../constants/betas'
|
||||
import { clearTerminal } from '../utils/terminal'
|
||||
@ -290,6 +310,8 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Disabled background notifier to avoid mid-screen logs during REPL
|
||||
|
||||
let inputPrompt = ''
|
||||
let renderContext: RenderOptions | undefined = {
|
||||
exitOnCtrlC: false,
|
||||
@ -417,6 +439,18 @@ ${commandList}`,
|
||||
} else {
|
||||
const isDefaultModel = await isDefaultSlowAndCapableModel()
|
||||
|
||||
// Prefetch update info before first render to place banner at top
|
||||
const updateInfo = await (async () => {
|
||||
try {
|
||||
const latest = await getLatestVersion()
|
||||
if (latest && gt(latest, MACRO.VERSION)) {
|
||||
const cmds = await getUpdateCommandSuggestions()
|
||||
return { version: latest as string, commands: cmds as string[] }
|
||||
}
|
||||
} catch {}
|
||||
return { version: null as string | null, commands: null as string[] | null }
|
||||
})()
|
||||
|
||||
render(
|
||||
<REPL
|
||||
commands={commands}
|
||||
@ -429,6 +463,8 @@ ${commandList}`,
|
||||
safeMode={safe}
|
||||
mcpClients={mcpClients}
|
||||
isDefaultModel={isDefaultModel}
|
||||
initialUpdateVersion={updateInfo.version}
|
||||
initialUpdateCommands={updateInfo.commands}
|
||||
/>,
|
||||
renderContext,
|
||||
)
|
||||
@ -1098,11 +1134,11 @@ ${commandList}`,
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
borderColor={theme.claude}
|
||||
borderColor={theme.kode}
|
||||
padding={1}
|
||||
width={'100%'}
|
||||
>
|
||||
<Text bold color={theme.claude}>
|
||||
<Text bold color={theme.kode}>
|
||||
Import MCP Servers from Claude Desktop
|
||||
</Text>
|
||||
|
||||
@ -1219,16 +1255,8 @@ ${commandList}`,
|
||||
// claude update
|
||||
program
|
||||
.command('update')
|
||||
.description('Check for updates and install if available')
|
||||
.description('Show manual upgrade commands (no auto-install)')
|
||||
.action(async () => {
|
||||
const useExternalUpdater = await checkGate(GATE_USE_EXTERNAL_UPDATER)
|
||||
if (useExternalUpdater) {
|
||||
// The external updater intercepts calls to "claude update", which means if we have received
|
||||
// this command at all, the extenral updater isn't installed on this machine.
|
||||
console.log(`This version of ${PRODUCT_NAME} is no longer supported.`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
logEvent('tengu_update_check', {})
|
||||
console.log(`Current version: ${MACRO.VERSION}`)
|
||||
console.log('Checking for updates...')
|
||||
@ -1246,30 +1274,12 @@ ${commandList}`,
|
||||
}
|
||||
|
||||
console.log(`New version available: ${latestVersion}`)
|
||||
console.log('Installing update...')
|
||||
|
||||
const status = await installGlobalPackage()
|
||||
|
||||
switch (status) {
|
||||
case 'success':
|
||||
console.log(`Successfully updated to version ${latestVersion}`)
|
||||
break
|
||||
case 'no_permissions':
|
||||
console.error('Error: Insufficient permissions to install update')
|
||||
console.error('Try running with sudo or fix npm permissions')
|
||||
process.exit(1)
|
||||
break
|
||||
case 'install_failed':
|
||||
console.error('Error: Failed to install update')
|
||||
process.exit(1)
|
||||
break
|
||||
case 'in_progress':
|
||||
console.error(
|
||||
'Error: Another instance is currently performing an update',
|
||||
)
|
||||
console.error('Please wait and try again later')
|
||||
process.exit(1)
|
||||
break
|
||||
const { getUpdateCommandSuggestions } = await import('../utils/autoUpdater')
|
||||
const cmds = await getUpdateCommandSuggestions()
|
||||
console.log('\nRun one of the following commands to update:')
|
||||
for (const c of cmds) console.log(` ${c}`)
|
||||
if (process.platform !== 'win32') {
|
||||
console.log('\nNote: you may need to prefix with "sudo" on macOS/Linux.')
|
||||
}
|
||||
process.exit(0)
|
||||
})
|
||||
@ -1501,9 +1511,23 @@ process.on('exit', () => {
|
||||
PersistentShell.getInstance().close()
|
||||
})
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
console.log('SIGINT')
|
||||
process.exit(0)
|
||||
function gracefulExit(code = 0) {
|
||||
try { resetCursor() } catch {}
|
||||
try { PersistentShell.getInstance().close() } catch {}
|
||||
process.exit(code)
|
||||
}
|
||||
|
||||
process.on('SIGINT', () => gracefulExit(0))
|
||||
process.on('SIGTERM', () => gracefulExit(0))
|
||||
// Windows CTRL+BREAK
|
||||
process.on('SIGBREAK', () => gracefulExit(0))
|
||||
process.on('unhandledRejection', err => {
|
||||
console.error('Unhandled rejection:', err)
|
||||
gracefulExit(1)
|
||||
})
|
||||
process.on('uncaughtException', err => {
|
||||
console.error('Uncaught exception:', err)
|
||||
gracefulExit(1)
|
||||
})
|
||||
|
||||
function resetCursor() {
|
||||
|
||||
@ -44,6 +44,7 @@ import type { WrappedClient } from '../services/mcpClient'
|
||||
import type { Tool } from '../Tool'
|
||||
import { AutoUpdaterResult } from '../utils/autoUpdater'
|
||||
import { getGlobalConfig, saveGlobalConfig } from '../utils/config'
|
||||
import { MACRO } from '../constants/macros'
|
||||
import { logEvent } from '../services/statsig'
|
||||
import { getNextAvailableLogForkNumber } from '../utils/log'
|
||||
import {
|
||||
@ -87,6 +88,9 @@ type Props = {
|
||||
mcpClients?: WrappedClient[]
|
||||
// Flag to indicate if current model is default
|
||||
isDefaultModel?: boolean
|
||||
// Update banner info passed from CLI before first render
|
||||
initialUpdateVersion?: string | null
|
||||
initialUpdateCommands?: string[] | null
|
||||
}
|
||||
|
||||
export type BinaryFeedbackContext = {
|
||||
@ -108,6 +112,8 @@ export function REPL({
|
||||
initialMessages,
|
||||
mcpClients = [],
|
||||
isDefaultModel = true,
|
||||
initialUpdateVersion,
|
||||
initialUpdateCommands,
|
||||
}: Props): React.ReactNode {
|
||||
// TODO: probably shouldn't re-read config from file synchronously on every keystroke
|
||||
const verbose = verboseFromCLI ?? getGlobalConfig().verbose
|
||||
@ -149,6 +155,10 @@ export function REPL({
|
||||
|
||||
const [binaryFeedbackContext, setBinaryFeedbackContext] =
|
||||
useState<BinaryFeedbackContext | null>(null)
|
||||
// New version banner: passed in from CLI to guarantee top placement
|
||||
const updateAvailableVersion = initialUpdateVersion ?? null
|
||||
const updateCommands = initialUpdateCommands ?? null
|
||||
// No separate Static for banner; it renders inside Logo
|
||||
|
||||
const getBinaryFeedbackResponse = useCallback(
|
||||
(
|
||||
@ -209,6 +219,8 @@ export function REPL({
|
||||
}
|
||||
}, [messages, showCostDialog, haveShownCostDialog])
|
||||
|
||||
// Update banner is provided by CLI at startup; no async check here.
|
||||
|
||||
const canUseTool = useCanUseTool(setToolUseConfirm)
|
||||
|
||||
async function onInit() {
|
||||
@ -478,7 +490,12 @@ export function REPL({
|
||||
type: 'static',
|
||||
jsx: (
|
||||
<Box flexDirection="column" key={`logo${forkNumber}`}>
|
||||
<Logo mcpClients={mcpClients} isDefaultModel={isDefaultModel} />
|
||||
<Logo
|
||||
mcpClients={mcpClients}
|
||||
isDefaultModel={isDefaultModel}
|
||||
updateBannerVersion={updateAvailableVersion}
|
||||
updateBannerCommands={updateCommands}
|
||||
/>
|
||||
<ProjectOnboarding workspaceDir={getOriginalCwd()} />
|
||||
</Box>
|
||||
),
|
||||
@ -602,6 +619,7 @@ export function REPL({
|
||||
|
||||
return (
|
||||
<PermissionProvider isBypassPermissionsModeAvailable={!safeMode}>
|
||||
{/* Update banner now renders inside Logo for stable placement */}
|
||||
<ModeIndicator />
|
||||
<React.Fragment key={`static-messages-${forkNumber}`}>
|
||||
<Static
|
||||
|
||||
@ -518,7 +518,7 @@ export async function getCompletionWithProfile(
|
||||
messageCount: opts.messages?.length || 0,
|
||||
streamMode: opts.stream,
|
||||
timestamp: new Date().toISOString(),
|
||||
modelProfileName: modelProfile?.modelName,
|
||||
modelProfileModelName: modelProfile?.modelName,
|
||||
modelProfileName: modelProfile?.name,
|
||||
})
|
||||
|
||||
|
||||
@ -107,7 +107,8 @@ export function logEvent(
|
||||
}
|
||||
|
||||
export const checkGate = memoize(async (gateName: string): Promise<boolean> => {
|
||||
return true
|
||||
// Default to disabled gates when Statsig is not active
|
||||
return false
|
||||
// if (env.isCI || process.env.NODE_ENV === 'test') {
|
||||
// return false
|
||||
// }
|
||||
@ -120,7 +121,7 @@ export const checkGate = memoize(async (gateName: string): Promise<boolean> => {
|
||||
})
|
||||
|
||||
export const useStatsigGate = (gateName: string, defaultValue = false) => {
|
||||
return true
|
||||
return false
|
||||
// const [gateValue, setGateValue] = React.useState(defaultValue)
|
||||
// React.useEffect(() => {
|
||||
// checkGate(gateName).then(setGateValue)
|
||||
|
||||
@ -263,22 +263,16 @@ export async function startAgentWatcher(onChange?: () => void): Promise<void> {
|
||||
* Stop watching agent configuration directories
|
||||
*/
|
||||
export async function stopAgentWatcher(): Promise<void> {
|
||||
const closePromises = watchers.map(watcher =>
|
||||
new Promise<void>((resolve) => {
|
||||
// FSWatcher.close() is synchronous and does not accept a callback on Node 18/20
|
||||
try {
|
||||
for (const watcher of watchers) {
|
||||
try {
|
||||
watcher.close((err) => {
|
||||
if (err) {
|
||||
console.error('Failed to close file watcher:', err)
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error closing watcher:', error)
|
||||
resolve()
|
||||
watcher.close()
|
||||
} catch (err) {
|
||||
console.error('Failed to close file watcher:', err)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
await Promise.allSettled(closePromises)
|
||||
watchers = []
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
watchers = []
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,13 +12,16 @@ import {
|
||||
} from 'fs'
|
||||
import { platform } from 'process'
|
||||
import { execFileNoThrow } from './execFileNoThrow'
|
||||
import { spawn } from 'child_process'
|
||||
import { logError } from './log'
|
||||
import { accessSync } from 'fs'
|
||||
import { CLAUDE_BASE_DIR } from './env'
|
||||
import { logEvent, getDynamicConfig } from '../services/statsig'
|
||||
import { lt } from 'semver'
|
||||
import { lt, gt } from 'semver'
|
||||
import { MACRO } from '../constants/macros'
|
||||
import { PRODUCT_COMMAND, PRODUCT_NAME } from '../constants/product'
|
||||
import { getGlobalConfig, saveGlobalConfig, isAutoUpdaterDisabled } from './config'
|
||||
import { env } from './env'
|
||||
export type InstallStatus =
|
||||
| 'success'
|
||||
| 'no_permissions'
|
||||
@ -49,14 +52,12 @@ export async function assertMinVersion(): Promise<void> {
|
||||
versionConfig.minVersion &&
|
||||
lt(MACRO.VERSION, versionConfig.minVersion)
|
||||
) {
|
||||
const suggestions = await getUpdateCommandSuggestions()
|
||||
const cmdLines = suggestions.map(c => ` ${c}`).join('\n')
|
||||
console.error(`
|
||||
It looks like your version of ${PRODUCT_NAME} (${MACRO.VERSION}) needs an update.
|
||||
A newer version (${versionConfig.minVersion} or higher) is required to continue.
|
||||
|
||||
To update, please run:
|
||||
${PRODUCT_COMMAND} update
|
||||
|
||||
This will ensure you have access to the latest features and improvements.
|
||||
您的 ${PRODUCT_NAME} 版本 (${MACRO.VERSION}) 过低,需要升级到 ${versionConfig.minVersion} 或更高版本。
|
||||
请手动执行以下任一命令进行升级:
|
||||
${cmdLines}
|
||||
`)
|
||||
process.exit(1)
|
||||
}
|
||||
@ -267,21 +268,48 @@ export function getPermissionsCommand(npmPrefix: string): string {
|
||||
}
|
||||
|
||||
export async function getLatestVersion(): Promise<string | null> {
|
||||
const abortController = new AbortController()
|
||||
setTimeout(() => abortController.abort(), 5000)
|
||||
// 1) Try npm CLI (fast when available)
|
||||
try {
|
||||
const abortController = new AbortController()
|
||||
setTimeout(() => abortController.abort(), 5000)
|
||||
const result = await execFileNoThrow(
|
||||
'npm',
|
||||
['view', MACRO.PACKAGE_URL, 'version'],
|
||||
abortController.signal,
|
||||
)
|
||||
if (result.code === 0) {
|
||||
const v = result.stdout.trim()
|
||||
if (v) return v
|
||||
}
|
||||
} catch {}
|
||||
|
||||
const result = await execFileNoThrow(
|
||||
'npm',
|
||||
['view', MACRO.PACKAGE_URL, 'version'],
|
||||
abortController.signal,
|
||||
)
|
||||
if (result.code !== 0) {
|
||||
// 2) Fallback: fetch npm registry (works in Bun/Node without npm)
|
||||
try {
|
||||
const controller = new AbortController()
|
||||
const timer = setTimeout(() => controller.abort(), 5000)
|
||||
const res = await fetch(
|
||||
`https://registry.npmjs.org/${encodeURIComponent(MACRO.PACKAGE_URL)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/vnd.npm.install-v1+json',
|
||||
'User-Agent': `${PRODUCT_NAME}/${MACRO.VERSION}`,
|
||||
},
|
||||
signal: controller.signal,
|
||||
},
|
||||
)
|
||||
clearTimeout(timer)
|
||||
if (!res.ok) return null
|
||||
const json: any = await res.json().catch(() => null)
|
||||
const latest = json && json['dist-tags'] && json['dist-tags'].latest
|
||||
return typeof latest === 'string' ? latest : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
return result.stdout.trim()
|
||||
}
|
||||
|
||||
export async function installGlobalPackage(): Promise<InstallStatus> {
|
||||
// Detect preferred package manager and install accordingly
|
||||
if (!acquireLock()) {
|
||||
logError('Another process is currently installing an update')
|
||||
// Log the lock contention to statsig
|
||||
@ -293,26 +321,138 @@ export async function installGlobalPackage(): Promise<InstallStatus> {
|
||||
}
|
||||
|
||||
try {
|
||||
const manager = await detectPackageManager()
|
||||
if (manager === 'npm') {
|
||||
const { hasPermissions } = await checkNpmPermissions()
|
||||
if (!hasPermissions) {
|
||||
return 'no_permissions'
|
||||
}
|
||||
// Stream实时输出,减少用户等待感
|
||||
const code = await runStreaming('npm', ['install', '-g', MACRO.PACKAGE_URL])
|
||||
if (code !== 0) {
|
||||
logError(`Failed to install new version via npm (exit ${code})`)
|
||||
return 'install_failed'
|
||||
}
|
||||
return 'success'
|
||||
}
|
||||
|
||||
if (manager === 'bun') {
|
||||
const code = await runStreaming('bun', ['add', '-g', `${MACRO.PACKAGE_URL}@latest`])
|
||||
if (code !== 0) {
|
||||
logError(`Failed to install new version via bun (exit ${code})`)
|
||||
return 'install_failed'
|
||||
}
|
||||
return 'success'
|
||||
}
|
||||
|
||||
// Fallback to npm if unknown
|
||||
const { hasPermissions } = await checkNpmPermissions()
|
||||
if (!hasPermissions) {
|
||||
return 'no_permissions'
|
||||
}
|
||||
|
||||
const installResult = await execFileNoThrow('npm', [
|
||||
'install',
|
||||
'-g',
|
||||
MACRO.PACKAGE_URL,
|
||||
])
|
||||
if (installResult.code !== 0) {
|
||||
logError(
|
||||
`Failed to install new version of claude: ${installResult.stdout} ${installResult.stderr}`,
|
||||
)
|
||||
return 'install_failed'
|
||||
}
|
||||
|
||||
if (!hasPermissions) return 'no_permissions'
|
||||
const code = await runStreaming('npm', ['install', '-g', MACRO.PACKAGE_URL])
|
||||
if (code !== 0) return 'install_failed'
|
||||
return 'success'
|
||||
} finally {
|
||||
// Ensure we always release the lock
|
||||
releaseLock()
|
||||
}
|
||||
}
|
||||
|
||||
export type PackageManager = 'npm' | 'bun'
|
||||
|
||||
export async function detectPackageManager(): Promise<PackageManager> {
|
||||
// Respect explicit override if provided later via config/env (future-proof)
|
||||
try {
|
||||
// Heuristic 1: npm available and global root resolvable
|
||||
const npmRoot = await execFileNoThrow('npm', ['-g', 'root'])
|
||||
if (npmRoot.code === 0 && npmRoot.stdout.trim()) {
|
||||
return 'npm'
|
||||
}
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
// Heuristic 2: running on a system with bun and installed path hints bun
|
||||
const bunVer = await execFileNoThrow('bun', ['--version'])
|
||||
if (bunVer.code === 0) {
|
||||
// BUN_INSTALL defaults to ~/.bun; if our package lives under that tree, prefer bun
|
||||
// If npm not detected but bun is available, choose bun
|
||||
return 'bun'
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// Default to npm when uncertain
|
||||
return 'npm'
|
||||
}
|
||||
|
||||
function runStreaming(cmd: string, args: string[]): Promise<number> {
|
||||
return new Promise(resolve => {
|
||||
// 打印正在使用的包管理器与命令,增强透明度
|
||||
try {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`> ${cmd} ${args.join(' ')}`)
|
||||
} catch {}
|
||||
|
||||
const child = spawn(cmd, args, {
|
||||
stdio: 'inherit',
|
||||
env: process.env,
|
||||
})
|
||||
child.on('close', code => resolve(code ?? 0))
|
||||
child.on('error', () => resolve(1))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate human-friendly update commands for the detected package manager.
|
||||
* Also includes an alternative manager command as fallback for users.
|
||||
*/
|
||||
export async function getUpdateCommandSuggestions(): Promise<string[]> {
|
||||
// Prefer Bun first, then npm (consistent, simple UX). Include @latest.
|
||||
return [
|
||||
`bun add -g ${MACRO.PACKAGE_URL}@latest`,
|
||||
`npm install -g ${MACRO.PACKAGE_URL}@latest`,
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Non-blocking update notifier (daily)
|
||||
* - Respects CI and disabled auto-updater
|
||||
* - Uses env.hasInternetAccess() to quickly skip offline cases
|
||||
* - Stores last check timestamp + last suggested version in global config
|
||||
*/
|
||||
export async function checkAndNotifyUpdate(): Promise<void> {
|
||||
try {
|
||||
if (process.env.NODE_ENV === 'test') return
|
||||
if (await isAutoUpdaterDisabled()) return
|
||||
if (await env.getIsDocker()) return
|
||||
if (!(await env.hasInternetAccess())) return
|
||||
|
||||
const config: any = getGlobalConfig()
|
||||
const now = Date.now()
|
||||
const DAY_MS = 24 * 60 * 60 * 1000
|
||||
const lastCheck = Number(config.lastUpdateCheckAt || 0)
|
||||
if (lastCheck && now - lastCheck < DAY_MS) return
|
||||
|
||||
const latest = await getLatestVersion()
|
||||
if (!latest) {
|
||||
// Still record the check to avoid spamming
|
||||
saveGlobalConfig({ ...config, lastUpdateCheckAt: now })
|
||||
return
|
||||
}
|
||||
|
||||
if (gt(latest, MACRO.VERSION)) {
|
||||
// Update stored state and print a low-noise hint
|
||||
saveGlobalConfig({
|
||||
...config,
|
||||
lastUpdateCheckAt: now,
|
||||
lastSuggestedVersion: latest,
|
||||
})
|
||||
const suggestions = await getUpdateCommandSuggestions()
|
||||
const first = suggestions[0]
|
||||
console.log(`New version available: ${latest}. Recommended: ${first}`)
|
||||
} else {
|
||||
saveGlobalConfig({ ...config, lastUpdateCheckAt: now })
|
||||
}
|
||||
} catch (error) {
|
||||
// Never block or throw; just log and move on
|
||||
logError(`update-notify: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
@ -173,6 +173,8 @@ export type GlobalConfig = {
|
||||
modelProfiles?: ModelProfile[] // Model configuration list
|
||||
modelPointers?: ModelPointers // Model pointer system
|
||||
defaultModelName?: string // Default model
|
||||
// Update notifications
|
||||
lastDismissedUpdateVersion?: string
|
||||
}
|
||||
|
||||
export const DEFAULT_GLOBAL_CONFIG: GlobalConfig = {
|
||||
@ -196,6 +198,7 @@ export const DEFAULT_GLOBAL_CONFIG: GlobalConfig = {
|
||||
reasoning: '',
|
||||
quick: '',
|
||||
},
|
||||
lastDismissedUpdateVersion: undefined,
|
||||
}
|
||||
|
||||
export const GLOBAL_CONFIG_KEYS = [
|
||||
|
||||
@ -98,12 +98,15 @@ export function isInDirectory(
|
||||
: normalizedCwd + sep
|
||||
|
||||
// Join with a base directory to make them absolute-like for comparison
|
||||
// Using 'dummy' as base to avoid any actual file system dependencies
|
||||
const fullPath = resolvePath(cwd(), normalizedCwd, normalizedPath)
|
||||
const fullCwd = resolvePath(cwd(), normalizedCwd)
|
||||
|
||||
// Check if the path starts with the cwd
|
||||
return fullPath.startsWith(fullCwd)
|
||||
// Robust subpath check using path.relative (case-insensitive on Windows)
|
||||
const rel = relative(fullCwd, fullPath)
|
||||
if (!rel || rel === '') return true
|
||||
if (rel.startsWith('..')) return false
|
||||
if (isAbsolute(rel)) return false
|
||||
return true
|
||||
}
|
||||
|
||||
export function readTextContent(
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { isAbsolute, resolve } from 'path'
|
||||
import { isAbsolute, resolve, relative, sep } from 'path'
|
||||
import { getCwd, getOriginalCwd } from '../state'
|
||||
|
||||
// In-memory storage for file permissions that resets each session
|
||||
@ -12,7 +12,26 @@ const writeFileAllowedDirectories: Set<string> = new Set()
|
||||
* @returns Absolute path
|
||||
*/
|
||||
export function toAbsolutePath(path: string): string {
|
||||
return isAbsolute(path) ? resolve(path) : resolve(getCwd(), path)
|
||||
const abs = isAbsolute(path) ? resolve(path) : resolve(getCwd(), path)
|
||||
return normalizeForCompare(abs)
|
||||
}
|
||||
|
||||
function normalizeForCompare(p: string): string {
|
||||
// Normalize separators and resolve .. and . segments
|
||||
const norm = resolve(p)
|
||||
// On Windows, comparisons should be case-insensitive
|
||||
return process.platform === 'win32' ? norm.toLowerCase() : norm
|
||||
}
|
||||
|
||||
function isSubpath(base: string, target: string): boolean {
|
||||
const rel = relative(base, target)
|
||||
// If different drive letters on Windows, relative returns the target path
|
||||
if (!rel || rel === '') return true
|
||||
// Not a subpath if it goes up to parent
|
||||
if (rel.startsWith('..')) return false
|
||||
// Not a subpath if absolute
|
||||
if (isAbsolute(rel)) return false
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
@ -22,7 +41,8 @@ export function toAbsolutePath(path: string): string {
|
||||
*/
|
||||
export function pathInOriginalCwd(path: string): boolean {
|
||||
const absolutePath = toAbsolutePath(path)
|
||||
return absolutePath.startsWith(toAbsolutePath(getOriginalCwd()))
|
||||
const base = toAbsolutePath(getOriginalCwd())
|
||||
return isSubpath(base, absolutePath)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -32,12 +52,8 @@ export function pathInOriginalCwd(path: string): boolean {
|
||||
*/
|
||||
export function hasReadPermission(directory: string): boolean {
|
||||
const absolutePath = toAbsolutePath(directory)
|
||||
|
||||
for (const allowedPath of readFileAllowedDirectories) {
|
||||
// Permission exists for this directory or a path prefix
|
||||
if (absolutePath.startsWith(allowedPath)) {
|
||||
return true
|
||||
}
|
||||
if (isSubpath(allowedPath, absolutePath)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@ -49,12 +65,8 @@ export function hasReadPermission(directory: string): boolean {
|
||||
*/
|
||||
export function hasWritePermission(directory: string): boolean {
|
||||
const absolutePath = toAbsolutePath(directory)
|
||||
|
||||
for (const allowedPath of writeFileAllowedDirectories) {
|
||||
// Permission exists for this directory or a path prefix
|
||||
if (absolutePath.startsWith(allowedPath)) {
|
||||
return true
|
||||
}
|
||||
if (isSubpath(allowedPath, absolutePath)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@ -65,10 +77,9 @@ export function hasWritePermission(directory: string): boolean {
|
||||
*/
|
||||
function saveReadPermission(directory: string): void {
|
||||
const absolutePath = toAbsolutePath(directory)
|
||||
|
||||
// Clean up any existing subdirectories of this path
|
||||
for (const allowedPath of readFileAllowedDirectories) {
|
||||
if (allowedPath.startsWith(absolutePath)) {
|
||||
// Remove any existing subpaths contained by this new path
|
||||
for (const allowedPath of Array.from(readFileAllowedDirectories)) {
|
||||
if (isSubpath(absolutePath, allowedPath)) {
|
||||
readFileAllowedDirectories.delete(allowedPath)
|
||||
}
|
||||
}
|
||||
@ -92,10 +103,8 @@ export function grantReadPermissionForOriginalDir(): void {
|
||||
*/
|
||||
function saveWritePermission(directory: string): void {
|
||||
const absolutePath = toAbsolutePath(directory)
|
||||
|
||||
// Clean up any existing subdirectories of this path
|
||||
for (const allowedPath of writeFileAllowedDirectories) {
|
||||
if (allowedPath.startsWith(absolutePath)) {
|
||||
for (const allowedPath of Array.from(writeFileAllowedDirectories)) {
|
||||
if (isSubpath(absolutePath, allowedPath)) {
|
||||
writeFileAllowedDirectories.delete(allowedPath)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync, unlinkSync, renameSync } from 'node:fs'
|
||||
import { join, dirname, normalize, resolve, extname } from 'node:path'
|
||||
import { join, dirname, normalize, resolve, extname, relative, isAbsolute } from 'node:path'
|
||||
import { homedir } from 'node:os'
|
||||
|
||||
/**
|
||||
@ -98,7 +98,12 @@ export class SecureFileService {
|
||||
|
||||
// 检查是否在允许的基础路径中
|
||||
const isInAllowedPath = Array.from(this.allowedBasePaths).some(basePath => {
|
||||
return absolutePath.startsWith(basePath)
|
||||
const base = resolve(basePath)
|
||||
const rel = relative(base, absolutePath)
|
||||
if (!rel || rel === '') return true
|
||||
if (rel.startsWith('..')) return false
|
||||
if (isAbsolute(rel)) return false
|
||||
return true
|
||||
})
|
||||
|
||||
if (!isInAllowedPath) {
|
||||
@ -556,4 +561,4 @@ export class SecureFileService {
|
||||
}
|
||||
|
||||
// 导出单例实例
|
||||
export const secureFileService = SecureFileService.getInstance()
|
||||
export const secureFileService = SecureFileService.getInstance()
|
||||
|
||||
@ -2,18 +2,16 @@ import { getGlobalConfig } from './config'
|
||||
|
||||
export interface Theme {
|
||||
bashBorder: string
|
||||
claude: string
|
||||
koding: string
|
||||
kode: string
|
||||
noting: string
|
||||
permission: string
|
||||
secondaryBorder: string
|
||||
text: string
|
||||
secondaryText: string
|
||||
suggestion: string
|
||||
// Semantic colors
|
||||
success: string
|
||||
error: string
|
||||
warning: string
|
||||
// UI colors
|
||||
primary: string
|
||||
secondary: string
|
||||
diff: {
|
||||
@ -25,9 +23,9 @@ export interface Theme {
|
||||
}
|
||||
|
||||
const lightTheme: Theme = {
|
||||
bashBorder: '#ff0087',
|
||||
claude: '#7aff59ff',
|
||||
koding: '#9dff00ff',
|
||||
bashBorder: '#FF6E57',
|
||||
kode: '#FFC233',
|
||||
noting: '#222222',
|
||||
permission: '#e9c61aff',
|
||||
secondaryBorder: '#999',
|
||||
text: '#000',
|
||||
@ -47,31 +45,31 @@ const lightTheme: Theme = {
|
||||
}
|
||||
|
||||
const lightDaltonizedTheme: Theme = {
|
||||
bashBorder: '#0066cc', // Blue instead of pink for better contrast
|
||||
claude: '#5f97cd', // Orange adjusted for deuteranopia
|
||||
koding: '#0000ff',
|
||||
permission: '#3366ff', // Brighter blue for better visibility
|
||||
bashBorder: '#FF6E57',
|
||||
kode: '#FFC233',
|
||||
noting: '#222222',
|
||||
permission: '#3366ff',
|
||||
secondaryBorder: '#999',
|
||||
text: '#000',
|
||||
secondaryText: '#666',
|
||||
suggestion: '#3366ff',
|
||||
success: '#006699', // Blue instead of green
|
||||
error: '#cc0000', // Pure red for better distinction
|
||||
warning: '#ff9900', // Orange adjusted for deuteranopia
|
||||
success: '#006699',
|
||||
error: '#cc0000',
|
||||
warning: '#ff9900',
|
||||
primary: '#000',
|
||||
secondary: '#666',
|
||||
diff: {
|
||||
added: '#99ccff', // Light blue instead of green
|
||||
removed: '#ffcccc', // Light red for better contrast
|
||||
added: '#99ccff',
|
||||
removed: '#ffcccc',
|
||||
addedDimmed: '#d1e7fd',
|
||||
removedDimmed: '#ffe9e9',
|
||||
},
|
||||
}
|
||||
|
||||
const darkTheme: Theme = {
|
||||
bashBorder: '#fd5db1',
|
||||
claude: '#5f97cd',
|
||||
koding: '#0000ff',
|
||||
bashBorder: '#FF6E57',
|
||||
kode: '#FFC233',
|
||||
noting: '#222222',
|
||||
permission: '#b1b9f9',
|
||||
secondaryBorder: '#888',
|
||||
text: '#fff',
|
||||
@ -91,32 +89,28 @@ const darkTheme: Theme = {
|
||||
}
|
||||
|
||||
const darkDaltonizedTheme: Theme = {
|
||||
bashBorder: '#3399ff', // Bright blue instead of pink
|
||||
claude: '#5f97cd', // Orange adjusted for deuteranopia
|
||||
koding: '#0000ff',
|
||||
permission: '#99ccff', // Light blue for better contrast
|
||||
bashBorder: '#FF6E57',
|
||||
kode: '#FFC233',
|
||||
noting: '#222222',
|
||||
permission: '#99ccff',
|
||||
secondaryBorder: '#888',
|
||||
text: '#fff',
|
||||
secondaryText: '#999',
|
||||
suggestion: '#99ccff',
|
||||
success: '#3399ff', // Bright blue instead of green
|
||||
error: '#ff6666', // Bright red for better visibility
|
||||
warning: '#ffcc00', // Yellow-orange for deuteranopia
|
||||
success: '#3399ff',
|
||||
error: '#ff6666',
|
||||
warning: '#ffcc00',
|
||||
primary: '#fff',
|
||||
secondary: '#999',
|
||||
diff: {
|
||||
added: '#004466', // Dark blue instead of green
|
||||
removed: '#660000', // Dark red for better contrast
|
||||
added: '#004466',
|
||||
removed: '#660000',
|
||||
addedDimmed: '#3e515b',
|
||||
removedDimmed: '#3e2c2c',
|
||||
},
|
||||
}
|
||||
|
||||
export type ThemeNames =
|
||||
| 'dark'
|
||||
| 'light'
|
||||
| 'light-daltonized'
|
||||
| 'dark-daltonized'
|
||||
export type ThemeNames = 'dark' | 'light' | 'light-daltonized' | 'dark-daltonized'
|
||||
|
||||
export function getTheme(overrideTheme?: ThemeNames): Theme {
|
||||
const config = getGlobalConfig()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user