# Getting Started with courieR

courieR syncs installed R packages between R versions on the same machine. You can migrate from an old version to a new one, keep two versions in parity, or selectively push packages in either direction — all from the R console or from a point-and-click dashboard.

## Installation

Install courieR from CRAN:

    install.packages("courieR")

The core CLI workflow (`find_routes()`, `manifest()`, `inventory()`, `ship()`) has no Shiny dependency. The dashboard (`open_hub()`) requires additional packages:

    install.packages(c("shiny", "bslib", "bsicons", "DT"))

## Step 1 — Discover Your R Installations

`find_routes()` scans the system and returns a data frame of every R version it can find:

    library(courieR)

    routes <- find_routes()
    routes
    #>   version                                          rscript_path is_current
    #> 1   4.4.2        C:/Program Files/R/R-4.4.2/bin/x64/Rscript.exe       TRUE
    #> 2   4.3.1  C:/Users/you/AppData/Local/Programs/R/R-4.3.1/bin/...      FALSE
    #> 3   4.1.3            C:/Users/you/Documents/R/R-4.1.3/bin/...      FALSE

`is_current = TRUE` marks the R session you are running right now.

### Platform detection

Platform | Locations checked
--- | ---
Windows | HKLM/HKCU registry, `%ProgramFiles%\R`, `%LOCALAPPDATA%\Programs\R`, `%USERPROFILE%\Documents\R`, rig
macOS | `/Library/Frameworks/R.framework`, `~/Library/Frameworks/R.framework`, Homebrew (`/opt/homebrew`, `/usr/local`), rig
Linux | `/opt/R` (rig system), `~/.local/share/rig/R` (rig user), conda envs, system `Rscript` on `$PATH`

To include a non-standard path, pass it explicitly:

    routes <- find_routes(search_paths = "/opt/custom-r/bin/Rscript")

## CLI Workflow

The four core functions form a pipeline:

    find_routes()  →  manifest()  →  inventory()  →  ship()
       discover        scan            compare         migrate

### Step 2 — Scan a library with `manifest()`

`manifest()` runs a subprocess under a given Rscript and returns every installed package with its version and source:

    # scan the first (newest) R
    src_pkgs <- manifest(rscript_path = routes$rscript_path[1])
    head(src_pkgs[, c("package", "version", "source")])
    #>     package version source
    #> 1     broom   1.0.7   CRAN
    #> 2    callr   3.7.6   CRAN
    #> 3   courieR   0.2.0 GitHub
    #> 4   ggplot2   3.5.1   CRAN
    #> 5      glue   1.8.0   CRAN
    #> 6   stringr   1.5.1   CRAN

Calling `manifest()` with no arguments scans the library of the current R session:

    my_pkgs <- manifest()
    nrow(my_pkgs)
    #> [1] 312

Base and recommended packages are included in the raw manifest. Filter them out before comparison:

    user_pkgs <- src_pkgs[is.na(src_pkgs$priority) | !(src_pkgs$priority %in% c("base", "recommended")), ]

### Step 3 — Compare two libraries with `inventory()`

`inventory()` takes two manifests and returns a classified diff:

    src_pkgs <- manifest(rscript_path = routes$rscript_path[2])  # old R
    tgt_pkgs <- manifest(rscript_path = routes$rscript_path[1])  # new R

    comp <- inventory(src_pkgs, tgt_pkgs)

The result is a list with three elements:

    # packages in source but missing from target
    nrow(comp$missing)
    #> [1] 47

    # packages where source has a newer version
    nrow(comp$outdated)
    #> [1] 12

    # packages at the same version in both
    nrow(comp$same)
    #> [1] 201

Inspect what needs to move:

    comp$missing[, c("package", "version", "source")]
    #>       package version  source
    #>  1:    bookdown   0.39    CRAN
    #>  2:  brms   2.21.0    CRAN
    #>  3:   officer   0.6.6    CRAN
    #>  ...

    comp$outdated[, c("package", "version_src", "version_tgt", "source")]
    #>    package version_src version_tgt source
    #> 1:  ggplot2       3.5.1       3.4.4   CRAN
    #> 2:   tibble       3.2.1       3.2.0   CRAN

### Step 4 — Migrate with `ship()`

