R/init.R

Defines functions .initialize_env_file .initialize_framework_db .update_quarto_output_dir .ensure_notebook_output_dir .replace_author_placeholders .resolve_project_author .configure_git_hooks .create_initial_commit .init_git_repo .cleanup_gitkeep_files bootstrap_project_init .remove_init .is_initialized .init_standard .customize_project_files .display_next_steps .delete_init_file .create_dev_rprofile .create_config_file .create_init_file

Documented in bootstrap_project_init .cleanup_gitkeep_files .configure_git_hooks .create_config_file .create_dev_rprofile .create_init_file .create_initial_commit .customize_project_files .delete_init_file .display_next_steps .init_git_repo .init_standard .is_initialized .remove_init

#' Create init.R from template
#' @keywords internal
.create_init_file <- function(project_name, type, lintr, subdir = NULL) {
  template_path <- system.file("templates/init.R", package = "framework")
  if (!file.exists(template_path)) {
    stop("Template init.R not found in package")
  }

  content <- readLines(template_path, warn = FALSE)

  # Replace placeholders
  content <- gsub("\\{\\{PROJECT_NAME\\}\\}", project_name, content)
  content <- gsub("\\{\\{PROJECT_TYPE\\}\\}", type, content)
  content <- gsub("\\{\\{LINTR\\}\\}", lintr, content)

  target_dir <- if (!is.null(subdir) && nzchar(subdir)) subdir else "."
  target_file <- file.path(target_dir, "init.R")

  writeLines(content, target_file)
  message(sprintf("Created %s", target_file))
}

#' Create settings.yml from template
#' @keywords internal
.create_config_file <- function(type = "analysis", attach_defaults = TRUE, subdir = NULL) {
  # Try type-specific template first, fall back to generic
  template_name <- sprintf("templates/settings.%s.yml", type)
  template_path <- system.file(template_name, package = "framework")

  if (!file.exists(template_path)) {
    # Fall back to generic template
    template_path <- system.file("templates/settings.yml", package = "framework")
    if (!file.exists(template_path)) {
      stop("Template settings.yml not found in package")
    }
  }

  target_dir <- if (!is.null(subdir) && nzchar(subdir)) subdir else "."
  target_file <- file.path(target_dir, "settings.yml")

  # Read template content
  content <- readLines(template_path, warn = FALSE)

  # If attach_defaults is TRUE, replace the packages section with structured format
  if (attach_defaults) {
    # Find the packages section
    packages_start <- grep("^\\s*packages:", content)
    if (length(packages_start) > 0) {
      # Find where packages section ends (next section or end of file)
      section_headers <- grep("^\\s*[a-z_]+:", content)
      next_section <- section_headers[section_headers > packages_start[1]]
      packages_end <- if (length(next_section) > 0) next_section[1] - 1 else length(content)

      # Remove old packages section
      content <- content[-(packages_start:packages_end)]

      # Insert new structured packages section
      new_packages <- c(
        "  packages:",
        "    # Auto-attached packages (available without library() calls)",
        "    - name: dplyr",
        "      attached: true",
        "    - name: tidyr",
        "      attached: true",
        "    - name: ggplot2",
        "      attached: true",
        "    # Installed but not auto-attached (use library() when needed)",
        "    - name: readr",
        "      attached: false",
        "    - name: stringr",
        "      attached: false",
        "    - name: scales",
        "      attached: false",
        ""
      )

      # Insert at the packages_start position
      content <- c(
        content[1:(packages_start - 1)],
        new_packages,
        content[packages_start:length(content)]
      )
    }
  }

  # Write modified content
  writeLines(content, target_file)
  message(sprintf("Created %s", target_file))
}

