chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,400 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import YAML, { isMap } from 'yaml'
|
||||
|
||||
import {
|
||||
LEON_PROFILE_PATH,
|
||||
PROFILE_CONFIG_PATH,
|
||||
PROFILE_DOT_ENV_PATH
|
||||
} from '@/leon-roots'
|
||||
|
||||
import { createSetupStatus } from './setup-status'
|
||||
|
||||
const CONFIG_PATH = PROFILE_CONFIG_PATH
|
||||
const CONFIG_SAMPLE_PATH = path.join(process.cwd(), 'config.sample.yml')
|
||||
const CONFIG_SCHEMA_PATH = path.join(
|
||||
process.cwd(),
|
||||
'schemas',
|
||||
'core-schemas',
|
||||
'config.json'
|
||||
)
|
||||
const PROFILE_CONFIG_SCHEMA_PATH = path.join(
|
||||
LEON_PROFILE_PATH,
|
||||
'schemas',
|
||||
'core-schemas',
|
||||
'config.json'
|
||||
)
|
||||
const LEGACY_DISABLED_PATH = path.join(LEON_PROFILE_PATH, 'disabled.json')
|
||||
const LEGACY_ALLOWED_PATH = path.join(LEON_PROFILE_PATH, 'allowed.json')
|
||||
const YAML_SCHEMA_COMMENT_PATTERN =
|
||||
/^# yaml-language-server: \$schema=.*(?:\r?\n)?/
|
||||
const PROFILE_YAML_SCHEMA_REFERENCE = './schemas/core-schemas/config.json'
|
||||
const ENV_LINE_SEPARATOR_PATTERN = /\r?\n/
|
||||
const OPTIONAL_STRING_CONFIG_PATHS = [
|
||||
['llm', 'default'],
|
||||
['llm', 'workflow'],
|
||||
['llm', 'agent'],
|
||||
['time_zone']
|
||||
]
|
||||
|
||||
function getPairKey(pair) {
|
||||
return String(pair.key?.value || pair.key || '').trim()
|
||||
}
|
||||
|
||||
function findPair(map, key) {
|
||||
return map.items.find((pair) => getPairKey(pair) === key) || null
|
||||
}
|
||||
|
||||
function mergeMissingMapPairs(targetMap, sampleMap, schema) {
|
||||
let addedCount = 0
|
||||
|
||||
for (const samplePair of sampleMap.items) {
|
||||
const key = getPairKey(samplePair)
|
||||
if (!key) {
|
||||
continue
|
||||
}
|
||||
|
||||
const targetPair = findPair(targetMap, key)
|
||||
if (!targetPair) {
|
||||
targetMap.items.push(samplePair.clone(schema))
|
||||
addedCount += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if (isMap(targetPair.value) && isMap(samplePair.value)) {
|
||||
addedCount += mergeMissingMapPairs(
|
||||
targetPair.value,
|
||||
samplePair.value,
|
||||
schema
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return addedCount
|
||||
}
|
||||
|
||||
function withProfileSchemaReference(content) {
|
||||
const schemaComment = `# yaml-language-server: $schema=${PROFILE_YAML_SCHEMA_REFERENCE}\n`
|
||||
|
||||
if (YAML_SCHEMA_COMMENT_PATTERN.test(content)) {
|
||||
return content.replace(YAML_SCHEMA_COMMENT_PATTERN, schemaComment)
|
||||
}
|
||||
|
||||
return `${schemaComment}${content}`
|
||||
}
|
||||
|
||||
async function syncProfileConfigSchema() {
|
||||
if (!fs.existsSync(CONFIG_SCHEMA_PATH)) {
|
||||
return
|
||||
}
|
||||
|
||||
await fs.promises.mkdir(path.dirname(PROFILE_CONFIG_SCHEMA_PATH), {
|
||||
recursive: true
|
||||
})
|
||||
await fs.promises.copyFile(CONFIG_SCHEMA_PATH, PROFILE_CONFIG_SCHEMA_PATH)
|
||||
}
|
||||
|
||||
function getEnvVariableName(line) {
|
||||
const trimmedLine = line.trim()
|
||||
|
||||
if (
|
||||
trimmedLine === '' ||
|
||||
trimmedLine.startsWith('#') ||
|
||||
!trimmedLine.includes('=')
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const variableName = trimmedLine.slice(0, trimmedLine.indexOf('=')).trim()
|
||||
|
||||
return /^[A-Z0-9_]+$/.test(variableName) ? variableName : null
|
||||
}
|
||||
|
||||
async function readLegacyDotEnvVariables() {
|
||||
if (!fs.existsSync(PROFILE_DOT_ENV_PATH)) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const dotEnvContent = await fs.promises.readFile(PROFILE_DOT_ENV_PATH, 'utf8')
|
||||
const values = {}
|
||||
|
||||
for (const line of dotEnvContent.split(ENV_LINE_SEPARATOR_PATTERN)) {
|
||||
const variableName = getEnvVariableName(line)
|
||||
|
||||
if (!variableName) {
|
||||
continue
|
||||
}
|
||||
|
||||
values[variableName] = line.slice(line.indexOf('=') + 1).replace(/\r$/, '')
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
function toBoolean(value) {
|
||||
const normalizedValue = String(value || '').trim().toLowerCase()
|
||||
|
||||
if (normalizedValue === 'true') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (normalizedValue === 'false') {
|
||||
return false
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function toPort(value) {
|
||||
const port = Number(value)
|
||||
|
||||
return Number.isInteger(port) && port > 0 && port <= 65_535 ? port : null
|
||||
}
|
||||
|
||||
function toContextFileList(value) {
|
||||
return String(value || '')
|
||||
.split(/[,;\n]/)
|
||||
.map((item) => item.trim())
|
||||
.filter((item) => item.length > 0)
|
||||
}
|
||||
|
||||
function toOptionalString(value) {
|
||||
const normalizedValue = String(value || '').trim()
|
||||
|
||||
return normalizedValue === '' ? null : normalizedValue
|
||||
}
|
||||
|
||||
function toRoutingMode(value) {
|
||||
const normalizedValue = String(value || '').trim()
|
||||
|
||||
return normalizedValue === 'workflow' ? 'controlled' : normalizedValue
|
||||
}
|
||||
|
||||
function isEmptyList(value) {
|
||||
return !Array.isArray(value) || value.length === 0
|
||||
}
|
||||
|
||||
function readLegacyAccessConfig(configPath) {
|
||||
if (!fs.existsSync(configPath)) {
|
||||
return {
|
||||
skills: [],
|
||||
tools: []
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(fs.readFileSync(configPath, 'utf8'))
|
||||
|
||||
return {
|
||||
skills: Array.isArray(parsed?.skills) ? parsed.skills : [],
|
||||
tools: Array.isArray(parsed?.tools) ? parsed.tools : []
|
||||
}
|
||||
} catch {
|
||||
return {
|
||||
skills: [],
|
||||
tools: []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeAccessList(values) {
|
||||
return [...new Set(
|
||||
values
|
||||
.filter((value) => typeof value === 'string')
|
||||
.map((value) => value.trim())
|
||||
.filter((value) => value.length > 0)
|
||||
)].sort((firstValue, secondValue) => firstValue.localeCompare(secondValue))
|
||||
}
|
||||
|
||||
function setDocumentValue(document, keyPath, value, shouldOverwrite) {
|
||||
if (!shouldOverwrite && document.getIn(keyPath) !== undefined) {
|
||||
return false
|
||||
}
|
||||
|
||||
document.setIn(keyPath, value)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function normalizeOptionalStringValues(document) {
|
||||
let normalizedCount = 0
|
||||
|
||||
for (const keyPath of OPTIONAL_STRING_CONFIG_PATHS) {
|
||||
const value = document.getIn(keyPath)
|
||||
|
||||
if (typeof value !== 'string' || value.trim() !== '') {
|
||||
continue
|
||||
}
|
||||
|
||||
document.setIn(keyPath, null)
|
||||
normalizedCount += 1
|
||||
}
|
||||
|
||||
return normalizedCount
|
||||
}
|
||||
|
||||
async function migrateLegacyConfigValues(document, shouldOverwriteScalarValues) {
|
||||
let migrationCount = 0
|
||||
const envValues = await readLegacyDotEnvVariables()
|
||||
const mappings = [
|
||||
['LEON_LANG', ['language'], (value) => value.trim()],
|
||||
['LEON_HOST', ['server', 'host'], (value) => value.trim()],
|
||||
['LEON_PORT', ['server', 'port'], toPort],
|
||||
['LEON_HTTP_PLUGINS', ['http_plugins', 'enabled'], toBoolean],
|
||||
[
|
||||
'LEON_HTTP_PLUGIN_ROOT_ROUTES',
|
||||
['http_plugins', 'allow_root_routes'],
|
||||
toBoolean
|
||||
],
|
||||
[
|
||||
'LEON_HTTP_PLUGIN_AUTH',
|
||||
['http_plugins', 'auth', 'enabled'],
|
||||
toBoolean
|
||||
],
|
||||
['LEON_ROUTING_MODE', ['routing', 'mode'], toRoutingMode],
|
||||
['LEON_MOOD', ['mood', 'mode'], (value) => value.trim()],
|
||||
['LEON_LLM', ['llm', 'default'], toOptionalString],
|
||||
['LEON_WORKFLOW_LLM', ['llm', 'workflow'], toOptionalString],
|
||||
['LEON_AGENT_LLM', ['llm', 'agent'], toOptionalString],
|
||||
[
|
||||
'LEON_LLAMACPP_BASE_URL',
|
||||
['llm', 'providers', 'llamacpp', 'base_url'],
|
||||
(value) => value.trim()
|
||||
],
|
||||
[
|
||||
'LEON_SGLANG_BASE_URL',
|
||||
['llm', 'providers', 'sglang', 'base_url'],
|
||||
(value) => value.trim()
|
||||
],
|
||||
['LEON_WAKE_WORD', ['voice', 'wake_word_enabled'], toBoolean],
|
||||
['LEON_ASR', ['voice', 'asr', 'enabled'], toBoolean],
|
||||
['LEON_ASR_PROVIDER', ['voice', 'asr', 'provider'], (value) => value.trim()],
|
||||
['LEON_TTS', ['voice', 'tts', 'enabled'], toBoolean],
|
||||
['LEON_TTS_PROVIDER', ['voice', 'tts', 'provider'], (value) => value.trim()],
|
||||
['LEON_TIME_ZONE', ['time_zone'], toOptionalString],
|
||||
['LEON_AFTER_SPEECH', ['after_speech_enabled'], toBoolean],
|
||||
['LEON_TELEMETRY', ['telemetry_enabled'], toBoolean],
|
||||
[
|
||||
'LEON_PY_TCP_SERVER_HOST',
|
||||
['python_tcp_server', 'host'],
|
||||
(value) => value.trim()
|
||||
],
|
||||
['LEON_PY_TCP_SERVER_PORT', ['python_tcp_server', 'port'], toPort],
|
||||
['LEON_DISABLED_CONTEXT_FILES', ['context', 'disabled_files'], toContextFileList]
|
||||
]
|
||||
|
||||
for (const [envName, keyPath, normalizeValue] of mappings) {
|
||||
if (!Object.hasOwn(envValues, envName)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const normalizedValue = normalizeValue(envValues[envName])
|
||||
if (
|
||||
normalizedValue === null ||
|
||||
normalizedValue === undefined ||
|
||||
(typeof normalizedValue === 'string' && normalizedValue === '')
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (setDocumentValue(
|
||||
document,
|
||||
keyPath,
|
||||
normalizedValue,
|
||||
shouldOverwriteScalarValues
|
||||
)) {
|
||||
migrationCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
const legacyAllowed = readLegacyAccessConfig(LEGACY_ALLOWED_PATH)
|
||||
const legacyDisabled = readLegacyAccessConfig(LEGACY_DISABLED_PATH)
|
||||
const accessMigrations = [
|
||||
[
|
||||
['availability', 'skills', 'allowed'],
|
||||
normalizeAccessList(legacyAllowed.skills)
|
||||
],
|
||||
[
|
||||
['availability', 'tools', 'allowed'],
|
||||
normalizeAccessList(legacyAllowed.tools)
|
||||
],
|
||||
[
|
||||
['availability', 'skills', 'disabled'],
|
||||
normalizeAccessList(legacyDisabled.skills)
|
||||
],
|
||||
[
|
||||
['availability', 'tools', 'disabled'],
|
||||
normalizeAccessList(legacyDisabled.tools)
|
||||
]
|
||||
]
|
||||
|
||||
for (const [keyPath, values] of accessMigrations) {
|
||||
if (values.length === 0 || !isEmptyList(document.getIn(keyPath))) {
|
||||
continue
|
||||
}
|
||||
|
||||
document.setIn(keyPath, values)
|
||||
migrationCount += 1
|
||||
}
|
||||
|
||||
return migrationCount + normalizeOptionalStringValues(document)
|
||||
}
|
||||
|
||||
async function mergeMissingConfigKeys() {
|
||||
const sampleContent = await fs.promises.readFile(CONFIG_SAMPLE_PATH, 'utf8')
|
||||
const configContent = await fs.promises.readFile(CONFIG_PATH, 'utf8')
|
||||
const sampleDocument = YAML.parseDocument(sampleContent)
|
||||
const configDocument = YAML.parseDocument(configContent)
|
||||
|
||||
if (!isMap(sampleDocument.contents) || !isMap(configDocument.contents)) {
|
||||
return 'config.yml: skipped invalid YAML document'
|
||||
}
|
||||
|
||||
const migrationCount = await migrateLegacyConfigValues(configDocument, false)
|
||||
const addedCount = mergeMissingMapPairs(
|
||||
configDocument.contents,
|
||||
sampleDocument.contents,
|
||||
configDocument.schema
|
||||
)
|
||||
|
||||
if (addedCount === 0 && migrationCount === 0) {
|
||||
return 'config.yml: up-to-date'
|
||||
}
|
||||
|
||||
await fs.promises.writeFile(
|
||||
CONFIG_PATH,
|
||||
withProfileSchemaReference(String(configDocument))
|
||||
)
|
||||
|
||||
return `config.yml: +${addedCount + migrationCount} setting${
|
||||
addedCount + migrationCount > 1 ? 's' : ''
|
||||
}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or merge the profile config.yml file.
|
||||
*/
|
||||
export default async () => {
|
||||
const status = createSetupStatus('Preparing config.yml...').start()
|
||||
|
||||
await syncProfileConfigSchema()
|
||||
|
||||
if (!fs.existsSync(CONFIG_PATH)) {
|
||||
await fs.promises.mkdir(path.dirname(CONFIG_PATH), { recursive: true })
|
||||
const sampleContent = await fs.promises.readFile(CONFIG_SAMPLE_PATH, 'utf8')
|
||||
const configDocument = YAML.parseDocument(
|
||||
withProfileSchemaReference(sampleContent)
|
||||
)
|
||||
await migrateLegacyConfigValues(configDocument, true)
|
||||
await fs.promises.writeFile(
|
||||
CONFIG_PATH,
|
||||
withProfileSchemaReference(String(configDocument))
|
||||
)
|
||||
status.succeed('config.yml: created')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
status.succeed(await mergeMissingConfigKeys())
|
||||
}
|
||||
Reference in New Issue
Block a user