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

View File

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

View File

@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { execSync } from "child_process"; import { execSync } from "child_process";
import { tmpdir } from "node:os"; 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"; import JSON5 from "json5";
export interface StatusLineModuleConfig { export interface StatusLineModuleConfig {
@@ -11,7 +11,8 @@ export interface StatusLineModuleConfig {
text: string; text: string;
color?: string; color?: string;
background?: 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 { export interface StatusLineThemeConfig {
@@ -162,7 +163,7 @@ function replaceVariables(text: string, variables: Record<string, string>): stri
} }
// Execute script and get output // 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 { try {
// Check if file exists // Check if file exists
await fs.access(scriptPath); 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 export is a function, call it with variables
if (typeof scriptModule === 'function') { if (typeof scriptModule === 'function') {
const result = scriptModule(variables); const result = scriptModule(variables, options);
// If returns a Promise, wait for it to complete // If returns a Promise, wait for it to complete
if (result instanceof Promise) { if (result instanceof Promise) {
return await result; return await result;
@@ -532,6 +533,37 @@ async function getProjectThemeConfig(): Promise<{ theme: StatusLineThemeConfig |
return { theme: null, style: 'default' }; 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) // 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 // When environment variable USE_SIMPLE_ICONS is set, or when a terminal that might not support Nerd Fonts is detected
function shouldUseSimpleTheme(): boolean { function shouldUseSimpleTheme(): boolean {
@@ -585,36 +617,7 @@ function canDisplayNerdFonts(): boolean {
return process.env.USE_SIMPLE_ICONS !== 'true'; return process.env.USE_SIMPLE_ICONS !== 'true';
} }
// Check if specific Unicode characters can be displayed correctly export async function parseStatusLineData(input: StatusLineInput, presetName?: string): Promise<string> {
// 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> {
try { try {
// Check if simple theme should be used // Check if simple theme should be used
const useSimpleTheme = shouldUseSimpleTheme(); 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 // 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; const effectiveTheme = useSimpleTheme || !canDisplayNerd ? SIMPLE_THEME : DEFAULT_THEME;
// Get theme configuration from home directory, or use the determined default configuration // Get theme configuration: preset config > home directory config > default theme
const { theme: projectTheme, style: currentStyle } = await getProjectThemeConfig(); 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; const theme = projectTheme || effectiveTheme;
// Get current working directory and Git branch // 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 // Render default style status line
async function renderDefaultStyle( async function renderDefaultStyle(
theme: StatusLineThemeConfig, theme: StatusLineThemeConfig,
@@ -831,7 +822,7 @@ async function renderDefaultStyle(
// If script type, execute script to get text // If script type, execute script to get text
let text = ""; let text = "";
if (module.type === "script" && module.scriptPath) { if (module.type === "script" && module.scriptPath) {
text = await executeScript(module.scriptPath, variables); text = await executeScript(module.scriptPath, variables, module.options);
} else { } else {
text = replaceVariables(module.text, variables); 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 preset info, config uses the applied userValues configuration
return { return {
...presetFile, ...presetFile,
config: loadConfigFromManifest(manifest), config: loadConfigFromManifest(manifest, presetDir),
userValues: manifest.userValues || {}, userValues: manifest.userValues || {},
}; };
} catch (error: any) { } catch (error: any) {

View File

@@ -450,7 +450,7 @@ export async function listPresets(): Promise<PresetInfo[]> {
version: manifest.version, version: manifest.version,
description: manifest.description, description: manifest.description,
author: manifest.author, author: manifest.author,
config: loadConfigFromManifest(manifest), config: loadConfigFromManifest(manifest, presetDir),
}); });
} catch { } catch {
// Ignore invalid preset directories (no manifest.json or read failed) // 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 * Responsible for parsing and validating configuration schema, handling conditional logic and variable replacement
*/ */
import path from 'path';
import { import {
RequiredInput, RequiredInput,
InputType, InputType,
@@ -180,8 +181,8 @@ export function getDynamicOptions(
return []; return [];
} }
// Parse provider reference (e.g. {{selectedProvider}}) // Parse provider reference (e.g. #{selectedProvider})
const providerId = String(providerField).replace(/^{{(.+)}}$/, '$1'); const providerId = String(providerField).replace(/^#{(.+)}$/, '$1');
const selectedProvider = values[providerId]; const selectedProvider = values[providerId];
if (!selectedProvider || !presetConfig.Providers) { if (!selectedProvider || !presetConfig.Providers) {
@@ -242,7 +243,7 @@ export function resolveOptions(
/** /**
* Template variable replacement * Template variable replacement
* Supports {{variable}} syntax * Supports #{variable} syntax (different from statusline's {{variable}} format)
*/ */
export function replaceTemplateVariables( export function replaceTemplateVariables(
template: any, template: any,
@@ -254,7 +255,7 @@ export function replaceTemplateVariables(
// Handle strings // Handle strings
if (typeof template === 'string') { if (typeof template === 'string') {
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => { return template.replace(/#{(\w+)}/g, (_, key) => {
return values[key] !== undefined ? String(values[key]) : ''; return values[key] !== undefined ? String(values[key]) : '';
}); });
} }
@@ -295,9 +296,9 @@ export function applyConfigMappings(
// Resolve value // Resolve value
let value: any; let value: any;
if (typeof mapping.value === 'string' && mapping.value.startsWith('{{')) { if (typeof mapping.value === 'string' && mapping.value.startsWith('#')) {
// Variable reference // Variable reference
const varName = mapping.value.replace(/^{{(.+)}}$/, '$1'); const varName = mapping.value.replace(/^#{(.+)}$/, '$1');
value = values[varName]; value = values[varName];
} else { } else {
// Fixed value // Fixed value
@@ -338,7 +339,7 @@ export function applyUserInputs(
const schemaFields = getSchemaFields(presetFile.schema); const schemaFields = getSchemaFields(presetFile.schema);
// 1. First apply template (if exists) // 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) { if (presetFile.template) {
config = replaceTemplateVariables(presetFile.template, values) as any; config = replaceTemplateVariables(presetFile.template, values) as any;
} else { } else {
@@ -347,7 +348,7 @@ export function applyUserInputs(
// These fields will be updated or replaced in subsequent configMappings // These fields will be updated or replaced in subsequent configMappings
config = presetFile.config ? { ...presetFile.config } : {}; 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; config = replaceTemplateVariables(config, values) as any;
// Finally, remove schema id fields (they should not appear in final configuration) // Finally, remove schema id fields (they should not appear in final configuration)
@@ -557,7 +558,7 @@ export function buildDependencyGraph(
if (field.options) { if (field.options) {
const options = field.options as any; const options = field.options as any;
if (options.type === 'models' && options.providerField) { if (options.type === 'models' && options.providerField) {
const providerId = String(options.providerField).replace(/^{{(.+)}}$/, '$1'); const providerId = String(options.providerField).replace(/^#{(.+)}$/, '$1');
deps.add(providerId); deps.add(providerId);
} }
} }
@@ -588,14 +589,78 @@ export function getAffectedFields(
return affected; 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 * Load configuration from Manifest and apply userValues
* Used when reading installed presets, applying user configuration values at runtime * Used when reading installed presets, applying user configuration values at runtime
* *
* @param manifest Manifest object (contains original configuration and userValues) * @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 * @returns Applied configuration object
*/ */
export function loadConfigFromManifest(manifest: ManifestFile): PresetConfigSection { export function loadConfigFromManifest(manifest: ManifestFile, presetDir?: string): PresetConfigSection {
// Convert manifest to PresetFile format // Convert manifest to PresetFile format
const presetFile: PresetFile = { const presetFile: PresetFile = {
metadata: { metadata: {
@@ -631,11 +696,25 @@ export function loadConfigFromManifest(manifest: ManifestFile): PresetConfigSect
} }
} }
let config: PresetConfigSection;
// If userValues exist, apply them // If userValues exist, apply them
if (manifest.userValues && Object.keys(manifest.userValues).length > 0) { 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 // Process StatusLine configuration (convert relative scriptPath to absolute path)
return presetFile.config; 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 // Automatically extract name and related configuration from preset's Providers
// Used when type is 'models' // 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) // Used when type is 'custom' (reserved)
source?: string; // Custom data source source?: string; // Custom data source
@@ -152,8 +152,8 @@ export interface PresetConfigSection {
// Template configuration (for dynamically generating configuration based on user input) // Template configuration (for dynamically generating configuration based on user input)
export interface TemplateConfig { export interface TemplateConfig {
// Template configuration using {{variable}} syntax // Template configuration using #{variable} syntax (different from statusline's {{variable}} format)
// Example: { "Providers": [{ "name": "{{providerName}}", "api_key": "{{apiKey}}" }] } // Example: { "Providers": [{ "name": "#{providerName}", "api_key": "#{apiKey}" }] }
[key: string]: any; [key: string]: any;
} }
@@ -163,7 +163,7 @@ export interface ConfigMapping {
target: string; target: string;
// Value source (references user input id, or uses fixed value) // 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) // Condition (optional, apply this mapping only when condition is met)
when?: Condition | Condition[]; when?: Condition | Condition[];