diff --git a/README.md b/README.md index 8f24fbb..82fe0d9 100644 --- a/README.md +++ b/README.md @@ -393,6 +393,28 @@ Permissions are also configured in `.letta/settings.json`: } ``` +## Updates + +Letta Code automatically checks for updates on startup and installs them in the background. + +### Auto updates + +* **Update checks**: Performed on startup +* **Update process**: Downloads and installs automatically in the background +* **Applying updates**: Updates take effect the next time you start Letta Code + +**Disable auto-updates:** +Set the `DISABLE_AUTOUPDATER` environment variable in your shell: +```bash +export DISABLE_AUTOUPDATER=1 +``` + +### Update manually + +```bash +letta update +``` + ## Self-hosting To use Letta Code with a self-hosted server, set `LETTA_BASE_URL` to your server IP, e.g. `export LETTA_BASE_URL="http://localhost:8283"`. diff --git a/src/index.ts b/src/index.ts index 2168f03..11f3a31 100755 --- a/src/index.ts +++ b/src/index.ts @@ -26,6 +26,9 @@ USAGE # headless letta -p "..." One-off prompt in headless mode (no TTY UI) + # maintenance + letta update Manually check for updates and install if available + OPTIONS -h, --help Show this help and exit -v, --version Print version and exit @@ -105,6 +108,12 @@ async function main() { await settingsManager.initialize(); const settings = settingsManager.getSettings(); + // Check for updates on startup (non-blocking) + const { checkAndAutoUpdate } = await import("./updater/auto-update"); + checkAndAutoUpdate().catch(() => { + // Silently ignore update failures + }); + // set LETTA_API_KEY from environment if available if (process.env.LETTA_API_KEY && !settings.env?.LETTA_API_KEY) { settings.env = settings.env || {}; @@ -165,7 +174,7 @@ async function main() { } // Check for subcommands - const _command = positionals[2]; // First positional after node and script + const command = positionals[2]; // First positional after node and script // Handle help flag first if (values.help) { @@ -180,6 +189,14 @@ async function main() { process.exit(0); } + // Handle update command + if (command === "update") { + const { manualUpdate } = await import("./updater/auto-update"); + const result = await manualUpdate(); + console.log(result.message); + process.exit(result.success ? 0 : 1); + } + const shouldContinue = (values.continue as boolean | undefined) ?? false; const forceNew = (values.new as boolean | undefined) ?? false; const freshBlocks = (values["fresh-blocks"] as boolean | undefined) ?? false; diff --git a/src/updater/auto-update.ts b/src/updater/auto-update.ts new file mode 100644 index 0000000..8a4555b --- /dev/null +++ b/src/updater/auto-update.ts @@ -0,0 +1,115 @@ +import { exec } from "node:child_process"; +import { promisify } from "node:util"; +import { getVersion } from "../version"; + +const execAsync = promisify(exec); + +interface UpdateCheckResult { + updateAvailable: boolean; + latestVersion?: string; + currentVersion: string; +} + +function isAutoUpdateEnabled(): boolean { + return process.env.DISABLE_AUTOUPDATER !== "1"; +} + +function isRunningLocally(): boolean { + const argv = process.argv[1] || ""; + + // If running from node_modules, it's npm installed (should auto-update) + // Otherwise it's local dev (source or built locally) + return !argv.includes("node_modules"); +} + +async function checkForUpdate(): Promise { + const currentVersion = getVersion(); + + try { + const { stdout } = await execAsync( + "npm view @letta-ai/letta-code version", + { timeout: 5000 }, + ); + const latestVersion = stdout.trim(); + + if (latestVersion !== currentVersion) { + return { + updateAvailable: true, + latestVersion, + currentVersion, + }; + } + } catch (_error) { + // Silently fail + } + + return { + updateAvailable: false, + currentVersion, + }; +} + +async function performUpdate(): Promise<{ success: boolean; error?: string }> { + try { + await execAsync("npm install -g @letta-ai/letta-code@latest", { + timeout: 60000, + }); + return { success: true }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } +} + +export async function checkAndAutoUpdate() { + if (!isAutoUpdateEnabled() || isRunningLocally()) { + return; + } + + const result = await checkForUpdate(); + + if (result.updateAvailable) { + await performUpdate(); + } +} + +export async function manualUpdate(): Promise<{ + success: boolean; + message: string; +}> { + if (isRunningLocally()) { + return { + success: false, + message: "Manual updates are disabled in development mode", + }; + } + + const result = await checkForUpdate(); + + if (!result.updateAvailable) { + return { + success: true, + message: `Already on latest version (${result.currentVersion})`, + }; + } + + console.log( + `Updating from ${result.currentVersion} to ${result.latestVersion}...`, + ); + + const updateResult = await performUpdate(); + + if (updateResult.success) { + return { + success: true, + message: `Updated to ${result.latestVersion}. Restart Letta Code to use the new version.`, + }; + } + + return { + success: false, + message: `Update failed: ${updateResult.error}`, + }; +}