Skip to content

Scan profiles from ~/.coi/profiles/ alongside ~/.config/coi/profiles/#278

Merged
mensfeld merged 4 commits intomasterfrom
profile-dirs-both-locations
Apr 6, 2026
Merged

Scan profiles from ~/.coi/profiles/ alongside ~/.config/coi/profiles/#278
mensfeld merged 4 commits intomasterfrom
profile-dirs-both-locations

Conversation

@mensfeld
Copy link
Copy Markdown
Owner

@mensfeld mensfeld commented Apr 5, 2026

Summary

Profiles can now live under any of /etc/coi/profiles/, ~/.config/coi/profiles/, ~/.coi/profiles/, or ./.coi/profiles/. All locations are merged into a single namespace so users can keep profiles alongside their XDG config or alongside runtime data (~/.coi/ — same parent as sessions/storage/logs).

Duplicate detection: If the same profile name is defined in more than one location, COI refuses to start and points to both files, asking the user to rename one. This keeps it unambiguous which profile is actually being applied and avoids silent override surprises across scan locations.

Motivation

Previously only ~/.config/coi/profiles/ was scanned from the user home area. Users who naturally placed profiles under ~/.coi/profiles/ (alongside sessions/storage/logs, which live under ~/.coi/) would see them silently ignored. This PR makes both locations work out of the box.

Changes

  • internal/config/config.go — new GetProfileParentDirs() helper returning all scan locations (/etc/coi, ~/.config/coi, ~/.coi, ./.coi, and $COI_CONFIG parent dir if set)
  • internal/config/loader.goLoad() now iterates GetProfileParentDirs() for profile scanning, independently from config-file loading. loadProfileDirectories() rejects duplicate profile names across scan locations with a clear error pointing to both source files
  • README.md + embedded WriteExample docs updated to list all four locations and explain the merge + duplicate-detection behavior
  • CHANGELOG.md entry under Improvements

Tests

Go unit tests (internal/config/loader_test.go + config_test.go):

  • TestGetProfileParentDirs / …WithCoiConfig / …IncludesHomeCoi
  • TestLoadProfilesFromHomeCoi — ~/.coi/profiles/ loads via Load()
  • TestLoadMergesProfilesFromBothHomeLocations — XDG + ~/.coi merged
  • TestLoadMergesProjectAndHomeProfiles — XDG + ~/.coi + project .coi merged
  • TestLoadDuplicateProfileAcrossLocationsFails — home-vs-home duplicate errors
  • TestLoadDuplicateProfileProjectVsHomeFails — project-vs-home duplicate errors
  • TestLoadProfileDirectoriesDuplicateNameError — direct loader test

Python integration tests (tests/config/test_profile_home_coi_dir.py) using a fake HOME:

  • test_profile_list_from_home_coi_dir
  • test_profile_info_from_home_coi_dir
  • test_profile_merges_all_locations — XDG + ~/.coi + project .coi all listed and resolvable
  • test_profile_duplicate_name_across_home_locations_fails
  • test_profile_duplicate_name_project_vs_home_fails

Test plan

  • go build ./...
  • go vet ./...
  • go test ./internal/config/
  • python -m pytest tests/config/test_profile_home_coi_dir.py -v (5 passed)
  • python -m pytest tests/config/test_profile_directory.py tests/config/test_profile_list.py tests/config/test_profile_inheritance_cli.py tests/config/test_default_profile.py (12 existing profile tests still green — no regressions)
  • ruff check tests/config/test_profile_home_coi_dir.py + ruff format --check

Profiles can now live under any of `/etc/coi/profiles/`,
`~/.config/coi/profiles/`, `~/.coi/profiles/`, or `./.coi/profiles/`.
All locations are merged into a single namespace so users can keep
profiles alongside their XDG config or alongside runtime data
(sessions/storage/logs under ~/.coi/).

If the same profile name is defined in more than one location, COI
refuses to start and points to both files, asking the user to rename
one. This keeps it unambiguous which profile is actually being applied
and avoids silent override surprises across scan locations.

Adds a new `GetProfileParentDirs()` helper that returns all scan
locations, and refactors `Load()` to iterate it independently from
config file loading.

