personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

install: auto-add ~/.local/bin to shell PATH via userpath

make install-service now calls userpath.append() after writing the sol symlink, adding bash/zsh/fish rc blocks when needed and printing a manual-fallback message if that fails.

Bump the Python dependency to userpath>=1.9.2,<2.

Co-Authored-By: Codex <codex@openai.com>

+174 -2
+2
INSTALL.md
··· 8 8 9 9 ## before you begin 10 10 11 + `make install-service` now auto-adds `~/.local/bin` to your shell `PATH` via the `userpath` library, updating `~/.bashrc`, `~/.zshrc`, or `~/.config/fish/config.fish` as needed. if `~/.local/bin` was not already on `PATH`, restart your shell after install or run `exec $SHELL -l` before continuing. 12 + 11 13 check if solstone is already installed and running: 12 14 13 15 ```bash
+1
pyproject.toml
··· 72 72 "python-slugify", 73 73 "rapidfuzz", 74 74 "typer", 75 + "userpath>=1.9.2,<2", 75 76 76 77 # Audio processing 77 78 "soundfile",
+130 -2
tests/test_install_guard.py
··· 5 5 6 6 import os 7 7 from pathlib import Path 8 + from unittest.mock import Mock 8 9 9 10 import pytest 10 11 ··· 170 171 171 172 172 173 class TestInstall: 174 + @pytest.fixture(autouse=True) 175 + def path_already_present(self, monkeypatch): 176 + monkeypatch.setattr( 177 + "think.install_guard.userpath.in_current_path", 178 + lambda _path: True, 179 + ) 180 + 173 181 def test_creates_symlink_on_absent(self, home_root, tmp_path, monkeypatch, capsys): 174 182 repo = make_repo(tmp_path) 175 183 rc, out, err = run_main(monkeypatch, capsys, repo, "install") 176 184 alias = install_guard.alias_path() 177 185 assert rc == 0 178 - assert out == "installed\n" 186 + assert out == "installed\npath: ~/.local/bin already on PATH\n" 179 187 assert err == "" 180 188 assert alias.is_symlink() 181 189 assert alias.resolve() == install_guard.expected_target(repo).resolve() ··· 186 194 alias = make_alias(home_root, original) 187 195 rc, out, err = run_main(monkeypatch, capsys, repo, "install") 188 196 assert rc == 0 189 - assert out == "installed\n" 197 + assert out == "installed\npath: ~/.local/bin already on PATH\n" 190 198 assert err == "" 191 199 assert alias.is_symlink() 192 200 assert alias.resolve() == original.resolve() 201 + 202 + def test_path_already_on_path_absent( 203 + self, home_root, tmp_path, monkeypatch, capsys 204 + ): 205 + repo = make_repo(tmp_path) 206 + append_mock = Mock(return_value=True) 207 + monkeypatch.setattr("think.install_guard.userpath.append", append_mock) 208 + rc, out, err = run_main(monkeypatch, capsys, repo, "install") 209 + alias = install_guard.alias_path() 210 + assert rc == 0 211 + assert out.endswith("path: ~/.local/bin already on PATH\n") 212 + assert err == "" 213 + assert alias.is_symlink() 214 + assert alias.resolve() == install_guard.expected_target(repo).resolve() 215 + append_mock.assert_not_called() 216 + 217 + def test_path_appended_restart_needed_absent( 218 + self, home_root, tmp_path, monkeypatch, capsys 219 + ): 220 + repo = make_repo(tmp_path) 221 + append_mock = Mock(return_value=True) 222 + restart_mock = Mock(return_value=True) 223 + monkeypatch.setattr( 224 + "think.install_guard.userpath.in_current_path", 225 + lambda _path: False, 226 + ) 227 + monkeypatch.setattr("think.install_guard.userpath.append", append_mock) 228 + monkeypatch.setattr( 229 + "think.install_guard.userpath.need_shell_restart", 230 + restart_mock, 231 + ) 232 + rc, out, err = run_main(monkeypatch, capsys, repo, "install") 233 + alias = install_guard.alias_path() 234 + assert rc == 0 235 + assert ( 236 + out == "installed\n" 237 + "path: added ~/.local/bin to shell PATH — restart your shell or run 'exec $SHELL -l' to pick it up\n" 238 + ) 239 + assert err == "" 240 + assert alias.is_symlink() 241 + assert alias.resolve() == install_guard.expected_target(repo).resolve() 242 + append_mock.assert_called_once_with( 243 + str(alias.parent), 244 + app_name="solstone", 245 + all_shells=True, 246 + ) 247 + restart_mock.assert_called_once_with(str(alias.parent)) 248 + 249 + def test_path_appended_no_restart_owned( 250 + self, home_root, tmp_path, monkeypatch, capsys 251 + ): 252 + repo = make_repo(tmp_path) 253 + alias = make_alias(home_root, ensure_expected_target(repo)) 254 + append_mock = Mock(return_value=True) 255 + restart_mock = Mock(return_value=False) 256 + monkeypatch.setattr( 257 + "think.install_guard.userpath.in_current_path", 258 + lambda _path: False, 259 + ) 260 + monkeypatch.setattr("think.install_guard.userpath.append", append_mock) 261 + monkeypatch.setattr( 262 + "think.install_guard.userpath.need_shell_restart", 263 + restart_mock, 264 + ) 265 + rc, out, err = run_main(monkeypatch, capsys, repo, "install") 266 + assert rc == 0 267 + assert out == "installed\npath: added ~/.local/bin to shell PATH\n" 268 + assert err == "" 269 + assert alias.is_symlink() 270 + assert alias.resolve() == install_guard.expected_target(repo).resolve() 271 + append_mock.assert_called_once_with( 272 + str(alias.parent), 273 + app_name="solstone", 274 + all_shells=True, 275 + ) 276 + restart_mock.assert_called_once_with(str(alias.parent)) 277 + 278 + def test_path_append_returns_false(self, home_root, tmp_path, monkeypatch, capsys): 279 + repo = make_repo(tmp_path) 280 + append_mock = Mock(return_value=False) 281 + monkeypatch.setattr( 282 + "think.install_guard.userpath.in_current_path", 283 + lambda _path: False, 284 + ) 285 + monkeypatch.setattr("think.install_guard.userpath.append", append_mock) 286 + rc, out, err = run_main(monkeypatch, capsys, repo, "install") 287 + alias = install_guard.alias_path() 288 + assert rc == 0 289 + assert ( 290 + out 291 + == 'installed\npath: could not auto-add ~/.local/bin to PATH — add this line to your shell rc manually: export PATH="$HOME/.local/bin:$PATH"\n' 292 + ) 293 + assert err == "" 294 + assert alias.is_symlink() 295 + assert alias.resolve() == install_guard.expected_target(repo).resolve() 296 + append_mock.assert_called_once_with( 297 + str(alias.parent), 298 + app_name="solstone", 299 + all_shells=True, 300 + ) 301 + 302 + def test_path_unexpected_exception(self, home_root, tmp_path, monkeypatch, capsys): 303 + repo = make_repo(tmp_path) 304 + append_mock = Mock(return_value=True) 305 + monkeypatch.setattr( 306 + "think.install_guard.userpath.in_current_path", 307 + Mock(side_effect=RuntimeError("boom")), 308 + ) 309 + monkeypatch.setattr("think.install_guard.userpath.append", append_mock) 310 + rc, out, err = run_main(monkeypatch, capsys, repo, "install") 311 + alias = install_guard.alias_path() 312 + assert rc == 0 313 + assert ( 314 + out 315 + == 'installed\npath: could not auto-add ~/.local/bin to PATH (RuntimeError: boom) — add this line to your shell rc manually: export PATH="$HOME/.local/bin:$PATH"\n' 316 + ) 317 + assert err == "" 318 + assert alias.is_symlink() 319 + assert alias.resolve() == install_guard.expected_target(repo).resolve() 320 + append_mock.assert_not_called() 193 321 194 322 def test_refuses_cross_repo(self, home_root, tmp_path, monkeypatch, capsys): 195 323 repo = make_repo(tmp_path).resolve()
+27
think/install_guard.py
··· 10 10 from enum import Enum 11 11 from pathlib import Path 12 12 13 + import userpath 14 + 13 15 14 16 class AliasState(Enum): 15 17 WORKTREE = "worktree" ··· 89 91 sys.stderr.write(format_error(state, curdir, alias, other_target) + "\n") 90 92 91 93 94 + def _ensure_user_bin_on_path(user_bin: Path) -> None: 95 + user_bin_str = str(user_bin) 96 + try: 97 + if userpath.in_current_path(user_bin_str): 98 + print("path: ~/.local/bin already on PATH") 99 + return 100 + if userpath.append(user_bin_str, app_name="solstone", all_shells=True): 101 + if userpath.need_shell_restart(user_bin_str): 102 + print( 103 + "path: added ~/.local/bin to shell PATH — restart your shell or run 'exec $SHELL -l' to pick it up" 104 + ) 105 + else: 106 + print("path: added ~/.local/bin to shell PATH") 107 + return 108 + print( 109 + 'path: could not auto-add ~/.local/bin to PATH — add this line to your shell rc manually: export PATH="$HOME/.local/bin:$PATH"' 110 + ) 111 + except Exception as exc: 112 + print( 113 + f'path: could not auto-add ~/.local/bin to PATH ({type(exc).__name__}: {exc}) — add this line to your shell rc manually: export PATH="$HOME/.local/bin:$PATH"' 114 + ) 115 + 116 + 92 117 def cmd_check(curdir: Path) -> int: 93 118 alias = alias_path() 94 119 state, other_target = check_alias(curdir) ··· 116 141 alias.parent.mkdir(parents=True, exist_ok=True) 117 142 alias.symlink_to(expected_target(curdir)) 118 143 print("installed") 144 + _ensure_user_bin_on_path(alias.parent) 119 145 return 0 120 146 if state is AliasState.OWNED: 121 147 alias.unlink() 122 148 alias.symlink_to(expected_target(curdir)) 123 149 print("installed") 150 + _ensure_user_bin_on_path(alias.parent) 124 151 return 0 125 152 126 153 _print_error(state, curdir, alias, other_target)
+14
uv.lock
··· 3784 3784 { name = "timefhuman" }, 3785 3785 { name = "typer" }, 3786 3786 { name = "tzlocal" }, 3787 + { name = "userpath" }, 3787 3788 { name = "weasyprint" }, 3788 3789 { name = "webrtcvad-wheels" }, 3789 3790 { name = "websockets" }, ··· 3837 3838 { name = "timefhuman" }, 3838 3839 { name = "typer" }, 3839 3840 { name = "tzlocal" }, 3841 + { name = "userpath", specifier = ">=1.9.2,<2" }, 3840 3842 { name = "weasyprint" }, 3841 3843 { name = "webrtcvad-wheels", specifier = ">=2.0.12" }, 3842 3844 { name = "websockets", specifier = ">=13.0" }, ··· 4313 4315 sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } 4314 4316 wheels = [ 4315 4317 { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, 4318 + ] 4319 + 4320 + [[package]] 4321 + name = "userpath" 4322 + version = "1.9.2" 4323 + source = { registry = "https://pypi.org/simple" } 4324 + dependencies = [ 4325 + { name = "click" }, 4326 + ] 4327 + sdist = { url = "https://files.pythonhosted.org/packages/d5/b7/30753098208505d7ff9be5b3a32112fb8a4cb3ddfccbbb7ba9973f2e29ff/userpath-1.9.2.tar.gz", hash = "sha256:6c52288dab069257cc831846d15d48133522455d4677ee69a9781f11dbefd815", size = 11140, upload-time = "2024-02-29T21:39:08.742Z" } 4328 + wheels = [ 4329 + { url = "https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl", hash = "sha256:2cbf01a23d655a1ff8fc166dfb78da1b641d1ceabf0fe5f970767d380b14e89d", size = 9065, upload-time = "2024-02-29T21:39:07.551Z" }, 4316 4330 ] 4317 4331 4318 4332 [[package]]