{"version":3,"file":"update-check.mjs","names":[],"sources":["../../src/feature/update-check.ts"],"sourcesContent":["import type { ResolvedPadroneRuntime } from '../core/runtime.ts';\n\n/**\n * Configuration for the update check feature.\n */\nexport type UpdateCheckConfig = {\n  /**\n   * The npm package name to check. Defaults to the program name.\n   */\n  packageName?: string;\n  /**\n   * Registry to check for updates.\n   * - `'npm'` — checks the npm registry (default)\n   * - A URL string — custom registry endpoint that returns JSON with a `version` or `dist-tags.latest` field\n   */\n  registry?: 'npm' | string;\n  /**\n   * How often to check for updates. Accepts shorthand like `'1d'`, `'12h'`, `'30m'`.\n   * Defaults to `'1d'` (once per day).\n   */\n  interval?: string;\n  /**\n   * Path to the cache file for storing the last check timestamp and latest version.\n   * Defaults to `~/.config/<programName>-update-check.json`.\n   */\n  cache?: string;\n  /**\n   * Environment variable name to disable update checks (e.g. `'MYAPP_NO_UPDATE_CHECK'`).\n   * When set to a truthy value, update checks are skipped.\n   * Defaults to `'<PROGRAM_NAME>_NO_UPDATE_CHECK'` (uppercased, hyphens to underscores).\n   */\n  disableEnvVar?: string;\n};\n\ntype CacheData = {\n  lastCheck: number;\n  latestVersion: string;\n};\n\n/**\n * Parses an interval string like '1d', '12h', '30m', '1w' into milliseconds.\n */\nexport function parseInterval(interval: string): number {\n  const match = interval.match(/^(\\d+)\\s*(ms|s|m|h|d|w)$/);\n  if (!match) return 86_400_000; // default 1d\n\n  const value = parseInt(match[1]!, 10);\n  const unit = match[2]!;\n\n  switch (unit) {\n    case 'ms':\n      return value;\n    case 's':\n      return value * 1000;\n    case 'm':\n      return value * 60_000;\n    case 'h':\n      return value * 3_600_000;\n    case 'd':\n      return value * 86_400_000;\n    case 'w':\n      return value * 604_800_000;\n    default:\n      return 86_400_000;\n  }\n}\n\n/**\n * Compares two semver version strings.\n * Returns true if `latest` is newer than `current`.\n */\nexport function isNewerVersion(current: string, latest: string): boolean {\n  const parse = (v: string) => {\n    const cleaned = v.replace(/^v/, '');\n    const parts = cleaned.split('-');\n    const nums = parts[0]!.split('.').map(Number);\n    return { major: nums[0] ?? 0, minor: nums[1] ?? 0, patch: nums[2] ?? 0, prerelease: parts[1] };\n  };\n\n  const c = parse(current);\n  const l = parse(latest);\n\n  // Don't notify about pre-release versions unless user is already on a pre-release\n  if (l.prerelease && !c.prerelease) return false;\n\n  if (l.major !== c.major) return l.major > c.major;\n  if (l.minor !== c.minor) return l.minor > c.minor;\n  if (l.patch !== c.patch) return l.patch > c.patch;\n  return false;\n}\n\n/**\n * Reads the update check cache file.\n */\nasync function readCache(cachePath: string): Promise<CacheData | undefined> {\n  try {\n    const { existsSync, readFileSync } = await import('node:fs');\n    if (!existsSync(cachePath)) return undefined;\n    const data = JSON.parse(readFileSync(cachePath, 'utf-8'));\n    if (typeof data.lastCheck === 'number' && typeof data.latestVersion === 'string') {\n      return data as CacheData;\n    }\n  } catch {\n    // Ignore errors\n  }\n  return undefined;\n}\n\n/**\n * Writes the update check cache file.\n */\nasync function writeCache(cachePath: string, data: CacheData): Promise<void> {\n  try {\n    const { existsSync, mkdirSync, writeFileSync } = await import('node:fs');\n    const { dirname } = await import('node:path');\n    const dir = dirname(cachePath);\n    if (!existsSync(dir)) {\n      mkdirSync(dir, { recursive: true });\n    }\n    writeFileSync(cachePath, JSON.stringify(data), 'utf-8');\n  } catch {\n    // Ignore errors — cache is best-effort\n  }\n}\n\n/**\n * Resolves the cache path, expanding `~` to the home directory.\n */\nasync function resolveCachePath(cachePath: string): Promise<string> {\n  const { homedir } = await import('node:os');\n  const { resolve } = await import('node:path');\n  if (cachePath.startsWith('~')) {\n    return cachePath.replace('~', homedir());\n  }\n  return resolve(cachePath);\n}\n\n/**\n * Fetches the latest version from the registry.\n */\nasync function fetchLatestVersion(packageName: string, registry: string): Promise<string | undefined> {\n  const url = registry === 'npm' ? `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest` : registry;\n\n  try {\n    const response = await fetch(url);\n    if (!response.ok) return undefined;\n    const data = (await response.json()) as Record<string, unknown>;\n\n    // npm registry returns { version: \"x.y.z\" }\n    if (typeof data.version === 'string') return data.version;\n\n    // Custom endpoint may return { \"dist-tags\": { latest: \"x.y.z\" } }\n    const distTags = data['dist-tags'] as Record<string, string> | undefined;\n    if (distTags?.latest) return distTags.latest;\n  } catch {\n    // Network errors are expected (offline, firewall, etc.)\n  }\n  return undefined;\n}\n\n/**\n * Formats the update notification message.\n */\nexport function formatUpdateMessage(currentVersion: string, latestVersion: string, packageName: string): string {\n  const updateCommand = `npm update -g ${packageName}`;\n  return `\\n  Update available: ${currentVersion} \\u2192 ${latestVersion}\\n  Run \"${updateCommand}\" to update\\n`;\n}\n\n/**\n * Checks for updates in the background. Returns a function that, when called,\n * prints the update notification if a newer version was found.\n *\n * This is designed to be non-blocking: the check starts immediately but the\n * result is only consumed after command execution completes.\n */\nexport async function createUpdateChecker(\n  programName: string,\n  currentVersion: string,\n  config: UpdateCheckConfig,\n  runtime: ResolvedPadroneRuntime,\n): Promise<() => void> {\n  const packageName = config.packageName ?? programName;\n  const registry = config.registry ?? 'npm';\n  const intervalMs = parseInterval(config.interval ?? '1d');\n  const disableEnvVar = config.disableEnvVar ?? `${programName.toUpperCase().replace(/-/g, '_')}_NO_UPDATE_CHECK`;\n\n  const defaultCachePath = `~/.config/${programName}-update-check.json`;\n  const cachePath = await resolveCachePath(config.cache ?? defaultCachePath);\n\n  // Check if disabled\n  const env = runtime.env();\n  if (env.CI || env.CONTINUOUS_INTEGRATION) return noop;\n  if (env[disableEnvVar]) return noop;\n  if (runtime.terminal && !runtime.terminal.isTTY) return noop;\n\n  // Check cache — if we checked recently, use cached result\n  const cached = await readCache(cachePath);\n  if (cached && Date.now() - cached.lastCheck < intervalMs) {\n    // Use cached version for display\n    if (isNewerVersion(currentVersion, cached.latestVersion)) {\n      return () => {\n        runtime.error(formatUpdateMessage(currentVersion, cached.latestVersion, packageName));\n      };\n    }\n    return noop;\n  }\n\n  // Start background fetch\n  const fetchPromise = fetchLatestVersion(packageName, registry).then(async (latestVersion) => {\n    if (latestVersion) {\n      await writeCache(cachePath, { lastCheck: Date.now(), latestVersion });\n      if (isNewerVersion(currentVersion, latestVersion)) {\n        return latestVersion;\n      }\n    }\n    return undefined;\n  });\n\n  // Return a function that blocks on the result (briefly — the fetch should be done by now)\n  let resolved: string | undefined | null = null; // null = not yet resolved\n  fetchPromise.then(\n    (v) => {\n      resolved = v;\n    },\n    () => {\n      resolved = undefined;\n    },\n  );\n\n  return () => {\n    // If the fetch already resolved, use the result synchronously\n    if (resolved !== null) {\n      if (resolved) {\n        runtime.error(formatUpdateMessage(currentVersion, resolved, packageName));\n      }\n      return;\n    }\n\n    // Otherwise, we can't block — just skip this time.\n    // The cache will be written when the promise resolves, so next invocation will show the message.\n  };\n}\n\nfunction noop() {}\n"],"mappings":"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"}