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",
|
"name": "@shareai-lab/kode",
|
||||||
"version": "1.0.82",
|
"version": "1.1.12",
|
||||||
"bin": {
|
"bin": {
|
||||||
"kode": "cli.js",
|
"kode": "cli.js",
|
||||||
"kwa": "cli.js",
|
"kwa": "cli.js",
|
||||||
"kd": "cli.js"
|
"kd": "cli.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=20.18.1"
|
||||||
},
|
},
|
||||||
"main": "cli.js",
|
"main": "cli.js",
|
||||||
"author": "ShareAI-lab <ai-lab@foxmail.com>",
|
"author": "ShareAI-lab <ai-lab@foxmail.com>",
|
||||||
@ -54,7 +54,7 @@
|
|||||||
"@modelcontextprotocol/sdk": "^1.15.1",
|
"@modelcontextprotocol/sdk": "^1.15.1",
|
||||||
"@statsig/js-client": "^3.18.2",
|
"@statsig/js-client": "^3.18.2",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/react": "^19.1.8",
|
"@types/react": "^19.1.12",
|
||||||
"ansi-escapes": "^7.0.0",
|
"ansi-escapes": "^7.0.0",
|
||||||
"chalk": "^5.4.1",
|
"chalk": "^5.4.1",
|
||||||
"cli-highlight": "^2.1.11",
|
"cli-highlight": "^2.1.11",
|
||||||
@ -68,7 +68,7 @@
|
|||||||
"glob": "^11.0.3",
|
"glob": "^11.0.3",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"ink": "^5.2.1",
|
"ink": "^6.2.3",
|
||||||
"ink-link": "^4.1.0",
|
"ink-link": "^4.1.0",
|
||||||
"ink-select-input": "^6.2.0",
|
"ink-select-input": "^6.2.0",
|
||||||
"ink-text-input": "^6.0.0",
|
"ink-text-input": "^6.0.0",
|
||||||
@ -79,7 +79,7 @@
|
|||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"node-html-parser": "^7.0.1",
|
"node-html-parser": "^7.0.1",
|
||||||
"openai": "^4.104.0",
|
"openai": "^4.104.0",
|
||||||
"react": "18.3.1",
|
"react": "^19.1.1",
|
||||||
"semver": "^7.7.2",
|
"semver": "^7.7.2",
|
||||||
"shell-quote": "^1.8.3",
|
"shell-quote": "^1.8.3",
|
||||||
"spawn-rx": "^5.1.2",
|
"spawn-rx": "^5.1.2",
|
||||||
|
|||||||
@ -12,8 +12,9 @@ async function build() {
|
|||||||
rmSync(file, { recursive: true, force: true });
|
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 wrapper = `#!/usr/bin/env node
|
||||||
|
|
||||||
const { spawn } = require('child_process');
|
const { spawn } = require('child_process');
|
||||||
@ -48,34 +49,50 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function runWithNode() {
|
function runWithNode() {
|
||||||
// Use local tsx installation
|
// Use local tsx installation; if missing, try PATH-resolved tsx
|
||||||
const tsxPath = path.join(__dirname, 'node_modules', '.bin', 'tsx');
|
const binDir = path.join(__dirname, 'node_modules', '.bin')
|
||||||
const child = spawn(tsxPath, [cliPath, ...args], {
|
const tsxPath = process.platform === 'win32'
|
||||||
|
? path.join(binDir, 'tsx.cmd')
|
||||||
|
: path.join(binDir, 'tsx')
|
||||||
|
|
||||||
|
const runPathTsx = () => {
|
||||||
|
const child2 = spawn('tsx', [cliPath, ...args], {
|
||||||
stdio: 'inherit',
|
stdio: 'inherit',
|
||||||
|
shell: process.platform === 'win32',
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
YOGA_WASM_PATH: path.join(__dirname, 'yoga.wasm')
|
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))
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
child.on('error', (err) => {
|
const child = spawn(tsxPath, [cliPath, ...args], {
|
||||||
if (err.code === 'ENOENT') {
|
stdio: 'inherit',
|
||||||
console.error('\\nError: tsx is required but not found.');
|
shell: process.platform === 'win32',
|
||||||
console.error('Please run: npm install');
|
env: {
|
||||||
process.exit(1);
|
...process.env,
|
||||||
} else {
|
YOGA_WASM_PATH: path.join(__dirname, 'yoga.wasm'),
|
||||||
console.error('Failed to start Kode:', err.message);
|
TSX_TSCONFIG_PATH: process.platform === 'win32' ? 'noop' : undefined
|
||||||
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);
|
writeFileSync('cli.js', wrapper);
|
||||||
chmodSync('cli.js', 0o755);
|
chmodSync('cli.js', 0o755);
|
||||||
|
|
||||||
// Create .npmrc
|
// Create .npmrc
|
||||||
const npmrc = `# Ensure tsx is installed
|
const npmrc = `# Ensure tsx is installed
|
||||||
auto-install-peers=true
|
auto-install-peers=true
|
||||||
|
|||||||
@ -1,56 +1,18 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
const { execSync } = require('child_process');
|
// This postinstall is intentionally minimal and cross-platform safe.
|
||||||
const fs = require('fs');
|
// npm/pnpm/yarn already create shims from package.json "bin" fields.
|
||||||
const path = require('path');
|
// We avoid attempting to create symlinks or relying on platform-specific tools like `which`/`where`.
|
||||||
|
|
||||||
const primaryCommand = 'kode';
|
function postinstallNotice() {
|
||||||
const alternativeCommands = ['kwa', 'kd'];
|
// Only print informational hints; never fail install.
|
||||||
|
|
||||||
function commandExists(cmd) {
|
|
||||||
try {
|
try {
|
||||||
execSync(`which ${cmd}`, { stdio: 'ignore' });
|
console.log('✅ @shareai-lab/kode installed. Commands available: kode, kwa, kd');
|
||||||
return true;
|
console.log(' If shell cannot find them, try reloading your terminal or reinstall globally:');
|
||||||
} catch {
|
console.log(' npm i -g @shareai-lab/kode (or use: npx @shareai-lab/kode)');
|
||||||
return false;
|
} 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') {
|
if (process.env.npm_lifecycle_event === 'postinstall') {
|
||||||
setupCommand();
|
postinstallNotice();
|
||||||
}
|
}
|
||||||
@ -462,7 +462,16 @@ async function openInEditor(filePath: string): Promise<void> {
|
|||||||
const projectDir = process.cwd()
|
const projectDir = process.cwd()
|
||||||
const homeDir = os.homedir()
|
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')
|
throw new Error('Access denied: File path outside allowed directories')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ export function AsciiLogo(): React.ReactNode {
|
|||||||
const theme = getTheme()
|
const theme = getTheme()
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" alignItems="flex-start">
|
<Box flexDirection="column" alignItems="flex-start">
|
||||||
<Text color={theme.claude}>{ASCII_LOGO}</Text>
|
<Text color={theme.kode}>{ASCII_LOGO}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,13 +45,13 @@ export function SelectOption({
|
|||||||
paddingRight: 1,
|
paddingRight: 1,
|
||||||
}),
|
}),
|
||||||
focusIndicator: () => ({
|
focusIndicator: () => ({
|
||||||
color: appTheme.claude,
|
color: appTheme.kode,
|
||||||
}),
|
}),
|
||||||
label: ({ isFocused, isSelected }: { isFocused: boolean; isSelected: boolean }) => ({
|
label: ({ isFocused, isSelected }: { isFocused: boolean; isSelected: boolean }) => ({
|
||||||
color: isSelected
|
color: isSelected
|
||||||
? appTheme.success
|
? appTheme.success
|
||||||
: isFocused
|
: isFocused
|
||||||
? appTheme.claude
|
? appTheme.kode
|
||||||
: appTheme.text,
|
: appTheme.text,
|
||||||
bold: isSelected,
|
bold: isSelected,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -66,7 +66,7 @@ export function Help({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" padding={1}>
|
<Box flexDirection="column" padding={1}>
|
||||||
<Text bold color={theme.claude}>
|
<Text bold color={theme.kode}>
|
||||||
{`${PRODUCT_NAME} v${MACRO.VERSION}`}
|
{`${PRODUCT_NAME} v${MACRO.VERSION}`}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
@ -150,7 +150,7 @@ export function Help({
|
|||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
{customCommands.map((cmd, i) => (
|
{customCommands.map((cmd, i) => (
|
||||||
<Box key={i} marginLeft={1}>
|
<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>
|
<Text> - {cmd.description}</Text>
|
||||||
{cmd.aliases && cmd.aliases.length > 0 && (
|
{cmd.aliases && cmd.aliases.length > 0 && (
|
||||||
<Text color={theme.secondaryText}>
|
<Text color={theme.secondaryText}>
|
||||||
|
|||||||
@ -13,9 +13,13 @@ export const MIN_LOGO_WIDTH = 50
|
|||||||
export function Logo({
|
export function Logo({
|
||||||
mcpClients,
|
mcpClients,
|
||||||
isDefaultModel = false,
|
isDefaultModel = false,
|
||||||
|
updateBannerVersion,
|
||||||
|
updateBannerCommands,
|
||||||
}: {
|
}: {
|
||||||
mcpClients: WrappedClient[]
|
mcpClients: WrappedClient[]
|
||||||
isDefaultModel?: boolean
|
isDefaultModel?: boolean
|
||||||
|
updateBannerVersion?: string | null
|
||||||
|
updateBannerCommands?: string[] | null
|
||||||
}): React.ReactNode {
|
}): React.ReactNode {
|
||||||
const width = Math.max(MIN_LOGO_WIDTH, getCwd().length + 12)
|
const width = Math.max(MIN_LOGO_WIDTH, getCwd().length + 12)
|
||||||
const theme = getTheme()
|
const theme = getTheme()
|
||||||
@ -35,15 +39,36 @@ export function Logo({
|
|||||||
return (
|
return (
|
||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
<Box
|
<Box
|
||||||
borderColor={theme.claude}
|
borderColor={theme.kode}
|
||||||
borderStyle="round"
|
borderStyle="round"
|
||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
gap={1}
|
gap={1}
|
||||||
paddingLeft={1}
|
paddingLeft={1}
|
||||||
|
marginRight={2}
|
||||||
width={width}
|
width={width}
|
||||||
>
|
>
|
||||||
|
{updateBannerVersion ? (
|
||||||
|
<Box flexDirection="column">
|
||||||
|
<Text color="yellow">New version available: {updateBannerVersion}</Text>
|
||||||
|
<Text>Run the following command to update:</Text>
|
||||||
<Text>
|
<Text>
|
||||||
<Text color={theme.claude}>✻</Text> Welcome to{' '}
|
{' '}
|
||||||
|
{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.kode}>✻</Text> Welcome to{' '}
|
||||||
<Text bold>{PRODUCT_NAME}</Text> <Text>research preview!</Text>
|
<Text bold>{PRODUCT_NAME}</Text> <Text>research preview!</Text>
|
||||||
</Text>
|
</Text>
|
||||||
{/* <AsciiLogo /> */}
|
{/* <AsciiLogo /> */}
|
||||||
|
|||||||
@ -44,7 +44,7 @@ export function ModelStatusDisplay({ onClose }: Props): React.ReactNode {
|
|||||||
<Box key={pointer} flexDirection="column" marginBottom={1}>
|
<Box key={pointer} flexDirection="column" marginBottom={1}>
|
||||||
<Text>
|
<Text>
|
||||||
🎯{' '}
|
🎯{' '}
|
||||||
<Text bold color={theme.claude}>
|
<Text bold color={theme.kode}>
|
||||||
{pointer.toUpperCase()}
|
{pointer.toUpperCase()}
|
||||||
</Text>{' '}
|
</Text>{' '}
|
||||||
→ {model.name}
|
→ {model.name}
|
||||||
@ -76,7 +76,7 @@ export function ModelStatusDisplay({ onClose }: Props): React.ReactNode {
|
|||||||
<Box key={pointer} flexDirection="column" marginBottom={1}>
|
<Box key={pointer} flexDirection="column" marginBottom={1}>
|
||||||
<Text>
|
<Text>
|
||||||
🎯{' '}
|
🎯{' '}
|
||||||
<Text bold color={theme.claude}>
|
<Text bold color={theme.kode}>
|
||||||
{pointer.toUpperCase()}
|
{pointer.toUpperCase()}
|
||||||
</Text>{' '}
|
</Text>{' '}
|
||||||
→ <Text color={theme.error}>❌ Not configured</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}>
|
<Box key={pointer} flexDirection="column" marginBottom={1}>
|
||||||
<Text>
|
<Text>
|
||||||
🎯{' '}
|
🎯{' '}
|
||||||
<Text bold color={theme.claude}>
|
<Text bold color={theme.kode}>
|
||||||
{pointer.toUpperCase()}
|
{pointer.toUpperCase()}
|
||||||
</Text>{' '}
|
</Text>{' '}
|
||||||
→{' '}
|
→{' '}
|
||||||
|
|||||||
@ -260,13 +260,13 @@ export function WelcomeBox(): React.ReactNode {
|
|||||||
const theme = getTheme()
|
const theme = getTheme()
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
borderColor={theme.claude}
|
borderColor={theme.kode}
|
||||||
borderStyle="round"
|
borderStyle="round"
|
||||||
paddingX={1}
|
paddingX={1}
|
||||||
width={MIN_LOGO_WIDTH}
|
width={MIN_LOGO_WIDTH}
|
||||||
>
|
>
|
||||||
<Text>
|
<Text>
|
||||||
<Text color={theme.claude}>✻</Text> Welcome to{' '}
|
<Text color={theme.kode}>✻</Text> Welcome to{' '}
|
||||||
<Text bold>{PRODUCT_NAME}</Text> research preview!
|
<Text bold>{PRODUCT_NAME}</Text> research preview!
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@ -596,7 +596,7 @@ function PromptInput({
|
|||||||
mode === 'bash'
|
mode === 'bash'
|
||||||
? theme.bashBorder
|
? theme.bashBorder
|
||||||
: mode === 'koding'
|
: mode === 'koding'
|
||||||
? theme.koding
|
? theme.noting
|
||||||
: theme.secondaryBorder
|
: theme.secondaryBorder
|
||||||
}
|
}
|
||||||
borderDimColor
|
borderDimColor
|
||||||
@ -614,7 +614,7 @@ function PromptInput({
|
|||||||
{mode === 'bash' ? (
|
{mode === 'bash' ? (
|
||||||
<Text color={theme.bashBorder}> ! </Text>
|
<Text color={theme.bashBorder}> ! </Text>
|
||||||
) : mode === 'koding' ? (
|
) : mode === 'koding' ? (
|
||||||
<Text color={theme.koding}> # </Text>
|
<Text color={theme.noting}> # </Text>
|
||||||
) : (
|
) : (
|
||||||
<Text color={isLoading ? theme.secondaryText : undefined}>
|
<Text color={isLoading ? theme.secondaryText : undefined}>
|
||||||
>
|
>
|
||||||
@ -668,7 +668,7 @@ function PromptInput({
|
|||||||
! for bash mode
|
! for bash mode
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
color={mode === 'koding' ? theme.koding : undefined}
|
color={mode === 'koding' ? theme.noting : undefined}
|
||||||
dimColor={mode !== 'koding'}
|
dimColor={mode !== 'koding'}
|
||||||
>
|
>
|
||||||
· # for AGENTS.md
|
· # for AGENTS.md
|
||||||
|
|||||||
@ -96,9 +96,9 @@ export function Spinner(): React.ReactNode {
|
|||||||
return (
|
return (
|
||||||
<Box flexDirection="row" marginTop={1}>
|
<Box flexDirection="row" marginTop={1}>
|
||||||
<Box flexWrap="nowrap" height={1} width={2}>
|
<Box flexWrap="nowrap" height={1} width={2}>
|
||||||
<Text color={getTheme().claude}>{frames[frame]}</Text>
|
<Text color={getTheme().kode}>{frames[frame]}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Text color={getTheme().claude}>{message.current}… </Text>
|
<Text color={getTheme().kode}>{message.current}… </Text>
|
||||||
<Text color={getTheme().secondaryText}>
|
<Text color={getTheme().secondaryText}>
|
||||||
({elapsedTime}s · <Text bold>esc</Text> to interrupt)
|
({elapsedTime}s · <Text bold>esc</Text> to interrupt)
|
||||||
</Text>
|
</Text>
|
||||||
@ -123,7 +123,7 @@ export function SimpleSpinner(): React.ReactNode {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexWrap="nowrap" height={1} width={2}>
|
<Box flexWrap="nowrap" height={1} width={2}>
|
||||||
<Text color={getTheme().claude}>{frames[frame]}</Text>
|
<Text color={getTheme().kode}>{frames[frame]}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,7 @@ export function TaskProgressMessage({ agentType, status, toolCount }: Props) {
|
|||||||
return (
|
return (
|
||||||
<Box flexDirection="column" marginTop={1}>
|
<Box flexDirection="column" marginTop={1}>
|
||||||
<Box flexDirection="row">
|
<Box flexDirection="row">
|
||||||
<Text color={theme.claude}>⎯ </Text>
|
<Text color={theme.kode}>⎯ </Text>
|
||||||
<Text color={theme.text} bold>
|
<Text color={theme.text} bold>
|
||||||
[{agentType}]
|
[{agentType}]
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@ -20,7 +20,7 @@ export function UserKodingInputMessage({
|
|||||||
return (
|
return (
|
||||||
<Box flexDirection="column" marginTop={addMargin ? 1 : 0} width="100%">
|
<Box flexDirection="column" marginTop={addMargin ? 1 : 0} width="100%">
|
||||||
<Box>
|
<Box>
|
||||||
<Text color={getTheme().koding}>#</Text>
|
<Text color={getTheme().noting}>#</Text>
|
||||||
<Text color={getTheme().secondaryText}> {input}</Text>
|
<Text color={getTheme().secondaryText}> {input}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@ -1,8 +1,25 @@
|
|||||||
#!/usr/bin/env -S node --no-warnings=ExperimentalWarning --enable-source-maps
|
#!/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 { initSentry } from '../services/sentry'
|
||||||
import { PRODUCT_COMMAND, PRODUCT_NAME } from '../constants/product'
|
import { PRODUCT_COMMAND, PRODUCT_NAME } from '../constants/product'
|
||||||
initSentry() // Initialize Sentry as early as possible
|
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!),
|
// 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
|
// 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.
|
// its use is solely because of its side-effects.
|
||||||
@ -74,8 +91,11 @@ import {
|
|||||||
getLatestVersion,
|
getLatestVersion,
|
||||||
installGlobalPackage,
|
installGlobalPackage,
|
||||||
assertMinVersion,
|
assertMinVersion,
|
||||||
|
getUpdateCommandSuggestions,
|
||||||
} from '../utils/autoUpdater'
|
} from '../utils/autoUpdater'
|
||||||
|
import { gt } from 'semver'
|
||||||
import { CACHE_PATHS } from '../utils/log'
|
import { CACHE_PATHS } from '../utils/log'
|
||||||
|
// import { checkAndNotifyUpdate } from '../utils/autoUpdater'
|
||||||
import { PersistentShell } from '../utils/PersistentShell'
|
import { PersistentShell } from '../utils/PersistentShell'
|
||||||
import { GATE_USE_EXTERNAL_UPDATER } from '../constants/betas'
|
import { GATE_USE_EXTERNAL_UPDATER } from '../constants/betas'
|
||||||
import { clearTerminal } from '../utils/terminal'
|
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 inputPrompt = ''
|
||||||
let renderContext: RenderOptions | undefined = {
|
let renderContext: RenderOptions | undefined = {
|
||||||
exitOnCtrlC: false,
|
exitOnCtrlC: false,
|
||||||
@ -417,6 +439,18 @@ ${commandList}`,
|
|||||||
} else {
|
} else {
|
||||||
const isDefaultModel = await isDefaultSlowAndCapableModel()
|
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(
|
render(
|
||||||
<REPL
|
<REPL
|
||||||
commands={commands}
|
commands={commands}
|
||||||
@ -429,6 +463,8 @@ ${commandList}`,
|
|||||||
safeMode={safe}
|
safeMode={safe}
|
||||||
mcpClients={mcpClients}
|
mcpClients={mcpClients}
|
||||||
isDefaultModel={isDefaultModel}
|
isDefaultModel={isDefaultModel}
|
||||||
|
initialUpdateVersion={updateInfo.version}
|
||||||
|
initialUpdateCommands={updateInfo.commands}
|
||||||
/>,
|
/>,
|
||||||
renderContext,
|
renderContext,
|
||||||
)
|
)
|
||||||
@ -1098,11 +1134,11 @@ ${commandList}`,
|
|||||||
<Box
|
<Box
|
||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
borderStyle="round"
|
borderStyle="round"
|
||||||
borderColor={theme.claude}
|
borderColor={theme.kode}
|
||||||
padding={1}
|
padding={1}
|
||||||
width={'100%'}
|
width={'100%'}
|
||||||
>
|
>
|
||||||
<Text bold color={theme.claude}>
|
<Text bold color={theme.kode}>
|
||||||
Import MCP Servers from Claude Desktop
|
Import MCP Servers from Claude Desktop
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
@ -1219,16 +1255,8 @@ ${commandList}`,
|
|||||||
// claude update
|
// claude update
|
||||||
program
|
program
|
||||||
.command('update')
|
.command('update')
|
||||||
.description('Check for updates and install if available')
|
.description('Show manual upgrade commands (no auto-install)')
|
||||||
.action(async () => {
|
.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', {})
|
logEvent('tengu_update_check', {})
|
||||||
console.log(`Current version: ${MACRO.VERSION}`)
|
console.log(`Current version: ${MACRO.VERSION}`)
|
||||||
console.log('Checking for updates...')
|
console.log('Checking for updates...')
|
||||||
@ -1246,30 +1274,12 @@ ${commandList}`,
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(`New version available: ${latestVersion}`)
|
console.log(`New version available: ${latestVersion}`)
|
||||||
console.log('Installing update...')
|
const { getUpdateCommandSuggestions } = await import('../utils/autoUpdater')
|
||||||
|
const cmds = await getUpdateCommandSuggestions()
|
||||||
const status = await installGlobalPackage()
|
console.log('\nRun one of the following commands to update:')
|
||||||
|
for (const c of cmds) console.log(` ${c}`)
|
||||||
switch (status) {
|
if (process.platform !== 'win32') {
|
||||||
case 'success':
|
console.log('\nNote: you may need to prefix with "sudo" on macOS/Linux.')
|
||||||
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
|
|
||||||
}
|
}
|
||||||
process.exit(0)
|
process.exit(0)
|
||||||
})
|
})
|
||||||
@ -1501,9 +1511,23 @@ process.on('exit', () => {
|
|||||||
PersistentShell.getInstance().close()
|
PersistentShell.getInstance().close()
|
||||||
})
|
})
|
||||||
|
|
||||||
process.on('SIGINT', () => {
|
function gracefulExit(code = 0) {
|
||||||
console.log('SIGINT')
|
try { resetCursor() } catch {}
|
||||||
process.exit(0)
|
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() {
|
function resetCursor() {
|
||||||
|
|||||||
@ -44,6 +44,7 @@ import type { WrappedClient } from '../services/mcpClient'
|
|||||||
import type { Tool } from '../Tool'
|
import type { Tool } from '../Tool'
|
||||||
import { AutoUpdaterResult } from '../utils/autoUpdater'
|
import { AutoUpdaterResult } from '../utils/autoUpdater'
|
||||||
import { getGlobalConfig, saveGlobalConfig } from '../utils/config'
|
import { getGlobalConfig, saveGlobalConfig } from '../utils/config'
|
||||||
|
import { MACRO } from '../constants/macros'
|
||||||
import { logEvent } from '../services/statsig'
|
import { logEvent } from '../services/statsig'
|
||||||
import { getNextAvailableLogForkNumber } from '../utils/log'
|
import { getNextAvailableLogForkNumber } from '../utils/log'
|
||||||
import {
|
import {
|
||||||
@ -87,6 +88,9 @@ type Props = {
|
|||||||
mcpClients?: WrappedClient[]
|
mcpClients?: WrappedClient[]
|
||||||
// Flag to indicate if current model is default
|
// Flag to indicate if current model is default
|
||||||
isDefaultModel?: boolean
|
isDefaultModel?: boolean
|
||||||
|
// Update banner info passed from CLI before first render
|
||||||
|
initialUpdateVersion?: string | null
|
||||||
|
initialUpdateCommands?: string[] | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BinaryFeedbackContext = {
|
export type BinaryFeedbackContext = {
|
||||||
@ -108,6 +112,8 @@ export function REPL({
|
|||||||
initialMessages,
|
initialMessages,
|
||||||
mcpClients = [],
|
mcpClients = [],
|
||||||
isDefaultModel = true,
|
isDefaultModel = true,
|
||||||
|
initialUpdateVersion,
|
||||||
|
initialUpdateCommands,
|
||||||
}: Props): React.ReactNode {
|
}: Props): React.ReactNode {
|
||||||
// TODO: probably shouldn't re-read config from file synchronously on every keystroke
|
// TODO: probably shouldn't re-read config from file synchronously on every keystroke
|
||||||
const verbose = verboseFromCLI ?? getGlobalConfig().verbose
|
const verbose = verboseFromCLI ?? getGlobalConfig().verbose
|
||||||
@ -149,6 +155,10 @@ export function REPL({
|
|||||||
|
|
||||||
const [binaryFeedbackContext, setBinaryFeedbackContext] =
|
const [binaryFeedbackContext, setBinaryFeedbackContext] =
|
||||||
useState<BinaryFeedbackContext | null>(null)
|
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(
|
const getBinaryFeedbackResponse = useCallback(
|
||||||
(
|
(
|
||||||
@ -209,6 +219,8 @@ export function REPL({
|
|||||||
}
|
}
|
||||||
}, [messages, showCostDialog, haveShownCostDialog])
|
}, [messages, showCostDialog, haveShownCostDialog])
|
||||||
|
|
||||||
|
// Update banner is provided by CLI at startup; no async check here.
|
||||||
|
|
||||||
const canUseTool = useCanUseTool(setToolUseConfirm)
|
const canUseTool = useCanUseTool(setToolUseConfirm)
|
||||||
|
|
||||||
async function onInit() {
|
async function onInit() {
|
||||||
@ -478,7 +490,12 @@ export function REPL({
|
|||||||
type: 'static',
|
type: 'static',
|
||||||
jsx: (
|
jsx: (
|
||||||
<Box flexDirection="column" key={`logo${forkNumber}`}>
|
<Box flexDirection="column" key={`logo${forkNumber}`}>
|
||||||
<Logo mcpClients={mcpClients} isDefaultModel={isDefaultModel} />
|
<Logo
|
||||||
|
mcpClients={mcpClients}
|
||||||
|
isDefaultModel={isDefaultModel}
|
||||||
|
updateBannerVersion={updateAvailableVersion}
|
||||||
|
updateBannerCommands={updateCommands}
|
||||||
|
/>
|
||||||
<ProjectOnboarding workspaceDir={getOriginalCwd()} />
|
<ProjectOnboarding workspaceDir={getOriginalCwd()} />
|
||||||
</Box>
|
</Box>
|
||||||
),
|
),
|
||||||
@ -602,6 +619,7 @@ export function REPL({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PermissionProvider isBypassPermissionsModeAvailable={!safeMode}>
|
<PermissionProvider isBypassPermissionsModeAvailable={!safeMode}>
|
||||||
|
{/* Update banner now renders inside Logo for stable placement */}
|
||||||
<ModeIndicator />
|
<ModeIndicator />
|
||||||
<React.Fragment key={`static-messages-${forkNumber}`}>
|
<React.Fragment key={`static-messages-${forkNumber}`}>
|
||||||
<Static
|
<Static
|
||||||
|
|||||||
@ -518,7 +518,7 @@ export async function getCompletionWithProfile(
|
|||||||
messageCount: opts.messages?.length || 0,
|
messageCount: opts.messages?.length || 0,
|
||||||
streamMode: opts.stream,
|
streamMode: opts.stream,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
modelProfileName: modelProfile?.modelName,
|
modelProfileModelName: modelProfile?.modelName,
|
||||||
modelProfileName: modelProfile?.name,
|
modelProfileName: modelProfile?.name,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -107,7 +107,8 @@ export function logEvent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const checkGate = memoize(async (gateName: string): Promise<boolean> => {
|
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') {
|
// if (env.isCI || process.env.NODE_ENV === 'test') {
|
||||||
// return false
|
// return false
|
||||||
// }
|
// }
|
||||||
@ -120,7 +121,7 @@ export const checkGate = memoize(async (gateName: string): Promise<boolean> => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const useStatsigGate = (gateName: string, defaultValue = false) => {
|
export const useStatsigGate = (gateName: string, defaultValue = false) => {
|
||||||
return true
|
return false
|
||||||
// const [gateValue, setGateValue] = React.useState(defaultValue)
|
// const [gateValue, setGateValue] = React.useState(defaultValue)
|
||||||
// React.useEffect(() => {
|
// React.useEffect(() => {
|
||||||
// checkGate(gateName).then(setGateValue)
|
// checkGate(gateName).then(setGateValue)
|
||||||
|
|||||||
@ -263,22 +263,16 @@ export async function startAgentWatcher(onChange?: () => void): Promise<void> {
|
|||||||
* Stop watching agent configuration directories
|
* Stop watching agent configuration directories
|
||||||
*/
|
*/
|
||||||
export async function stopAgentWatcher(): Promise<void> {
|
export async function stopAgentWatcher(): Promise<void> {
|
||||||
const closePromises = watchers.map(watcher =>
|
// FSWatcher.close() is synchronous and does not accept a callback on Node 18/20
|
||||||
new Promise<void>((resolve) => {
|
|
||||||
try {
|
try {
|
||||||
watcher.close((err) => {
|
for (const watcher of watchers) {
|
||||||
if (err) {
|
try {
|
||||||
|
watcher.close()
|
||||||
|
} catch (err) {
|
||||||
console.error('Failed to close file watcher:', err)
|
console.error('Failed to close file watcher:', err)
|
||||||
}
|
}
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error closing watcher:', error)
|
|
||||||
resolve()
|
|
||||||
}
|
}
|
||||||
})
|
} finally {
|
||||||
)
|
|
||||||
|
|
||||||
await Promise.allSettled(closePromises)
|
|
||||||
watchers = []
|
watchers = []
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -12,13 +12,16 @@ import {
|
|||||||
} from 'fs'
|
} from 'fs'
|
||||||
import { platform } from 'process'
|
import { platform } from 'process'
|
||||||
import { execFileNoThrow } from './execFileNoThrow'
|
import { execFileNoThrow } from './execFileNoThrow'
|
||||||
|
import { spawn } from 'child_process'
|
||||||
import { logError } from './log'
|
import { logError } from './log'
|
||||||
import { accessSync } from 'fs'
|
import { accessSync } from 'fs'
|
||||||
import { CLAUDE_BASE_DIR } from './env'
|
import { CLAUDE_BASE_DIR } from './env'
|
||||||
import { logEvent, getDynamicConfig } from '../services/statsig'
|
import { logEvent, getDynamicConfig } from '../services/statsig'
|
||||||
import { lt } from 'semver'
|
import { lt, gt } from 'semver'
|
||||||
import { MACRO } from '../constants/macros'
|
import { MACRO } from '../constants/macros'
|
||||||
import { PRODUCT_COMMAND, PRODUCT_NAME } from '../constants/product'
|
import { PRODUCT_COMMAND, PRODUCT_NAME } from '../constants/product'
|
||||||
|
import { getGlobalConfig, saveGlobalConfig, isAutoUpdaterDisabled } from './config'
|
||||||
|
import { env } from './env'
|
||||||
export type InstallStatus =
|
export type InstallStatus =
|
||||||
| 'success'
|
| 'success'
|
||||||
| 'no_permissions'
|
| 'no_permissions'
|
||||||
@ -49,14 +52,12 @@ export async function assertMinVersion(): Promise<void> {
|
|||||||
versionConfig.minVersion &&
|
versionConfig.minVersion &&
|
||||||
lt(MACRO.VERSION, versionConfig.minVersion)
|
lt(MACRO.VERSION, versionConfig.minVersion)
|
||||||
) {
|
) {
|
||||||
|
const suggestions = await getUpdateCommandSuggestions()
|
||||||
|
const cmdLines = suggestions.map(c => ` ${c}`).join('\n')
|
||||||
console.error(`
|
console.error(`
|
||||||
It looks like your version of ${PRODUCT_NAME} (${MACRO.VERSION}) needs an update.
|
您的 ${PRODUCT_NAME} 版本 (${MACRO.VERSION}) 过低,需要升级到 ${versionConfig.minVersion} 或更高版本。
|
||||||
A newer version (${versionConfig.minVersion} or higher) is required to continue.
|
请手动执行以下任一命令进行升级:
|
||||||
|
${cmdLines}
|
||||||
To update, please run:
|
|
||||||
${PRODUCT_COMMAND} update
|
|
||||||
|
|
||||||
This will ensure you have access to the latest features and improvements.
|
|
||||||
`)
|
`)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
@ -267,21 +268,48 @@ export function getPermissionsCommand(npmPrefix: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getLatestVersion(): Promise<string | null> {
|
export async function getLatestVersion(): Promise<string | null> {
|
||||||
|
// 1) Try npm CLI (fast when available)
|
||||||
|
try {
|
||||||
const abortController = new AbortController()
|
const abortController = new AbortController()
|
||||||
setTimeout(() => abortController.abort(), 5000)
|
setTimeout(() => abortController.abort(), 5000)
|
||||||
|
|
||||||
const result = await execFileNoThrow(
|
const result = await execFileNoThrow(
|
||||||
'npm',
|
'npm',
|
||||||
['view', MACRO.PACKAGE_URL, 'version'],
|
['view', MACRO.PACKAGE_URL, 'version'],
|
||||||
abortController.signal,
|
abortController.signal,
|
||||||
)
|
)
|
||||||
if (result.code !== 0) {
|
if (result.code === 0) {
|
||||||
|
const v = result.stdout.trim()
|
||||||
|
if (v) return v
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
// 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 null
|
||||||
}
|
}
|
||||||
return result.stdout.trim()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function installGlobalPackage(): Promise<InstallStatus> {
|
export async function installGlobalPackage(): Promise<InstallStatus> {
|
||||||
|
// Detect preferred package manager and install accordingly
|
||||||
if (!acquireLock()) {
|
if (!acquireLock()) {
|
||||||
logError('Another process is currently installing an update')
|
logError('Another process is currently installing an update')
|
||||||
// Log the lock contention to statsig
|
// Log the lock contention to statsig
|
||||||
@ -293,26 +321,138 @@ export async function installGlobalPackage(): Promise<InstallStatus> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const manager = await detectPackageManager()
|
||||||
|
if (manager === 'npm') {
|
||||||
const { hasPermissions } = await checkNpmPermissions()
|
const { hasPermissions } = await checkNpmPermissions()
|
||||||
if (!hasPermissions) {
|
if (!hasPermissions) {
|
||||||
return 'no_permissions'
|
return 'no_permissions'
|
||||||
}
|
}
|
||||||
|
// Stream实时输出,减少用户等待感
|
||||||
const installResult = await execFileNoThrow('npm', [
|
const code = await runStreaming('npm', ['install', '-g', MACRO.PACKAGE_URL])
|
||||||
'install',
|
if (code !== 0) {
|
||||||
'-g',
|
logError(`Failed to install new version via npm (exit ${code})`)
|
||||||
MACRO.PACKAGE_URL,
|
|
||||||
])
|
|
||||||
if (installResult.code !== 0) {
|
|
||||||
logError(
|
|
||||||
`Failed to install new version of claude: ${installResult.stdout} ${installResult.stderr}`,
|
|
||||||
)
|
|
||||||
return 'install_failed'
|
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 code = await runStreaming('npm', ['install', '-g', MACRO.PACKAGE_URL])
|
||||||
|
if (code !== 0) return 'install_failed'
|
||||||
return 'success'
|
return 'success'
|
||||||
} finally {
|
} finally {
|
||||||
// Ensure we always release the lock
|
// Ensure we always release the lock
|
||||||
releaseLock()
|
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
|
modelProfiles?: ModelProfile[] // Model configuration list
|
||||||
modelPointers?: ModelPointers // Model pointer system
|
modelPointers?: ModelPointers // Model pointer system
|
||||||
defaultModelName?: string // Default model
|
defaultModelName?: string // Default model
|
||||||
|
// Update notifications
|
||||||
|
lastDismissedUpdateVersion?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_GLOBAL_CONFIG: GlobalConfig = {
|
export const DEFAULT_GLOBAL_CONFIG: GlobalConfig = {
|
||||||
@ -196,6 +198,7 @@ export const DEFAULT_GLOBAL_CONFIG: GlobalConfig = {
|
|||||||
reasoning: '',
|
reasoning: '',
|
||||||
quick: '',
|
quick: '',
|
||||||
},
|
},
|
||||||
|
lastDismissedUpdateVersion: undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GLOBAL_CONFIG_KEYS = [
|
export const GLOBAL_CONFIG_KEYS = [
|
||||||
|
|||||||
@ -98,12 +98,15 @@ export function isInDirectory(
|
|||||||
: normalizedCwd + sep
|
: normalizedCwd + sep
|
||||||
|
|
||||||
// Join with a base directory to make them absolute-like for comparison
|
// 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 fullPath = resolvePath(cwd(), normalizedCwd, normalizedPath)
|
||||||
const fullCwd = resolvePath(cwd(), normalizedCwd)
|
const fullCwd = resolvePath(cwd(), normalizedCwd)
|
||||||
|
|
||||||
// Check if the path starts with the cwd
|
// Robust subpath check using path.relative (case-insensitive on Windows)
|
||||||
return fullPath.startsWith(fullCwd)
|
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(
|
export function readTextContent(
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { isAbsolute, resolve } from 'path'
|
import { isAbsolute, resolve, relative, sep } from 'path'
|
||||||
import { getCwd, getOriginalCwd } from '../state'
|
import { getCwd, getOriginalCwd } from '../state'
|
||||||
|
|
||||||
// In-memory storage for file permissions that resets each session
|
// In-memory storage for file permissions that resets each session
|
||||||
@ -12,7 +12,26 @@ const writeFileAllowedDirectories: Set<string> = new Set()
|
|||||||
* @returns Absolute path
|
* @returns Absolute path
|
||||||
*/
|
*/
|
||||||
export function toAbsolutePath(path: string): string {
|
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 {
|
export function pathInOriginalCwd(path: string): boolean {
|
||||||
const absolutePath = toAbsolutePath(path)
|
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 {
|
export function hasReadPermission(directory: string): boolean {
|
||||||
const absolutePath = toAbsolutePath(directory)
|
const absolutePath = toAbsolutePath(directory)
|
||||||
|
|
||||||
for (const allowedPath of readFileAllowedDirectories) {
|
for (const allowedPath of readFileAllowedDirectories) {
|
||||||
// Permission exists for this directory or a path prefix
|
if (isSubpath(allowedPath, absolutePath)) return true
|
||||||
if (absolutePath.startsWith(allowedPath)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -49,12 +65,8 @@ export function hasReadPermission(directory: string): boolean {
|
|||||||
*/
|
*/
|
||||||
export function hasWritePermission(directory: string): boolean {
|
export function hasWritePermission(directory: string): boolean {
|
||||||
const absolutePath = toAbsolutePath(directory)
|
const absolutePath = toAbsolutePath(directory)
|
||||||
|
|
||||||
for (const allowedPath of writeFileAllowedDirectories) {
|
for (const allowedPath of writeFileAllowedDirectories) {
|
||||||
// Permission exists for this directory or a path prefix
|
if (isSubpath(allowedPath, absolutePath)) return true
|
||||||
if (absolutePath.startsWith(allowedPath)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -65,10 +77,9 @@ export function hasWritePermission(directory: string): boolean {
|
|||||||
*/
|
*/
|
||||||
function saveReadPermission(directory: string): void {
|
function saveReadPermission(directory: string): void {
|
||||||
const absolutePath = toAbsolutePath(directory)
|
const absolutePath = toAbsolutePath(directory)
|
||||||
|
// Remove any existing subpaths contained by this new path
|
||||||
// Clean up any existing subdirectories of this path
|
for (const allowedPath of Array.from(readFileAllowedDirectories)) {
|
||||||
for (const allowedPath of readFileAllowedDirectories) {
|
if (isSubpath(absolutePath, allowedPath)) {
|
||||||
if (allowedPath.startsWith(absolutePath)) {
|
|
||||||
readFileAllowedDirectories.delete(allowedPath)
|
readFileAllowedDirectories.delete(allowedPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -92,10 +103,8 @@ export function grantReadPermissionForOriginalDir(): void {
|
|||||||
*/
|
*/
|
||||||
function saveWritePermission(directory: string): void {
|
function saveWritePermission(directory: string): void {
|
||||||
const absolutePath = toAbsolutePath(directory)
|
const absolutePath = toAbsolutePath(directory)
|
||||||
|
for (const allowedPath of Array.from(writeFileAllowedDirectories)) {
|
||||||
// Clean up any existing subdirectories of this path
|
if (isSubpath(absolutePath, allowedPath)) {
|
||||||
for (const allowedPath of writeFileAllowedDirectories) {
|
|
||||||
if (allowedPath.startsWith(absolutePath)) {
|
|
||||||
writeFileAllowedDirectories.delete(allowedPath)
|
writeFileAllowedDirectories.delete(allowedPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync, unlinkSync, renameSync } from 'node:fs'
|
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'
|
import { homedir } from 'node:os'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -98,7 +98,12 @@ export class SecureFileService {
|
|||||||
|
|
||||||
// 检查是否在允许的基础路径中
|
// 检查是否在允许的基础路径中
|
||||||
const isInAllowedPath = Array.from(this.allowedBasePaths).some(basePath => {
|
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) {
|
if (!isInAllowedPath) {
|
||||||
|
|||||||
@ -2,18 +2,16 @@ import { getGlobalConfig } from './config'
|
|||||||
|
|
||||||
export interface Theme {
|
export interface Theme {
|
||||||
bashBorder: string
|
bashBorder: string
|
||||||
claude: string
|
kode: string
|
||||||
koding: string
|
noting: string
|
||||||
permission: string
|
permission: string
|
||||||
secondaryBorder: string
|
secondaryBorder: string
|
||||||
text: string
|
text: string
|
||||||
secondaryText: string
|
secondaryText: string
|
||||||
suggestion: string
|
suggestion: string
|
||||||
// Semantic colors
|
|
||||||
success: string
|
success: string
|
||||||
error: string
|
error: string
|
||||||
warning: string
|
warning: string
|
||||||
// UI colors
|
|
||||||
primary: string
|
primary: string
|
||||||
secondary: string
|
secondary: string
|
||||||
diff: {
|
diff: {
|
||||||
@ -25,9 +23,9 @@ export interface Theme {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const lightTheme: Theme = {
|
const lightTheme: Theme = {
|
||||||
bashBorder: '#ff0087',
|
bashBorder: '#FF6E57',
|
||||||
claude: '#7aff59ff',
|
kode: '#FFC233',
|
||||||
koding: '#9dff00ff',
|
noting: '#222222',
|
||||||
permission: '#e9c61aff',
|
permission: '#e9c61aff',
|
||||||
secondaryBorder: '#999',
|
secondaryBorder: '#999',
|
||||||
text: '#000',
|
text: '#000',
|
||||||
@ -47,31 +45,31 @@ const lightTheme: Theme = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const lightDaltonizedTheme: Theme = {
|
const lightDaltonizedTheme: Theme = {
|
||||||
bashBorder: '#0066cc', // Blue instead of pink for better contrast
|
bashBorder: '#FF6E57',
|
||||||
claude: '#5f97cd', // Orange adjusted for deuteranopia
|
kode: '#FFC233',
|
||||||
koding: '#0000ff',
|
noting: '#222222',
|
||||||
permission: '#3366ff', // Brighter blue for better visibility
|
permission: '#3366ff',
|
||||||
secondaryBorder: '#999',
|
secondaryBorder: '#999',
|
||||||
text: '#000',
|
text: '#000',
|
||||||
secondaryText: '#666',
|
secondaryText: '#666',
|
||||||
suggestion: '#3366ff',
|
suggestion: '#3366ff',
|
||||||
success: '#006699', // Blue instead of green
|
success: '#006699',
|
||||||
error: '#cc0000', // Pure red for better distinction
|
error: '#cc0000',
|
||||||
warning: '#ff9900', // Orange adjusted for deuteranopia
|
warning: '#ff9900',
|
||||||
primary: '#000',
|
primary: '#000',
|
||||||
secondary: '#666',
|
secondary: '#666',
|
||||||
diff: {
|
diff: {
|
||||||
added: '#99ccff', // Light blue instead of green
|
added: '#99ccff',
|
||||||
removed: '#ffcccc', // Light red for better contrast
|
removed: '#ffcccc',
|
||||||
addedDimmed: '#d1e7fd',
|
addedDimmed: '#d1e7fd',
|
||||||
removedDimmed: '#ffe9e9',
|
removedDimmed: '#ffe9e9',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const darkTheme: Theme = {
|
const darkTheme: Theme = {
|
||||||
bashBorder: '#fd5db1',
|
bashBorder: '#FF6E57',
|
||||||
claude: '#5f97cd',
|
kode: '#FFC233',
|
||||||
koding: '#0000ff',
|
noting: '#222222',
|
||||||
permission: '#b1b9f9',
|
permission: '#b1b9f9',
|
||||||
secondaryBorder: '#888',
|
secondaryBorder: '#888',
|
||||||
text: '#fff',
|
text: '#fff',
|
||||||
@ -91,32 +89,28 @@ const darkTheme: Theme = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const darkDaltonizedTheme: Theme = {
|
const darkDaltonizedTheme: Theme = {
|
||||||
bashBorder: '#3399ff', // Bright blue instead of pink
|
bashBorder: '#FF6E57',
|
||||||
claude: '#5f97cd', // Orange adjusted for deuteranopia
|
kode: '#FFC233',
|
||||||
koding: '#0000ff',
|
noting: '#222222',
|
||||||
permission: '#99ccff', // Light blue for better contrast
|
permission: '#99ccff',
|
||||||
secondaryBorder: '#888',
|
secondaryBorder: '#888',
|
||||||
text: '#fff',
|
text: '#fff',
|
||||||
secondaryText: '#999',
|
secondaryText: '#999',
|
||||||
suggestion: '#99ccff',
|
suggestion: '#99ccff',
|
||||||
success: '#3399ff', // Bright blue instead of green
|
success: '#3399ff',
|
||||||
error: '#ff6666', // Bright red for better visibility
|
error: '#ff6666',
|
||||||
warning: '#ffcc00', // Yellow-orange for deuteranopia
|
warning: '#ffcc00',
|
||||||
primary: '#fff',
|
primary: '#fff',
|
||||||
secondary: '#999',
|
secondary: '#999',
|
||||||
diff: {
|
diff: {
|
||||||
added: '#004466', // Dark blue instead of green
|
added: '#004466',
|
||||||
removed: '#660000', // Dark red for better contrast
|
removed: '#660000',
|
||||||
addedDimmed: '#3e515b',
|
addedDimmed: '#3e515b',
|
||||||
removedDimmed: '#3e2c2c',
|
removedDimmed: '#3e2c2c',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ThemeNames =
|
export type ThemeNames = 'dark' | 'light' | 'light-daltonized' | 'dark-daltonized'
|
||||||
| 'dark'
|
|
||||||
| 'light'
|
|
||||||
| 'light-daltonized'
|
|
||||||
| 'dark-daltonized'
|
|
||||||
|
|
||||||
export function getTheme(overrideTheme?: ThemeNames): Theme {
|
export function getTheme(overrideTheme?: ThemeNames): Theme {
|
||||||
const config = getGlobalConfig()
|
const config = getGlobalConfig()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user