exist-shell 0.1.1__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. {exist_shell-0.1.1 → exist_shell-0.2.0}/.github/workflows/docs.yml +2 -2
  2. {exist_shell-0.1.1 → exist_shell-0.2.0}/.github/workflows/e2e.yml +4 -4
  3. {exist_shell-0.1.1 → exist_shell-0.2.0}/.github/workflows/release.yml +2 -2
  4. {exist_shell-0.1.1 → exist_shell-0.2.0}/.github/workflows/ruff.yml +3 -3
  5. {exist_shell-0.1.1 → exist_shell-0.2.0}/.github/workflows/tests.yml +3 -3
  6. {exist_shell-0.1.1 → exist_shell-0.2.0}/.github/workflows/ty.yml +4 -4
  7. {exist_shell-0.1.1 → exist_shell-0.2.0}/.gitignore +2 -0
  8. exist_shell-0.2.0/CHANGELOG.md +83 -0
  9. {exist_shell-0.1.1 → exist_shell-0.2.0}/Makefile +2 -2
  10. {exist_shell-0.1.1 → exist_shell-0.2.0}/PKG-INFO +60 -3
  11. {exist_shell-0.1.1 → exist_shell-0.2.0}/README.md +59 -2
  12. {exist_shell-0.1.1 → exist_shell-0.2.0}/docs/api.md +8 -0
  13. {exist_shell-0.1.1 → exist_shell-0.2.0}/docs/commands.md +77 -1
  14. {exist_shell-0.1.1 → exist_shell-0.2.0}/docs/index.md +1 -1
  15. {exist_shell-0.1.1 → exist_shell-0.2.0}/docs/installation.md +24 -2
  16. {exist_shell-0.1.1 → exist_shell-0.2.0}/docs/sync.md +25 -1
  17. {exist_shell-0.1.1 → exist_shell-0.2.0}/pyproject.toml +5 -5
  18. {exist_shell-0.1.1 → exist_shell-0.2.0}/scripts/e2e/sections/T05_put.sh +7 -0
  19. {exist_shell-0.1.1 → exist_shell-0.2.0}/scripts/e2e/sections/T07_cat.sh +9 -0
  20. {exist_shell-0.1.1 → exist_shell-0.2.0}/scripts/e2e/sections/T08_cp.sh +35 -0
  21. {exist_shell-0.1.1 → exist_shell-0.2.0}/scripts/e2e/sections/T09_rm.sh +23 -0
  22. {exist_shell-0.1.1 → exist_shell-0.2.0}/scripts/e2e/sections/T12_sync.sh +100 -1
  23. {exist_shell-0.1.1 → exist_shell-0.2.0}/scripts/e2e/sections/T13_mv.sh +37 -0
  24. {exist_shell-0.1.1 → exist_shell-0.2.0}/scripts/e2e/sections/T14_exec.sh +43 -0
  25. exist_shell-0.2.0/scripts/e2e/sections/T19_find.sh +68 -0
  26. {exist_shell-0.1.1 → exist_shell-0.2.0}/scripts/e2e.sh +2 -0
  27. {exist_shell-0.1.1 → exist_shell-0.2.0}/src/exist_shell/cache.py +95 -12
  28. {exist_shell-0.1.1 → exist_shell-0.2.0}/src/exist_shell/client/__init__.py +7 -3
  29. exist_shell-0.2.0/src/exist_shell/client/_base.py +102 -0
  30. {exist_shell-0.1.1 → exist_shell-0.2.0}/src/exist_shell/client/_collections.py +62 -13
  31. {exist_shell-0.1.1 → exist_shell-0.2.0}/src/exist_shell/client/_documents.py +64 -23
  32. exist_shell-0.2.0/src/exist_shell/client/_queries.py +105 -0
  33. {exist_shell-0.1.1 → exist_shell-0.2.0}/src/exist_shell/commands/cat.py +1 -1
  34. {exist_shell-0.1.1 → exist_shell-0.2.0}/src/exist_shell/commands/chmod.py +15 -20
  35. {exist_shell-0.1.1 → exist_shell-0.2.0}/src/exist_shell/commands/chown.py +15 -20
  36. {exist_shell-0.1.1 → exist_shell-0.2.0}/src/exist_shell/commands/cp.py +5 -1
  37. {exist_shell-0.1.1 → exist_shell-0.2.0}/src/exist_shell/commands/exec.py +45 -8
  38. exist_shell-0.2.0/src/exist_shell/commands/find.py +64 -0
  39. {exist_shell-0.1.1 → exist_shell-0.2.0}/src/exist_shell/commands/mv.py +7 -0
  40. exist_shell-0.2.0/src/exist_shell/commands/rm.py +47 -0
  41. exist_shell-0.2.0/src/exist_shell/commands/sync.py +1405 -0
  42. {exist_shell-0.1.1 → exist_shell-0.2.0}/src/exist_shell/completions.py +126 -12
  43. {exist_shell-0.1.1 → exist_shell-0.2.0}/src/exist_shell/config.py +34 -2
  44. {exist_shell-0.1.1 → exist_shell-0.2.0}/src/exist_shell/exceptions.py +28 -1
  45. {exist_shell-0.1.1 → exist_shell-0.2.0}/src/exist_shell/main.py +20 -1
  46. {exist_shell-0.1.1 → exist_shell-0.2.0}/src/exist_shell/utils.py +44 -7
  47. exist_shell-0.2.0/tests/__init__.py +1 -0
  48. {exist_shell-0.1.1 → exist_shell-0.2.0}/tests/conftest.py +8 -0
  49. exist_shell-0.2.0/tests/test_cache.py +335 -0
  50. {exist_shell-0.1.1 → exist_shell-0.2.0}/tests/test_client.py +584 -5
  51. {exist_shell-0.1.1 → exist_shell-0.2.0}/tests/test_commands_cat.py +19 -0
  52. {exist_shell-0.1.1 → exist_shell-0.2.0}/tests/test_commands_chmod.py +38 -0
  53. {exist_shell-0.1.1 → exist_shell-0.2.0}/tests/test_commands_chown.py +27 -0
  54. {exist_shell-0.1.1 → exist_shell-0.2.0}/tests/test_commands_collection.py +36 -0
  55. {exist_shell-0.1.1 → exist_shell-0.2.0}/tests/test_commands_cp.py +34 -0
  56. {exist_shell-0.1.1 → exist_shell-0.2.0}/tests/test_commands_edit.py +14 -0
  57. {exist_shell-0.1.1 → exist_shell-0.2.0}/tests/test_commands_exec.py +101 -1
  58. exist_shell-0.2.0/tests/test_commands_find.py +146 -0
  59. {exist_shell-0.1.1 → exist_shell-0.2.0}/tests/test_commands_group.py +37 -0
  60. {exist_shell-0.1.1 → exist_shell-0.2.0}/tests/test_commands_ls.py +17 -0
  61. {exist_shell-0.1.1 → exist_shell-0.2.0}/tests/test_commands_mkdir.py +7 -0
  62. {exist_shell-0.1.1 → exist_shell-0.2.0}/tests/test_commands_mv.py +51 -0
  63. {exist_shell-0.1.1 → exist_shell-0.2.0}/tests/test_commands_put.py +17 -0
  64. exist_shell-0.2.0/tests/test_commands_rm.py +129 -0
  65. {exist_shell-0.1.1 → exist_shell-0.2.0}/tests/test_commands_server.py +35 -0
  66. exist_shell-0.2.0/tests/test_commands_sync.py +1791 -0
  67. {exist_shell-0.1.1 → exist_shell-0.2.0}/tests/test_commands_user.py +68 -0
  68. {exist_shell-0.1.1 → exist_shell-0.2.0}/tests/test_completions.py +154 -17
  69. {exist_shell-0.1.1 → exist_shell-0.2.0}/tests/test_config.py +104 -6
  70. exist_shell-0.2.0/tests/test_exceptions.py +55 -0
  71. exist_shell-0.2.0/tests/test_main.py +45 -0
  72. exist_shell-0.2.0/tests/test_utils.py +258 -0
  73. {exist_shell-0.1.1 → exist_shell-0.2.0}/tests/test_xquery.py +32 -0
  74. {exist_shell-0.1.1 → exist_shell-0.2.0}/uv.lock +48 -48
  75. exist_shell-0.1.1/CHANGELOG.md +0 -52
  76. exist_shell-0.1.1/src/exist_shell/client/_base.py +0 -65
  77. exist_shell-0.1.1/src/exist_shell/client/_queries.py +0 -37
  78. exist_shell-0.1.1/src/exist_shell/commands/rm.py +0 -23
  79. exist_shell-0.1.1/src/exist_shell/commands/sync.py +0 -767
  80. exist_shell-0.1.1/tests/__init__.py +0 -0
  81. exist_shell-0.1.1/tests/test_cache.py +0 -181
  82. exist_shell-0.1.1/tests/test_commands_rm.py +0 -66
  83. exist_shell-0.1.1/tests/test_commands_sync.py +0 -636
  84. exist_shell-0.1.1/tests/test_utils.py +0 -141
  85. {exist_shell-0.1.1 → exist_shell-0.2.0}/.gitguardian.yaml +0 -0
  86. {exist_shell-0.1.1 → exist_shell-0.2.0}/.github/dependabot.yml +0 -0
  87. {exist_shell-0.1.1 → exist_shell-0.2.0}/CLAUDE.md +0 -0
  88. {exist_shell-0.1.1 → exist_shell-0.2.0}/LICENSE +0 -0
  89. {exist_shell-0.1.1 → exist_shell-0.2.0}/docs/completion.md +0 -0
  90. {exist_shell-0.1.1 → exist_shell-0.2.0}/docs/configuration.md +0 -0
  91. {exist_shell-0.1.1 → exist_shell-0.2.0}/docs/development.md +0 -0
  92. {exist_shell-0.1.1 → exist_shell-0.2.0}/mkdocs.yml +0 -0
  93. {exist_shell-0.1.1 → exist_shell-0.2.0}/scripts/e2e/docker.sh +0 -0
  94. {exist_shell-0.1.1 → exist_shell-0.2.0}/scripts/e2e/lib.sh +0 -0
  95. {exist_shell-0.1.1 → exist_shell-0.2.0}/scripts/e2e/sections/T02_server.sh +0 -0
  96. {exist_shell-0.1.1 → exist_shell-0.2.0}/scripts/e2e/sections/T03_collection.sh +0 -0
  97. {exist_shell-0.1.1 → exist_shell-0.2.0}/scripts/e2e/sections/T04_ls.sh +0 -0
  98. {exist_shell-0.1.1 → exist_shell-0.2.0}/scripts/e2e/sections/T06_ls_after.sh +0 -0
  99. {exist_shell-0.1.1 → exist_shell-0.2.0}/scripts/e2e/sections/T10_mkdir.sh +0 -0
  100. {exist_shell-0.1.1 → exist_shell-0.2.0}/scripts/e2e/sections/T11_edit.sh +0 -0
  101. {exist_shell-0.1.1 → exist_shell-0.2.0}/scripts/e2e/sections/T15_user.sh +0 -0
  102. {exist_shell-0.1.1 → exist_shell-0.2.0}/scripts/e2e/sections/T16_group.sh +0 -0
  103. {exist_shell-0.1.1 → exist_shell-0.2.0}/scripts/e2e/sections/T17_chown.sh +0 -0
  104. {exist_shell-0.1.1 → exist_shell-0.2.0}/scripts/e2e/sections/T18_chmod.sh +0 -0
  105. {exist_shell-0.1.1 → exist_shell-0.2.0}/src/exist_shell/__init__.py +0 -0
  106. {exist_shell-0.1.1 → exist_shell-0.2.0}/src/exist_shell/client/_groups.py +0 -0
  107. {exist_shell-0.1.1 → exist_shell-0.2.0}/src/exist_shell/client/_permissions.py +0 -0
  108. {exist_shell-0.1.1 → exist_shell-0.2.0}/src/exist_shell/client/_users.py +0 -0
  109. {exist_shell-0.1.1 → exist_shell-0.2.0}/src/exist_shell/commands/__init__.py +0 -0
  110. {exist_shell-0.1.1 → exist_shell-0.2.0}/src/exist_shell/commands/collection.py +0 -0
  111. {exist_shell-0.1.1 → exist_shell-0.2.0}/src/exist_shell/commands/edit.py +0 -0
  112. {exist_shell-0.1.1 → exist_shell-0.2.0}/src/exist_shell/commands/group.py +0 -0
  113. {exist_shell-0.1.1 → exist_shell-0.2.0}/src/exist_shell/commands/ls.py +0 -0
  114. {exist_shell-0.1.1 → exist_shell-0.2.0}/src/exist_shell/commands/mkdir.py +0 -0
  115. {exist_shell-0.1.1 → exist_shell-0.2.0}/src/exist_shell/commands/put.py +0 -0
  116. {exist_shell-0.1.1 → exist_shell-0.2.0}/src/exist_shell/commands/server.py +0 -0
  117. {exist_shell-0.1.1 → exist_shell-0.2.0}/src/exist_shell/commands/user.py +0 -0
  118. {exist_shell-0.1.1 → exist_shell-0.2.0}/src/exist_shell/models.py +0 -0
  119. {exist_shell-0.1.1 → exist_shell-0.2.0}/src/exist_shell/xquery.py +0 -0
