444 lines
14 KiB
JavaScript
444 lines
14 KiB
JavaScript
import fs from 'node:fs'
|
|
|
|
import {
|
|
CACHE_PATH,
|
|
LEON_HOME_PATH,
|
|
LEON_PROFILES_PATH,
|
|
LEON_PROFILE_PATH,
|
|
LEON_TOOLKITS_PATH,
|
|
MODELS_PATH,
|
|
PROFILE_CONTEXT_PATH,
|
|
PROFILE_AGENT_SKILLS_PATH,
|
|
PROFILE_LOGS_PATH,
|
|
PROFILE_MEMORY_PATH,
|
|
PROFILE_NATIVE_SKILLS_PATH,
|
|
PROFILE_SKILLS_PATH,
|
|
PROFILE_TOOLS_PATH,
|
|
TMP_PATH,
|
|
IS_GITHUB_ACTIONS
|
|
} from '@/constants'
|
|
import { LogHelper } from '@/helpers/log-helper'
|
|
import { NetworkHelper } from '@/helpers/network-helper'
|
|
|
|
import buildApp from '../app/build-app'
|
|
import buildServer from '../build-server'
|
|
import train from '../train/train'
|
|
import generateClientInterfaceToken from '../generate/generate-client-interface-token'
|
|
import generateJSONSchemas from '../generate/generate-json-schemas'
|
|
|
|
import setupDotenv, { updateDotEnvVariable } from './setup-dotenv'
|
|
import setupConfig from './setup-config'
|
|
import { CONFIG_MANAGER } from '@/config'
|
|
import setupCore from './setup-core'
|
|
import setupNode from './setup-node'
|
|
import setupPNPM from './setup-pnpm'
|
|
import setupNativeNodeModules from './setup-native-node-modules'
|
|
import setupPython from './setup-python'
|
|
import setupUV from './setup-uv'
|
|
import setupNodejsBridgeEnv from './setup-nodejs-bridge-env'
|
|
import setupPythonBridgeEnv from './setup-python-bridge-env'
|
|
import setupToolsDependencies from './setup-tools-dependencies'
|
|
import setupToolsSettings from './setup-tools-settings'
|
|
import setupSkills from './setup-skills/setup-skills'
|
|
import setupTCPServerEnv from './setup-tcp-server-env'
|
|
import setupCMake from './setup-cmake'
|
|
import setupNinja from './setup-ninja'
|
|
import setupLlamaCPP from './setup-llama-cpp'
|
|
import setupLocalLLM from './setup-local-llm'
|
|
import setupQMDLLM from './setup-qmd-llm'
|
|
import setupVoiceResources from './setup-voice-resources.js'
|
|
import inspectLocalAICapability from './local-ai-capability'
|
|
import {
|
|
inspectLocalAISetupState,
|
|
inspectVoiceSetupState
|
|
} from './inspect-setup-state'
|
|
import postSetup from './post-setup'
|
|
import { printSetupBanner } from './setup-banner'
|
|
import { tellSetupCompletionJoke } from './setup-jokes'
|
|
import setupPreferences from './setup-preferences'
|
|
import { createSetupStatus } from './setup-status'
|
|
import { SetupUI } from './setup-ui'
|
|
import createInstanceID from './create-instance-id'
|
|
import setFfprobePermissions from './set-ffprobe-permissions'
|
|
import setupGitHooks from './setup-git-hooks'
|
|
|
|
const LOCAL_LLM_TARGET_VALUE = 'llamacpp'
|
|
|
|
/**
|
|
* Create Leon home directories that setup and runtime expect to exist.
|
|
*/
|
|
async function ensureLeonHomeStructure() {
|
|
const status = createSetupStatus('Preparing Leon home...').start()
|
|
|
|
await Promise.all([
|
|
fs.promises.mkdir(LEON_HOME_PATH, { recursive: true }),
|
|
fs.promises.mkdir(LEON_PROFILES_PATH, { recursive: true }),
|
|
fs.promises.mkdir(LEON_PROFILE_PATH, { recursive: true }),
|
|
fs.promises.mkdir(CACHE_PATH, { recursive: true }),
|
|
fs.promises.mkdir(LEON_TOOLKITS_PATH, { recursive: true }),
|
|
fs.promises.mkdir(MODELS_PATH, { recursive: true }),
|
|
fs.promises.mkdir(TMP_PATH, { recursive: true }),
|
|
fs.promises.mkdir(PROFILE_CONTEXT_PATH, { recursive: true }),
|
|
fs.promises.mkdir(PROFILE_MEMORY_PATH, { recursive: true }),
|
|
fs.promises.mkdir(PROFILE_LOGS_PATH, { recursive: true }),
|
|
fs.promises.mkdir(PROFILE_SKILLS_PATH, { recursive: true }),
|
|
fs.promises.mkdir(PROFILE_NATIVE_SKILLS_PATH, { recursive: true }),
|
|
fs.promises.mkdir(PROFILE_AGENT_SKILLS_PATH, { recursive: true }),
|
|
fs.promises.mkdir(PROFILE_TOOLS_PATH, { recursive: true })
|
|
])
|
|
|
|
status.succeed('Leon home: ready')
|
|
}
|
|
|
|
function isExplicitLocalLLMTarget(value) {
|
|
const normalizedValue = (value || '').trim()
|
|
|
|
return (
|
|
normalizedValue.startsWith('llamacpp/') ||
|
|
normalizedValue.startsWith('sglang/') ||
|
|
normalizedValue.startsWith('/')
|
|
)
|
|
}
|
|
|
|
function getOptionalLLMTarget(value) {
|
|
return typeof value === 'string' ? value.trim() : ''
|
|
}
|
|
|
|
async function resolveExistingLLMChoice() {
|
|
const llmConfig = CONFIG_MANAGER.getConfig().llm
|
|
const leonLLM = getOptionalLLMTarget(llmConfig.default)
|
|
const leonWorkflowLLM = getOptionalLLMTarget(llmConfig.workflow)
|
|
const leonAgentLLM = getOptionalLLMTarget(llmConfig.agent)
|
|
const overrideTargets = [leonWorkflowLLM, leonAgentLLM].filter(Boolean)
|
|
|
|
if (overrideTargets.length > 0) {
|
|
return {
|
|
hasResolvedChoice: true,
|
|
setupLocalAI: overrideTargets.some((target) =>
|
|
isExplicitLocalLLMTarget(target)
|
|
),
|
|
targetType: overrideTargets.some((target) =>
|
|
isExplicitLocalLLMTarget(target)
|
|
)
|
|
? 'explicitLocal'
|
|
: 'remote',
|
|
label: overrideTargets.join(', ')
|
|
}
|
|
}
|
|
|
|
if (leonLLM === '') {
|
|
return {
|
|
hasResolvedChoice: false,
|
|
setupLocalAI: false,
|
|
targetType: 'disabled',
|
|
label: ''
|
|
}
|
|
}
|
|
|
|
return {
|
|
hasResolvedChoice: true,
|
|
setupLocalAI: isExplicitLocalLLMTarget(leonLLM),
|
|
targetType: isExplicitLocalLLMTarget(leonLLM)
|
|
? 'explicitLocal'
|
|
: 'remote',
|
|
label: leonLLM
|
|
}
|
|
}
|
|
|
|
async function syncLLMSetupChoice(preferences) {
|
|
CONFIG_MANAGER.reload()
|
|
const llmConfig = CONFIG_MANAGER.getConfig().llm
|
|
const leonLLM = getOptionalLLMTarget(llmConfig.default)
|
|
const leonWorkflowLLM = getOptionalLLMTarget(llmConfig.workflow)
|
|
const leonAgentLLM = getOptionalLLMTarget(llmConfig.agent)
|
|
const hasExplicitModeOverride =
|
|
leonWorkflowLLM !== '' || leonAgentLLM !== ''
|
|
const hasExplicitGlobalTarget =
|
|
leonLLM !== ''
|
|
|
|
if (
|
|
preferences.remoteLLMProvider &&
|
|
preferences.remoteLLMModel &&
|
|
preferences.remoteLLMAPIKeyEnv &&
|
|
preferences.remoteLLMAPIKey
|
|
) {
|
|
await updateDotEnvVariable(
|
|
preferences.remoteLLMAPIKeyEnv,
|
|
preferences.remoteLLMAPIKey.trim()
|
|
)
|
|
|
|
if (!hasExplicitModeOverride) {
|
|
await CONFIG_MANAGER.setValue(
|
|
['llm', 'default'],
|
|
`${preferences.remoteLLMProvider}/${preferences.remoteLLMModel}`
|
|
)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
if (hasExplicitModeOverride || hasExplicitGlobalTarget) {
|
|
return
|
|
}
|
|
|
|
if (preferences.setupLocalAI) {
|
|
await CONFIG_MANAGER.setValue(['llm', 'default'], LOCAL_LLM_TARGET_VALUE)
|
|
|
|
return
|
|
}
|
|
|
|
await CONFIG_MANAGER.setValue(['llm', 'default'], null)
|
|
}
|
|
// Do not load ".env" file because it is not created yet
|
|
|
|
/**
|
|
* Main entry to set up Leon
|
|
*/
|
|
;(async () => {
|
|
// Track setup execution state for user-facing error reporting and interruption handling.
|
|
let currentStep = 'bootstrap'
|
|
let shutdownSignal = null
|
|
let preferences = {
|
|
setupLocalAI: false,
|
|
setupVoice: false
|
|
}
|
|
let localAICapability = null
|
|
let localAISetupState = {
|
|
isInstalled: false,
|
|
label: ''
|
|
}
|
|
let voiceSetupState = {
|
|
isInstalled: false
|
|
}
|
|
const getExitCodeFromSignal = (signal) => (signal === 'SIGINT' ? 130 : 143)
|
|
|
|
// Clean up process signal listeners when setup exits normally or with an error.
|
|
const cleanupSignalHandlers = () => {
|
|
process.off('SIGINT', handleShutdownSignal)
|
|
process.off('SIGTERM', handleShutdownSignal)
|
|
}
|
|
|
|
// Abort active downloads and exit cleanly when setup is interrupted.
|
|
const handleShutdownSignal = (signal) => {
|
|
if (shutdownSignal) {
|
|
process.exit(getExitCodeFromSignal(signal))
|
|
}
|
|
|
|
shutdownSignal = signal
|
|
|
|
const abortedDownloadCount = NetworkHelper.abortActiveDownloads(
|
|
`Setup interrupted by ${signal}`
|
|
)
|
|
|
|
if (abortedDownloadCount === 0) {
|
|
process.exit(getExitCodeFromSignal(signal))
|
|
}
|
|
}
|
|
|
|
process.on('SIGINT', handleShutdownSignal)
|
|
process.on('SIGTERM', handleShutdownSignal)
|
|
|
|
try {
|
|
// Print the setup banner outside CI so the local install feels branded.
|
|
if (!IS_GITHUB_ACTIONS) {
|
|
printSetupBanner()
|
|
}
|
|
|
|
// Ask setup questions first so the rest of the install can run unattended.
|
|
if (!IS_GITHUB_ACTIONS) {
|
|
SetupUI.section('Quick Setup')
|
|
|
|
currentStep = 'inspectLocalAICapability'
|
|
const capabilityStatus = createSetupStatus(
|
|
'Checking what this computer can handle...'
|
|
).start()
|
|
localAICapability = await inspectLocalAICapability()
|
|
|
|
if (localAICapability.canInstallLocalAI) {
|
|
capabilityStatus.succeed(
|
|
'Local AI supported: better privacy and less to configure online'
|
|
)
|
|
} else {
|
|
capabilityStatus.stop()
|
|
SetupUI.info('Local AI is not supported on this computer')
|
|
}
|
|
|
|
currentStep = 'resolveExistingLLMChoice'
|
|
const existingLLMChoice = await resolveExistingLLMChoice()
|
|
currentStep = 'inspectLocalAISetupState'
|
|
localAISetupState = inspectLocalAISetupState()
|
|
currentStep = 'inspectVoiceSetupState'
|
|
voiceSetupState = inspectVoiceSetupState()
|
|
|
|
currentStep = 'setupPreferences'
|
|
preferences = await setupPreferences(
|
|
localAICapability,
|
|
existingLLMChoice,
|
|
localAISetupState,
|
|
voiceSetupState
|
|
)
|
|
}
|
|
|
|
// Prepare the local runtime, bridges, skills, and shared memory models.
|
|
SetupUI.section('Base Setup')
|
|
|
|
currentStep = 'ensureLeonHomeStructure'
|
|
await ensureLeonHomeStructure()
|
|
currentStep = 'setupDotenv'
|
|
await setupDotenv()
|
|
currentStep = 'generateJSONSchemas'
|
|
await generateJSONSchemas()
|
|
currentStep = 'setupConfig'
|
|
await setupConfig()
|
|
currentStep = 'syncLLMSetupChoice'
|
|
await syncLLMSetupChoice(preferences)
|
|
currentStep = 'setupCore'
|
|
await setupCore()
|
|
if (!IS_GITHUB_ACTIONS) {
|
|
currentStep = 'setupNode'
|
|
await setupNode()
|
|
currentStep = 'setupPNPM'
|
|
await setupPNPM()
|
|
currentStep = 'setupNativeNodeModules'
|
|
await setupNativeNodeModules()
|
|
currentStep = 'setupPython'
|
|
await setupPython()
|
|
currentStep = 'setupUV'
|
|
await setupUV()
|
|
} else {
|
|
SetupUI.info(
|
|
'Skipping portable Node.js, pnpm, Python and uv setup because it is running in CI'
|
|
)
|
|
}
|
|
currentStep = 'setupNodejsBridgeEnv'
|
|
await setupNodejsBridgeEnv()
|
|
currentStep = 'setupPythonBridgeEnv'
|
|
await setupPythonBridgeEnv()
|
|
currentStep = 'setupTCPServerEnv'
|
|
await setupTCPServerEnv()
|
|
currentStep = 'setupToolsDependencies'
|
|
await setupToolsDependencies()
|
|
currentStep = 'setupToolsSettings'
|
|
await setupToolsSettings()
|
|
currentStep = 'setupSkills'
|
|
await setupSkills()
|
|
if (!IS_GITHUB_ACTIONS) {
|
|
currentStep = 'setupQMDLLM'
|
|
await setupQMDLLM()
|
|
} else {
|
|
SetupUI.info('Skipping QMD model setup because it is running in CI')
|
|
}
|
|
|
|
if (!IS_GITHUB_ACTIONS) {
|
|
// Install local AI components based on the earlier capability check and answers.
|
|
SetupUI.section('Local AI')
|
|
|
|
if (preferences.setupLocalAI) {
|
|
currentStep = 'setupCMake'
|
|
await setupCMake()
|
|
currentStep = 'setupNinja'
|
|
await setupNinja()
|
|
currentStep = 'setupLlamaCPP'
|
|
await setupLlamaCPP()
|
|
currentStep = 'setupLocalLLM'
|
|
await setupLocalLLM(localAICapability)
|
|
} else if (localAICapability?.canInstallLocalAI) {
|
|
SetupUI.info('I will skip local AI for now. You can add it later.')
|
|
} else {
|
|
SetupUI.info('I will skip local AI because this computer does not support it.')
|
|
}
|
|
|
|
if (preferences.setupVoice) {
|
|
currentStep = 'setupVoiceResources'
|
|
await setupVoiceResources()
|
|
} else {
|
|
SetupUI.info('I will skip voice setup for now. You can add it later.')
|
|
}
|
|
} else {
|
|
// Skip hardware-specific setup in CI where local AI and voice stacks are not needed.
|
|
SetupUI.info(
|
|
'Skipping CMake, Ninja, llama.cpp, local LLM, QMD models, NVIDIA, and PyTorch setups because it is running in CI'
|
|
)
|
|
}
|
|
|
|
if (!preferences.setupVoice) {
|
|
SetupUI.info('I will skip voice model downloads for now.')
|
|
}
|
|
|
|
// Finalize generated assets and instance-specific setup metadata.
|
|
SetupUI.section('Finishing Up')
|
|
|
|
currentStep = 'generateClientInterfaceToken'
|
|
await generateClientInterfaceToken()
|
|
currentStep = 'train'
|
|
await train()
|
|
currentStep = 'setFfprobePermissions'
|
|
await setFfprobePermissions()
|
|
currentStep = 'createInstanceID'
|
|
await createInstanceID()
|
|
currentStep = 'setupGitHooks'
|
|
await setupGitHooks()
|
|
currentStep = 'buildApp'
|
|
{
|
|
const status = createSetupStatus('Building app...').start()
|
|
|
|
try {
|
|
await buildApp({ quiet: true })
|
|
status.succeed('App: ready')
|
|
} catch (error) {
|
|
status.fail('Failed to build app')
|
|
throw error
|
|
}
|
|
}
|
|
currentStep = 'buildServer'
|
|
{
|
|
const status = createSetupStatus('Building server...').start()
|
|
|
|
try {
|
|
await buildServer({ quiet: true })
|
|
status.succeed('Server: ready')
|
|
} catch (error) {
|
|
status.fail('Failed to build server')
|
|
throw error
|
|
}
|
|
}
|
|
|
|
if (!IS_GITHUB_ACTIONS) {
|
|
printSetupBanner()
|
|
}
|
|
|
|
SetupUI.recap([
|
|
'Setup complete',
|
|
`Local AI: ${preferences.setupLocalAI ? 'enabled' : 'skipped'}`,
|
|
`Voice: ${preferences.setupVoice ? 'enabled' : 'skipped'}`
|
|
])
|
|
tellSetupCompletionJoke()
|
|
console.log('')
|
|
SetupUI.successHighlight('Hooray! I\'m installed and ready to go!')
|
|
console.log('')
|
|
SetupUI.bullet(
|
|
`Follow my creator to get regular updates about me: ${SetupUI.underlined(
|
|
'https://x.com/grenlouis'
|
|
)}`
|
|
)
|
|
console.log('')
|
|
currentStep = 'postSetup'
|
|
await postSetup()
|
|
} catch (e) {
|
|
// Exit with the original signal code when setup was intentionally interrupted.
|
|
if (shutdownSignal) {
|
|
cleanupSignalHandlers()
|
|
process.exit(getExitCodeFromSignal(shutdownSignal))
|
|
}
|
|
|
|
// Surface the failing phase clearly when setup stops on an unexpected error.
|
|
LogHelper.error(
|
|
`Setup failed during ${currentStep}: ${
|
|
e instanceof Error ? e.stack || e.message : String(e)
|
|
}`
|
|
)
|
|
cleanupSignalHandlers()
|
|
process.exit(1)
|
|
}
|
|
})()
|