Skip to content

Commit 81b5027

Browse files
lunaluxiedjango23
andauthored
Add configuration option to skip search paths (#107)
* feat(config): add configuration option for skip search paths * docs(readme): Update readme to describe the skip_paths configuration * docs(readme): add optional directory argument to CLI summary. update CLI usage summary to include optional directory argument mirroring actual CLI help text. * feat(tests): add [skip_paths] config behavior test * docs(changelog): add [skip_paths] config option to changelog * feat(config): scan directories beyond home via [scan] extra Add a [scan] section to ~/.config/asimov/config with one `extra =` line per additional root; each is scanned on every run alongside the home directory. - Resolve all roots into a single ASIMOV_SCAN_DIRS array; every consumer (Spotlight cache, path cache scoping, mdfind discovery, find traversal) loops over it instead of a lone ASIMOV_SCAN_DIR - A positional CLI argument still overrides everything (scans only that path) - Configured dirs that don't exist are warned about (honoring --quiet) and skipped, so an unmounted volume doesn't abort the run - Nested/duplicate roots are pruned so no tree is traversed twice Closes #108 * docs: prepare v0.9.0 stable release - CHANGELOG: promote [scan] and Go-cache entries into [0.9.0], date 2026-07-26 - README: Install split into Homebrew (homebrew-core) + curl; document [scan]; drop the retired django23 tap; note the pending homebrew-core 0.3.0→0.9.0 bump - UPGRADING: point install commands at the stable curl one-liner - CONTRIBUTING: Homebrew distribution is homebrew-core; mark tap targets legacy - Makefile: verify-release falls back to `brew install asimov` (core), not the tap * chore(release): bump version to 0.10.0 Stable release carrying the v0.9.0-beta fixes plus the new [scan] multi-scan feature. A new feature is a minor bump under semver, so the beta line's 0.9.0 becomes 0.10.0 for stable (there was no 0.9.0 stable). * docs(readme): correct homebrew-core version note (version-agnostic) * docs,build: homebrew-core autobumps; drop retired tap tooling - Makefile: remove bump-formula/ship-formula/TAP_DIR (tap archived); release target points at homebrew-core autobump; verify-release notes the ~3h lag - README: homebrew note is now version-agnostic (formula auto-updates) - CONTRIBUTING: release flow drops the tap steps; document that asimov is on Homebrew's autobump list (version PRs are automatic; only metadata needs a manual PR) * docs(contributing): add concise release pipeline overview (PR → release → brew) * chore: gitignore docs/triage (local maintainer notes) * ci: test across both macOS bash versions in a parallel matrix asimov starts with #!/usr/bin/env bash, so it runs under whichever bash comes first on PATH. On macOS that is one of two very different things: /bin/bash, still 3.2 because Apple froze it at the last GPLv2 release and what most users get, or a 5.x build from Homebrew, which is what a development machine usually has. They disagree on array and IFS semantics, so until now a change could pass `make check` locally and CI and still be broken for users. CI now runs os × bash (4 cells, all concurrent, fail-fast off so one failing combination cannot hide the others). ShellCheck moves into its own job because it is interpreter-independent and only needs to run once; it runs alongside the test cells rather than four times over. A concurrency group cancels an in-flight run when the same ref is pushed again. Locally, `make test-system-bash` runs the suite under 3.2, and `make test BASH_BIN=<path>` pins any interpreter. Both go through the new scripts/test.sh, which sets ASIMOV_TEST_BASH; the Bats helper then launches asimov with that interpreter explicitly, so Bats itself keeps running under its own bash rather than being dragged down to 3.2 by a PATH shim. `make test BATS_JOBS=N` runs tests concurrently (needs GNU parallel). Each test already gets its own temp HOME in setup(), so there is no shared state. * docs(readme): add a "What Asimov doesn't do" section Covers the three most common misconceptions: Asimov sets Time Machine exclusions only, it does not hide directories from Spotlight, and it does not shrink backups that already exist. Adds a short verification recipe for the "my node_modules is still in Time Machine" case. Refs #90, #45 * feat(prune): add an asimov prune subcommand Reports Time Machine exclusions whose directory no longer exists and compacts Asimov's own path cache. The premise of #38 turned out not to hold for Asimov's own work: every version in git history calls `tmutil addexclusion PATH`, which stores the exclusion as an extended attribute on the directory, so deleting the directory deletes the exclusion with it. Nothing is left behind. Staleness lives in the other mechanism — `tmutil addexclusion -p`, which records the path in Time Machine's system preferences and survives the directory forever. Asimov has never written that list, but other tools and manual commands do, and nothing surfaces the leftovers. So prune reports rather than removes: the entries are system-wide and not ours to delete. It prints the `sudo tmutil removeexclusion -p` command instead. The only file it writes is Asimov's own cache. Closes #38 * fix(cache): survive an unreadable or unwritable cache The cache under ~/.cache/asimov is an optimisation, but nothing treated it as optional. A bare `cat` on an unreadable state file failed under `set -Eeu -o pipefail` and aborted the run immediately, printing nothing but "Permission denied" — no context, no fix, exit 1. The cause was Asimov itself. ensure_cache_dir chowned the cache directory to the console user when running as root, but the state files were created *after* that chown, so every `sudo asimov` left root-owned files behind and broke the next run as the user. Guard every cache read and write behind cache_readable/cache_writable, warn once naming the reset command, and carry on without the cache. Create the state files before the chown so appends — which never change an existing file's owner — keep them owned by the user who has to read them next. Closes #122 * feat(doctor): add an asimov doctor subcommand Checks the install rather than the projects: which asimov the shell actually runs, whether a schedule is installed and loaded, whether the cache is readable and writable, whether the config parses, and whether tmutil can read exclusions at all. Exits 1 if it finds anything. Two rules keep it safe to run part-way through a migration. It never writes — fixes are printed, not applied. And it never executes another asimov binary it finds: v0.3.0 parses no arguments at all, so running it to ask its version would start a real scan, so versions are read out of the file instead. The test helper now strips any real asimov install from PATH, since doctor inspects PATH and would otherwise report a different result per machine. Refs #122 * docs: cover upgrading from v0.3.0 and document doctor v0.3.0 is the version most people have, and it parses no arguments at all: `asimov --version` and `asimov doctor` are both ignored and it goes straight to scanning. So the upgrade notes lead with a version check that reads the file instead of running it, and put `asimov doctor` last, after the new binary is in place. Also documents the three leftovers that outlive a v0.3.0 install — the LaunchAgent, the old cellar, and a root-owned cache — and that Asimov is a one-shot scan, not a daemon, so an empty `ps aux | grep asimov` after `brew services start` is expected. Refs #122 * chore(lint): shellcheck the launchctl test mock (#124) Every other script under tests/bin/ is linted; the launchctl mock added alongside `asimov doctor` was missed. Co-authored-by: django23 <827397+django23@users.noreply.github.com> * docs: release 0.11.0 (#125) * test(doctor): derive the expected version from the script The version assertion hardcoded 0.10.0, so it failed the moment the release bump landed. Read it from `asimov --version` instead. * docs: release 0.11.0 --------- Co-authored-by: django23 <827397+django23@users.noreply.github.com> * feat(config): fold [skip_paths] into ASIMOV_SKIP_PATHS and teach doctor about it Follow-up to the [skip_paths] work: append the configured paths to ASIMOV_SKIP_PATHS at definition time instead of merging them into a local array in each consumer, so the find expression and the Spotlight pass stay in step, add the section to the doctor config validator (it reported [skip_paths] as an unknown section), and cover both in tests. The behaviour test passed an absolute path to create_project, which prefixes $HOME - so the project was built outside the skipped directory and the test passed for the wrong reason. Fixed, plus a control case that fails without the config, a multi-entry case, and one proving [fixed_dirs] still wins inside a skipped path. --------- Co-authored-by: django23 <827397+django23@users.noreply.github.com> Co-authored-by: Django E <djangoboy@gmail.com>
1 parent 25677dc commit 81b5027

7 files changed

Lines changed: 92 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
88

99
### Added
1010

11+
- `[skip_paths]` config section: name directories that Asimov should never search, alongside the built-in `~/.Trash` and `~/Library`. Thanks [@lunaluxie](https://github.com/lunaluxie)!
12+
1113
- `asimov doctor` now checks the data files and reports how many sentinels and fixed directories are loaded, so an incomplete install is diagnosed rather than just failing.
1214

1315
### Changed

README.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ If `mdfind` doesn't list your projects, the run isn't reaching them. The two usu
131131
## Usage
132132

133133
```
134-
asimov [--dry-run] [--verbose] [--quiet] [--stats] [--no-read-cache] [--no-write-cache] [--help] [--version]
134+
asimov [--dry-run] [--verbose] [--quiet] [--stats] [--no-read-cache] [--no-write-cache] [--help] [--version] [directory]
135135
asimov prune [--quiet]
136136
asimov doctor [--quiet]
137137
```
@@ -274,6 +274,20 @@ extra = ~/my-build-cache # plus any paths you name (always excluded when
274274
extra = ~/golang/pkg/mod # e.g. a Go module cache under a custom GOPATH
275275
```
276276

277+
### Skip parts of your home directory
278+
279+
Asimov never descends into `~/.Trash` or `~/Library`. Add directories of your own under `[skip_paths]` and they're left alone too - not searched, never excluded:
280+
281+
```ini
282+
[skip_paths]
283+
extra = ~/Music # one "extra =" line per directory
284+
extra = ~/Pictures
285+
```
286+
287+
This only narrows the *search*. Global caches you opted into under `[fixed_dirs]` are still excluded, even inside a skipped path.
288+
289+
If all your code lives in one folder, scanning just that folder (`asimov ~/Code`, see [usage](#usage)) is usually simpler than skipping everything else.
290+
277291
## Other install methods
278292

279293
**From source:**

lib/asimov/config.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ load_config() {
1414
ASIMOV_CONFIG_EXTRA_SENTINELS=()
1515
ASIMOV_CONFIG_DISABLED_SENTINELS=()
1616
ASIMOV_CONFIG_SCAN_DIRS=()
17+
ASIMOV_CONFIG_EXTRA_SKIP_PATHS=()
1718

1819
[[ -f "$ASIMOV_CONFIG_FILE" ]] || return 0
1920

@@ -52,6 +53,10 @@ load_config() {
5253
sentinels:disabled)
5354
ASIMOV_CONFIG_DISABLED_SENTINELS+=("$value")
5455
;;
56+
skip_paths:extra)
57+
value="${value/#\~/$HOME}"
58+
ASIMOV_CONFIG_EXTRA_SKIP_PATHS+=("$value")
59+
;;
5560
esac
5661
fi
5762
done < "$ASIMOV_CONFIG_FILE"

lib/asimov/data.sh

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,9 @@ load_fixed_dirs() {
6464
done < "$file"
6565
}
6666

67-
# Load data/skip-paths.tsv into ASIMOV_SKIP_PATHS (absolute).
67+
# Load data/skip-paths.tsv into ASIMOV_SKIP_PATHS (absolute), then append the
68+
# paths named under [skip_paths] in the config. Both the find expression and the
69+
# Spotlight top-up read this one array, so they cannot drift apart.
6870
load_skip_paths() {
6971
local file="${ASIMOV_DATA}/skip-paths.tsv"
7072
require_data_file "$file"
@@ -74,6 +76,10 @@ load_skip_paths() {
7476
[[ -z "$path" || "$path" == '#'* ]] && continue
7577
ASIMOV_SKIP_PATHS+=("${ASIMOV_ROOT}/${path}")
7678
done < "$file"
79+
80+
for path in ${ASIMOV_CONFIG_EXTRA_SKIP_PATHS[@]+"${ASIMOV_CONFIG_EXTRA_SKIP_PATHS[@]}"}; do
81+
ASIMOV_SKIP_PATHS+=("$path")
82+
done
7783
}
7884

7985
# Every sentinel record in effect for this run, one "dir sentinel" pair per line:

lib/asimov/doctor.sh

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ doctor_check_config() {
271271
if [[ "$line" =~ ^\[([a-z_]+)\]$ ]]; then
272272
section="${BASH_REMATCH[1]}"
273273
case "$section" in
274-
fixed_dirs|scan|sentinels) ;;
274+
fixed_dirs|scan|sentinels|skip_paths) ;;
275275
*)
276276
unknown=$((unknown + 1))
277277
doctor_problem "unknown section [${section}] in ${ASIMOV_CONFIG_FILE}"
@@ -284,11 +284,11 @@ doctor_check_config() {
284284
key="${BASH_REMATCH[1]}"
285285
case "${section}:${key}" in
286286
fixed_dirs:enabled|fixed_dirs:extra|scan:extra|\
287-
sentinels:extra|sentinels:disabled) ;;
287+
sentinels:extra|sentinels:disabled|skip_paths:extra) ;;
288288
*)
289289
# A key under an already-reported section is the same fault.
290290
case "$section" in
291-
fixed_dirs|scan|sentinels)
291+
fixed_dirs|scan|sentinels|skip_paths)
292292
unknown=$((unknown + 1))
293293
doctor_problem "unknown key '${key}' under [${section}] in ${ASIMOV_CONFIG_FILE}"
294294
;;

tests/behavior.bats

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,46 @@ load test_helper
120120
[[ "$(count_exclusions)" -eq 0 ]]
121121
}
122122

123+
@test "skips directories given in [skip_paths] config" {
124+
write_config "[skip_paths]
125+
extra = ~/Music"
126+
create_project "Music/Code/First-Project" "composer.json" "vendor"
127+
run_asimov
128+
refute_excluded "${HOME}/Music/Code/First-Project/vendor"
129+
[[ "$(count_exclusions)" -eq 0 ]]
130+
}
131+
132+
@test "excludes the same project when [skip_paths] is absent" {
133+
create_project "Music/Code/First-Project" "composer.json" "vendor"
134+
run_asimov
135+
assert_excluded "${HOME}/Music/Code/First-Project/vendor"
136+
}
137+
138+
@test "[skip_paths] accepts several entries" {
139+
write_config "[skip_paths]
140+
extra = ~/Music
141+
extra = ~/Pictures"
142+
create_project "Music/First-Project" "composer.json" "vendor"
143+
create_project "Pictures/Second-Project" "package.json" "node_modules"
144+
create_project "Code/Third-Project" "package.json" "node_modules"
145+
run_asimov
146+
refute_excluded "${HOME}/Music/First-Project/vendor"
147+
refute_excluded "${HOME}/Pictures/Second-Project/node_modules"
148+
assert_excluded "${HOME}/Code/Third-Project/node_modules"
149+
}
150+
151+
@test "[skip_paths] does not suppress fixed directories inside it" {
152+
write_config "[fixed_dirs]
153+
enabled = true
154+
extra = ~/Music/build-cache
155+
156+
[skip_paths]
157+
extra = ~/Music"
158+
mkdir -p "${HOME}/Music/build-cache"
159+
run_asimov
160+
assert_excluded "${HOME}/Music/build-cache"
161+
}
162+
123163
# =============================================================================
124164
# Nested project handling
125165
# =============================================================================

tests/doctor.bats

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,26 @@ enabled = true"
260260
[[ "$output" == *".config/asimov/config"* ]]
261261
}
262262

263+
@test "doctor accepts a [skip_paths] section" {
264+
write_config "[skip_paths]
265+
extra = ~/Music"
266+
267+
run_asimov doctor
268+
269+
[[ "$status" -eq 0 ]]
270+
[[ "$output" == *"looks valid"* ]]
271+
}
272+
273+
@test "doctor flags an unknown key under [skip_paths]" {
274+
write_config "[skip_paths]
275+
exrta = ~/Music"
276+
277+
run_asimov doctor
278+
279+
[[ "$status" -eq 1 ]]
280+
[[ "$output" == *"exrta"* ]]
281+
}
282+
263283
@test "doctor flags an unknown config key" {
264284
write_config "[fixed_dirs]
265285
enbaled = true"

0 commit comments

Comments
 (0)