Covered by Go unit tests (home-only loading, cross-location merge,
home-vs-home duplicate error, project-vs-home duplicate error) and
Python integration tests exercising `coi profile list` / `coi profile
info` with a fake HOME across XDG, ~/.coi, and project .coi locations.
@mensfeld mensfeld requested a review from Copilot April 6, 2026 17:49
mensfeld added 2 commits April 6, 2026 19:49
Simplify config hierarchy to only ~/.coi/ (user) and ./.coi/ (project).
The system-wide /etc/coi/ and XDG ~/.config/coi/ locations are no longer
scanned for config files or profiles. This keeps all COI data (config,
profiles, sessions, storage, logs) co-located under ~/.coi/.

Updates GetConfigPaths(), GetProfileParentDirs(), all tests, docs, CI
workflow, and error messages to reflect the new two-location model.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR expands COI’s profile discovery to also scan ~/.coi/profiles/ (alongside existing system/user/project locations), merging all discovered profiles into a single namespace and failing fast on duplicate profile names across locations.

Changes:

  • Add GetProfileParentDirs() to centralize profile scan roots (system, user config, user ~/.coi, project, and $COI_CONFIG parent).
  • Update Load() to scan profiles from GetProfileParentDirs() and reject duplicate profile names across scan locations.
  • Update documentation and add/extend Go + Python tests to cover new scan behavior and duplicate detection.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
internal/config/config.go Adds GetProfileParentDirs() helper defining profile scan roots.
internal/config/loader.go Scans profiles via GetProfileParentDirs() and errors on duplicate profile names across locations; updates embedded example docs.
internal/config/config_test.go Adds unit tests for GetProfileParentDirs() behavior.
internal/config/loader_test.go Adds unit tests ensuring Load() scans/merges across locations and rejects duplicates.
tests/config/test_profile_home_coi_dir.py Adds integration tests verifying CLI behavior for ~/.coi/profiles/, merging, and duplicate errors.
README.md Documents the expanded set of profile locations and duplicate-name behavior.
CHANGELOG.md Notes the improvement to scan ~/.coi/profiles/ and describes merged-namespace behavior.
Comments suppressed due to low confidence (1)

CHANGELOG.md:71

  • Changelog entry says “Profiles from all scan locations” and then lists 4 directories, but the code also scans dirname($COI_CONFIG) when COI_CONFIG is set. Either include that in the list or adjust the wording to avoid implying the list is exhaustive.
- [Improvement] **Standardize CLI `Use` strings** — All positional argument placeholders now follow a consistent `<required>` / `[optional]` lowercase-kebab-case convention. Affected commands: `tmux send`, `tmux capture`, `info`, `run`.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +211 to +219

if len(dirs) != 2 {
t.Fatalf("Expected exactly 2 profile parent dirs (user + project), got %d: %v", len(dirs), dirs)
}

