R/ai_sync.R

Defines functions ai_sync_context

Documented in ai_sync_context

#' Sync AI Assistant Context Files
#'
#' Copies content from the canonical AI assistant file to all other AI files,
#' adding a warning header to non-canonical files.
#'
#' @param config_file Path to configuration file (default: auto-detect settings.yml/settings.yml)
#' @param force Logical; if TRUE, overwrite even if target is newer (default: FALSE)
#' @param verbose Logical; if TRUE (default), show sync messages
#'
#' @return Invisible list with sync results
#'
#' @details
#' This function reads the `ai.canonical_file` setting from settings.yml and
#' copies its content to all other AI assistant instruction files that exist
#' in the project.
#'
#' The canonical file is copied as-is. Non-canonical files receive a warning
#' header indicating they are auto-synced and should not be edited directly.
#'
#' Supported files:
#' \itemize{
#'   \item **AGENTS.md** - Cross-platform AI assistants
#'   \item **CLAUDE.md** - Claude Code
#'   \item **.github/copilot-instructions.md** - GitHub Copilot
#' }
#'
#' @examples
#' \donttest{
#' if (FALSE) {
#' # Sync AI context files
#' ai_sync_context()
#'
#' # Force sync even if targets are newer
#' ai_sync_context(force = TRUE)
#'
#' # Silent sync (for git hooks)
#' ai_sync_context(verbose = FALSE)
#' }
#' }
#'
#' @export
ai_sync_context <- function(config_file = NULL,
                            force = FALSE,
                            verbose = TRUE) {

  # Validate arguments
  checkmate::assert_string(config_file, min.chars = 1, null.ok = TRUE)
  checkmate::assert_logical(force, len = 1)
  checkmate::assert_logical(verbose, len = 1)

  if (is.null(config_file)) {
    config_file <- .get_settings_file()
    if (is.null(config_file)) {
      config_file <- "settings.yml"
    }
  }

  # Check if config exists
  if (!file.exists(config_file)) {
    if (verbose) {
      message("[x] Config file not found: ", config_file)
    }
    return(invisible(list(success = FALSE, reason = "config_not_found")))
  }

  # Read configuration
  canonical_file <- config("ai.canonical_file", config_file = config_file, default = "")

  if (canonical_file == "" || is.null(canonical_file)) {
    if (verbose) {
      message("Note: No canonical AI file configured")
      message("  Set ai.canonical_file in settings.yml to enable sync")
    }
    return(invisible(list(success = FALSE, reason = "not_configured")))
  }

  # Check if canonical file exists
  if (!file.exists(canonical_file)) {
    if (verbose) {
      message("[x] Canonical file not found: ", canonical_file)
    }
    return(invisible(list(success = FALSE, reason = "canonical_not_found")))
  }

  # Read canonical content
  canonical_content <- readLines(canonical_file, warn = FALSE)

  # Define all possible AI files
  ai_files <- c(
    "AGENTS.md",
    "CLAUDE.md",
    ".github/copilot-instructions.md"
  )

  # Filter to existing files that aren't the canonical
  target_files <- ai_files[ai_files != canonical_file & file.exists(ai_files)]

  if (length(target_files) == 0) {
    if (verbose) {
      message("Note: No other AI files to sync")
    }
    return(invisible(list(success = TRUE, synced = 0)))
  }

  # Sync each target file
  synced_count <- 0
  sync_results <- list()

  for (target_file in target_files) {
    # Check if update needed (unless force = TRUE)
    if (!force) {
      canonical_mtime <- file.info(canonical_file)$mtime
      target_mtime <- file.info(target_file)$mtime

      if (!is.na(target_mtime) && target_mtime >= canonical_mtime) {
        if (verbose) {
          message("  [ok] ", target_file, " (already up to date)")
        }
        sync_results[[target_file]] <- "up_to_date"
        next
      }
    }

    # Create warning header
    header <- c(
      "<!-- DO NOT EDIT THIS FILE DIRECTLY -->",
      "<!-- This file is auto-synced from the canonical source -->",
      sprintf("<!-- Canonical file: %s -->", canonical_file),
      "<!-- Edit the canonical file and run: framework sync:ai-context -->",
      "<!-- Or enable git hooks for automatic sync -->",
      ""
    )

    # Combine header + canonical content
    new_content <- c(header, canonical_content)

    # Write to target
    tryCatch({
      writeLines(new_content, target_file)
      synced_count <- synced_count + 1
      sync_results[[target_file]] <- "synced"
      if (verbose) {
        message("  [ok] Synced to ", target_file)
      }
    }, error = function(e) {
      sync_results[[target_file]] <- paste0("error: ", e$message)
      if (verbose) {
        message("  [x] Failed to sync ", target_file, ": ", e$message)
      }
    })
  }

  if (verbose && synced_count > 0) {
    message("\n[ok] Synced ", synced_count, " file(s) from ", canonical_file)
  }

  invisible(list(
    success = TRUE,
    canonical = canonical_file,
    synced = synced_count,
    results = sync_results
  ))
}

Try the framework package in your browser

Any scripts or data that you put into this service are public.

framework documentation built on Feb. 18, 2026, 1:07 a.m.