fix statusline error

This commit is contained in:
musistudio
2026-01-01 14:51:13 +08:00
parent 10c69a586b
commit d149517026
7 changed files with 198 additions and 114 deletions

View File

@@ -12,7 +12,7 @@ import { activateCommand } from "./utils/activateCommand";
import { readConfigFile } from "./utils";
import { version } from "../package.json";
import { spawn, exec } from "child_process";
import {PID_FILE, readPresetFile, REFERENCE_COUNT_FILE} from "@CCR/shared";
import {getPresetDir, loadConfigFromManifest, PID_FILE, readPresetFile, REFERENCE_COUNT_FILE} from "@CCR/shared";
import fs, { existsSync, readFileSync } from "fs";
import { join } from "path";
import { parseStatusLineData, StatusLineInput } from "./utils/statusline";
@@ -81,7 +81,7 @@ async function waitForService(
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
const isRunning = await isServiceRunning()
const isRunning = isServiceRunning()
if (isRunning) {
// Wait for an additional short period to ensure service is fully ready
await new Promise((resolve) => setTimeout(resolve, 500));
@@ -93,43 +93,46 @@ async function waitForService(
}
async function main() {
const isRunning = await isServiceRunning()
const isRunning = isServiceRunning()
// If command is not a known command, check if it's a preset
if (command && !KNOWN_COMMANDS.includes(command)) {
const presetData: any = await readPresetFile(command);
const manifest = await readPresetFile(command);
if (presetData) {
// This is a preset, execute code command
if (manifest) {
// This is a preset, load its configuration
const presetDir = getPresetDir(command);
const config = loadConfigFromManifest(manifest, presetDir);
// Execute code command
const codeArgs = process.argv.slice(3); // Get remaining arguments
// Check noServer configuration
const shouldStartServer = presetData.noServer !== true;
const shouldStartServer = config.noServer !== true;
// Build environment variable overrides
let envOverrides: Record<string, string> | undefined;
let envOverrides: Record<string, string> = {};
// Handle provider configuration (supports both old and new formats)
let provider: any = null;
// Old format: presetData.provider is the provider name
if (presetData.provider && typeof presetData.provider === 'string') {
const config = await readConfigFile();
provider = config.Providers?.find((p: any) => p.name === presetData.provider);
// Old format: config.provider is the provider name
if (config.provider && typeof config.provider === 'string') {
const globalConfig = await readConfigFile();
provider = globalConfig.Providers?.find((p: any) => p.name === config.provider);
}
// New format: presetData.Providers is an array of providers
else if (presetData.Providers && presetData.Providers.length > 0) {
provider = presetData.Providers[0];
// New format: config.Providers is an array of providers
else if (config.Providers && config.Providers.length > 0) {
provider = config.Providers[0];
}
// If noServer is not true, use local server baseurl
if (shouldStartServer) {
const config = await readConfigFile();
const port = config.PORT || 3456;
const presetName = command;
const globalConfig = await readConfigFile();
const port = globalConfig.PORT || 3456;
envOverrides = {
...envOverrides,
ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}/preset/${presetName}`,
ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}/preset/${command}`,
};
} else if (provider) {
// Handle api_base_url, remove /v1/messages suffix
@@ -157,10 +160,9 @@ async function main() {
// Build PresetConfig
const presetConfig: PresetConfig = {
noServer: presetData.noServer,
claudeCodeSettings: presetData.claudeCodeSettings,
provider: presetData.provider,
router: presetData.router,
noServer: config.noServer,
claudeCodeSettings: config.claudeCodeSettings,
StatusLine: config.StatusLine
};
if (shouldStartServer && !isRunning) {
@@ -179,7 +181,7 @@ async function main() {
startProcess.unref();
if (await waitForService()) {
executeCodeCommand(codeArgs, presetConfig, envOverrides);
executeCodeCommand(codeArgs, presetConfig, envOverrides, command);
} else {
console.error(
"Service startup timeout, please manually run `ccr start` to start the service"
@@ -192,7 +194,7 @@ async function main() {
console.error("Service is not running. Please start it first with `ccr start`");
process.exit(1);
}
executeCodeCommand(codeArgs, presetConfig, envOverrides);
executeCodeCommand(codeArgs, presetConfig, envOverrides, command);
}
return;
} else {
@@ -245,7 +247,9 @@ async function main() {
process.stdin.on("end", async () => {
try {
const input: StatusLineInput = JSON.parse(inputData);
const statusLine = await parseStatusLineData(input);
// Check if preset name is provided as argument
const presetName = process.argv[3];
const statusLine = await parseStatusLineData(input, presetName);
console.log(statusLine);
} catch (error) {
console.error("Error parsing status line data:", error);

View File

@@ -18,13 +18,15 @@ export interface PresetConfig {
};
provider?: string;
router?: Record<string, any>;
StatusLine?: any; // Preset's StatusLine configuration
[key: string]: any;
}
export async function executeCodeCommand(
args: string[] = [],
presetConfig?: PresetConfig | null,
envOverrides?: Record<string, string>
envOverrides?: Record<string, string>,
presetName?: string // Preset name for statusline command
) {
// Set environment variables using shared function
const config = await readConfigFile();
@@ -40,11 +42,19 @@ export async function executeCodeCommand(
env: env as ClaudeSettingsFlag['env']
};
// Add statusLine if StatusLine is configured
if (config?.StatusLine?.enabled) {
// Add statusLine configuration
// Priority: preset.StatusLine > global config.StatusLine
const statusLineConfig = presetConfig?.StatusLine || config?.StatusLine;
if (statusLineConfig?.enabled) {
// If using preset, pass preset name to statusline command
const statuslineCommand = presetName
? `ccr statusline ${presetName}`
: "ccr statusline";
settingsFlag.statusLine = {
type: "command",
command: "ccr statusline",
command: statuslineCommand,
padding: 0,
}
}

View File

@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
import path from "node:path";
import { execSync } from "child_process";
import { tmpdir } from "node:os";
import { CONFIG_FILE, HOME_DIR } from "@CCR/shared";
import { CONFIG_FILE, HOME_DIR, readPresetFile, getPresetDir, loadConfigFromManifest } from "@CCR/shared";
import JSON5 from "json5";
export interface StatusLineModuleConfig {
@@ -11,7 +11,8 @@ export interface StatusLineModuleConfig {
text: string;
color?: string;
background?: string;
scriptPath?: string; // Path to the Node.js script file to execute for script-type modules
scriptPath?: string;
options?: Record<string, any>;
}
export interface StatusLineThemeConfig {
@@ -162,7 +163,7 @@ function replaceVariables(text: string, variables: Record<string, string>): stri
}
// Execute script and get output
async function executeScript(scriptPath: string, variables: Record<string, string>): Promise<string> {
async function executeScript(scriptPath: string, variables: Record<string, string>, options?: Record<string, any>): Promise<string> {
try {
// Check if file exists
await fs.access(scriptPath);
@@ -172,7 +173,7 @@ async function executeScript(scriptPath: string, variables: Record<string, strin
// If export is a function, call it with variables
if (typeof scriptModule === 'function') {
const result = scriptModule(variables);
const result = scriptModule(variables, options);
// If returns a Promise, wait for it to complete
if (result instanceof Promise) {
return await result;
@@ -532,6 +533,37 @@ async function getProjectThemeConfig(): Promise<{ theme: StatusLineThemeConfig |
return { theme: null, style: 'default' };
}
// Read theme configuration from preset
async function getPresetThemeConfig(presetName: string): Promise<{ theme: StatusLineThemeConfig | null, style: string }> {
try {
// Read preset manifest
const manifest = await readPresetFile(presetName);
if (!manifest) {
return { theme: null, style: 'default' };
}
// Load preset configuration (applies userValues if present)
const presetDir = getPresetDir(presetName);
const config = loadConfigFromManifest(manifest, presetDir);
// Check if there's StatusLine configuration in preset
if (config.StatusLine) {
// Get current style, default to 'default'
const currentStyle = config.StatusLine.currentStyle || 'default';
// Check if there's configuration for the corresponding style
if (config.StatusLine[currentStyle] && config.StatusLine[currentStyle].modules) {
return { theme: config.StatusLine[currentStyle], style: currentStyle };
}
}
} catch (error) {
// Return null if reading fails
// console.error("Failed to read preset theme config:", error);
}
return { theme: null, style: 'default' };
}
// Check if simple theme should be used (fallback scheme)
// When environment variable USE_SIMPLE_ICONS is set, or when a terminal that might not support Nerd Fonts is detected
function shouldUseSimpleTheme(): boolean {
@@ -585,36 +617,7 @@ function canDisplayNerdFonts(): boolean {
return process.env.USE_SIMPLE_ICONS !== 'true';
}
// Check if specific Unicode characters can be displayed correctly
// This is a simple heuristic check
function canDisplayUnicodeCharacter(char: string): boolean {
// For Nerd Font icons, we assume UTF-8 terminals can display them
// But accurate detection is difficult, so we rely on environment variables and terminal type detection
try {
// Check if terminal supports UTF-8
const lang = process.env.LANG || process.env.LC_ALL || process.env.LC_CTYPE || '';
if (lang.includes('UTF-8') || lang.includes('utf8') || lang.includes('UTF8')) {
return true;
}
// Check LC_* environment variables
const lcVars = ['LC_ALL', 'LC_CTYPE', 'LANG'];
for (const lcVar of lcVars) {
const value = process.env[lcVar];
if (value && (value.includes('UTF-8') || value.includes('utf8'))) {
return true;
}
}
} catch (e) {
// If check fails, default to true
return true;
}
// By default, assume it can be displayed
return true;
}
export async function parseStatusLineData(input: StatusLineInput): Promise<string> {
export async function parseStatusLineData(input: StatusLineInput, presetName?: string): Promise<string> {
try {
// Check if simple theme should be used
const useSimpleTheme = shouldUseSimpleTheme();
@@ -625,8 +628,24 @@ export async function parseStatusLineData(input: StatusLineInput): Promise<strin
// Determine which theme to use: use simple theme if user forces it or Nerd Fonts cannot be displayed
const effectiveTheme = useSimpleTheme || !canDisplayNerd ? SIMPLE_THEME : DEFAULT_THEME;
// Get theme configuration from home directory, or use the determined default configuration
const { theme: projectTheme, style: currentStyle } = await getProjectThemeConfig();
// Get theme configuration: preset config > home directory config > default theme
let projectTheme: StatusLineThemeConfig | null = null;
let currentStyle = 'default';
if (presetName) {
// Try to get theme configuration from preset first
const presetConfig = await getPresetThemeConfig(presetName);
projectTheme = presetConfig.theme;
currentStyle = presetConfig.style;
}
// If preset theme not found or no preset specified, try home directory config
if (!projectTheme) {
const homeConfig = await getProjectThemeConfig();
projectTheme = homeConfig.theme;
currentStyle = homeConfig.style;
}
const theme = projectTheme || effectiveTheme;
// Get current working directory and Git branch
@@ -784,34 +803,6 @@ export async function parseStatusLineData(input: StatusLineInput): Promise<strin
}
}
// Read theme configuration from user home directory (specified style)
async function getProjectThemeConfigForStyle(style: string): Promise<StatusLineThemeConfig | null> {
try {
// Only use fixed configuration file in home directory
const configPath = CONFIG_FILE;
// Check if configuration file exists
try {
await fs.access(configPath);
} catch {
return null;
}
const configContent = await fs.readFile(configPath, "utf-8");
const config = JSON5.parse(configContent);
// Check if there's StatusLine configuration
if (config.StatusLine && config.StatusLine[style] && config.StatusLine[style].modules) {
return config.StatusLine[style];
}
} catch (error) {
// Return null if reading fails
// console.error("Failed to read theme config:", error);
}
return null;
}
// Render default style status line
async function renderDefaultStyle(
theme: StatusLineThemeConfig,
@@ -831,7 +822,7 @@ async function renderDefaultStyle(
// If script type, execute script to get text
let text = "";
if (module.type === "script" && module.scriptPath) {
text = await executeScript(module.scriptPath, variables);
text = await executeScript(module.scriptPath, variables, module.options);
} else {
text = replaceVariables(module.text, variables);
}

View File

@@ -279,7 +279,7 @@ export const createServer = async (config: any): Promise<any> => {
// Return preset info, config uses the applied userValues configuration
return {
...presetFile,
config: loadConfigFromManifest(manifest),
config: loadConfigFromManifest(manifest, presetDir),
userValues: manifest.userValues || {},
};
} catch (error: any) {

View File

@@ -450,7 +450,7 @@ export async function listPresets(): Promise<PresetInfo[]> {
version: manifest.version,
description: manifest.description,
author: manifest.author,
config: loadConfigFromManifest(manifest),
config: loadConfigFromManifest(manifest, presetDir),
});
} catch {
// Ignore invalid preset directories (no manifest.json or read failed)

View File

@@ -3,6 +3,7 @@
* Responsible for parsing and validating configuration schema, handling conditional logic and variable replacement
*/
import path from 'path';
import {
RequiredInput,
InputType,
@@ -180,8 +181,8 @@ export function getDynamicOptions(
return [];
}
// Parse provider reference (e.g. {{selectedProvider}})
const providerId = String(providerField).replace(/^{{(.+)}}$/, '$1');
// Parse provider reference (e.g. #{selectedProvider})
const providerId = String(providerField).replace(/^#{(.+)}$/, '$1');
const selectedProvider = values[providerId];
if (!selectedProvider || !presetConfig.Providers) {
@@ -242,7 +243,7 @@ export function resolveOptions(
/**
* Template variable replacement
* Supports {{variable}} syntax
* Supports #{variable} syntax (different from statusline's {{variable}} format)
*/
export function replaceTemplateVariables(
template: any,
@@ -254,7 +255,7 @@ export function replaceTemplateVariables(
// Handle strings
if (typeof template === 'string') {
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
return template.replace(/#{(\w+)}/g, (_, key) => {
return values[key] !== undefined ? String(values[key]) : '';
});
}
@@ -295,9 +296,9 @@ export function applyConfigMappings(
// Resolve value
let value: any;
if (typeof mapping.value === 'string' && mapping.value.startsWith('{{')) {
if (typeof mapping.value === 'string' && mapping.value.startsWith('#')) {
// Variable reference
const varName = mapping.value.replace(/^{{(.+)}}$/, '$1');
const varName = mapping.value.replace(/^#{(.+)}$/, '$1');
value = values[varName];
} else {
// Fixed value
@@ -338,7 +339,7 @@ export function applyUserInputs(
const schemaFields = getSchemaFields(presetFile.schema);
// 1. First apply template (if exists)
// template completely defines configuration structure, using {{variable}} placeholders
// template completely defines configuration structure, using #{variable} placeholders
if (presetFile.template) {
config = replaceTemplateVariables(presetFile.template, values) as any;
} else {
@@ -347,7 +348,7 @@ export function applyUserInputs(
// These fields will be updated or replaced in subsequent configMappings
config = presetFile.config ? { ...presetFile.config } : {};
// Replace placeholders in config (e.g. {{apiKey}} -> actual value)
// Replace placeholders in config (e.g. #{apiKey} -> actual value)
config = replaceTemplateVariables(config, values) as any;
// Finally, remove schema id fields (they should not appear in final configuration)
@@ -557,7 +558,7 @@ export function buildDependencyGraph(
if (field.options) {
const options = field.options as any;
if (options.type === 'models' && options.providerField) {
const providerId = String(options.providerField).replace(/^{{(.+)}}$/, '$1');
const providerId = String(options.providerField).replace(/^#{(.+)}$/, '$1');
deps.add(providerId);
}
}
@@ -588,14 +589,78 @@ export function getAffectedFields(
return affected;
}
/**
* Process StatusLine configuration, convert relative scriptPath to absolute path
* @param statusLineConfig StatusLine configuration
* @param presetDir Preset directory path
*/
function processStatusLineConfig(statusLineConfig: any, presetDir?: string): any {
if (!statusLineConfig || typeof statusLineConfig !== 'object') {
return statusLineConfig;
}
const result = { ...statusLineConfig };
// Process each theme's modules
for (const themeKey of Object.keys(result)) {
const theme = result[themeKey];
if (theme && typeof theme === 'object' && theme.modules) {
const modules = Array.isArray(theme.modules) ? theme.modules : [];
const processedModules = modules.map((module: any) => {
// If module has scriptPath and presetDir is provided, convert to absolute path
if (module.scriptPath && presetDir && !module.scriptPath.startsWith('/')) {
return {
...module,
scriptPath: path.join(presetDir, module.scriptPath)
};
}
return module;
});
result[themeKey] = {
...theme,
modules: processedModules
};
}
}
return result;
}
/**
* Process transformers configuration, convert relative path to absolute path
* @param transformersConfig Transformers configuration array
* @param presetDir Preset directory path
*/
function processTransformersConfig(transformersConfig: any[], presetDir?: string): any[] {
if (!transformersConfig || !Array.isArray(transformersConfig)) {
return transformersConfig;
}
if (!presetDir) {
return transformersConfig;
}
return transformersConfig.map((transformer: any) => {
// If transformer has path and it's a relative path, convert to absolute path
if (transformer.path && !transformer.path.startsWith('/')) {
return {
...transformer,
path: path.join(presetDir, transformer.path)
};
}
return transformer;
});
}
/**
* Load configuration from Manifest and apply userValues
* Used when reading installed presets, applying user configuration values at runtime
*
* @param manifest Manifest object (contains original configuration and userValues)
* @param presetDir Optional preset directory path (for resolving relative paths like scriptPath)
* @returns Applied configuration object
*/
export function loadConfigFromManifest(manifest: ManifestFile): PresetConfigSection {
export function loadConfigFromManifest(manifest: ManifestFile, presetDir?: string): PresetConfigSection {
// Convert manifest to PresetFile format
const presetFile: PresetFile = {
metadata: {
@@ -631,11 +696,25 @@ export function loadConfigFromManifest(manifest: ManifestFile): PresetConfigSect
}
}
let config: PresetConfigSection;
// If userValues exist, apply them
if (manifest.userValues && Object.keys(manifest.userValues).length > 0) {
return applyUserInputs(presetFile, manifest.userValues);
config = applyUserInputs(presetFile, manifest.userValues);
} else {
// If no userValues, use original configuration directly
config = presetFile.config;
}
// If no userValues, return original configuration directly
return presetFile.config;
// Process StatusLine configuration (convert relative scriptPath to absolute path)
if (config.StatusLine) {
config.StatusLine = processStatusLineConfig(config.StatusLine, presetDir);
}
// Process transformers configuration (convert relative path to absolute path)
if (config.transformers) {
config.transformers = processTransformersConfig(config.transformers, presetDir);
}
return config;
}

View File

@@ -42,7 +42,7 @@ export interface DynamicOptions {
// Automatically extract name and related configuration from preset's Providers
// Used when type is 'models'
providerField?: string; // Point to provider selector field path (e.g. "{{selectedProvider}}")
providerField?: string; // Point to provider selector field path (e.g. "#{selectedProvider}")
// Used when type is 'custom' (reserved)
source?: string; // Custom data source
@@ -152,8 +152,8 @@ export interface PresetConfigSection {
// Template configuration (for dynamically generating configuration based on user input)
export interface TemplateConfig {
// Template configuration using {{variable}} syntax
// Example: { "Providers": [{ "name": "{{providerName}}", "api_key": "{{apiKey}}" }] }
// Template configuration using #{variable} syntax (different from statusline's {{variable}} format)
// Example: { "Providers": [{ "name": "#{providerName}", "api_key": "#{apiKey}" }] }
[key: string]: any;
}
@@ -163,7 +163,7 @@ export interface ConfigMapping {
target: string;
// Value source (references user input id, or uses fixed value)
value: string | any; // If string and starts with {{, treated as variable reference
value: string | any; // If string and starts with #, treated as variable reference (e.g. #{fieldId})
// Condition (optional, apply this mapping only when condition is met)
when?: Condition | Condition[];