exist-shell 0.1.2__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.2 → exist_shell-0.2.0}/.github/workflows/docs.yml +2 -2
  2. {exist_shell-0.1.2 → exist_shell-0.2.0}/.github/workflows/e2e.yml +4 -4
  3. {exist_shell-0.1.2 → exist_shell-0.2.0}/.github/workflows/release.yml +2 -2
  4. {exist_shell-0.1.2 → exist_shell-0.2.0}/.github/workflows/ruff.yml +3 -3
  5. {exist_shell-0.1.2 → exist_shell-0.2.0}/.github/workflows/tests.yml +3 -3
  6. {exist_shell-0.1.2 → exist_shell-0.2.0}/.github/workflows/ty.yml +4 -4
  7. {exist_shell-0.1.2 → exist_shell-0.2.0}/.gitignore +2 -0
  8. exist_shell-0.2.0/CHANGELOG.md +83 -0
  9. {exist_shell-0.1.2 → exist_shell-0.2.0}/Makefile +2 -2
  10. {exist_shell-0.1.2 → exist_shell-0.2.0}/PKG-INFO +37 -2
  11. {exist_shell-0.1.2 → exist_shell-0.2.0}/README.md +36 -1
  12. {exist_shell-0.1.2 → exist_shell-0.2.0}/docs/commands.md +77 -1
  13. {exist_shell-0.1.2 → exist_shell-0.2.0}/docs/sync.md +25 -1
  14. {exist_shell-0.1.2 → exist_shell-0.2.0}/pyproject.toml +5 -5
  15. {exist_shell-0.1.2 → exist_shell-0.2.0}/scripts/e2e/sections/T05_put.sh +7 -0
  16. {exist_shell-0.1.2 → exist_shell-0.2.0}/scripts/e2e/sections/T07_cat.sh +9 -0
  17. {exist_shell-0.1.2 → exist_shell-0.2.0}/scripts/e2e/sections/T08_cp.sh +35 -0
  18. {exist_shell-0.1.2 → exist_shell-0.2.0}/scripts/e2e/sections/T09_rm.sh +23 -0
  19. {exist_shell-0.1.2 → exist_shell-0.2.0}/scripts/e2e/sections/T12_sync.sh +100 -1
  20. {exist_shell-0.1.2 → exist_shell-0.2.0}/scripts/e2e/sections/T13_mv.sh +37 -0
  21. {exist_shell-0.1.2 → exist_shell-0.2.0}/scripts/e2e/sections/T14_exec.sh +43 -0
  22. exist_shell-0.2.0/scripts/e2e/sections/T19_find.sh +68 -0
  23. {exist_shell-0.1.2 → exist_shell-0.2.0}/scripts/e2e.sh +2 -0
  24. {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/cache.py +95 -12
  25. {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/client/__init__.py +7 -3
  26. exist_shell-0.2.0/src/exist_shell/client/_base.py +102 -0
  27. {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/client/_collections.py +62 -13
  28. {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/client/_documents.py +64 -23
  29. exist_shell-0.2.0/src/exist_shell/client/_queries.py +105 -0
  30. {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/commands/cat.py +1 -1
  31. {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/commands/chmod.py +15 -20
  32. {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/commands/chown.py +15 -20
  33. {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/commands/cp.py +5 -1
  34. {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/commands/exec.py +45 -8
  35. exist_shell-0.2.0/src/exist_shell/commands/find.py +64 -0
  36. {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/commands/mv.py +7 -0
  37. exist_shell-0.2.0/src/exist_shell/commands/rm.py +47 -0
  38. exist_shell-0.2.0/src/exist_shell/commands/sync.py +1405 -0
  39. {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/completions.py +126 -12
  40. {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/config.py +34 -2
  41. {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/exceptions.py +28 -1
  42. {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/main.py +20 -1
  43. {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/utils.py +44 -7
  44. exist_shell-0.2.0/tests/__init__.py +1 -0
  45. {exist_shell-0.1.2 → exist_shell-0.2.0}/tests/conftest.py +8 -0
  46. exist_shell-0.2.0/tests/test_cache.py +335 -0
  47. {exist_shell-0.1.2 → exist_shell-0.2.0}/tests/test_client.py +584 -5
  48. {exist_shell-0.1.2 → exist_shell-0.2.0}/tests/test_commands_cat.py +19 -0
  49. {exist_shell-0.1.2 → exist_shell-0.2.0}/tests/test_commands_chmod.py +38 -0
  50. {exist_shell-0.1.2 → exist_shell-0.2.0}/tests/test_commands_chown.py +27 -0
  51. {exist_shell-0.1.2 → exist_shell-0.2.0}/tests/test_commands_collection.py +36 -0
  52. {exist_shell-0.1.2 → exist_shell-0.2.0}/tests/test_commands_cp.py +34 -0
  53. {exist_shell-0.1.2 → exist_shell-0.2.0}/tests/test_commands_edit.py +14 -0
  54. {exist_shell-0.1.2 → exist_shell-0.2.0}/tests/test_commands_exec.py +101 -1
  55. exist_shell-0.2.0/tests/test_commands_find.py +146 -0
  56. {exist_shell-0.1.2 → exist_shell-0.2.0}/tests/test_commands_group.py +37 -0
  57. {exist_shell-0.1.2 → exist_shell-0.2.0}/tests/test_commands_ls.py +17 -0
  58. {exist_shell-0.1.2 → exist_shell-0.2.0}/tests/test_commands_mkdir.py +7 -0
  59. {exist_shell-0.1.2 → exist_shell-0.2.0}/tests/test_commands_mv.py +51 -0
  60. {exist_shell-0.1.2 → exist_shell-0.2.0}/tests/test_commands_put.py +17 -0
  61. exist_shell-0.2.0/tests/test_commands_rm.py +129 -0
  62. {exist_shell-0.1.2 → exist_shell-0.2.0}/tests/test_commands_server.py +35 -0
  63. exist_shell-0.2.0/tests/test_commands_sync.py +1791 -0
  64. {exist_shell-0.1.2 → exist_shell-0.2.0}/tests/test_commands_user.py +68 -0
  65. {exist_shell-0.1.2 → exist_shell-0.2.0}/tests/test_completions.py +154 -17
  66. {exist_shell-0.1.2 → exist_shell-0.2.0}/tests/test_config.py +104 -6
  67. exist_shell-0.2.0/tests/test_exceptions.py +55 -0
  68. exist_shell-0.2.0/tests/test_main.py +45 -0
  69. exist_shell-0.2.0/tests/test_utils.py +258 -0
  70. {exist_shell-0.1.2 → exist_shell-0.2.0}/tests/test_xquery.py +32 -0
  71. {exist_shell-0.1.2 → exist_shell-0.2.0}/uv.lock +48 -48
  72. exist_shell-0.1.2/CHANGELOG.md +0 -58
  73. exist_shell-0.1.2/src/exist_shell/client/_base.py +0 -65
  74. exist_shell-0.1.2/src/exist_shell/client/_queries.py +0 -37
  75. exist_shell-0.1.2/src/exist_shell/commands/rm.py +0 -23
  76. exist_shell-0.1.2/src/exist_shell/commands/sync.py +0 -767
  77. exist_shell-0.1.2/tests/__init__.py +0 -0
  78. exist_shell-0.1.2/tests/test_cache.py +0 -181
  79. exist_shell-0.1.2/tests/test_commands_rm.py +0 -66
  80. exist_shell-0.1.2/tests/test_commands_sync.py +0 -636
  81. exist_shell-0.1.2/tests/test_utils.py +0 -141
  82. {exist_shell-0.1.2 → exist_shell-0.2.0}/.gitguardian.yaml +0 -0
  83. {exist_shell-0.1.2 → exist_shell-0.2.0}/.github/dependabot.yml +0 -0
  84. {exist_shell-0.1.2 → exist_shell-0.2.0}/CLAUDE.md +0 -0
  85. {exist_shell-0.1.2 → exist_shell-0.2.0}/LICENSE +0 -0
  86. {exist_shell-0.1.2 → exist_shell-0.2.0}/docs/api.md +0 -0
  87. {exist_shell-0.1.2 → exist_shell-0.2.0}/docs/completion.md +0 -0
  88. {exist_shell-0.1.2 → exist_shell-0.2.0}/docs/configuration.md +0 -0
  89. {exist_shell-0.1.2 → exist_shell-0.2.0}/docs/development.md +0 -0
  90. {exist_shell-0.1.2 → exist_shell-0.2.0}/docs/index.md +0 -0
  91. {exist_shell-0.1.2 → exist_shell-0.2.0}/docs/installation.md +0 -0
  92. {exist_shell-0.1.2 → exist_shell-0.2.0}/mkdocs.yml +0 -0
  93. {exist_shell-0.1.2 → exist_shell-0.2.0}/scripts/e2e/docker.sh +0 -0
  94. {exist_shell-0.1.2 → exist_shell-0.2.0}/scripts/e2e/lib.sh +0 -0
  95. {exist_shell-0.1.2 → exist_shell-0.2.0}/scripts/e2e/sections/T02_server.sh +0 -0
  96. {exist_shell-0.1.2 → exist_shell-0.2.0}/scripts/e2e/sections/T03_collection.sh +0 -0
  97. {exist_shell-0.1.2 → exist_shell-0.2.0}/scripts/e2e/sections/T04_ls.sh +0 -0
  98. {exist_shell-0.1.2 → exist_shell-0.2.0}/scripts/e2e/sections/T06_ls_after.sh +0 -0
  99. {exist_shell-0.1.2 → exist_shell-0.2.0}/scripts/e2e/sections/T10_mkdir.sh +0 -0
  100. {exist_shell-0.1.2 → exist_shell-0.2.0}/scripts/e2e/sections/T11_edit.sh +0 -0
  101. {exist_shell-0.1.2 → exist_shell-0.2.0}/scripts/e2e/sections/T15_user.sh +0 -0
  102. {exist_shell-0.1.2 → exist_shell-0.2.0}/scripts/e2e/sections/T16_group.sh +0 -0
  103. {exist_shell-0.1.2 → exist_shell-0.2.0}/scripts/e2e/sections/T17_chown.sh +0 -0
  104. {exist_shell-0.1.2 → exist_shell-0.2.0}/scripts/e2e/sections/T18_chmod.sh +0 -0
  105. {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/__init__.py +0 -0
  106. {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/client/_groups.py +0 -0
  107. {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/client/_permissions.py +0 -0
  108. {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/client/_users.py +0 -0
  109. {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/commands/__init__.py +0 -0
  110. {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/commands/collection.py +0 -0
  111. {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/commands/edit.py +0 -0
  112. {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/commands/group.py +0 -0
  113. {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/commands/ls.py +0 -0
  114. {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/commands/mkdir.py +0 -0
  115. {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/commands/put.py +0 -0
  116. {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/commands/server.py +0 -0
  117. {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/commands/user.py +0 -0
  118. {exist_shell-0.1.2 → exist_shell-0.2.0}/src/exist_shell/models.py +0 -0
  119. {exist_shell-0.1.2 → 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.2
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
@@ -135,11 +135,13 @@ Configuration is stored at `~/.config/exsh/config.toml`.
135
135
  | `exsh put <file> <nick>:<path>` | Upload a local file to a collection |
136
136
  | `exsh cp <src> <dst>` | Copy a document (local ↔ remote or remote ↔ remote) |
137
137
  | `exsh edit <nick>:<path>` | Open a document in `$EDITOR`, re-upload if changed |
138
- | `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 |
139
139
  | `exsh mkdir <nick>:<path>` | Create a collection |
140
140
  | `exsh sync <local> <nick>[:<path>]` | Push a local folder to a remote collection |
141
141
  | `exsh sync <nick>[:<path>] <local>` | Pull a remote collection to a local folder |
142
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` |
143
145
  | `exsh server add <host>` | Register a server |
144
146
  | `exsh server ls` | List registered servers |
145
147
  | `exsh server rm <nick>` | Remove a server (and its collections) |
@@ -185,6 +187,9 @@ exsh rm mydata:reports/2025/old.xml
185
187
  # Delete multiple documents
186
188
  exsh rm mydata:reports/2025/a.xml mydata:reports/2025/b.xml
187
189
 
190
+ # Delete a whole collection (prompts unless --yes is also given)
191
+ exsh rm --recursive mydata:reports/2025
192
+
188
193
  # Create a subcollection
189
194
  exsh mkdir mydata:reports/2026
190
195
 
@@ -200,6 +205,18 @@ exsh exec mydata:/ --no-fix -f query.xq
200
205
  # List locally available XQuery validators
201
206
  exsh exec --list-validators
202
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
+
203
220
  # Push a local folder to the server (only transfers changed files)
204
221
  exsh sync ./reports mydata:reports
205
222
 
@@ -212,6 +229,9 @@ exsh sync --dry-run ./reports mydata:reports
212
229
  # Push and remove files on the server that no longer exist locally
213
230
  exsh sync --delete ./reports mydata:reports
214
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
+
215
235
  # Change owner and group of a document
216
236
  exsh chown alice:editors mydata:reports/annual.xml
217
237
 
@@ -238,13 +258,26 @@ exsh chmod -R 0644 mydata:data
238
258
 
239
259
  **Conflicts** (both sides changed since last sync) are reported and skipped — use `--force` to override.
240
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
+
241
268
  **Options:**
242
269
 
243
270
  | Flag | Effect |
244
271
  |------|--------|
245
272
  | `--force` / `-f` | Transfer all files, bypassing change detection |
246
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 |
247
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 |
248
281
  | `--verbose` / `-v` | Also print unchanged (skipped) files |
249
282
  | `--checkpoint-every N` | Flush the manifest every N files (default: 100); allows interrupted syncs to resume near the point of failure |
250
283
 
@@ -263,6 +296,8 @@ exsh --install-completion zsh
263
296
  exsh --install-completion fish
264
297
  ```
265
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
+
266
301
  ## Development
267
302
 
268
303
  ```bash
@@ -104,11 +104,13 @@ Configuration is stored at `~/.config/exsh/config.toml`.
104
104
  | `exsh put <file> <nick>:<path>` | Upload a local file to a collection |
105
105
  | `exsh cp <src> <dst>` | Copy a document (local ↔ remote or remote ↔ remote) |
106
106
  | `exsh edit <nick>:<path>` | Open a document in `$EDITOR`, re-upload if changed |
107
- | `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 |
108
108
  | `exsh mkdir <nick>:<path>` | Create a collection |
109
109
  | `exsh sync <local> <nick>[:<path>]` | Push a local folder to a remote collection |
110
110
  | `exsh sync <nick>[:<path>] <local>` | Pull a remote collection to a local folder |
111
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` |
112
114
  | `exsh server add <host>` | Register a server |
113
115
  | `exsh server ls` | List registered servers |
114
116
  | `exsh server rm <nick>` | Remove a server (and its collections) |
@@ -154,6 +156,9 @@ exsh rm mydata:reports/2025/old.xml
154
156
  # Delete multiple documents
155
157
  exsh rm mydata:reports/2025/a.xml mydata:reports/2025/b.xml
156
158
 
159
+ # Delete a whole collection (prompts unless --yes is also given)
160
+ exsh rm --recursive mydata:reports/2025
161
+
157
162
  # Create a subcollection
158
163
  exsh mkdir mydata:reports/2026
159
164
 
@@ -169,6 +174,18 @@ exsh exec mydata:/ --no-fix -f query.xq
169
174
  # List locally available XQuery validators
170
175
  exsh exec --list-validators
171
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
+
172
189
  # Push a local folder to the server (only transfers changed files)
173
190
  exsh sync ./reports mydata:reports
174
191
 
@@ -181,6 +198,9 @@ exsh sync --dry-run ./reports mydata:reports
181
198
  # Push and remove files on the server that no longer exist locally
182
199
  exsh sync --delete ./reports mydata:reports
183
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
+
184
204
  # Change owner and group of a document
185
205
  exsh chown alice:editors mydata:reports/annual.xml
186
206
 
@@ -207,13 +227,26 @@ exsh chmod -R 0644 mydata:data
207
227
 
208
228
  **Conflicts** (both sides changed since last sync) are reported and skipped — use `--force` to override.
209
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
+
210
237
  **Options:**
211
238
 
212
239
  | Flag | Effect |
213
240
  |------|--------|
214
241
  | `--force` / `-f` | Transfer all files, bypassing change detection |
215
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 |
216
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 |
217
250
  | `--verbose` / `-v` | Also print unchanged (skipped) files |
218
251
  | `--checkpoint-every N` | Flush the manifest every N files (default: 100); allows interrupted syncs to resume near the point of failure |
219
252
 
@@ -232,6 +265,8 @@ exsh --install-completion zsh
232
265
  exsh --install-completion fish
233
266
  ```
234
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
+
235
270
  ## Development
236
271
 
237
272
  ```bash
@@ -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
  ---
@@ -3,7 +3,7 @@
3
3
  Synchronise a local directory tree with a remote eXist collection, transferring only files that have actually changed.
4
4
 
5
5
  ```
6
- exsh sync <source> <dest> [--force] [--fail-fast] [--dry-run] [--delete] [--verbose] [--checkpoint-every N]
6
+ exsh sync <source> <dest> [--force] [--fail-fast] [--dry-run] [--delete] [--verbose] [--jobs N] [--checkpoint-every N]
7
7
  ```
8
8
 
9
9
  ## Direction
@@ -78,6 +78,7 @@ Use `--force` to override conflict detection and transfer the source uncondition
78
78
  | `--dry-run / -n` | Print what would happen without moving any data |
79
79
  | `--delete` | Remove files and empty folders on the destination that no longer exist on the source |
80
80
  | `--verbose / -v` | Also print unchanged (skipped) files |
81
+ | `--jobs N / -j N` | Number of parallel transfer workers (default: 4); set to 1 for sequential |
81
82
  | `--checkpoint-every N` | Flush the manifest to disk every N files (default: 100) |
82
83
 
83
84
  ## Output
@@ -126,6 +127,29 @@ exsh sync --delete ./reports mydata:reports
126
127
  exsh sync --force ./reports mydata:reports
127
128
  ```
128
129
 
130
+ ## Parallelism
131
+
132
+ By default `exsh sync` runs up to **4 transfers concurrently** — both for file uploads/downloads and for the initial remote directory listing (BFS walk). This significantly reduces wall-clock time when syncing collections with many small files over a high-latency connection.
133
+
134
+ ```bash
135
+ # Use 8 workers for a large collection
136
+ exsh sync --jobs 8 ./data myserver:data
137
+
138
+ # Restore sequential behaviour (one file at a time)
139
+ exsh sync --jobs 1 ./data myserver:data
140
+ ```
141
+
142
+ `--fail-fast` always runs sequentially regardless of `--jobs`, to guarantee that no file after the first failure is transferred.
143
+
144
+ ### Interrupting a sync
145
+
146
+ Pressing **Ctrl+C** during a sync:
147
+
148
+ - Cancels any pending (not yet started) transfers immediately.
149
+ - Waits for at most `--jobs` in-flight transfers to finish (they cannot be interrupted mid-request).
150
+ - Saves the manifest so the next run resumes from where it stopped.
151
+ - Exits with code **130** (the POSIX convention for Ctrl+C).
152
+
129
153
  ## Large collections and resumability
130
154
 
131
155
  For collections with many files, the manifest is flushed to disk every 100 files by default. If a sync is interrupted (network failure, process kill, etc.), the next run reads the saved manifest and skips files that were already transferred, restarting near the point of failure rather than from scratch.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "exist-shell"
7
- version = "0.1.2"
7
+ version = "0.2.0"
8
8
  description = "Command-line tool to interact with eXist-db via REST"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -41,7 +41,7 @@ Issues = "https://github.com/ambs/exist-shell/issues"
41
41
  Changelog = "https://github.com/ambs/exist-shell/blob/main/CHANGELOG.md"
42
42
 
43
43
  [project.scripts]
44
- exsh = "exist_shell.main:app"
44
+ exsh = "exist_shell.main:cli"
45
45
 
46
46
  [tool.hatch.build.targets.wheel]
47
47
  packages = ["src/exist_shell"]
@@ -54,9 +54,9 @@ convention = "google"
54
54
 
55
55
  [dependency-groups]
56
56
  dev = [
57
- "pytest>=8",
57
+ "pytest>=9.1.1",
58
58
  "pytest-cov>=5",
59
59
  "pytest-httpx>=0.30",
60
- "ruff>=0.15.16",
61
- "ty>=0.0.46",
60
+ "ruff>=0.15.18",
61
+ "ty>=0.0.54",
62
62
  ]
@@ -8,6 +8,7 @@ section_T05_put() {
8
8
  # Prepare local files (persist for T06+)
9
9
  printf '<hello>world</hello>' > "${TMPDIR_E2E}/hello.xml"
10
10
  printf '\x00\x01\x02\x03' > "${TMPDIR_E2E}/data.bin"
11
+ printf 'xquery version "3.1";\n<computed>{2 + 2}</computed>' > "${TMPDIR_E2E}/script.xq"
11
12
 
12
13
  # T05.1 — upload XML from file
13
14
  assert_exit0 "T05.1 put XML file" \
@@ -67,4 +68,10 @@ section_T05_put() {
67
68
  # T05.12 — binary file is uploaded without XML validation
68
69
  assert_exit0 "T05.12 put binary skips XML validation" \
69
70
  "${EXSH[@]}" put testcol:/data.bin -f "${TMPDIR_E2E}/data.bin" --mime application/octet-stream
71
+
72
+ # T05.13 — .xq file stored as application/xquery (eXist's executable resource type;
73
+ # --mime is required since exsh has no built-in guess for this extension). Regression
74
+ # coverage for #141/#144: cat/cp must return the source below, not the query's output.
75
+ assert_exit0 "T05.13 put .xq file as application/xquery" \
76
+ "${EXSH[@]}" put testcol:/script.xq -f "${TMPDIR_E2E}/script.xq" --mime application/xquery
70
77
  }