@@ -16,10 +16,10 @@ jobs:
16
16
  deploy:
17
17
  runs-on: ubuntu-latest
18
18
  steps:
19
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
19
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
20
20
 
21
21
  - name: Set up Python
22
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
22
+ uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
23
23
  with:
24
24
  python-version: "3.13"
25
25
 
@@ -14,7 +14,7 @@ jobs:
14
14
  outputs:
15
15
  flags: ${{ steps.list.outputs.flags }}
16
16
  steps:
17
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
17
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
18
18
  - name: Build image matrix from --list-images
19
19
  id: list
20
20
  run: |
@@ -31,9 +31,9 @@ jobs:
31
31
  flag: ${{ fromJson(needs.matrix.outputs.flags) }}
32
32
  fail-fast: false
33
33
  steps:
34
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
34
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
35
35
  - run: sudo apt-get install -y libxml2-utils
36
- - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
36
+ - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
37
37
  with:
38
38
  enable-cache: true
39
39
  - run: uv sync
@@ -88,7 +88,7 @@ jobs:
88
88
  done
89
89
  } > comment.md
90
90
  cat comment.md
91
- - uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v2
91
+ - uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v2
92
92
  with:
93
93
  header: e2e
94
94
  path: comment.md
@@ -15,7 +15,7 @@ jobs:
15
15
  if: github.actor == 'ambs'
