mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 02:14:32 +00:00
feat(global-env): add global environment variables injection for third-party profiles
- add global_env config section to unified config types and loader - inject global env vars at runtime for cliproxy and copilot profiles - add GET/PUT /api/global-env endpoints for UI management - add Global Env tab to Settings page with enable toggle and var management - add GlobalEnvIndicator component showing injected vars in profile editors - support ?tab=globalenv query param for direct navigation from indicators
This commit is contained in:
1 parent
25cfb5b65e
commit
5d343260c7
11 files changed
+1091
-353
No files matched your search
@@ -11,6 +11,7 @@ import {
|
|||||||
displayWebSearchStatus,
|
displayWebSearchStatus,
|
||||||
getWebSearchHookEnv,
|
getWebSearchHookEnv,
|
||||||
} from './utils/websearch-manager';
|
} from './utils/websearch-manager';
|
||||||
|
import { getGlobalEnvConfig } from './config/unified-config-loader';
|
||||||
|
|
||||||
// Import extracted command handlers
|
// Import extracted command handlers
|
||||||
import { handleVersionCommand } from './commands/version-command';
|
import { handleVersionCommand } from './commands/version-command';
|
||||||
@@ -494,7 +495,11 @@ async function main(): Promise<void> {
|
|||||||
// Use --settings flag (backward compatible)
|
// Use --settings flag (backward compatible)
|
||||||
const expandedSettingsPath = getSettingsPath(profileInfo.name);
|
const expandedSettingsPath = getSettingsPath(profileInfo.name);
|
||||||
const webSearchEnv = getWebSearchHookEnv();
|
const webSearchEnv = getWebSearchHookEnv();
|
||||||
|
// Get global env vars (DISABLE_TELEMETRY, etc.) for third-party profiles
|
||||||
|
const globalEnvConfig = getGlobalEnvConfig();
|
||||||
|
const globalEnv = globalEnvConfig.enabled ? globalEnvConfig.env : {};
|
||||||
const envVars: NodeJS.ProcessEnv = {
|
const envVars: NodeJS.ProcessEnv = {
|
||||||
|
...globalEnv,
|
||||||
...webSearchEnv,
|
...webSearchEnv,
|
||||||
CCS_PROFILE_TYPE: 'settings', // Signal to WebSearch hook this is a third-party provider
|
CCS_PROFILE_TYPE: 'settings', // Signal to WebSearch hook this is a third-party provider
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { getCcsDir } from '../utils/config-manager';
|
|||||||
import { warn } from '../utils/ui';
|
import { warn } from '../utils/ui';
|
||||||
import { CLIProxyProvider, ProviderConfig, ProviderModelMapping } from './types';
|
import { CLIProxyProvider, ProviderConfig, ProviderModelMapping } from './types';
|
||||||
import { getModelMappingFromConfig, getEnvVarsFromConfig } from './base-config-loader';
|
import { getModelMappingFromConfig, getEnvVarsFromConfig } from './base-config-loader';
|
||||||
import { loadOrCreateUnifiedConfig } from '../config/unified-config-loader';
|
import { loadOrCreateUnifiedConfig, getGlobalEnvConfig } from '../config/unified-config-loader';
|
||||||
|
|
||||||
/** Settings file structure for user overrides */
|
/** Settings file structure for user overrides */
|
||||||
interface ProviderSettings {
|
interface ProviderSettings {
|
||||||
@@ -380,6 +380,18 @@ export function getClaudeEnvVars(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get global env vars to inject into all third-party profiles.
|
||||||
|
* Returns empty object if disabled.
|
||||||
|
*/
|
||||||
|
function getGlobalEnvVars(): Record<string, string> {
|
||||||
|
const globalEnvConfig = getGlobalEnvConfig();
|
||||||
|
if (!globalEnvConfig.enabled) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return globalEnvConfig.env;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get effective environment variables for provider
|
* Get effective environment variables for provider
|
||||||
*
|
*
|
||||||
@@ -388,7 +400,7 @@ export function getClaudeEnvVars(
|
|||||||
* 2. User settings file (~/.ccs/{provider}.settings.json) if exists
|
* 2. User settings file (~/.ccs/{provider}.settings.json) if exists
|
||||||
* 3. Bundled defaults from PROVIDER_CONFIGS
|
* 3. Bundled defaults from PROVIDER_CONFIGS
|
||||||
*
|
*
|
||||||
* This allows users to customize model mappings without code changes.
|
* All results are merged with global_env vars (telemetry/reporting disables).
|
||||||
* User takes full responsibility for custom settings.
|
* User takes full responsibility for custom settings.
|
||||||
*/
|
*/
|
||||||
export function getEffectiveEnvVars(
|
export function getEffectiveEnvVars(
|
||||||
@@ -396,6 +408,9 @@ export function getEffectiveEnvVars(
|
|||||||
port: number = CLIPROXY_DEFAULT_PORT,
|
port: number = CLIPROXY_DEFAULT_PORT,
|
||||||
customSettingsPath?: string
|
customSettingsPath?: string
|
||||||
): NodeJS.ProcessEnv {
|
): NodeJS.ProcessEnv {
|
||||||
|
// Get global env vars (DISABLE_TELEMETRY, etc.)
|
||||||
|
const globalEnv = getGlobalEnvVars();
|
||||||
|
|
||||||
// Priority 1: Custom settings path (for user-defined variants)
|
// Priority 1: Custom settings path (for user-defined variants)
|
||||||
if (customSettingsPath) {
|
if (customSettingsPath) {
|
||||||
const expandedPath = customSettingsPath.replace(/^~/, require('os').homedir());
|
const expandedPath = customSettingsPath.replace(/^~/, require('os').homedir());
|
||||||
@@ -405,8 +420,8 @@ export function getEffectiveEnvVars(
|
|||||||
const settings: ProviderSettings = JSON.parse(content);
|
const settings: ProviderSettings = JSON.parse(content);
|
||||||
|
|
||||||
if (settings.env && typeof settings.env === 'object') {
|
if (settings.env && typeof settings.env === 'object') {
|
||||||
// Custom variant settings found - use them
|
// Custom variant settings found - merge with global env
|
||||||
return settings.env;
|
return { ...globalEnv, ...settings.env };
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Invalid JSON - fall through to provider defaults
|
// Invalid JSON - fall through to provider defaults
|
||||||
@@ -427,9 +442,8 @@ export function getEffectiveEnvVars(
|
|||||||
const settings: ProviderSettings = JSON.parse(content);
|
const settings: ProviderSettings = JSON.parse(content);
|
||||||
|
|
||||||
if (settings.env && typeof settings.env === 'object') {
|
if (settings.env && typeof settings.env === 'object') {
|
||||||
// User override found - use their settings
|
// User override found - merge with global env
|
||||||
// Note: User is responsible for correctness
|
return { ...globalEnv, ...settings.env };
|
||||||
return settings.env;
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Invalid JSON or structure - fall through to defaults
|
// Invalid JSON or structure - fall through to defaults
|
||||||
@@ -437,8 +451,8 @@ export function getEffectiveEnvVars(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// No override or invalid - use bundled defaults
|
// No override or invalid - use bundled defaults merged with global env
|
||||||
return getClaudeEnvVars(provider, port);
|
return { ...globalEnv, ...getClaudeEnvVars(provider, port) };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import {
|
|||||||
createEmptyUnifiedConfig,
|
createEmptyUnifiedConfig,
|
||||||
UNIFIED_CONFIG_VERSION,
|
UNIFIED_CONFIG_VERSION,
|
||||||
DEFAULT_COPILOT_CONFIG,
|
DEFAULT_COPILOT_CONFIG,
|
||||||
|
DEFAULT_GLOBAL_ENV,
|
||||||
|
GlobalEnvConfig,
|
||||||
} from './unified-config-types';
|
} from './unified-config-types';
|
||||||
import { isUnifiedConfigEnabled } from './feature-flags';
|
import { isUnifiedConfigEnabled } from './feature-flags';
|
||||||
|
|
||||||
@@ -170,6 +172,11 @@ function mergeWithDefaults(partial: Partial<UnifiedConfig>): UnifiedConfig {
|
|||||||
wait_on_limit: partial.copilot?.wait_on_limit ?? DEFAULT_COPILOT_CONFIG.wait_on_limit,
|
wait_on_limit: partial.copilot?.wait_on_limit ?? DEFAULT_COPILOT_CONFIG.wait_on_limit,
|
||||||
model: partial.copilot?.model ?? DEFAULT_COPILOT_CONFIG.model,
|
model: partial.copilot?.model ?? DEFAULT_COPILOT_CONFIG.model,
|
||||||
},
|
},
|
||||||
|
// Global env - injected into all non-Claude subscription profiles
|
||||||
|
global_env: {
|
||||||
|
enabled: partial.global_env?.enabled ?? true,
|
||||||
|
env: partial.global_env?.env ?? { ...DEFAULT_GLOBAL_ENV },
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,6 +343,28 @@ function generateYamlWithComments(config: UnifiedConfig): string {
|
|||||||
lines.push('');
|
lines.push('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Global env section
|
||||||
|
if (config.global_env) {
|
||||||
|
lines.push('# ----------------------------------------------------------------------------');
|
||||||
|
lines.push(
|
||||||
|
'# Global Environment Variables: Injected into all non-Claude subscription profiles'
|
||||||
|
);
|
||||||
|
lines.push('# These env vars disable telemetry/reporting for third-party providers.');
|
||||||
|
lines.push('# Configure via Dashboard (`ccs config`) > Global Env tab.');
|
||||||
|
lines.push('#');
|
||||||
|
lines.push('# Default variables:');
|
||||||
|
lines.push('# DISABLE_BUG_COMMAND: Disables /bug command (not supported by proxy)');
|
||||||
|
lines.push('# DISABLE_ERROR_REPORTING: Disables error reporting to Anthropic');
|
||||||
|
lines.push('# DISABLE_TELEMETRY: Disables usage telemetry');
|
||||||
|
lines.push('# ----------------------------------------------------------------------------');
|
||||||
|
lines.push(
|
||||||
|
yaml
|
||||||
|
.dump({ global_env: config.global_env }, { indent: 2, lineWidth: -1, quotingType: '"' })
|
||||||
|
.trim()
|
||||||
|
);
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
|
||||||
return lines.join('\n');
|
return lines.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,3 +491,15 @@ export function getWebSearchConfig(): {
|
|||||||
gemini: config.websearch?.gemini,
|
gemini: config.websearch?.gemini,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get global_env configuration.
|
||||||
|
* Returns defaults if not configured.
|
||||||
|
*/
|
||||||
|
export function getGlobalEnvConfig(): GlobalEnvConfig {
|
||||||
|
const config = loadOrCreateUnifiedConfig();
|
||||||
|
return {
|
||||||
|
enabled: config.global_env?.enabled ?? true,
|
||||||
|
env: config.global_env?.env ?? { ...DEFAULT_GLOBAL_ENV },
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -185,6 +185,28 @@ export interface CopilotConfig {
|
|||||||
haiku_model?: string;
|
haiku_model?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global environment variables configuration.
|
||||||
|
* These env vars are injected into ALL non-Claude subscription profiles.
|
||||||
|
* Useful for disabling telemetry, bug commands, error reporting, etc.
|
||||||
|
*/
|
||||||
|
export interface GlobalEnvConfig {
|
||||||
|
/** Enable global env injection (default: true) */
|
||||||
|
enabled: boolean;
|
||||||
|
/** Environment variables to inject */
|
||||||
|
env: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default global env vars for third-party profiles.
|
||||||
|
* These disable Claude Code telemetry/reporting since we're using proxy.
|
||||||
|
*/
|
||||||
|
export const DEFAULT_GLOBAL_ENV: Record<string, string> = {
|
||||||
|
DISABLE_BUG_COMMAND: '1',
|
||||||
|
DISABLE_ERROR_REPORTING: '1',
|
||||||
|
DISABLE_TELEMETRY: '1',
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WebSearch configuration.
|
* WebSearch configuration.
|
||||||
* Uses CLI tools (Gemini CLI, Grok CLI, OpenCode) for third-party profiles.
|
* Uses CLI tools (Gemini CLI, Grok CLI, OpenCode) for third-party profiles.
|
||||||
@@ -234,6 +256,8 @@ export interface UnifiedConfig {
|
|||||||
preferences: PreferencesConfig;
|
preferences: PreferencesConfig;
|
||||||
/** WebSearch configuration */
|
/** WebSearch configuration */
|
||||||
websearch?: WebSearchConfig;
|
websearch?: WebSearchConfig;
|
||||||
|
/** Global environment variables for all non-Claude subscription profiles */
|
||||||
|
global_env?: GlobalEnvConfig;
|
||||||
/** Copilot API configuration (GitHub Copilot proxy) */
|
/** Copilot API configuration (GitHub Copilot proxy) */
|
||||||
copilot?: CopilotConfig;
|
copilot?: CopilotConfig;
|
||||||
}
|
}
|
||||||
@@ -307,6 +331,10 @@ export function createEmptyUnifiedConfig(): UnifiedConfig {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
global_env: {
|
||||||
|
enabled: true,
|
||||||
|
env: { ...DEFAULT_GLOBAL_ENV },
|
||||||
|
},
|
||||||
copilot: { ...DEFAULT_COPILOT_CONFIG },
|
copilot: { ...DEFAULT_COPILOT_CONFIG },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import { CopilotConfig } from '../config/unified-config-types';
|
import { CopilotConfig } from '../config/unified-config-types';
|
||||||
|
import { getGlobalEnvConfig } from '../config/unified-config-loader';
|
||||||
import { checkAuthStatus, isCopilotApiInstalled } from './copilot-auth';
|
import { checkAuthStatus, isCopilotApiInstalled } from './copilot-auth';
|
||||||
import { isDaemonRunning, startDaemon } from './copilot-daemon';
|
import { isDaemonRunning, startDaemon } from './copilot-daemon';
|
||||||
import { ensureCopilotApi } from './copilot-package-manager';
|
import { ensureCopilotApi } from './copilot-package-manager';
|
||||||
@@ -127,9 +128,14 @@ export async function executeCopilotProfile(
|
|||||||
// Generate environment for Claude
|
// Generate environment for Claude
|
||||||
const copilotEnv = generateCopilotEnv(config);
|
const copilotEnv = generateCopilotEnv(config);
|
||||||
|
|
||||||
// Merge with current environment
|
// Get global env vars (DISABLE_TELEMETRY, etc.) for third-party profiles
|
||||||
|
const globalEnvConfig = getGlobalEnvConfig();
|
||||||
|
const globalEnv = globalEnvConfig.enabled ? globalEnvConfig.env : {};
|
||||||
|
|
||||||
|
// Merge with current environment (global env first, copilot overrides)
|
||||||
const env = {
|
const env = {
|
||||||
...process.env,
|
...process.env,
|
||||||
|
...globalEnv,
|
||||||
...copilotEnv,
|
...copilotEnv,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1789,7 +1789,7 @@ import {
|
|||||||
getInstalledVersion as getCopilotInstalledVersion,
|
getInstalledVersion as getCopilotInstalledVersion,
|
||||||
} from '../copilot';
|
} from '../copilot';
|
||||||
import { DEFAULT_COPILOT_CONFIG } from '../config/unified-config-types';
|
import { DEFAULT_COPILOT_CONFIG } from '../config/unified-config-types';
|
||||||
import { loadOrCreateUnifiedConfig } from '../config/unified-config-loader';
|
import { loadOrCreateUnifiedConfig, getGlobalEnvConfig } from '../config/unified-config-loader';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/copilot/status - Get Copilot status (auth + daemon + install info)
|
* GET /api/copilot/status - Get Copilot status (auth + daemon + install info)
|
||||||
@@ -2081,3 +2081,50 @@ apiRoutes.put('/copilot/settings/raw', (req: Request, res: Response): void => {
|
|||||||
res.status(500).json({ error: (error as Error).message });
|
res.status(500).json({ error: (error as Error).message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ==================== Global Environment Variables ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/global-env - Get global environment variables configuration
|
||||||
|
* Returns the global_env section from config.yaml
|
||||||
|
*/
|
||||||
|
apiRoutes.get('/global-env', (_req: Request, res: Response): void => {
|
||||||
|
try {
|
||||||
|
const config = getGlobalEnvConfig();
|
||||||
|
res.json(config);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: (error as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT /api/global-env - Update global environment variables configuration
|
||||||
|
* Updates the global_env section in config.yaml
|
||||||
|
*/
|
||||||
|
apiRoutes.put('/global-env', (req: Request, res: Response): void => {
|
||||||
|
try {
|
||||||
|
const { enabled, env } = req.body;
|
||||||
|
const config = loadOrCreateUnifiedConfig();
|
||||||
|
|
||||||
|
// Validate env is an object with string values
|
||||||
|
if (env !== undefined && typeof env === 'object' && env !== null) {
|
||||||
|
for (const [key, value] of Object.entries(env)) {
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
res.status(400).json({ error: `Invalid value for ${key}: must be a string` });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update global_env section
|
||||||
|
config.global_env = {
|
||||||
|
enabled: enabled ?? config.global_env?.enabled ?? true,
|
||||||
|
env: env ?? config.global_env?.env ?? {},
|
||||||
|
};
|
||||||
|
|
||||||
|
saveUnifiedConfig(config);
|
||||||
|
res.json({ success: true, config: config.global_env });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: (error as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -57,6 +57,7 @@ import {
|
|||||||
useDeletePreset,
|
useDeletePreset,
|
||||||
} from '@/hooks/use-cliproxy';
|
} from '@/hooks/use-cliproxy';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { GlobalEnvIndicator } from '@/components/global-env-indicator';
|
||||||
import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
|
import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
|
||||||
|
|
||||||
// Lazy load CodeEditor
|
// Lazy load CodeEditor
|
||||||
@@ -543,7 +544,7 @@ export function ProviderEditor({
|
|||||||
Invalid JSON syntax
|
Invalid JSON syntax
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex-1 overflow-hidden px-6 pb-6 pt-4">
|
<div className="flex-1 overflow-hidden px-6 pb-4 pt-4">
|
||||||
<div className="h-full border rounded-md overflow-hidden bg-background">
|
<div className="h-full border rounded-md overflow-hidden bg-background">
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
value={rawJsonContent}
|
value={rawJsonContent}
|
||||||
@@ -553,6 +554,12 @@ export function ProviderEditor({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Global Env Indicator */}
|
||||||
|
<div className="mx-6 mb-4">
|
||||||
|
<div className="border rounded-md overflow-hidden">
|
||||||
|
<GlobalEnvIndicator profileEnv={settings?.env} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { useCopilot, type CopilotModel, type CopilotPlanTier } from '@/hooks/use
|
|||||||
import { Loader2, Save, Code2, X, Info, RefreshCw, Sparkles, Zap, Check } from 'lucide-react';
|
import { Loader2, Save, Code2, X, Info, RefreshCw, Sparkles, Zap, Check } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { ConfirmDialog } from '@/components/confirm-dialog';
|
import { ConfirmDialog } from '@/components/confirm-dialog';
|
||||||
|
import { GlobalEnvIndicator } from '@/components/global-env-indicator';
|
||||||
|
|
||||||
// Lazy load CodeEditor
|
// Lazy load CodeEditor
|
||||||
const CodeEditor = lazy(() =>
|
const CodeEditor = lazy(() =>
|
||||||
@@ -726,7 +727,7 @@ export function CopilotConfigForm() {
|
|||||||
Invalid JSON syntax
|
Invalid JSON syntax
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex-1 overflow-hidden px-6 pb-6 pt-4">
|
<div className="flex-1 overflow-hidden px-6 pb-4 pt-4">
|
||||||
<div className="h-full border rounded-md overflow-hidden bg-background">
|
<div className="h-full border rounded-md overflow-hidden bg-background">
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
value={rawJsonContent}
|
value={rawJsonContent}
|
||||||
@@ -736,6 +737,12 @@ export function CopilotConfigForm() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Global Env Indicator */}
|
||||||
|
<div className="mx-6 mb-4">
|
||||||
|
<div className="border rounded-md overflow-hidden">
|
||||||
|
<GlobalEnvIndicator profileEnv={rawSettings?.settings?.env} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
/**
|
||||||
|
* Global Environment Variables Indicator
|
||||||
|
*
|
||||||
|
* Shows which env vars from global_env will be injected at runtime.
|
||||||
|
* Displayed below the Raw Configuration (JSON) section in profile editors.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { Settings2, ChevronDown, ChevronUp, ExternalLink, Info } from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
||||||
|
interface GlobalEnvConfig {
|
||||||
|
enabled: boolean;
|
||||||
|
env: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GlobalEnvIndicatorProps {
|
||||||
|
/** Current profile's env vars (to show which are overridden) */
|
||||||
|
profileEnv?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GlobalEnvIndicator({ profileEnv = {} }: GlobalEnvIndicatorProps) {
|
||||||
|
const [config, setConfig] = useState<GlobalEnvConfig | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchConfig();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchConfig = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const res = await fetch('/api/global-env');
|
||||||
|
if (!res.ok) throw new Error('Failed to load');
|
||||||
|
const data = await res.json();
|
||||||
|
setConfig(data);
|
||||||
|
} catch {
|
||||||
|
setConfig(null);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Don't render if loading or disabled or no vars
|
||||||
|
if (loading) return null;
|
||||||
|
if (!config?.enabled) return null;
|
||||||
|
|
||||||
|
const envVars = config.env || {};
|
||||||
|
const envKeys = Object.keys(envVars);
|
||||||
|
if (envKeys.length === 0) return null;
|
||||||
|
|
||||||
|
// Check which keys are already in profile (won't be overridden)
|
||||||
|
const injectedKeys = envKeys.filter((key) => !(key in profileEnv));
|
||||||
|
const overriddenKeys = envKeys.filter((key) => key in profileEnv);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border-t bg-muted/20">
|
||||||
|
{/* Header - clickable to expand */}
|
||||||
|
<button
|
||||||
|
onClick={() => setExpanded(!expanded)}
|
||||||
|
className="w-full px-4 py-2 flex items-center gap-2 hover:bg-muted/30 transition-colors"
|
||||||
|
>
|
||||||
|
<Info className="w-4 h-4 text-blue-500" />
|
||||||
|
<span className="text-xs text-muted-foreground flex-1 text-left">
|
||||||
|
<span className="font-medium text-foreground">{injectedKeys.length}</span> global env var
|
||||||
|
{injectedKeys.length !== 1 ? 's' : ''} will be injected at runtime
|
||||||
|
{overriddenKeys.length > 0 && (
|
||||||
|
<span className="text-amber-600 dark:text-amber-400 ml-1">
|
||||||
|
({overriddenKeys.length} overridden by profile)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
{expanded ? (
|
||||||
|
<ChevronUp className="w-4 h-4 text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="w-4 h-4 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Expanded content */}
|
||||||
|
{expanded && (
|
||||||
|
<div className="px-4 pb-3 space-y-2">
|
||||||
|
{/* Injected vars */}
|
||||||
|
{injectedKeys.length > 0 && (
|
||||||
|
<div className="space-y-1">
|
||||||
|
{injectedKeys.map((key) => (
|
||||||
|
<div
|
||||||
|
key={key}
|
||||||
|
className="flex items-center gap-2 text-xs font-mono bg-green-500/10 text-green-700 dark:text-green-400 px-2 py-1 rounded"
|
||||||
|
>
|
||||||
|
<span className="text-green-500">+</span>
|
||||||
|
<span className="truncate">
|
||||||
|
{key}={envVars[key]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Overridden vars (profile takes precedence) */}
|
||||||
|
{overriddenKeys.length > 0 && (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-xs text-muted-foreground">Skipped (profile already defines):</p>
|
||||||
|
{overriddenKeys.map((key) => (
|
||||||
|
<div
|
||||||
|
key={key}
|
||||||
|
className="flex items-center gap-2 text-xs font-mono bg-amber-500/10 text-amber-700 dark:text-amber-400 px-2 py-1 rounded"
|
||||||
|
>
|
||||||
|
<span className="text-amber-500">~</span>
|
||||||
|
<span className="truncate">{key}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Link to settings */}
|
||||||
|
<div className="pt-2 border-t border-border/50">
|
||||||
|
<Button variant="ghost" size="sm" asChild className="h-7 text-xs gap-1.5 -ml-2">
|
||||||
|
<Link to="/settings?tab=globalenv">
|
||||||
|
<Settings2 className="w-3.5 h-3.5" />
|
||||||
|
Configure in Settings
|
||||||
|
<ExternalLink className="w-3 h-3 opacity-50" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import { Save, Loader2, Code2, Trash2, RefreshCw, Plus, X, Info } from 'lucide-r
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { CopyButton } from '@/components/ui/copy-button';
|
import { CopyButton } from '@/components/ui/copy-button';
|
||||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
||||||
|
import { GlobalEnvIndicator } from '@/components/global-env-indicator';
|
||||||
|
|
||||||
// Lazy load CodeEditor to reduce initial bundle size
|
// Lazy load CodeEditor to reduce initial bundle size
|
||||||
const CodeEditor = lazy(() =>
|
const CodeEditor = lazy(() =>
|
||||||
@@ -412,7 +413,7 @@ export function ProfileEditor({ profileName, onDelete }: ProfileEditorProps) {
|
|||||||
Invalid JSON syntax
|
Invalid JSON syntax
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex-1 overflow-hidden px-6 pb-6 pt-4">
|
<div className="flex-1 overflow-hidden px-6 pb-4 pt-4">
|
||||||
<div className="h-full border rounded-md overflow-hidden bg-background">
|
<div className="h-full border rounded-md overflow-hidden bg-background">
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
value={rawJsonContent}
|
value={rawJsonContent}
|
||||||
@@ -422,6 +423,12 @@ export function ProfileEditor({ profileName, onDelete }: ProfileEditorProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Global Env Indicator */}
|
||||||
|
<div className="mx-6 mb-4">
|
||||||
|
<div className="border rounded-md overflow-hidden">
|
||||||
|
<GlobalEnvIndicator profileEnv={settings?.env} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
|
|||||||
+783
-339
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user