{"version":3,"file":"update-service.mjs","names":["execFile","execFileCb"],"sources":["../../../src/services/update-service.ts"],"sourcesContent":["/**\n * Update Service — in-place update from GitHub + Fly restart.\n *\n * Flow:\n *   1. Clone latest code from GitHub (shallow, depth 1)\n *   2. Install deps + build + pack tarball\n *   3. Extract tarball over the installed extension\n *   4. Restart the Fly machine via Machines API\n *\n * All steps report progress via a callback so the user sees\n * live updates in Telegram.\n *\n * Prerequisites:\n *   - FLY_API_TOKEN (for restart) — already required for /flykeys etc.\n *   - git, npm, node in the container — present in Dockerfile layer 1\n */\n\nimport { execFile as execFileCb } from 'node:child_process';\nimport { existsSync, mkdirSync, rmSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { promisify } from 'node:util';\nimport { isFlyControlAvailable, restartAllMachines } from './fly-control-service.js';\n\nconst execFile = promisify(execFileCb);\n\n// ─── Config ──────────────────────────────────────────────────────────────\n\nconst REPO_OWNER = 'clawnchdev';\nconst REPO_NAME = 'openclawnch';\nconst BRANCH = 'master';\n\n/** Build the clone URL, injecting GITHUB_TOKEN for private repos. */\nfunction getRepoUrl(): string {\n  const token = process.env.GITHUB_TOKEN;\n  if (token) return `https://${token}@github.com/${REPO_OWNER}/${REPO_NAME}.git`;\n  return `https://github.com/${REPO_OWNER}/${REPO_NAME}.git`;\n}\nconst WORK_DIR = '/tmp/openclawnch-update';\nconst EXTENSION_DEST = '/usr/local/lib/node_modules/@clawnch/openclawnch';\n\n/** Max time for the entire update (5 min). */\nconst UPDATE_TIMEOUT_MS = 5 * 60_000;\n\n/** Max time per shell command (3 min). */\nconst CMD_TIMEOUT_MS = 3 * 60_000;\n\n// ─── Types ───────────────────────────────────────────────────────────────\n\nexport type ProgressFn = (msg: string) => void | Promise<void>;\n\nexport interface UpdateResult {\n  success: boolean;\n  message: string;\n  newCommit?: string;\n  commits?: string[];\n  durationMs?: number;\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────\n\nasync function run(\n  cmd: string,\n  args: string[],\n  opts: { cwd?: string; timeout?: number } = {},\n): Promise<string> {\n  const { stdout } = await execFile(cmd, args, {\n    cwd: opts.cwd,\n    timeout: opts.timeout ?? CMD_TIMEOUT_MS,\n    maxBuffer: 10 * 1024 * 1024, // 10MB\n    env: { ...process.env, NODE_ENV: 'development' }, // Need dev deps for build\n  });\n  return stdout.trim();\n}\n\n/** Get the currently installed commit hash (if available). */\nexport function getCurrentCommit(): string | null {\n  try {\n    // Check for a baked-in commit file (set during npm pack)\n    const commitFile = join(EXTENSION_DEST, '.commit');\n    if (existsSync(commitFile)) {\n      return readFileSync(commitFile, 'utf8').trim();\n    }\n    // Fallback: try package.json version\n    const pkg = join(EXTENSION_DEST, 'package.json');\n    if (existsSync(pkg)) {\n      const data = JSON.parse(readFileSync(pkg, 'utf8'));\n      return data.version ?? null;\n    }\n  } catch { /* */ }\n  return null;\n}\n\n// ─── Check for Updates ──────────────────────────────────────────────────\n\nexport interface UpdateCheck {\n  available: boolean;\n  currentRef: string | null;\n  remoteRef: string | null;\n  newCommits: string[];\n}\n\nexport async function checkForUpdates(): Promise<UpdateCheck> {\n  const current = getCurrentCommit();\n\n  try {\n    // Get remote HEAD sha\n    const lsRemote = await run('git', ['ls-remote', getRepoUrl(), `refs/heads/${BRANCH}`]);\n    const remoteRef = lsRemote.split(/\\s/)[0] ?? null;\n\n    if (!remoteRef) {\n      return { available: false, currentRef: current, remoteRef: null, newCommits: [] };\n    }\n\n    // If we don't know our current commit, assume update is available\n    if (!current || current !== remoteRef?.slice(0, current.length)) {\n      // Get recent commit messages\n      let newCommits: string[] = [];\n      try {\n        const log = await run('git', [\n          'ls-remote', '--refs', getRepoUrl(),\n        ], { timeout: 10_000 });\n        // Can't get commit messages without cloning, so just report \"updates available\"\n        newCommits = ['(clone required to see details)'];\n      } catch { /* */ }\n\n      return {\n        available: true,\n        currentRef: current,\n        remoteRef: remoteRef.slice(0, 12),\n        newCommits,\n      };\n    }\n\n    return { available: false, currentRef: current, remoteRef: remoteRef.slice(0, 12), newCommits: [] };\n  } catch {\n    return { available: false, currentRef: current, remoteRef: null, newCommits: [] };\n  }\n}\n\n// ─── Perform Update ─────────────────────────────────────────────────────\n\nexport async function performUpdate(progress: ProgressFn): Promise<UpdateResult> {\n  const start = Date.now();\n\n  // ── Preflight ──────────────────────────────────────────────────────\n  if (!isFlyControlAvailable()) {\n    return {\n      success: false,\n      message: 'Fly.io not configured. Set FLY_API_TOKEN to enable /update.\\n'\n        + 'Run: `fly secrets set FLY_API_TOKEN=\"$(fly tokens create deploy -a <app>)\" -a <app>`',\n    };\n  }\n\n  try {\n    // ── Step 1: Clone ────────────────────────────────────────────────\n    await progress('1/6 Cloning latest code...');\n    if (existsSync(WORK_DIR)) rmSync(WORK_DIR, { recursive: true, force: true });\n    mkdirSync(WORK_DIR, { recursive: true });\n    await run('git', ['clone', '--depth', '1', '--branch', BRANCH, getRepoUrl(), WORK_DIR]);\n\n    // Read the new commit SHA\n    const newCommit = await run('git', ['rev-parse', '--short', 'HEAD'], { cwd: WORK_DIR });\n    const commitMsg = await run('git', ['log', '-1', '--format=%s'], { cwd: WORK_DIR });\n    await progress(`1/6 Cloned: ${newCommit} — ${commitMsg}`);\n\n    // ── Step 2: Install deps ─────────────────────────────────────────\n    await progress('2/6 Installing dependencies...');\n    // Check if pnpm is available, fall back to npm\n    let pm = 'npm';\n    try {\n      await run('pnpm', ['--version']);\n      pm = 'pnpm';\n    } catch { /* npm fallback */ }\n\n    if (pm === 'pnpm') {\n      await run('pnpm', ['install', '--frozen-lockfile'], { cwd: WORK_DIR });\n    } else {\n      await run('npm', ['install'], { cwd: WORK_DIR });\n    }\n    await progress('2/6 Dependencies installed');\n\n    // ── Step 3: Build ────────────────────────────────────────────────\n    await progress('3/6 Building...');\n    await run(pm, ['run', 'build'], { cwd: WORK_DIR });\n    await progress('3/6 Build complete');\n\n    // ── Step 4: Pack tarball ─────────────────────────────────────────\n    await progress('4/6 Packing extension...');\n    // npm pack creates a .tgz in the cwd\n    const packOutput = await run(pm, ['pack'], { cwd: WORK_DIR });\n    const tgzName = packOutput.split('\\n').pop()?.trim() ?? '';\n    const tgzPath = join(WORK_DIR, tgzName);\n\n    if (!existsSync(tgzPath)) {\n      return { success: false, message: `Pack failed — tarball not found at ${tgzPath}` };\n    }\n    await progress('4/6 Tarball packed');\n\n    // ── Step 5: Extract over existing ────────────────────────────────\n    await progress('5/6 Installing update...');\n    // Clear existing and extract\n    if (existsSync(EXTENSION_DEST)) {\n      // Preserve node_modules (Uniswap deps installed separately in Dockerfile)\n      const nodeModules = join(EXTENSION_DEST, 'node_modules');\n      const hasNodeModules = existsSync(nodeModules);\n\n      // Extract tarball (npm pack creates package/ prefix)\n      await run('tar', [\n        'xzf', tgzPath,\n        '-C', EXTENSION_DEST,\n        '--strip-components=1',\n      ]);\n\n      // Write the commit SHA for future version detection\n      const { writeFileSync } = await import('node:fs');\n      writeFileSync(join(EXTENSION_DEST, '.commit'), newCommit, 'utf8');\n    }\n    await progress('5/6 Update installed');\n\n    // ── Step 6: Restart ──────────────────────────────────────────────\n    await progress('6/6 Restarting machine...');\n    const restarted = await restartAllMachines();\n\n    // ── Cleanup ──────────────────────────────────────────────────────\n    try { rmSync(WORK_DIR, { recursive: true, force: true }); } catch { /* */ }\n\n    const duration = Date.now() - start;\n    return {\n      success: true,\n      message: `Updated to ${newCommit}. ${restarted.length} machine(s) restarting.\\n`\n        + `Duration: ${(duration / 1000).toFixed(1)}s`,\n      newCommit,\n      durationMs: duration,\n    };\n  } catch (err) {\n    // Cleanup on failure\n    try { rmSync(WORK_DIR, { recursive: true, force: true }); } catch { /* */ }\n\n    const msg = err instanceof Error ? err.message : String(err);\n    return {\n      success: false,\n      message: `Update failed: ${msg}`,\n      durationMs: Date.now() - start,\n    };\n  }\n}\n"],"mappings":"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"}