`ship()` takes a source and target Rscript path, computes the plan internally, and runs it:

    result <- ship(
      source_path = routes$rscript_path[2],   # old R — package source
      target_path = routes$rscript_path[1]    # new R — install destination
    )

#### Always dry-run first

    result <- ship(
      source_path = routes$rscript_path[2],
      target_path = routes$rscript_path[1],
      dry_run = TRUE
    )

    # review the plan before anything is installed
    print(result$plan)
    #>         package action  pak_spec
    #>  1:    bookdown install bookdown
    #>  2:        brms install     brms
    #>  3:     ggplot2 upgrade  ggplot2
    #>  ...

#### Include version upgrades

By default, `ship()` only installs missing packages. Pass `upgrade = TRUE` to also update packages that are present but at an older version (mirrors what the Sync tab does):

    result <- ship(
      source_path = routes$rscript_path[2],
      target_path = routes$rscript_path[1],
      upgrade = TRUE
    )

#### Check results

    # summary counts
    result$summary
    #> $installed   47
    #> $upgraded    12
    #> $failed       1
    #> $skipped      0

    # per-package results
    result$results[result$results$status == "error", c("package", "message")]
    #>    package                         message
    #> 1:    rJava  installation of rJava failed...

#### Migrate GitHub and Bioconductor packages

`ship()` detects source-specific packages automatically. CRAN packages are reinstalled from CRAN; GitHub packages become `"owner/repo"` pak specs; Bioconductor packages use `"bioc::pkg"` specs. No extra configuration needed.

    # mixed-source example — all handled automatically
    result$plan[, c("package", "source", "pak_spec")]
    #>    package   source        pak_spec
    #> 1:  ggplot2     CRAN         ggplot2
    #> 2:  courieR   GitHub  lennon-li/courieR
    #> 3:    DESeq2  Bioconductor  bioc::DESeq2

## Common Recipes

### One-way migration (old R → new R)

    library(courieR)

    routes  <- find_routes()
    old_r   <- routes$rscript_path[!routes$is_current][1]
    new_r   <- routes$rscript_path[routes$is_current]

    # dry run
    ship(source_path = old_r, target_path = new_r, dry_run = TRUE, upgrade = TRUE)

    # for real
    result <- ship(source_path = old_r, target_path = new_r, upgrade = TRUE)
    result$summary

### Two-way sync (keep two R versions in parity)

    r_a <- routes$rscript_path[1]
    r_b <- routes$rscript_path[2]

    # push everything A has to B
    ship(source_path = r_a, target_path = r_b, upgrade = TRUE)

    # push everything B has to A
    ship(source_path = r_b, target_path = r_a, upgrade = TRUE)

### Inspect before migrating

    src  <- manifest(rscript_path = old_r)
    tgt  <- manifest(rscript_path = new_r)
    comp <- inventory(src, tgt)

    # only migrate CRAN packages — skip GitHub/unknown sources
    cran_missing <- comp$missing[comp$missing$source == "CRAN", ]
    cat(nrow(cran_missing), "CRAN packages to install\n")

### Save a manifest to disk

    pkgs <- manifest(rscript_path = routes$rscript_path[1])
    write.csv(pkgs[, c("package", "version", "source")], "my_packages.csv", row.names = FALSE)

Restore from a saved manifest on a new machine:

    saved <- read.csv("my_packages.csv", stringsAsFactors = FALSE)
    # pak installs from a character vector of package names
    pak::pkg_install(saved$package)

## Dashboard

If you prefer point-and-click, `open_hub()` launches a Shiny dashboard that wraps the same pipeline. Install the dashboard dependencies first if you haven’t already:

    install.packages(c("shiny", "bslib", "bsicons", "DT"))
    open_hub()

The Sync tab mirrors the `ship()` CLI workflow. The Advanced tab exposes `manifest()` output and lets you inspect any detected R installation’s full package list.

## Tips

- courieR skips base and recommended packages — only user-installed packages are migrated
- `ship()` uses `pak` under the hood, which resolves dependencies automatically
- GitHub packages require the source repository to be public, or a `GITHUB_PAT` to be set
- If a package fails, check `result$results` — the `message` column has the pak error
- On Linux without rig, `find_routes()` may only detect the R on `$PATH`; pass additional paths via `search_paths` if needed