#' Create development .Rprofile
#' @keywords internal
.create_dev_rprofile <- function(subdir = NULL) {
  target_dir <- if (!is.null(subdir) && nzchar(subdir)) subdir else "."
  target_file <- file.path(target_dir, ".Rprofile")

  # Get the user's home directory
  home_dir <- Sys.getenv("HOME")
  framework_dev_path <- file.path(home_dir, "code", "framework")

  rprofile_content <- sprintf('# Development .Rprofile for Framework
# Auto-generated by init(.dev_mode = TRUE)
# This .Rprofile overrides library() to load framework from development directory

# Store original library function
original_library <- base::library

# Override library function
library <- function(package, help = NULL, pos = 2, lib.loc = NULL,
                    character.only = FALSE, logical.return = FALSE,
                    warn.conflicts = TRUE, quietly = FALSE,
                    verbose = getOption("verbose")) {

  # Get the package name
  if (!character.only) {
    package <- as.character(substitute(package))
  }

  # If it\'s framework, use our custom loading
  if (package == "framework") {
    # First try to load from development directory
    dev_path <- "%s"
    if (dir.exists(dev_path)) {
      if (requireNamespace("devtools", quietly = TRUE)) {
        env <- devtools::load_all(dev_path, export_all = FALSE, quiet = TRUE)
        message("Framework loaded from development directory: ", dev_path)
        return(invisible(env))
      } else {
        warning("devtools package required for dev_mode. Install with: install.packages(\\"devtools\\")")
      }
    }

    # If not in development, try to load as installed package
    if (requireNamespace("framework", quietly = TRUE)) {
      original_library("framework", character.only = TRUE, quietly = quietly,
                      warn.conflicts = warn.conflicts)
      message("Framework loaded from installed package")
      return(invisible(TRUE))
    }

    warning("Framework not found - neither in ", dev_path, " nor as installed package")
    return(invisible(NULL))
  }

  # For all other packages, use the original library function
  original_library(package = package, help = help, pos = pos, lib.loc = lib.loc,
                  character.only = TRUE, logical.return = logical.return,
                  warn.conflicts = warn.conflicts, quietly = quietly,
                  verbose = verbose)
}

message("Framework dev mode active - will load from: %s")
', framework_dev_path, framework_dev_path)

  writeLines(rprofile_content, target_file)
  message(sprintf("Created development .Rprofile: %s", target_file))
  message(sprintf("  Framework will load from: %s", framework_dev_path))
}


#' Delete init.R after successful initialization
#' @keywords internal
.delete_init_file <- function(subdir = NULL) {
  target_dir <- if (!is.null(subdir) && nzchar(subdir)) subdir else "."
  init_file <- file.path(target_dir, "init.R")

  # Only delete if init.R exists
  if (!file.exists(init_file)) {
    return(invisible(NULL))
  }

  # Delete init.R
  tryCatch({
    file.remove(init_file)
    message("\u2713 Cleaned up init.R (use bootstrap_project_init() to recreate for reference)")
  }, error = function(e) {
    warning(sprintf("Could not delete init.R: %s", e$message))
  })

  invisible(NULL)
}

#' Display next steps after initialization
#' @keywords internal
.display_next_steps <- function(project_name = NULL, type = "project", use_renv = FALSE) {
  message("")
  message("\u2713 Framework project initialized successfully!")
  message("")

  # Show summary of settings
  message("Project Configuration:")
  if (!is.null(project_name)) {
    message(sprintf("  -Name: %s", project_name))
  }
  message(sprintf("  -Type: %s", type))
  message(sprintf("  -renv: %s", if (use_renv) "enabled" else "disabled"))
  message("")

  message("Next steps:")
  message("  1. Review and edit settings.yml")
  message("  2. Start a new R session in this directory")
  message("  3. Run:")
  message("       library(framework)")
  message("       scaffold()")
  message("  4. Start analyzing!")
  message("")
  message("Optional:")
  message("  -Add database connections: configure_connection()")
  message("  -Store secrets: Edit .env file directly")
  message("")

  # Additional context based on project type
  if (type == "course") {
    message("Course-specific features:")
    message("  -slides/ - Author lecture decks (render to slides/_rendered/{{ slug }}.html)")
    message("  -assignments/ - Organize homework and lab materials")
    message("  -data/ - Store shared datasets for demonstrations")
    message("")
  } else if (type == "presentation") {
    message("Presentation tips:")
    message("  -Use make_notebook() to create your presentation")
    message("  -Quarto reveal.js format recommended")
    message("")
  }
}


#' Customize project files with user-specific substitutions
#' @keywords internal
.customize_project_files <- function(target_dir, author_name = NULL, author_email = NULL, author_affiliation = NULL) {
  # Get author info from environment if not provided
  if (is.null(author_name)) author_name <- Sys.getenv("FW_AUTHOR_NAME", "Your Name")
  if (is.null(author_email)) author_email <- Sys.getenv("FW_AUTHOR_EMAIL", "")
  if (is.null(author_affiliation)) author_affiliation <- Sys.getenv("FW_AUTHOR_AFFILIATION", "")

  # Find all .qmd and .Rmd files that might have author placeholders
  notebook_files <- c(
    list.files(file.path(target_dir, "notebooks"), pattern = "\\.(qmd|Rmd)$", full.names = TRUE, recursive = TRUE),
    list.files(file.path(target_dir, "slides"), pattern = "\\.(qmd|Rmd)$", full.names = TRUE, recursive = TRUE),
    list.files(file.path(target_dir, "assignments"), pattern = "\\.(qmd|Rmd)$", full.names = TRUE, recursive = TRUE),
    list.files(file.path(target_dir, "course_docs"), pattern = "\\.(qmd|Rmd)$", full.names = TRUE, recursive = TRUE)
  )

  # Remove non-existent paths
  notebook_files <- notebook_files[file.exists(notebook_files)]

  # Apply substitutions to each file
  for (file_path in notebook_files) {
    if (file.exists(file_path)) {
      content <- readLines(file_path, warn = FALSE)

      # Replace author placeholder patterns
      # Pattern 1: "author: Your Name" or "author: [AUTHOR]"
      content <- gsub('author:\\s*"?Your Name"?', paste0('author: "', author_name, '"'), content)
      content <- gsub('author:\\s*"?\\[AUTHOR\\]"?', paste0('author: "', author_name, '"'), content)

      # Pattern 2: Just "Your Name" in author field
      content <- gsub('author:\\s+Your Name', paste0('author: ', author_name), content)

      writeLines(content, file_path)
    }
  }

  # Also update settings/author.yml if it exists
  author_yml <- file.path(target_dir, "settings", "author.yml")
  if (file.exists(author_yml)) {
    content <- readLines(author_yml, warn = FALSE)
    if (!is.null(author_name) && nzchar(author_name) && author_name != "Your Name") {
      content <- gsub('name:\\s*"?Your Name"?', paste0('name: "', author_name, '"'), content)
    }
    if (!is.null(author_email) && nzchar(author_email)) {
      content <- gsub('email:\\s*"?"?', paste0('email: "', author_email, '"'), content)
    }
    if (!is.null(author_affiliation) && nzchar(author_affiliation)) {
      content <- gsub('affiliation:\\s*"?"?', paste0('affiliation: "', author_affiliation, '"'), content)
    }
    writeLines(content, author_yml)
  }
}

#' Standard initialization process (shared by both paths)
#' @keywords internal
.init_standard <- function(project_name, type, lintr, author_name = NULL, author_email = NULL, author_affiliation = NULL, default_notebook_format = NULL, subdir, force, use_git = TRUE) {
  # Validate arguments (already validated in init, but keep for internal calls)
  checkmate::assert_string(project_name, min.chars = 1, null.ok = TRUE)
  checkmate::assert_string(type, min.chars = 1)
  checkmate::assert_string(lintr, min.chars = 1)
  checkmate::assert_string(author_name, min.chars = 1, null.ok = TRUE)
  checkmate::assert_string(author_email, min.chars = 1, null.ok = TRUE)
  checkmate::assert_string(author_affiliation, min.chars = 1, null.ok = TRUE)
  # Handle empty string as NULL
  if (!is.null(default_notebook_format) && !nzchar(default_notebook_format)) {
    default_notebook_format <- NULL
  }
  if (!is.null(default_notebook_format)) {
    checkmate::assert_choice(default_notebook_format, c("quarto", "rmarkdown"))
  }
  checkmate::assert_string(subdir, min.chars = 1, null.ok = TRUE)
  checkmate::assert_flag(force)

  # NOTE: project_create() already checked for existing settings file, no need to check again here
  target_dir <- if (!is.null(subdir) && nzchar(subdir)) subdir else "."

  # Derive project name
  if (!is.null(project_name)) {
    # Convert to lowercase and sanitize for filenames:
    # 1. Convert to lowercase
    # 2. Convert spaces to hyphens
    # 3. Remove all special characters except hyphens
    rproj_name <- tolower(project_name)
    rproj_name <- gsub("\\s+", "-", rproj_name)
    rproj_name <- gsub("[^a-z0-9-]", "", rproj_name)
  } else {
    project_name <- basename(getwd())
    # Apply same sanitization to derived name
    rproj_name <- tolower(project_name)
    rproj_name <- gsub("\\s+", "-", rproj_name)
    rproj_name <- gsub("[^a-z0-9-]", "", rproj_name)
  }

  # Validate template style files
  lintr_template <- system.file("templates", paste0(".lintr.", lintr), package = "framework")
  if (!file.exists(lintr_template)) stop(sprintf("Lintr style '%s' not found", lintr))

  # Remove existing *.Rproj file (only one per project)
  target_dir <- if (!is.null(subdir) && nzchar(subdir)) subdir else "."
  existing_rproj <- list.files(path = target_dir, pattern = "\\.Rproj$", full.names = TRUE)
  if (length(existing_rproj)) file.remove(existing_rproj)

  # Copy and rename .Rproj file
  rproj_template <- system.file("templates", "project.Rproj", package = "framework")
  if (!file.exists(rproj_template)) stop("Template project.Rproj file not found in package.")
  rproj_target <- file.path(target_dir, paste0(rproj_name, ".Rproj"))
  file.copy(rproj_template, rproj_target, overwrite = TRUE)

  # Create IDE configuration files (VS Code workspace and settings)
  .create_ide_configs(rproj_name, target_dir, python = FALSE)

  # Copy and rename other template files
  template_dir <- system.file("templates", package = "framework")
  template_files <- list.files(template_dir, full.names = TRUE, all.files = TRUE, no.. = TRUE)

  for (file in template_files) {
    fname <- basename(file)

    # Only copy specific utility templates to new projects
    # Most templates are handled separately (settings, AI context, notebooks, etc.)
    copy_templates <- list(
      ".editorconfig" = ".editorconfig",
      ".lintr.default" = ".lintr",
      "scaffold.R" = "scaffold.R"
    )

    if (!fname %in% names(copy_templates)) next

    new_name <- copy_templates[[fname]]
    target_path <- file.path(target_dir, new_name)
    dir.create(dirname(target_path), showWarnings = FALSE, recursive = TRUE)

    success <- file.copy(file, target_path, overwrite = TRUE)
    if (!success) warning(sprintf("Failed to copy template file: %s to %s", file, target_path))

    # Substitute {subdir} in YAML-like config files
    if (grepl("\\.ya?ml$", new_name)) {
      content <- readLines(target_path)
      subdir_prefix <- if (!is.null(subdir) && nzchar(subdir)) paste0(subdir, "/") else ""
      content <- gsub("\\{subdir\\}", subdir_prefix, content)
      writeLines(content, target_path)
    }
  }

  # Copy project structure
  structure_dir <- system.file("project_structure", type, package = "framework")
  if (!dir.exists(structure_dir)) stop(sprintf("Project type '%s' not found", type))

  all_dirs <- list.dirs(structure_dir, recursive = TRUE, full.names = TRUE)
  all_dirs <- all_dirs[all_dirs != structure_dir]

  for (dir in all_dirs) {
    rel_path <- sub(paste0("^", structure_dir, "/?"), "", dir)
    target_path <- file.path(target_dir, rel_path)
    dir.create(target_path, showWarnings = FALSE, recursive = TRUE)
  }

  structure_files <- list.files(structure_dir, recursive = TRUE, full.names = TRUE, all.files = TRUE)
  for (file in structure_files) {
    rel_path <- sub(paste0("^", structure_dir, "/?"), "", file)

    # Skip connections.yml for presentation projects (no database by default)
    if (type == "presentation" && grepl("settings/connections\\.yml$", rel_path)) {
      next
    }

    target_path <- file.path(target_dir, rel_path)

    # Copy file as-is (project_structure files don't use .fr extension)
    file.copy(file, target_path, overwrite = TRUE)
  }

  # README.md is now part of project_structure and copied above

  # Post-copy customization hook: Apply user-specific substitutions
  .customize_project_files(
    target_dir = target_dir,
    author_name = author_name,
    author_email = author_email,
    author_affiliation = author_affiliation
  )

  # Rename settings/ directory based on user preference
  config_dir_pref <- Sys.getenv("FW_CONFIG_DIR", "settings")
  if (config_dir_pref == "config" && dir.exists(file.path(target_dir, "settings"))) {
    # Rename settings/ to config/
    file.rename(
      from = file.path(target_dir, "settings"),
      to = file.path(target_dir, "config")
    )

    # Update references in settings file
    config_path <- .get_settings_file(target_dir)
    if (is.null(config_path)) {
      config_path <- file.path(target_dir, "settings.yml")
    }

    if (file.exists(config_path)) {
      config_content <- readLines(config_path, warn = FALSE)
      # Replace "settings/" with "config/" in references
      config_content <- gsub("settings/", "config/", config_content, fixed = TRUE)
      writeLines(config_content, config_path)
    }
  }

  # Remove template directories when config maps them to project root (".")
  config_path_for_dirs <- .get_settings_file(target_dir)
  if (!is.null(config_path_for_dirs) && file.exists(config_path_for_dirs)) {
    config_for_dirs <- tryCatch(
      settings_read(config_path_for_dirs),
      error = function(e) NULL
    )

    if (!is.null(config_for_dirs) && !is.null(config_for_dirs$directories)) {
      for (dir_name in names(config_for_dirs$directories)) {
        dir_value <- config_for_dirs$directories[[dir_name]]
        if (is.character(dir_value) && length(dir_value) == 1) {
          dir_value_trim <- trimws(dir_value)
          if (dir_value_trim %in% c(".", "./", "")) {
            candidate_path <- file.path(target_dir, dir_name)
            if (dir.exists(candidate_path)) {
              contents <- list.files(candidate_path, all.files = TRUE, no.. = TRUE)
              if (length(contents) == 0) {
                unlink(candidate_path, recursive = TRUE)
              }
            }
          }
        }
      }
    }
  }

  # Update settings.yml (or config.yml) with author information and notebook format if provided
  has_author_info <- (!is.null(author_name) && nzchar(author_name)) ||
                      (!is.null(author_email) && nzchar(author_email)) ||
                      (!is.null(author_affiliation) && nzchar(author_affiliation))
  has_format_pref <- !is.null(default_notebook_format) && nzchar(default_notebook_format)

  if (has_author_info || has_format_pref) {
    # Find settings file (prefer settings.yml, fallback to config.yml)
    config_path <- .get_settings_file(target_dir)
    if (is.null(config_path)) {
      config_path <- file.path(target_dir, "settings.yml")
    }

    if (file.exists(config_path)) {
      config_content <- readLines(config_path, warn = FALSE)
      config_modified <- FALSE

      update_author_split <- function(author_file) {
        author_path <- file.path(dirname(config_path), author_file)
        if (!file.exists(author_path)) {
          return(FALSE)
        }

        author_yaml <- tryCatch(yaml::read_yaml(author_path), error = function(e) NULL)
        if (is.null(author_yaml)) {
          return(FALSE)
        }

        if (is.null(author_yaml$author) || !is.list(author_yaml$author)) {
          author_yaml$author <- list()
        }

        if (!is.null(author_name) && nzchar(author_name)) {
          author_yaml$author$name <- author_name
        }
        if (!is.null(author_email) && nzchar(author_email)) {
          author_yaml$author$email <- author_email
        }
        if (!is.null(author_affiliation) && nzchar(author_affiliation)) {
          author_yaml$author$affiliation <- author_affiliation
        }

        yaml::write_yaml(author_yaml, author_path)
        TRUE
      }

      update_options_split <- function(options_file) {
        options_path <- file.path(dirname(config_path), options_file)
        if (!file.exists(options_path)) {
          return(FALSE)
        }

        options_yaml <- tryCatch(yaml::read_yaml(options_path), error = function(e) NULL)
        if (is.null(options_yaml)) {
          return(FALSE)
        }

        if (is.null(options_yaml$options) || !is.list(options_yaml$options)) {
          options_yaml$options <- list()
        }

        if (!is.null(default_notebook_format) && nzchar(default_notebook_format)) {
          options_yaml$options$default_notebook_format <- default_notebook_format
        }

        yaml::write_yaml(options_yaml, options_path)
        TRUE
      }

      # Update author section if provided
      if (has_author_info) {
        author_start <- grep("^  author:", config_content)
        if (length(author_start) > 0) {
        author_entry <- config_content[author_start[1]]
        author_file <- trimws(sub("^  author:\\s*", "", author_entry))

          updated_split <- FALSE
          if (nzchar(author_file) && grepl("\\.ya?ml$", author_file, ignore.case = TRUE)) {
            updated_split <- update_author_split(author_file)
          }

          if (!updated_split) {
            # Inline fallback
            next_section_pattern <- "^  [a-z_]+:"
            all_sections <- grep(next_section_pattern, config_content)
            next_section <- all_sections[all_sections > author_start[1]]
            author_end <- if (length(next_section) > 0) next_section[1] - 1 else length(config_content)

            for (i in author_start:author_end) {
              if (!is.null(author_name) && nzchar(author_name) && grepl("^    name:", config_content[i])) {
                config_content[i] <- sprintf("    name: \"%s\"", author_name)
                config_modified <- TRUE
              }
              if (!is.null(author_email) && nzchar(author_email) && grepl("^    email:", config_content[i])) {
                config_content[i] <- sprintf("    email: \"%s\"", author_email)
                config_modified <- TRUE
              }
              if (!is.null(author_affiliation) && nzchar(author_affiliation) && grepl("^    affiliation:", config_content[i])) {
                config_content[i] <- sprintf("    affiliation: \"%s\"", author_affiliation)
                config_modified <- TRUE
              }
            }
          }
        }
      }

      # Update default_notebook_format if provided
      if (has_format_pref) {
        options_line <- grep("^  options:", config_content)
        updated_split <- FALSE

        if (length(options_line) > 0) {
        options_entry <- config_content[options_line[1]]
        options_file <- trimws(sub("^  options:\\s*", "", options_entry))
          if (nzchar(options_file) && grepl("\\.ya?ml$", options_file, ignore.case = TRUE)) {
            updated_split <- update_options_split(options_file)
          }
        }

        if (!updated_split) {
          format_line <- grep("^  default_notebook_format:", config_content)
          if (length(format_line) > 0) {
            config_content[format_line[1]] <- sprintf("  default_notebook_format: %s", default_notebook_format)
            config_modified <- TRUE
          }
        }
      }

      if (config_modified) {
        writeLines(config_content, config_path)
      }
    }
  }

  # Ensure author placeholders in starter notebooks use resolved name
  .replace_author_placeholders(target_dir)
  .initialize_framework_db(target_dir)
  .initialize_env_file(target_dir)

  # Initialization complete (settings.yml/config.yml serves as marker)
  message(sprintf("Project '%s' initialized successfully!", project_name))
}

#' Check if project is initialized
#'
#' Checks for existence of settings.yml/settings.yml to determine initialization status.
#'
#' @param subdir Optional subdirectory to check.
#' @return Logical indicating if project is initialized.
#' @keywords internal
.is_initialized <- function(subdir = NULL) {
  # Validate arguments
  checkmate::assert_string(subdir, min.chars = 1, null.ok = TRUE)

  target_dir <- if (!is.null(subdir) && nzchar(subdir)) subdir else "."
  !is.null(.get_settings_file(target_dir))
}

#' Remove initialization
#'
#' Removes settings.yml/settings.yml to mark project as uninitialized.
#' WARNING: This will delete your project configuration!
#'
#' @param subdir Optional subdirectory to check.
#' @return Logical indicating if removal was successful.
#' @keywords internal
.remove_init <- function(subdir = NULL) {
  # Validate arguments
  checkmate::assert_string(subdir, min.chars = 1, null.ok = TRUE)

  target_dir <- if (!is.null(subdir) && nzchar(subdir)) subdir else "."
  config_file <- .get_settings_file(target_dir)
  if (!is.null(config_file) && file.exists(config_file)) {
    warning("Removing settings file - your project configuration will be deleted!", call. = FALSE)
    unlink(config_file)
    TRUE
  } else {
    FALSE
  }
}

#' Bootstrap project initialization file
#'
#' Generates an init.R file showing the initialization logic.
#' Useful for documentation and understanding how the project was set up.
#'
#' @param output_file Path where init.R should be written. Default: "init.R"
#' @return Invisibly returns TRUE on success
#' @keywords internal
#' @export
bootstrap_project_init <- function(output_file = "init.R") {
  checkmate::assert_string(output_file, min.chars = 1)

  template_path <- system.file("templates/init.R", package = "framework")
  if (!file.exists(template_path)) {
    stop("Template init.R not found in package")
  }

  # Read template
  content <- readLines(template_path, warn = FALSE)

  # Add header explaining this is generated
  header <- c(
    "# This file was generated by bootstrap_project_init() for reference purposes.",
    "# It shows the initialization logic used by framework::project_create().",
    "# You can safely delete this file.",
    "",
    ""
  )

  content <- c(header, content)

  # Write file
  writeLines(content, output_file)
  message(sprintf("[ok] Created %s", output_file))
  message("  This file shows initialization logic for reference.")
  message("  Placeholders like {{PROJECT_NAME}} would be replaced during actual project_create().")

  invisible(TRUE)
}

#' Remove .gitkeep files from data/ and functions/ directories
#' @keywords internal
.cleanup_gitkeep_files <- function(target_dir = ".") {
  # Find all .gitkeep files in data/ and functions/ directories
  data_gitkeeps <- list.files(
    path = file.path(target_dir, "data"),
    pattern = "^\\.gitkeep$",
    recursive = TRUE,
    full.names = TRUE,
    all.files = TRUE
  )

  functions_gitkeeps <- list.files(
    path = file.path(target_dir, "functions"),
    pattern = "^\\.gitkeep$",
    recursive = TRUE,
    full.names = TRUE,
    all.files = TRUE
  )

  all_gitkeeps <- c(data_gitkeeps, functions_gitkeeps)

  if (length(all_gitkeeps) > 0) {
    removed_count <- sum(file.remove(all_gitkeeps))
    if (removed_count > 0) {
      message(sprintf("\u2713 Cleaned up %d .gitkeep file%s", removed_count, if (removed_count == 1) "" else "s"))
    }
  }

  invisible(NULL)
}

#' Initialize git repository
#' @keywords internal
.init_git_repo <- function(target_dir = ".") {
  # Check git is available
  if (!nzchar(Sys.which("git"))) {
    message("Note: Git not found. Skipping repository initialization.")
    return(invisible(NULL))
  }

  git_dir <- file.path(target_dir, ".git")

  if (file.exists(git_dir)) {
    return(invisible(NULL))
  }

  tryCatch({
    # Initialize git
    old_wd <- getwd()
    on.exit(setwd(old_wd), add = TRUE)

    if (!is.null(target_dir) && target_dir != ".") {
      setwd(target_dir)
    }

    # Initialize repo
    init_status <- system("git init", ignore.stdout = TRUE, ignore.stderr = TRUE)
    if (init_status != 0) {
      stop("git init failed")
    }

    # Add all files
    add_status <- system("git add .", ignore.stdout = TRUE, ignore.stderr = TRUE)
    if (add_status != 0) {
      stop("git add failed")
    }

    # Force-add .gitignore files in private directories (defense-in-depth)
    private_gitignores <- c(
      "inputs/raw/.gitignore",
      "inputs/intermediate/.gitignore",
      "inputs/final/.gitignore",
      "inputs/reference/.gitignore",
      "outputs/private/.gitignore"
    )
    for (gitignore_path in private_gitignores) {
      if (file.exists(gitignore_path)) {
        system(paste0("git add -f ", gitignore_path), ignore.stdout = TRUE, ignore.stderr = TRUE)
      }
    }

    message("\u2713 Git repository initialized")
  }, error = function(e) {
    message("Note: Could not initialize git repository. You can run 'git init' manually if needed.")
  })

  invisible(NULL)
}

#' Create initial git commit after all initialization is complete
#' @keywords internal
.create_initial_commit <- function(target_dir = ".") {
  # Check git is available
  if (!nzchar(Sys.which("git"))) {
    return(invisible(NULL))
  }

  # Change to target directory
  old_wd <- getwd()
  on.exit(setwd(old_wd), add = TRUE)

  if (!is.null(target_dir) && nzchar(target_dir) && target_dir != ".") {
    if (dir.exists(target_dir)) {
      setwd(target_dir)
    } else {
      target_path <- tryCatch(
        normalizePath(target_dir, winslash = "/", mustWork = TRUE),
        error = function(e) NULL
      )
      if (!is.null(target_path) && dir.exists(target_path)) {
        setwd(target_path)
      }
    }
  }

  # Check if we're in a git repo
  git_check <- system("git rev-parse --git-dir", ignore.stdout = TRUE, ignore.stderr = TRUE)
  if (git_check != 0) {
    return(invisible(NULL))
  }

  # Check if there are any commits yet
  has_commits <- system("git rev-parse HEAD", ignore.stdout = TRUE, ignore.stderr = TRUE) == 0

  if (!has_commits) {
    # No commits yet - add all files (including any created after init, like .github/)
    system("git add -A", ignore.stdout = TRUE, ignore.stderr = TRUE)

    # Check identity before committing
    user_name <- system("git config user.name", intern = TRUE, ignore.stderr = TRUE)
    user_email <- system("git config user.email", intern = TRUE, ignore.stderr = TRUE)

    if (length(user_name) == 0 || length(user_email) == 0) {
      message("Note: Skipping initial commit. Configure git user with:")
      message("  git config user.name \"Your Name\"")
      message("  git config user.email \"your.email@example.com\"")
    } else {
      msg_file <- tempfile("framework_init_commit_")
      writeLines("Project initialized.", msg_file)
      on.exit(unlink(msg_file), add = TRUE)
      commit_result <- system2("git", c("commit", "-F", msg_file), stdout = TRUE, stderr = TRUE)

      if (is.null(attr(commit_result, "status")) || identical(attr(commit_result, "status"), 0)) {
        message("\u2713 Initial commit created")
      } else {
        message("Note: Could not create initial commit. Configure git user with:")
        message("  git config user.name \"Your Name\"")
        message("  git config user.email \"your.email@example.com\"")
      }
    }
  }

  invisible(NULL)
}

#' Configure git hooks based on environment variables
#' @keywords internal
.configure_git_hooks <- function(target_dir = ".") {
  # Read hook configuration from environment variables (set by new-project.sh)
  hooks_enabled <- Sys.getenv("FW_HOOKS_ENABLED", "FALSE")
  ai_sync_enabled <- Sys.getenv("FW_HOOK_AI_SYNC", "FALSE")
  data_security_enabled <- Sys.getenv("FW_HOOK_DATA_SECURITY", "FALSE")
  ai_canonical <- Sys.getenv("FW_AI_CANONICAL", "")

  # Convert string booleans to logical
  hooks_enabled <- toupper(hooks_enabled) == "TRUE"
  ai_sync_enabled <- toupper(ai_sync_enabled) == "TRUE"
  data_security_enabled <- toupper(data_security_enabled) == "TRUE"

  if (!hooks_enabled) {
    return(invisible(NULL))
  }

  config_path <- .get_settings_file(target_dir)
  if (is.null(config_path)) {
    config_path <- file.path(target_dir, "settings.yml")
  }

  if (file.exists(config_path)) {
    tryCatch({
      config_content <- readLines(config_path, warn = FALSE)
      config_modified <- FALSE

      update_git_split <- function(git_file) {
        git_path <- file.path(dirname(config_path), git_file)
        if (!file.exists(git_path)) {
          return(FALSE)
        }
        git_yaml <- tryCatch(yaml::read_yaml(git_path), error = function(e) NULL)
        if (is.null(git_yaml)) {
          return(FALSE)
        }
        if (is.null(git_yaml$git) || !is.list(git_yaml$git)) {
          git_yaml$git <- list()
        }
        if (is.null(git_yaml$git$hooks) || !is.list(git_yaml$git$hooks)) {
          git_yaml$git$hooks <- list()
        }
        git_yaml$git$hooks$ai_sync <- ai_sync_enabled
        git_yaml$git$hooks$data_security <- data_security_enabled
        yaml::write_yaml(git_yaml, git_path)
        TRUE
      }

      update_ai_split <- function(ai_file) {
        if (!nzchar(ai_canonical)) {
          return(FALSE)
        }
        ai_path <- file.path(dirname(config_path), ai_file)
        if (!file.exists(ai_path)) {
          return(FALSE)
        }
        ai_yaml <- tryCatch(yaml::read_yaml(ai_path), error = function(e) NULL)
        if (is.null(ai_yaml)) {
          return(FALSE)
        }
        if (is.null(ai_yaml$ai) || !is.list(ai_yaml$ai)) {
          ai_yaml$ai <- list()
        }
        ai_yaml$ai$canonical_file <- ai_canonical
        yaml::write_yaml(ai_yaml, ai_path)
        TRUE
      }

      git_line <- grep("^  git:", config_content)
      git_updated <- FALSE
      if (length(git_line) > 0) {
        git_entry <- config_content[git_line[1]]
        git_file <- trimws(sub("^  git:\\s*", "", git_entry))
        if (nzchar(git_file) && grepl("\\.ya?ml$", git_file, ignore.case = TRUE)) {
          git_updated <- update_git_split(git_file)
        }
      }
      if (!git_updated) {
        config_content <- gsub(
          "^(\\s*ai_sync:\\s*)false(\\s*.*)?$",
          sprintf("\\1%s\\2", tolower(ai_sync_enabled)),
          config_content
        )
        config_content <- gsub(
          "^(\\s*data_security:\\s*)false(\\s*.*)?$",
          sprintf("\\1%s\\2", tolower(data_security_enabled)),
          config_content
        )
        config_modified <- TRUE
      }

      ai_line <- grep("^  ai:", config_content)
      ai_updated <- FALSE
      if (length(ai_line) > 0) {
        ai_entry <- config_content[ai_line[1]]
        ai_file <- trimws(sub("^  ai:\\s*", "", ai_entry))
        if (nzchar(ai_file) && grepl("\\.ya?ml$", ai_file, ignore.case = TRUE)) {
          ai_updated <- update_ai_split(ai_file)
        }
      }
      if (!ai_updated && nzchar(ai_canonical)) {
        config_content <- gsub(
          "^(\\s*canonical_file:\\s*)\"\"(\\s*.*)?$",
          sprintf("\\1\"%s\"\\2", ai_canonical),
          config_content
        )
        config_modified <- TRUE
      }

      if (config_modified) {
        writeLines(config_content, config_path)
      }
    }, error = function(e) {
      warning("Could not update settings file with hook settings: ", e$message)
    })
  }

  # Install hooks if any are enabled
  if (ai_sync_enabled || data_security_enabled) {
    tryCatch({
      # Change to target directory for hooks_install
      old_wd <- getwd()
      on.exit(setwd(old_wd), add = TRUE)

      if (!is.null(target_dir) && target_dir != ".") {
        setwd(target_dir)
      }

      # Install hooks
      git_hooks_install(config_file = (.get_settings_file() %||% "settings.yml"), force = TRUE, verbose = FALSE)

      # Show message about what was installed
      hooks_msg <- character()
      if (ai_sync_enabled) hooks_msg <- c(hooks_msg, "AI context sync")
      if (data_security_enabled) hooks_msg <- c(hooks_msg, "data security check")

      message(sprintf("[ok] Installed git hooks: %s", paste(hooks_msg, collapse = ", ")))
    }, error = function(e) {
      message("Note: Could not install git hooks. You can run 'framework hooks:install' later")
    })
  }

  invisible(NULL)
}

.resolve_project_author <- function(target_dir = ".") {
  old_wd <- getwd()
  on.exit(setwd(old_wd), add = TRUE)

  if (!is.null(target_dir) && nzchar(target_dir) && target_dir != ".") {
    if (dir.exists(target_dir)) {
      setwd(target_dir)
    }
  }

  cfg <- tryCatch(settings_read(), error = function(e) NULL)
  author_name <- cfg$author$name
  if (is.null(author_name) || !nzchar(author_name)) {
    author_name <- "Your Name"
  }
  author_name
}

.replace_author_placeholders <- function(target_dir = ".") {
  author_name <- .resolve_project_author(target_dir)

  notebook_files <- list.files(
    path = target_dir,
    pattern = "\\.(qmd|QMD|Rmd|rmd)$",
    recursive = TRUE,
    full.names = TRUE
  )

  pattern <- 'author:\\s*("Your Name"|!expr config\\$author\\$name|"`r config\\$author\\$name`")'
  replacement <- sprintf('author: "%s"', author_name)

  for (file in notebook_files) {
    lines <- readLines(file, warn = FALSE)
    new_lines <- gsub(pattern, replacement, lines)
    if (!identical(lines, new_lines)) {
      writeLines(new_lines, file)
    }
  }

  .ensure_notebook_output_dir(target_dir, author_name = author_name)

  invisible(NULL)
}

.ensure_notebook_output_dir <- function(target_dir = ".", author_name = NULL) {
  config_path <- .get_settings_file(target_dir)
  if (is.null(config_path)) {
    config_path = file.path(target_dir, "settings.yml")
  }

  if (!file.exists(config_path)) {
    return(invisible(NULL))
  }

  cfg <- tryCatch(settings_read(config_path), error = function(e) NULL)
  if (is.null(cfg)) {
    return(invisible(NULL))
  }

  notebook_conf <- cfg$notebook
  if (is.null(notebook_conf)) {
    notebook_conf <- cfg$options$notebook
  }
  directories_conf <- cfg$directories

  notebooks_dir <- directories_conf$notebooks
  if (is.null(notebooks_dir) && is.list(cfg$options$notebook) && !is.null(cfg$options$notebook$dir)) {
    notebooks_dir <- cfg$options$notebook$dir
  }

  desired_output <- "_rendered"
  if (is.list(notebook_conf) && !is.null(notebook_conf$output_dir) && nzchar(notebook_conf$output_dir)) {
    desired_output <- notebook_conf$output_dir
  }

  if (!is.null(notebooks_dir) && nzchar(notebooks_dir) && notebooks_dir != "." && notebooks_dir != "./") {
    if (!startsWith(desired_output, notebooks_dir)) {
      desired_output <- file.path(notebooks_dir, basename(desired_output))
    }
  }

  .update_quarto_output_dir(target_dir, desired_output)

  invisible(NULL)
}

.update_quarto_output_dir <- function(target_dir = ".", output_dir) {
  quarto_file <- file.path(target_dir, "_quarto.yml")
  if (!file.exists(quarto_file)) {
    return(invisible(NULL))
  }

  lines <- readLines(quarto_file, warn = FALSE)
  pattern <- '^\\s*output-dir:\\s*(.*)$'
  replacement <- sprintf('output-dir: %s', output_dir)
  new_lines <- sub(pattern, replacement, lines)

  if (!identical(lines, new_lines)) {
    writeLines(new_lines, quarto_file)
  }

  invisible(NULL)
}

.initialize_framework_db <- function(target_dir = ".") {
  template_db <- system.file("templates", "framework.db", package = "framework")
  if (!nzchar(template_db) || !file.exists(template_db)) {
    return(invisible(NULL))
  }

  old_wd <- getwd()
  on.exit(setwd(old_wd), add = TRUE)

  if (!is.null(target_dir) && nzchar(target_dir) && target_dir != ".") {
    if (dir.exists(target_dir)) {
      setwd(target_dir)
    }
  }

  if (!file.exists("framework.db")) {
    if (file.copy(template_db, "framework.db", overwrite = FALSE)) {
      message("\u2713 Initialized framework.db")
    }
  }

  invisible(NULL)
}

.initialize_env_file <- function(target_dir = ".") {
  env_path <- file.path(target_dir, ".env")

  if (file.exists(env_path)) {
    return(invisible(NULL))
  }

  template <- NULL
  config <- try(read_frameworkrc(use_defaults = TRUE), silent = TRUE)
  if (!inherits(config, "try-error") && !is.null(config$defaults$env)) {
    template <- env_resolve_lines(config$defaults$env)
  } else {
    template <- env_default_template_lines()
  }

  writeLines(template, env_path)
  message("\u2713 Created .env with default connection placeholders")

  invisible(NULL)
}

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.