16
16
  environment: release
17
17
  steps:
18
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
18
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
19
19
 
20
20
  - name: Verify signed tag
21
21
  run: |
@@ -24,7 +24,7 @@ jobs:
24
24
  env:
25
25
  GPG_PUBLIC_KEY: ${{ secrets.GPG_PUBLIC_KEY }}
26
26
 
27
- - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
27
+ - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
28
28
  with:
29
29
  enable-cache: true
30
30
 
@@ -12,8 +12,8 @@ jobs:
12
12
  ruff:
13
13
  runs-on: ubuntu-latest
14
14
  steps:
15
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
16
- - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
15
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
16
+ - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
17
17
  with:
18
18
  enable-cache: true
19
19
  - run: uv sync
@@ -37,7 +37,7 @@ jobs:
37
37
  echo '```'
38
38
  } > comment.md
39
39
  fi
40
- - uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v2
40
+ - uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v2
41
41
  if: github.event_name == 'pull_request'
42
42
  with:
43
43
  header: ruff
@@ -12,8 +12,8 @@ jobs:
12
12
  test:
13
13
  runs-on: ubuntu-latest
14
14
  steps:
15
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
16
- - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
15
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
16
+ - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
17
17
  with:
18
18
  enable-cache: true
19
19
  - run: uv sync
@@ -34,7 +34,7 @@ jobs:
34
34
  echo "**${test_line}** "
35
35
  echo "**Coverage: ${coverage}**"
36
36
  } > comment.md
37
- - uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v2
37
+ - uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v2
38
38
  if: github.event_name == 'pull_request'
39
39
  with:
40
40
  header: tests
@@ -12,15 +12,15 @@ jobs:
12
12
  typecheck:
13
13
  runs-on: ubuntu-latest
14
14
  steps:
15
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
16
- - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
15
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
16
+ - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
17
17
  with:
18
18
  enable-cache: true
19
19
  - run: uv sync
20
20
  - name: Run ty
21
21
  id: ty
22
22
  continue-on-error: true
23
- run: uv run ty check src/ 2>&1 | tee ty.log
23
+ run: uv run ty check src/ tests/ 2>&1 | tee ty.log
24
24
  - name: Build PR comment
25
25
  if: github.event_name == 'pull_request'
26
26
  run: |
@@ -37,7 +37,7 @@ jobs:
37
37
  echo '```'
38
38
  } > comment.md
39
39
  fi
40
- - uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v2
40
+ - uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v2
41
41
  if: github.event_name == 'pull_request'
42
42
  with:
43
43
  header: ty
@@ -8,3 +8,5 @@ dist/
8
8
  .ty/
9
9
  .coverage
10
10
  coverage.xml
11
+ plans/
12
+ CLAUDE.local.md
@@ -0,0 +1,83 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0 - 2026-07-28
4
+
5
+ ### Fixes
6
+
7
+ - `config.toml` (which stores server passwords in plaintext) was written with default umask permissions — typically world-readable on multi-user machines. It is now written `0600` with the config directory at `0700`, and `exsh` warns on stderr when an existing config file is group/world-readable
8
+ - Non-401/404 HTTP errors (most notably `403 Forbidden`) leaked a raw `httpx` traceback; they now exit cleanly with a one-line error message. `cp` local→remote also gained the XML well-formedness check that `put` and `sync` already had
9
+ - The sync manifest was keyed only by the remote path, so syncing the same remote collection against two different local directories shared one manifest and silently corrupted each other's per-file state (false conflicts, wrong skip decisions). The manifest key now includes the resolved local directory; existing manifests are re-keyed via a one-time full recheck on the next sync
10
+ - A single failed transfer during sync left the rest of the queue finishing silently in the background with no progress output, ending in a generic unattributed error. Failures are now reported per file (`! <path> (error: ...)`) as they happen, the remaining queue keeps draining, the summary shows a `failed` count, and sync exits non-zero when anything failed
11
+ - `is_remote()` classified any argument containing `:` as a `nick:path` remote target, so Windows paths (`C:\data\doc.xml`) and any local path with a colon in a directory component were always misparsed as a remote nick. A prefix is now only treated as a nick when it matches a configured collection, isn't a single-letter drive prefix (`C:\`, `C:/`), and doesn't itself contain a path separator. Server/collection nicks must now be at least 2 characters, guaranteeing a configured nick can never collide with a Windows drive letter
12
+ - `mv` of a collection onto itself (`exsh mv nick:/a nick:/a`) or into itself via a trailing slash (`exsh mv nick:/a nick:/a/`) copied the contents onto/into the source and then recursively deleted it, destroying the data. `mv` now aborts with an error when the target is the same as, or nested inside, the source, for both collections and single documents
13
+ - `cat`/`cp`/`mv`/`edit`/`sync` downloading an executable resource (`.xql`, `.xqm`) ran the query on the server and returned its result instead of the document's raw source
14
+ - The above fix relied on eXist's `_source=yes` REST parameter, which requires the path to be explicitly allowlisted in the server's `descriptor.xml` — not the default for anything under `/db`, regardless of the caller's own read permission. This returned `403 Forbidden` for every executable-resource download (`.xq`, `.xql`, `.xqm`, `.xquery`, `.xqy`, `.xqws`) on an unconfigured (i.e. most) server. These resources are now fetched via `util:binary-doc()`, which only needs the read + query-eval permission already required to execute them
15
+ - `rm <nick>:<path>` on a path that is a collection silently deleted the entire subtree with no confirmation, since eXist's `DELETE` is recursive on collections. `rm` now refuses a collection target unless `--recursive`/`-r` is given, and prompts for confirmation unless `--yes`/`-y` is also passed
16
+ - `create_collection`, `is_collection`, and `move_document` embedded paths/names into generated XQuery without escaping `"`/`&`, unlike every other client method — a path or name containing either broke the query (or, in `move_document`, changed its meaning)
17
+ - `rm` and `find --remove` never invalidated the completion cache after deleting, so deleted documents kept appearing in tab completion until the cache TTL expired
18
+
19
+ ### Enhancements
20
+
21
+ - **Independent client timeouts**: connect (10s), read (30s), and write (10s) now have separate budgets instead of one flat 30s timeout for every phase, and timeout errors say which phase failed — "Cannot connect to ..." for handshake failures vs "Server at ... did not respond in time" for read/write timeouts.
22
+ - **Faster, correct bash tab-completion for `nick:path` targets**: the generated bash completion script now handles the `:` word-break correctly (no more `dlp:dlp:...` insertions), completing a collection no longer needs an extra backspace before continuing into it, and completion listings are filtered and capped server-side and cached per prefix so progressive typing reuses a single fetch. Re-run `exsh --install-completion bash` after upgrading to pick up the new script.
23
+ - **Exit code 130 on Ctrl+C** for the whole CLI (previously sync-only), including during shell completion itself, which Typer/click don't handle on their own.
24
+ - **Parallel sync (`--jobs N`)**: uploads, downloads, and remote directory listings now run concurrently. The `--jobs N` flag (default: 4) controls the number of parallel workers. Use `--jobs 1` to restore fully sequential behaviour.
25
+ - **Clean Ctrl+C handling**: interrupting a sync now exits with code 130, saves the manifest (so the next run can resume), and prints a single `Interrupted.` message instead of a Python traceback.
26
+ - **`exsh find`**: locate documents by XPath expression anywhere in their content (`exsh find <nick>[:<path>] --query 'foo[@type="draft"]'`), with `--remove`/`--yes` to delete every match in one step.
27
+ - **`exsh exec --resource <nick>:<path.xql>`**: execute a stored `.xql`/`.xqm` resource in place (a plain `GET`, matching how eXist runs these resources natively) instead of downloading it and re-running it locally. Repeat `-p/--param NAME=VALUE` to forward query-string parameters as external variables.
28
+ - **`exsh sync --exclude`**: repeatable glob patterns to skip paths on both sides of a sync (`--exclude '*.tmp' --exclude build`). A pattern with `/` matches that relative path and its subtree; one without `/` matches any path segment at any depth. Patterns persist in the sync manifest per (server, remote path, local folder), so later runs keep excluding without the flag; new patterns merge into the stored list and `--clear-exclude` resets it. Excluded paths are never transferred and never deleted by `--delete`. Files synced before becoming excluded are deleted on both sides after confirmation (`--yes` skips the prompt, declining keeps the files, `--keep-excluded` keeps them without asking); either way they stop being tracked.
29
+
30
+ ## 0.1.2 - 2026-06-23
31
+
32
+ ### Documentation
33
+
34
+ - Document installing `exsh` from PyPI (`uv tool install`, `pipx install`, `uvx --from`); the existing `git+https://...` instructions are kept as an alternative for tracking unreleased commits
35
+
36
+ ## 0.1.1 - 2026-06-23
37
+
38
+ ### Fixes
39
+
40
+ - Complete PyPI project metadata: `readme`, `license` (MIT), `authors`, `classifiers`, `project.urls`, and `keywords` were all missing, leaving the 0.1.0 PyPI listing with no description, license, or links
41
+ - Add `LICENSE` file (MIT)
42
+
43
+ ## 0.1.0 - 2026-06-23
44
+
45
+ ### Commands
46
+
47
+ | Command | Description |
48
+ |---------|-------------|
49
+ | `server add / ls / rm` | Register, list, and remove eXist-db server configurations; `rm` cascade-removes associated collections |
50
+ | `server rename <old> <new>` | Rename a server nick; all registered collections are updated automatically |
51
+ | `collection add / new / ls / rm` | Manage named collection shortcuts; `new` creates the collection on the server and registers it in one step (idempotent: no-op if already exists) |
52
+ | `ls [path]` | List documents and sub-collections at a remote path; `--sort name\|time`, `--reverse`, `--names-only` |
53
+ | `cat <path>` | Print a remote document to stdout; `--raw` skips binary detection |
54
+ | `put <path>` | Upload a document from stdin or a local file; auto-detects MIME type |
55
+ | `cp <src> <dst>` | Copy documents local↔remote or remote↔remote |
56
+ | `rm <path...>` | Delete one or more remote documents |
57
+ | `mkdir <path>` | Create a remote collection (idempotent) |
58
+ | `edit <path>` | Download, open in `$VISUAL`/`$EDITOR`, re-upload if changed |
59
+ | `sync push/pull <dir> <path>` | Sync a local directory tree with a remote collection; supports `--delete`, `--dry-run`, `--force` |
60
+ | `mv <src> <dst>` | Move or rename a document or collection; trailing `/` on dest moves into that collection; cross-server moves not supported |
61
+ | `exec <nick>[:<path>]` | Read an XQuery script from a file or stdin, optionally preprocess it (version declaration, functx import), validate locally with BaseX or Saxon (auto-detected via PATH), then execute against eXist and print the result; supports `--no-fix`, `--no-validate`, `--validator`, `--list-validators` |
62
+ | `user ls` | List user accounts and their groups; accepts `@server` positional |
63
+ | `user add <user[@server]>` | Create a user account (prompts for password) |
64
+ | `user rm <user[@server]>` | Remove a user account |
65
+ | `user info <user[@server]>` | Show user account details |
66
+ | `user passwd <user[@server]>` | Change a user's password; `--stdin` for scripting |
67
+ | `group ls` | List groups and their members; accepts `@server` positional |
68
+ | `group add <group[@server]>` | Create a group |
69
+ | `group rm <group[@server]>` | Remove a group |
70
+ | `chown <spec> <path>` | Change owner and/or group of a document or collection; `--recursive / -R` for collections; spec forms: `owner`, `:group`, `owner:group` |
71
+ | `chmod <mode> <path>` | Change POSIX permissions of a document or collection; `--recursive / -R` for collections; accepts octal (`0755`) or symbolic (`u+x`, `go-w`, `a=rw`) modes |
72
+
73
+ ### Infrastructure
74
+
75
+ - Shell completion for collection/document paths (with TTL cache)
76
+ - `<name>@<server>` syntax for ad-hoc server targeting in `collection add`, `collection new`, `user *`, and `group *`
77
+ - `--server` option on all commands completes registered server nicks
78
+ - Shell completion for `<name>@<server>` and `@<server>` argument forms
79
+ - Path traversal validation and URL encoding
80
+ - Google-style docstrings enforced via ruff
81
+ - CI: pytest + coverage (Codecov), ruff, ty, e2e test suite against live eXist-db in Docker
82
+ - CI: sticky PR comments for tests (counts + coverage), ruff, ty, and e2e workflows
83
+ - CI: e2e workflow with dynamic image matrix from `--list-images`; `fail-fast: false`
@@ -9,7 +9,7 @@ test: ## Run tests
9
9
  uv run pytest
10
10
 
11
11
  ruff: ## Lint with ruff
12
- uv run ruff check src/
12
+ uv run ruff check src/ tests/
13
13
 
14
14
  ty: ## Type-check with ty
15
- uv run ty check src/
15
+ uv run ty check src/ tests/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: exist-shell
3
- Version: 0.1.1
3
+ Version: 0.2.0
4
4
  Summary: Command-line tool to interact with eXist-db via REST
5
5
  Project-URL: Homepage, https://github.com/ambs/exist-shell
6
6
  Project-URL: Repository, https://github.com/ambs/exist-shell
@@ -50,7 +50,13 @@ A command-line tool to interact with an [eXist-db](https://exist-db.org) server
50
50
  Install system-wide with `uv tool`:
51
51
 
52
52
  ```bash
53
- uv tool install git+https://github.com/ambs/exist-shell
53
+ uv tool install exist-shell
54
+ ```
55
+
56
+ Or with `pipx`:
57
+
58
+ ```bash
59
+ pipx install exist-shell
54
60
  ```
55
61
 
56
62
  This places `exsh` on your `PATH`. Verify with:
@@ -65,6 +71,22 @@ To uninstall:
65
71
  uv tool uninstall exist-shell
66
72
  ```
67
73
 
74
+ ### Run without installing
75
+
76
+ ```bash
77
+ uvx --from exist-shell exsh --version
78
+ ```
79
+
80
+ `uvx` fetches the package into a temporary, cached environment and runs it — handy for one-off use or trying out a new release.
81
+
82
+ ### Install from git
83
+
84
+ To track an unreleased commit instead of a PyPI release:
85
+
86
+ ```bash
87
+ uv tool install git+https://github.com/ambs/exist-shell
88
+ ```
89
+
68
90
  ## Configuration
69
91
 
70
92
  ### Add a server
@@ -113,11 +135,13 @@ Configuration is stored at `~/.config/exsh/config.toml`.
113
135
  | `exsh put <file> <nick>:<path>` | Upload a local file to a collection |
114
136
  | `exsh cp <src> <dst>` | Copy a document (local ↔ remote or remote ↔ remote) |
115
137
  | `exsh edit <nick>:<path>` | Open a document in `$EDITOR`, re-upload if changed |
116
- | `exsh rm <nick>:<path>...` | Delete one or more documents |
138
+ | `exsh rm <nick>:<path>...` | Delete one or more documents; `--recursive`/`-r` (with `--yes`/`-y`) to delete a collection |
117
139
  | `exsh mkdir <nick>:<path>` | Create a collection |
118
140
  | `exsh sync <local> <nick>[:<path>]` | Push a local folder to a remote collection |
119
141
  | `exsh sync <nick>[:<path>] <local>` | Pull a remote collection to a local folder |
120
142
  | `exsh exec <nick>[:<path>]` | Execute an XQuery script on a server |
143
+ | `exsh exec --resource <nick>:<path.xql>` | Execute a stored resource in place, forwarding `-p name=value` params |
144
+ | `exsh find <nick>[:<path>] --query <xpath>` | Find documents matching an XPath expression, with optional `--remove` |
121
145
  | `exsh server add <host>` | Register a server |
122
146
  | `exsh server ls` | List registered servers |
123
147
  | `exsh server rm <nick>` | Remove a server (and its collections) |
@@ -163,6 +187,9 @@ exsh rm mydata:reports/2025/old.xml
163
187
  # Delete multiple documents
164
188
  exsh rm mydata:reports/2025/a.xml mydata:reports/2025/b.xml
165
189
 
190
+ # Delete a whole collection (prompts unless --yes is also given)
191
+ exsh rm --recursive mydata:reports/2025
192
+
166
193
  # Create a subcollection
167
194
  exsh mkdir mydata:reports/2026
168
195
 
@@ -178,6 +205,18 @@ exsh exec mydata:/ --no-fix -f query.xq
178
205
  # List locally available XQuery validators
179
206
  exsh exec --list-validators
180
207
 
208
+ # Execute a stored .xql/.xqm resource in place, rather than downloading it first
209
+ exsh exec --resource mydata:/report.xql
210
+
211
+ # ...forwarding query-string parameters as external variables
212
+ exsh exec --resource mydata:/report.xql -p from=2026-01-01 -p to=2026-12-31
213
+
214
+ # List documents containing a matching element anywhere in their tree
215
+ exsh find mydata:reports --query 'foo[@type="draft"]'
216
+
217
+ # Delete every match, skipping the confirmation prompt
218
+ exsh find mydata:reports --query 'foo[@type="draft"]' --remove --yes
219
+
181
220
  # Push a local folder to the server (only transfers changed files)
182
221
  exsh sync ./reports mydata:reports
183
222
 
@@ -190,6 +229,9 @@ exsh sync --dry-run ./reports mydata:reports
190
229
  # Push and remove files on the server that no longer exist locally
191
230
  exsh sync --delete ./reports mydata:reports
192
231
 
232
+ # Sync everything except temp files and the build folder (remembered for later runs)
233
+ exsh sync --exclude '*.tmp' --exclude build ./reports mydata:reports
234
+
193
235
  # Change owner and group of a document
194
236
  exsh chown alice:editors mydata:reports/annual.xml
195
237
 
@@ -216,13 +258,26 @@ exsh chmod -R 0644 mydata:data
216
258
 
217
259
  **Conflicts** (both sides changed since last sync) are reported and skipped — use `--force` to override.
218
260
 
261
+ **Excludes:** `--exclude`/`-e <pattern>` (repeatable) skips matching paths on both sides of the sync — they are neither transferred nor ever deleted by `--delete`. Patterns are `fnmatch`-style (`*`, `?`, `[seq]`), matched against the path relative to the sync root:
262
+
263
+ - A pattern containing `/` matches that relative path and everything below it (`build/sub` also excludes `build/sub/x.xml`). Note that `*` in such a pattern also crosses `/` (plain `fnmatch`, unlike gitignore): `build/*.xml` matches `build/sub/x.xml` too.
264
+ - A pattern without `/` matches any single path segment at any depth — `build` excludes `build/`, `a/build/`, and their contents; `*.tmp` excludes temp files everywhere.
265
+
266
+ Patterns are persisted in the sync manifest, per (server, remote path, local folder), so later runs keep excluding without repeating the flag. New `--exclude` patterns are merged into the stored list; `--clear-exclude` wipes it (combine both to replace the list in one run). Files that were synced before becoming excluded are deleted **on both sides** after a confirmation prompt — `--yes` skips the prompt, declining (or running non-interactively without `--yes`) keeps the files, and `--keep-excluded` keeps them without asking. In every case they stop being tracked.
267
+
219
268
  **Options:**
220
269
 
221
270
  | Flag | Effect |
222
271
  |------|--------|
223
272
  | `--force` / `-f` | Transfer all files, bypassing change detection |
224
273
  | `--dry-run` / `-n` | Show what would happen without transferring |
274
+ | `--fail-fast` | Stop on the first conflict, invalid XML, or transfer error (runs sequentially; the manifest is saved so the run can resume) |
275
+ | `--jobs N` / `-j` | Number of parallel transfer workers (default: 4); `--jobs 1` for fully sequential behaviour |
225
276
  | `--delete` | Remove files and empty folders on the destination that no longer exist on the source |
277
+ | `--exclude PATTERN` / `-e` | Skip matching paths on both sides (repeatable); merged into the list stored for this sync pair |
278
+ | `--clear-exclude` | Wipe the stored exclude list before applying any `--exclude` given in the same run |
279
+ | `--keep-excluded` | Keep previously synced copies of newly excluded paths; only stop tracking them |
280
+ | `--yes` / `-y` | Skip the confirmation prompt when deleting previously synced, newly excluded files |
226
281
  | `--verbose` / `-v` | Also print unchanged (skipped) files |
227
282
  | `--checkpoint-every N` | Flush the manifest every N files (default: 100); allows interrupted syncs to resume near the point of failure |
228
283
 
@@ -241,6 +296,8 @@ exsh --install-completion zsh
241
296
  exsh --install-completion fish
242
297
  ```
243
298
 
299
+ Upgrading? Re-run `exsh --install-completion bash` to pick up fixes to the generated script — it's written once to `~/.bash_completions/` (or equivalent) and isn't updated automatically.
300
+
244
301
  ## Development
245
302
 
246
303
  ```bash
@@ -19,7 +19,13 @@ A command-line tool to interact with an [eXist-db](https://exist-db.org) server
19
19
  Install system-wide with `uv tool`:
20
20
 
21
21
  ```bash
22
- uv tool install git+https://github.com/ambs/exist-shell
22
+ uv tool install exist-shell
23
+ ```
24
+
25
+ Or with `pipx`:
26
+
27
+ ```bash
28
+ pipx install exist-shell
23
29
  ```
24
30
 
25
31
  This places `exsh` on your `PATH`. Verify with:
@@ -34,6 +40,22 @@ To uninstall:
34
40
  uv tool uninstall exist-shell
35
41
  ```
36
42
 
43
+ ### Run without installing
44
+
45
+ ```bash
46
+ uvx --from exist-shell exsh --version
47
+ ```
48
+
49
+ `uvx` fetches the package into a temporary, cached environment and runs it — handy for one-off use or trying out a new release.
50
+
51
+ ### Install from git
52
+
53
+ To track an unreleased commit instead of a PyPI release:
54
+
55
+ ```bash
56
+ uv tool install git+https://github.com/ambs/exist-shell
57
+ ```
58
+
37
59
  ## Configuration
38
60
 
39
61
  ### Add a server
@@ -82,11 +104,13 @@ Configuration is stored at `~/.config/exsh/config.toml`.
82
104
  | `exsh put <file> <nick>:<path>` | Upload a local file to a collection |
83
105
  | `exsh cp <src> <dst>` | Copy a document (local ↔ remote or remote ↔ remote) |
84
106
  | `exsh edit <nick>:<path>` | Open a document in `$EDITOR`, re-upload if changed |
85
- | `exsh rm <nick>:<path>...` | Delete one or more documents |
107
+ | `exsh rm <nick>:<path>...` | Delete one or more documents; `--recursive`/`-r` (with `--yes`/`-y`) to delete a collection |
86
108
  | `exsh mkdir <nick>:<path>` | Create a collection |
87
109
  | `exsh sync <local> <nick>[:<path>]` | Push a local folder to a remote collection |
88
110
  | `exsh sync <nick>[:<path>] <local>` | Pull a remote collection to a local folder |
89
111
  | `exsh exec <nick>[:<path>]` | Execute an XQuery script on a server |
112
+ | `exsh exec --resource <nick>:<path.xql>` | Execute a stored resource in place, forwarding `-p name=value` params |
113
+ | `exsh find <nick>[:<path>] --query <xpath>` | Find documents matching an XPath expression, with optional `--remove` |
90
114
  | `exsh server add <host>` | Register a server |
91
115
  | `exsh server ls` | List registered servers |
92
116
  | `exsh server rm <nick>` | Remove a server (and its collections) |
@@ -132,6 +156,9 @@ exsh rm mydata:reports/2025/old.xml
132
156
  # Delete multiple documents
133
157
  exsh rm mydata:reports/2025/a.xml mydata:reports/2025/b.xml
134
158
 
159
+ # Delete a whole collection (prompts unless --yes is also given)
160
+ exsh rm --recursive mydata:reports/2025
161
+
135
162
  # Create a subcollection
136
163
  exsh mkdir mydata:reports/2026
137
164
 
@@ -147,6 +174,18 @@ exsh exec mydata:/ --no-fix -f query.xq
147
174
  # List locally available XQuery validators
148
175
  exsh exec --list-validators
149
176
 
177
+ # Execute a stored .xql/.xqm resource in place, rather than downloading it first
178
+ exsh exec --resource mydata:/report.xql
179
+
180
+ # ...forwarding query-string parameters as external variables
181
+ exsh exec --resource mydata:/report.xql -p from=2026-01-01 -p to=2026-12-31
182
+
183
+ # List documents containing a matching element anywhere in their tree
184
+ exsh find mydata:reports --query 'foo[@type="draft"]'
185
+
186
+ # Delete every match, skipping the confirmation prompt
187
+ exsh find mydata:reports --query 'foo[@type="draft"]' --remove --yes
188
+
150
189
  # Push a local folder to the server (only transfers changed files)
151
190
  exsh sync ./reports mydata:reports
152
191
 
@@ -159,6 +198,9 @@ exsh sync --dry-run ./reports mydata:reports
159
198
  # Push and remove files on the server that no longer exist locally
160
199
  exsh sync --delete ./reports mydata:reports
161
200
 
201
+ # Sync everything except temp files and the build folder (remembered for later runs)
202
+ exsh sync --exclude '*.tmp' --exclude build ./reports mydata:reports
203
+
162
204
  # Change owner and group of a document
163
205
  exsh chown alice:editors mydata:reports/annual.xml
164
206
 
@@ -185,13 +227,26 @@ exsh chmod -R 0644 mydata:data
185
227
 
186
228
  **Conflicts** (both sides changed since last sync) are reported and skipped — use `--force` to override.
187
229
 
230
+ **Excludes:** `--exclude`/`-e <pattern>` (repeatable) skips matching paths on both sides of the sync — they are neither transferred nor ever deleted by `--delete`. Patterns are `fnmatch`-style (`*`, `?`, `[seq]`), matched against the path relative to the sync root:
231
+
232
+ - A pattern containing `/` matches that relative path and everything below it (`build/sub` also excludes `build/sub/x.xml`). Note that `*` in such a pattern also crosses `/` (plain `fnmatch`, unlike gitignore): `build/*.xml` matches `build/sub/x.xml` too.
233
+ - A pattern without `/` matches any single path segment at any depth — `build` excludes `build/`, `a/build/`, and their contents; `*.tmp` excludes temp files everywhere.
234
+
235
+ Patterns are persisted in the sync manifest, per (server, remote path, local folder), so later runs keep excluding without repeating the flag. New `--exclude` patterns are merged into the stored list; `--clear-exclude` wipes it (combine both to replace the list in one run). Files that were synced before becoming excluded are deleted **on both sides** after a confirmation prompt — `--yes` skips the prompt, declining (or running non-interactively without `--yes`) keeps the files, and `--keep-excluded` keeps them without asking. In every case they stop being tracked.
236
+
188
237
  **Options:**
189
238
 
190
239
  | Flag | Effect |
191
240
  |------|--------|
192
241
  | `--force` / `-f` | Transfer all files, bypassing change detection |
193
242
  | `--dry-run` / `-n` | Show what would happen without transferring |
243
+ | `--fail-fast` | Stop on the first conflict, invalid XML, or transfer error (runs sequentially; the manifest is saved so the run can resume) |
244
+ | `--jobs N` / `-j` | Number of parallel transfer workers (default: 4); `--jobs 1` for fully sequential behaviour |
194
245
  | `--delete` | Remove files and empty folders on the destination that no longer exist on the source |
246
+ | `--exclude PATTERN` / `-e` | Skip matching paths on both sides (repeatable); merged into the list stored for this sync pair |
247
+ | `--clear-exclude` | Wipe the stored exclude list before applying any `--exclude` given in the same run |
248
+ | `--keep-excluded` | Keep previously synced copies of newly excluded paths; only stop tracking them |
249
+ | `--yes` / `-y` | Skip the confirmation prompt when deleting previously synced, newly excluded files |
195
250
  | `--verbose` / `-v` | Also print unchanged (skipped) files |
196
251
  | `--checkpoint-every N` | Flush the manifest every N files (default: 100); allows interrupted syncs to resume near the point of failure |
197
252
 
@@ -210,6 +265,8 @@ exsh --install-completion zsh
210
265
  exsh --install-completion fish
211
266
  ```
212
267
 
268
+ Upgrading? Re-run `exsh --install-completion bash` to pick up fixes to the generated script — it's written once to `~/.bash_completions/` (or equivalent) and isn't updated automatically.
269
+
213
270
  ## Development
214
271
 
215
272
  ```bash
@@ -4,6 +4,14 @@
4
4
 
5
5
  ## Installation
6
6
 
7
+ ```bash
8
+ pip install exist-shell
9
+ # or with uv
10
+ uv add exist-shell
11
+ ```
12
+
13
+ To track an unreleased commit instead of a PyPI release:
14
+
7
15
  ```bash
8
16
  pip install git+https://github.com/ambs/exist-shell
9
17
  # or with uv
@@ -181,11 +181,20 @@ EDITOR=nano exsh edit mydata:config.xml
181
181
  Delete one or more documents from an eXist collection.
182
182
 
183
183
  ```
184
- exsh rm <nick>:<path>...
184
+ exsh rm [--recursive] [--yes] <nick>:<path>...
185
185
  ```
186
186
 
187
187
  Multiple paths can be supplied in a single invocation. Deletion is permanent — there is no undo.
188
188
 
189
+ A target that is itself a collection is refused by default (`Error: '<nick>:<path>' is a collection; use -r/--recursive to delete it and everything under it.`) rather than silently deleting the whole subtree. Pass `--recursive` to allow it; unless `--yes` is also given, `rm` first prompts for confirmation naming the collection being removed.
190
+
191
+ ### Options
192
+
193
+ | Flag | Effect |
194
+ |------|--------|
195
+ | `--recursive / -r` | Allow a target that is a collection to be deleted, along with everything under it. |
196
+ | `--yes / -y` | Skip the confirmation prompt when deleting a collection. |
197
+
189
198
  ### Examples
190
199
 
191
200
  ```bash
@@ -194,6 +203,12 @@ exsh rm mydata:reports/2025/old.xml
194
203
 
195
204
  # Delete multiple documents at once
196
205
  exsh rm mydata:reports/2025/a.xml mydata:reports/2025/b.xml
206
+
207
+ # Delete a whole collection (prompts for confirmation)
208
+ exsh rm --recursive mydata:reports/2025
209
+
210
+ # Delete a whole collection without prompting
211
+ exsh rm --recursive --yes mydata:reports/2025
197
212
  ```
198
213
 
199
214
  ---
@@ -271,6 +286,7 @@ Execute an XQuery script on an eXist-db server and print the result to stdout.
271
286
 
272
287
  ```
273
288
  exsh exec <nick>[:<path>] [-f FILE] [--no-fix] [--no-validate] [--validator NAME]
289
+ exsh exec --resource <nick>:<path.xql> [-p NAME=VALUE ...]
274
290
  ```
275
291
 
276
292
  The query is read from `--file` or from stdin. Before sending, `exsh` optionally preprocesses the source and validates it locally:
@@ -291,10 +307,18 @@ The first installed validator found on `PATH` is used automatically. Use `--vali
291
307
 
292
308
  Run `exsh exec --list-validators` to see which validators are available on the current machine.
293
309
 
310
+ ### Executing a stored resource
311
+
312
+ `--resource <nick>:<path.xql>` runs a `.xql`/`.xqm` script that's already stored on the server in place, instead of sending local source. This is a plain `GET` on the resource — the same mechanism eXist uses to execute these MIME types automatically — so it relies on the resource being invoked in its own stored collection context, which matters for scripts that assume that context (e.g. relative `doc()`/`collection()` lookups).
313
+
314
+ `--resource` is mutually exclusive with the `<nick>[:<path>]` argument, and skips local preprocessing/validation entirely, since there's no local source to fix up or check. Repeat `-p/--param NAME=VALUE` to forward query-string parameters, which the resource can read as external variables.
315
+
294
316
  ### Options
295
317
 
296
318
  | Flag | Description |
297
319
  |------|-------------|
320
+ | `--resource <nick>:<path.xql>` | Execute a stored resource in place instead of local code. |
321
+ | `-p / --param NAME=VALUE` | Query-string parameter to forward with `--resource` (repeatable). |
298
322
  | `-f / --file FILE` | XQuery file to execute. When omitted, stdin is read. |
299
323
  | `--no-fix` | Skip preprocessing (version declaration, namespace imports). |
300
324
  | `--no-validate` | Skip local validation even if a validator is installed. |
@@ -324,6 +348,58 @@ exsh exec mydata:/ --validator basex -f query.xq
324
348
 
325
349
  # Check which validators are available locally
326
350
  exsh exec --list-validators
351
+
352
+ # Execute a stored resource in place, rather than downloading it first
353
+ exsh exec --resource mydata:/report.xql
354
+
355
+ # ...forwarding query-string parameters as external variables
356
+ exsh exec --resource mydata:/report.xql -p from=2026-01-01 -p to=2026-12-31
357
+ ```
358
+
359
+ ---
360
+
361
+ ## find
362
+
363
+ Locate documents whose content matches an XPath expression, and optionally delete every match in one step.
364
+
365
+ ```
366
+ exsh find <nick>[:<path>] --query <xpath> [--remove] [--yes]
367
+ ```
368
+
369
+ The expression given to `--query` is evaluated recursively under the target collection (equivalent to `collection(...)//(QUERY)`), and each matching document is printed as a `<nick>:<path>` target — the same format `rm` and every other command accepts. Because `rm` takes its targets as arguments (not from standard input), pipe `find` output through `xargs` to feed it into `rm`:
370
+
371
+ ```bash
372
+ exsh find mydata:reports --query 'foo[@type="draft"]' | xargs -r exsh rm
373
+ ```
374
+
375
+ Deleting matches directly with `--remove` is usually simpler; the pipe form is useful when you want to filter the list first.
376
+
377
+ Without `--remove`, `find` only lists matches — this is inherently a dry run. With `--remove`, matches are deleted as they're printed; unless `--yes` is also given, `find` first prompts for confirmation showing the number of matching documents. If a document reported by the search has already disappeared by the time it is deleted, `find` warns and continues, then exits non-zero.
378
+
379
+ > **Security note:** the `--query` expression is embedded into a server-side XQuery without validation or sandboxing, so it can execute arbitrary XQuery with your configured server credentials (the same trust model as `exsh exec`). Only pass expressions from a trusted source.
380
+
381
+ ### Options
382
+
383
+ | Flag | Description |
384
+ |------|-------------|
385
+ | `--query / -q XPATH` | XPath expression to match, evaluated recursively under the target (required). |
386
+ | `--remove` | Delete matching documents instead of just listing them. |
387
+ | `--yes / -y` | Skip the confirmation prompt when used with `--remove`. |
388
+
389
+ ### Examples
390
+
391
+ ```bash
392
+ # List documents containing a matching element anywhere in their tree
393
+ exsh find mydata:reports --query 'foo[@type="draft"]'
394
+
395
+ # Match on an attribute nested deeper in the document
396
+ exsh find mydata:/ --query 'section/foo[@type="draft"]'
397
+
398
+ # Delete every match, with a confirmation prompt showing the match count
399
+ exsh find mydata:reports --query 'foo[@type="draft"]' --remove
400
+
401
+ # Delete every match, skipping the confirmation prompt
402
+ exsh find mydata:reports --query 'foo[@type="draft"]' --remove --yes
327
403
  ```
328
404
 
329
405
  ---
@@ -21,7 +21,7 @@
21
21
 
22
22
  ```bash
23
23
  # 1. Install
24
- uv tool install git+https://github.com/ambs/exist-shell
24
+ uv tool install exist-shell
25
25
 
26
26
  # 2. Register your server
27
27
  exsh server add localhost --port 8080 --user admin