// Check that user config path contains .config
homeDir, _ := os.UserHomeDir()
expectedUserPath := filepath.Join(homeDir, ".config/coi/config.toml")
if paths[1] != expectedUserPath {
t.Errorf("User config path: expected %q, got %q", expectedUserPath, paths[1])
workDir, _ := os.Getwd()

expected := []string{
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TestGetProfileParentDirs builds expected paths using os.UserHomeDir()/os.Getwd() while ignoring their errors, but GetProfileParentDirs() has explicit fallbacks (/tmp and .) when those calls fail. If either call errors in the test environment, the expected values will diverge and the test will fail. Mirror the same fallback logic in the test (or assert membership/prefixes rather than exact equality) to make this test robust.

Copilot uses AI. Check for mistakes.
README.md Outdated
```

Profile directories are scanned at all config levels (`/etc/coi/profiles/`, `~/.config/coi/profiles/`, `.coi/profiles/`). See the [Profiles wiki page](https://github.com/mensfeld/code-on-incus/wiki/Profiles) for complete documentation.
Profile directories are scanned under `~/.coi/profiles/` (user) and `./.coi/profiles/` (project). Profiles from both locations are merged into a single namespace — if the same profile name is defined in both, COI refuses to start and asks you to rename one so it's always unambiguous which profile is being applied. See the [Profiles wiki page](https://github.com/mensfeld/code-on-incus/wiki/Profiles) for complete documentation.
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sentence claims profiles are scanned from “all known locations” but the implementation also scans dirname($COI_CONFIG) via GetProfileParentDirs(). Either mention the COI_CONFIG parent directory here or soften the wording so the README list remains accurate.

Suggested change
Profile directories are scanned under `~/.coi/profiles/` (user) and `./.coi/profiles/` (project). Profiles from both locations are merged into a single namespace — if the same profile name is defined in both, COI refuses to start and asks you to rename one so it's always unambiguous which profile is being applied. See the [Profiles wiki page](https://github.com/mensfeld/code-on-incus/wiki/Profiles) for complete documentation.
Profile directories are scanned from `~/.coi/profiles/` (user), `./.coi/profiles/` (project), and profile locations derived from the parent directory of the active `COI_CONFIG`. Profiles from all discovered locations are merged into a single namespace — if the same profile name is defined in more than one location, COI refuses to start and asks you to rename one so it's always unambiguous which profile is being applied. See the [Profiles wiki page](https://github.com/mensfeld/code-on-incus/wiki/Profiles) for complete documentation.

Copilot uses AI. Check for mistakes.
Comment on lines 422 to 429
# Profile directory scan locations:
# 1. ~/.coi/profiles/NAME/config.toml
# 2. .coi/profiles/NAME/config.toml
#
# Profiles from both locations are merged into a single namespace — if the
# same name is defined in both, COI will refuse to start and ask you to
# rename one.
#
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The embedded example docs list 4 profile scan locations, but GetProfileParentDirs() also scans dirname($COI_CONFIG) when COI_CONFIG is set. Please update this list (or clarify it’s the default set excluding COI_CONFIG) so the embedded documentation matches runtime behavior.

Suggested change
# Profile directory scan locations:
# 1. ~/.coi/profiles/NAME/config.toml
# 2. .coi/profiles/NAME/config.toml
#
# Profiles from both locations are merged into a single namespaceif the
# same name is defined in both, COI will refuse to start and ask you to
# rename one.
#
# Default profile directory scan locations:
# 1. ~/.coi/profiles/NAME/config.toml
# 2. .coi/profiles/NAME/config.toml
#
# Additional scan location when COI_CONFIG is set:
# 3. dirname($COI_CONFIG)/profiles/NAME/config.toml
#
# Profiles from all scanned locations are merged into a single namespaceif
# the same name is defined in more than one location, COI will refuse to
# start and ask you to rename one.
#

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +10
Profiles can live under either of:
- ~/.coi/profiles/
- ./.coi/profiles/ (project-local)

Profiles from both locations are merged together. If the same profile name is
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Module docstring lists the supported profile locations as an exhaustive set, but the Go implementation also scans dirname($COI_CONFIG) when COI_CONFIG is set. Consider mentioning that optional location here (or changing wording) so the test documentation matches behavior.

Suggested change
Profiles can live under either of:
- ~/.coi/profiles/
- ./.coi/profiles/ (project-local)
Profiles from both locations are merged together. If the same profile name is
This module covers these profile locations:
- ~/.coi/profiles/
- ./.coi/profiles/ (project-local)
The implementation may also scan dirname($COI_CONFIG) when COI_CONFIG is set,
but those scenarios are not exercised by these tests. Profiles from the home
and project locations are merged together. If the same profile name is

Copilot uses AI. Check for mistakes.
The enable_monitoring and enable_monitoring_low_thresholds fixtures in
test_security_monitoring.py were writing config to ~/.config/coi/config.toml
which is no longer read after the config location simplification. Update
both fixtures to write to ~/.coi/config.toml.

Also address Copilot review comments: mention dirname($COI_CONFIG) scan
location in README, embedded example docs, and Python test docstrings.
@mensfeld mensfeld merged commit 2b8ccd1 into master Apr 6, 2026
13 checks passed
@mensfeld mensfeld deleted the profile-dirs-both-locations branch April 6, 2026 19:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants