exist-shell 0.1.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 (103) hide show
  1. exist_shell-0.1.0/.gitguardian.yaml +5 -0
  2. exist_shell-0.1.0/.github/dependabot.yml +17 -0
  3. exist_shell-0.1.0/.github/workflows/docs.yml +30 -0
  4. exist_shell-0.1.0/.github/workflows/e2e.yml +104 -0
  5. exist_shell-0.1.0/.github/workflows/release.yml +34 -0
  6. exist_shell-0.1.0/.github/workflows/ruff.yml +47 -0
  7. exist_shell-0.1.0/.github/workflows/tests.yml +49 -0
  8. exist_shell-0.1.0/.github/workflows/ty.yml +47 -0
  9. exist_shell-0.1.0/.gitignore +10 -0
  10. exist_shell-0.1.0/CHANGELOG.md +45 -0
  11. exist_shell-0.1.0/CLAUDE.md +108 -0
  12. exist_shell-0.1.0/Makefile +15 -0
  13. exist_shell-0.1.0/PKG-INFO +10 -0
  14. exist_shell-0.1.0/README.md +227 -0
  15. exist_shell-0.1.0/docs/api.md +251 -0
  16. exist_shell-0.1.0/docs/commands.md +743 -0
  17. exist_shell-0.1.0/docs/completion.md +52 -0
  18. exist_shell-0.1.0/docs/configuration.md +118 -0
  19. exist_shell-0.1.0/docs/development.md +82 -0
  20. exist_shell-0.1.0/docs/index.md +68 -0
  21. exist_shell-0.1.0/docs/installation.md +55 -0
  22. exist_shell-0.1.0/docs/sync.md +152 -0
  23. exist_shell-0.1.0/mkdocs.yml +58 -0
  24. exist_shell-0.1.0/pyproject.toml +37 -0
  25. exist_shell-0.1.0/scripts/e2e/docker.sh +79 -0
  26. exist_shell-0.1.0/scripts/e2e/lib.sh +154 -0
  27. exist_shell-0.1.0/scripts/e2e/sections/T02_server.sh +168 -0
  28. exist_shell-0.1.0/scripts/e2e/sections/T03_collection.sh +184 -0
  29. exist_shell-0.1.0/scripts/e2e/sections/T04_ls.sh +32 -0
  30. exist_shell-0.1.0/scripts/e2e/sections/T05_put.sh +70 -0
  31. exist_shell-0.1.0/scripts/e2e/sections/T06_ls_after.sh +62 -0
  32. exist_shell-0.1.0/scripts/e2e/sections/T07_cat.sh +56 -0
  33. exist_shell-0.1.0/scripts/e2e/sections/T08_cp.sh +78 -0
  34. exist_shell-0.1.0/scripts/e2e/sections/T09_rm.sh +43 -0
  35. exist_shell-0.1.0/scripts/e2e/sections/T10_mkdir.sh +44 -0
  36. exist_shell-0.1.0/scripts/e2e/sections/T11_edit.sh +69 -0
  37. exist_shell-0.1.0/scripts/e2e/sections/T12_sync.sh +229 -0
  38. exist_shell-0.1.0/scripts/e2e/sections/T13_mv.sh +186 -0
  39. exist_shell-0.1.0/scripts/e2e/sections/T14_exec.sh +135 -0
  40. exist_shell-0.1.0/scripts/e2e/sections/T15_user.sh +125 -0
  41. exist_shell-0.1.0/scripts/e2e/sections/T16_group.sh +66 -0
  42. exist_shell-0.1.0/scripts/e2e/sections/T17_chown.sh +83 -0
  43. exist_shell-0.1.0/scripts/e2e/sections/T18_chmod.sh +88 -0
  44. exist_shell-0.1.0/scripts/e2e.sh +125 -0
  45. exist_shell-0.1.0/src/exist_shell/__init__.py +5 -0
  46. exist_shell-0.1.0/src/exist_shell/cache.py +186 -0
  47. exist_shell-0.1.0/src/exist_shell/client/__init__.py +19 -0
  48. exist_shell-0.1.0/src/exist_shell/client/_base.py +65 -0
  49. exist_shell-0.1.0/src/exist_shell/client/_collections.py +158 -0
  50. exist_shell-0.1.0/src/exist_shell/client/_documents.py +134 -0
  51. exist_shell-0.1.0/src/exist_shell/client/_groups.py +116 -0
  52. exist_shell-0.1.0/src/exist_shell/client/_permissions.py +172 -0
  53. exist_shell-0.1.0/src/exist_shell/client/_queries.py +37 -0
  54. exist_shell-0.1.0/src/exist_shell/client/_users.py +158 -0
  55. exist_shell-0.1.0/src/exist_shell/commands/__init__.py +1 -0
  56. exist_shell-0.1.0/src/exist_shell/commands/cat.py +51 -0
  57. exist_shell-0.1.0/src/exist_shell/commands/chmod.py +236 -0
  58. exist_shell-0.1.0/src/exist_shell/commands/chown.py +121 -0
  59. exist_shell-0.1.0/src/exist_shell/commands/collection.py +189 -0
  60. exist_shell-0.1.0/src/exist_shell/commands/cp.py +142 -0
  61. exist_shell-0.1.0/src/exist_shell/commands/edit.py +85 -0
  62. exist_shell-0.1.0/src/exist_shell/commands/exec.py +65 -0
  63. exist_shell-0.1.0/src/exist_shell/commands/group.py +183 -0
  64. exist_shell-0.1.0/src/exist_shell/commands/ls.py +66 -0
  65. exist_shell-0.1.0/src/exist_shell/commands/mkdir.py +24 -0
  66. exist_shell-0.1.0/src/exist_shell/commands/mv.py +124 -0
  67. exist_shell-0.1.0/src/exist_shell/commands/put.py +63 -0
  68. exist_shell-0.1.0/src/exist_shell/commands/rm.py +23 -0
  69. exist_shell-0.1.0/src/exist_shell/commands/server.py +114 -0
  70. exist_shell-0.1.0/src/exist_shell/commands/sync.py +767 -0
  71. exist_shell-0.1.0/src/exist_shell/commands/user.py +300 -0
  72. exist_shell-0.1.0/src/exist_shell/completions.py +221 -0
  73. exist_shell-0.1.0/src/exist_shell/config.py +233 -0
  74. exist_shell-0.1.0/src/exist_shell/exceptions.py +68 -0
  75. exist_shell-0.1.0/src/exist_shell/main.py +64 -0
  76. exist_shell-0.1.0/src/exist_shell/models.py +61 -0
  77. exist_shell-0.1.0/src/exist_shell/utils.py +179 -0
  78. exist_shell-0.1.0/src/exist_shell/xquery.py +267 -0
  79. exist_shell-0.1.0/tests/__init__.py +0 -0
  80. exist_shell-0.1.0/tests/conftest.py +56 -0
  81. exist_shell-0.1.0/tests/test_cache.py +181 -0
  82. exist_shell-0.1.0/tests/test_client.py +941 -0
  83. exist_shell-0.1.0/tests/test_commands_cat.py +81 -0
  84. exist_shell-0.1.0/tests/test_commands_chmod.py +321 -0
  85. exist_shell-0.1.0/tests/test_commands_chown.py +209 -0
  86. exist_shell-0.1.0/tests/test_commands_collection.py +266 -0
  87. exist_shell-0.1.0/tests/test_commands_cp.py +150 -0
  88. exist_shell-0.1.0/tests/test_commands_edit.py +252 -0
  89. exist_shell-0.1.0/tests/test_commands_exec.py +176 -0
  90. exist_shell-0.1.0/tests/test_commands_group.py +262 -0
  91. exist_shell-0.1.0/tests/test_commands_ls.py +146 -0
  92. exist_shell-0.1.0/tests/test_commands_mkdir.py +60 -0
  93. exist_shell-0.1.0/tests/test_commands_mv.py +241 -0
  94. exist_shell-0.1.0/tests/test_commands_put.py +144 -0
  95. exist_shell-0.1.0/tests/test_commands_rm.py +66 -0
  96. exist_shell-0.1.0/tests/test_commands_server.py +269 -0
  97. exist_shell-0.1.0/tests/test_commands_sync.py +636 -0
  98. exist_shell-0.1.0/tests/test_commands_user.py +512 -0
  99. exist_shell-0.1.0/tests/test_completions.py +462 -0
  100. exist_shell-0.1.0/tests/test_config.py +225 -0
  101. exist_shell-0.1.0/tests/test_utils.py +141 -0
  102. exist_shell-0.1.0/tests/test_xquery.py +249 -0
  103. exist_shell-0.1.0/uv.lock +638 -0
@@ -0,0 +1,5 @@
1
+ paths-ignore:
2
+ - "tests/**"
3
+ - "tests"
4
+ - "scripts/e2e/**"
5
+ - "scripts/e2e"
@@ -0,0 +1,17 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "github-actions"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "weekly"
7
+ open-pull-requests-limit: 5
8
+ cooldown:
9
+ default-days: 14
10
+
11
+ - package-ecosystem: "uv"
12
+ directory: "/"
13
+ schedule:
14
+ interval: "weekly"
15
+ open-pull-requests-limit: 5
16
+ cooldown:
17
+ default-days: 14
@@ -0,0 +1,30 @@
1
+ name: docs
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ paths:
7
+ - "docs/**"
8
+ - "mkdocs.yml"
9
+ - ".github/workflows/docs.yml"
10
+ workflow_dispatch:
11
+
12
+ permissions:
13
+ contents: write
14
+
15
+ jobs:
16
+ deploy:
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
20
+
21
+ - name: Set up Python
22
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
23
+ with:
24
+ python-version: "3.13"
25
+
26
+ - name: Install MkDocs
27
+ run: pip install mkdocs-material
28
+
29
+ - name: Deploy to GitHub Pages
30
+ run: mkdocs gh-deploy --force
@@ -0,0 +1,104 @@
1
+ name: e2e
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ permissions:
9
+ pull-requests: write
10
+
11
+ jobs:
12
+ matrix:
13
+ runs-on: ubuntu-latest
14
+ outputs:
15
+ flags: ${{ steps.list.outputs.flags }}
16
+ steps:
17
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
18
+ - name: Build image matrix from --list-images
19
+ id: list
20
+ run: |
21
+ flags=$(bash scripts/e2e.sh --list-images \
22
+ | awk '/^\s+--/{print $1}' \
23
+ | jq -R . | jq -sc .)
24
+ echo "flags=${flags}" >> "$GITHUB_OUTPUT"
25
+
26
+ e2e:
27
+ needs: matrix
28
+ runs-on: ubuntu-latest
29
+ strategy:
30
+ matrix:
31
+ flag: ${{ fromJson(needs.matrix.outputs.flags) }}
32
+ fail-fast: false
33
+ steps:
34
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
35
+ - run: sudo apt-get install -y libxml2-utils
36
+ - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
37
+ with:
38
+ enable-cache: true
39
+ - run: uv sync
40
+ - name: Run e2e tests (${{ matrix.flag }})
41
+ id: run
42
+ continue-on-error: true
43
+ run: bash scripts/e2e.sh ${{ matrix.flag }} 2>&1 | tee e2e.log
44
+ - name: Save result artifact
45
+ if: always()
46
+ run: |
47
+ flag="${{ matrix.flag }}"
48
+ slug="${flag#--}"
49
+ image=$(grep "^Using image:" e2e.log | awk '{print $3}' || echo "$flag")
50
+ summary=$(grep -E "[0-9]+ passed, [0-9]+ failed" e2e.log | tail -1 | xargs || echo "no summary")
51
+ if grep -qF "FAILURES DETECTED" e2e.log 2>/dev/null; then
52
+ outcome="failure"
53
+ else
54
+ outcome="success"
55
+ fi
56
+ printf '%s\n%s\n%s\n' "${outcome}" "${image}" "${summary}" \
57
+ > "result-${slug}.txt"
58
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
59
+ if: always()
60
+ with:
61
+ name: e2e-result-${{ matrix.flag }}
62
+ path: result-*.txt
63
+
64
+ post-comment:
65
+ needs: [e2e]
66
+ runs-on: ubuntu-latest
67
+ if: always() && github.event_name == 'pull_request'
68
+ steps:
69
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
70
+ with:
71
+ pattern: e2e-result-*
72
+ merge-multiple: true
73
+ - name: Build PR comment
74
+ run: |
75
+ {
76
+ echo "## 🐋 e2e Tests"
77
+ echo ""
78
+ echo "| Image | Result | Tests |"
79
+ echo "|-------|--------|-------|"
80
+ for f in result-*.txt; do
81
+ [[ -f "$f" ]] || continue
82
+ mapfile -t lines < "$f"
83
+ status="${lines[0]}"
84
+ image="${lines[1]}"
85
+ summary="${lines[2]}"
86
+ [[ "$status" == "success" ]] && icon="✅" || icon="❌"
87
+ echo "| \`${image}\` | ${icon} | ${summary} |"
88
+ done
89
+ } > comment.md
90
+ cat comment.md
91
+ - uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v2
92
+ with:
93
+ header: e2e
94
+ path: comment.md
95
+ - name: Fail if any image had test failures
96
+ run: |
97
+ for f in result-*.txt; do
98
+ [[ -f "$f" ]] || continue
99
+ read -r status _ < "$f"
100
+ if [[ "$status" != "success" ]]; then
101
+ echo "::error::E2E failures detected — see the comment above for details."
102
+ exit 1
103
+ fi
104
+ done
@@ -0,0 +1,34 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ permissions:
9
+ contents: read
10
+ id-token: write
11
+
12
+ jobs:
13
+ release:
14
+ runs-on: ubuntu-latest
15
+ if: github.actor == 'ambs'
16
+ environment: release
17
+ steps:
18
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
19
+
20
+ - name: Verify signed tag
21
+ run: |
22
+ gpg --import <<< "$GPG_PUBLIC_KEY"
23
+ git verify-tag "${{ github.ref_name }}"
24
+ env:
25
+ GPG_PUBLIC_KEY: ${{ secrets.GPG_PUBLIC_KEY }}
26
+
27
+ - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
28
+ with:
29
+ enable-cache: true
30
+
31
+ - run: uv build
32
+
33
+ - name: Publish to PyPI
34
+ uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
@@ -0,0 +1,47 @@
1
+ name: Ruff
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ permissions:
9
+ pull-requests: write
10
+
11
+ jobs:
12
+ ruff:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
16
+ - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
17
+ with:
18
+ enable-cache: true
19
+ - run: uv sync
20
+ - name: Run Ruff
21
+ id: ruff
22
+ continue-on-error: true
23
+ run: uv run ruff check src/ 2>&1 | tee ruff.log
24
+ - name: Build PR comment
25
+ if: github.event_name == 'pull_request'
26
+ run: |
27
+ status="${{ steps.ruff.outcome }}"
28
+ if [[ "$status" == "success" ]]; then
29
+ echo "## 🔍 Ruff — ✅ No issues" > comment.md
30
+ else
31
+ count=$(grep -cE "\.py:[0-9]" ruff.log 2>/dev/null || echo 0)
32
+ {
33
+ echo "## 🔍 Ruff — ❌ ${count} violation(s)"
34
+ echo ""
35
+ echo '```'
36
+ cat ruff.log
37
+ echo '```'
38
+ } > comment.md
39
+ fi
40
+ - uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v2
41
+ if: github.event_name == 'pull_request'
42
+ with:
43
+ header: ruff
44
+ path: comment.md
45
+ - name: Propagate failure
46
+ if: steps.ruff.outcome == 'failure'
47
+ run: exit 1
@@ -0,0 +1,49 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ permissions:
9
+ pull-requests: write
10
+
11
+ jobs:
12
+ test:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
16
+ - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
17
+ with:
18
+ enable-cache: true
19
+ - run: uv sync
20
+ - name: Run tests
21
+ id: pytest
22
+ continue-on-error: true
23
+ run: uv run pytest --cov=exist_shell --cov-report=xml --cov-report=term 2>&1 | tee pytest.log
24
+ - name: Build PR comment
25
+ if: github.event_name == 'pull_request'
26
+ run: |
27
+ status="${{ steps.pytest.outcome }}"
28
+ [[ "$status" == "success" ]] && icon="✅ Passed" || icon="❌ Failed"
29
+ test_line=$(grep -oE "[0-9]+ (passed|failed|error(s)?)[^,=]*" pytest.log | tr '\n' ', ' | sed 's/, $//' || echo "—")
30
+ coverage=$(grep "^TOTAL" pytest.log | awk '{print $NF}' || echo "—")
31
+ {
32
+ echo "## 🧪 Tests — ${icon}"
33
+ echo ""
34
+ echo "**${test_line}** "
35
+ echo "**Coverage: ${coverage}**"
36
+ } > comment.md
37
+ - uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v2
38
+ if: github.event_name == 'pull_request'
39
+ with:
40
+ header: tests
41
+ path: comment.md
42
+ - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
43
+ with:
44
+ token: ${{ secrets.CODECOV_TOKEN }}
45
+ files: coverage.xml
46
+ fail_ci_if_error: false
47
+ - name: Propagate failure
48
+ if: steps.pytest.outcome == 'failure'
49
+ run: exit 1
@@ -0,0 +1,47 @@
1
+ name: ty
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ permissions:
9
+ pull-requests: write
10
+
11
+ jobs:
12
+ typecheck:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
16
+ - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
17
+ with:
18
+ enable-cache: true
19
+ - run: uv sync
20
+ - name: Run ty
21
+ id: ty
22
+ continue-on-error: true
23
+ run: uv run ty check src/ 2>&1 | tee ty.log
24
+ - name: Build PR comment
25
+ if: github.event_name == 'pull_request'
26
+ run: |
27
+ status="${{ steps.ty.outcome }}"
28
+ if [[ "$status" == "success" ]]; then
29
+ echo "## 🔎 ty — ✅ No errors" > comment.md
30
+ else
31
+ count=$(grep -cE "^error\[" ty.log 2>/dev/null || echo 0)
32
+ {
33
+ echo "## 🔎 ty — ❌ ${count} error(s)"
34
+ echo ""
35
+ echo '```'
36
+ cat ty.log
37
+ echo '```'
38
+ } > comment.md
39
+ fi
40
+ - uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v2
41
+ if: github.event_name == 'pull_request'
42
+ with:
43
+ header: ty
44
+ path: comment.md
45
+ - name: Propagate failure
46
+ if: steps.ty.outcome == 'failure'
47
+ run: exit 1
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ *.egg-info/
5
+ dist/
6
+ .pytest_cache/
7
+ .ruff_cache/
8
+ .ty/
9
+ .coverage
10
+ coverage.xml
@@ -0,0 +1,45 @@
1
+ # Changelog
2
+
3
+ ## unreleased
4
+
5
+ ## 0.1.0 - 2026-06-23
6
+
7
+ ### Commands
8
+
9
+ | Command | Description |
10
+ |---------|-------------|
11
+ | `server add / ls / rm` | Register, list, and remove eXist-db server configurations; `rm` cascade-removes associated collections |
12
+ | `server rename <old> <new>` | Rename a server nick; all registered collections are updated automatically |
13
+ | `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) |
14
+ | `ls [path]` | List documents and sub-collections at a remote path; `--sort name\|time`, `--reverse`, `--names-only` |
15
+ | `cat <path>` | Print a remote document to stdout; `--raw` skips binary detection |
16
+ | `put <path>` | Upload a document from stdin or a local file; auto-detects MIME type |
17
+ | `cp <src> <dst>` | Copy documents local↔remote or remote↔remote |
18
+ | `rm <path...>` | Delete one or more remote documents |
19
+ | `mkdir <path>` | Create a remote collection (idempotent) |
20
+ | `edit <path>` | Download, open in `$VISUAL`/`$EDITOR`, re-upload if changed |
21
+ | `sync push/pull <dir> <path>` | Sync a local directory tree with a remote collection; supports `--delete`, `--dry-run`, `--force` |
22
+ | `mv <src> <dst>` | Move or rename a document or collection; trailing `/` on dest moves into that collection; cross-server moves not supported |
23
+ | `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` |
24
+ | `user ls` | List user accounts and their groups; accepts `@server` positional |
25
+ | `user add <user[@server]>` | Create a user account (prompts for password) |
26
+ | `user rm <user[@server]>` | Remove a user account |
27
+ | `user info <user[@server]>` | Show user account details |
28
+ | `user passwd <user[@server]>` | Change a user's password; `--stdin` for scripting |
29
+ | `group ls` | List groups and their members; accepts `@server` positional |
30
+ | `group add <group[@server]>` | Create a group |
31
+ | `group rm <group[@server]>` | Remove a group |
32
+ | `chown <spec> <path>` | Change owner and/or group of a document or collection; `--recursive / -R` for collections; spec forms: `owner`, `:group`, `owner:group` |
33
+ | `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 |
34
+
35
+ ### Infrastructure
36
+
37
+ - Shell completion for collection/document paths (with TTL cache)
38
+ - `<name>@<server>` syntax for ad-hoc server targeting in `collection add`, `collection new`, `user *`, and `group *`
39
+ - `--server` option on all commands completes registered server nicks
40
+ - Shell completion for `<name>@<server>` and `@<server>` argument forms
41
+ - Path traversal validation and URL encoding
42
+ - Google-style docstrings enforced via ruff
43
+ - CI: pytest + coverage (Codecov), ruff, ty, e2e test suite against live eXist-db in Docker
44
+ - CI: sticky PR comments for tests (counts + coverage), ruff, ty, and e2e workflows
45
+ - CI: e2e workflow with dynamic image matrix from `--list-images`; `fail-fast: false`
@@ -0,0 +1,108 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project
6
+
7
+ `exsh` — a command-line tool to interact with an eXist-db server via its REST API. Subcommand structure (like git). Designed to work with shell pipes for scripting.
8
+
9
+ ## Dev setup
10
+
11
+ Dependencies are managed with [uv](https://docs.astral.sh/uv/).
12
+
13
+ ```bash
14
+ uv sync # install all deps including dev group
15
+ exsh --help
16
+ exsh --version
17
+ ```
18
+
19
+ ## Commands
20
+
21
+ ```bash
22
+ make checks # run ruff, ty and tests
23
+ make test # run tests
24
+ make ruff # lint with ruff
25
+ make ty # type-check with ty
26
+ ```
27
+
28
+ ## Branching and pull requests
29
+
30
+ All changes must go through a pull request — never commit directly to `main`.
31
+
32
+ 1. Create a feature branch: `git checkout -b feature/<short-description>`
33
+ 2. Make changes, commit with a signed commit (`git commit -S`)
34
+ 3. Before opening a PR, always fetch and rebase onto the latest `main`:
35
+ ```bash
36
+ git fetch origin main
37
+ git rebase origin/main
38
+ ```
39
+ 4. Push the branch and open a PR via `gh pr create`
40
+ 5. Do not merge without the PR being reviewed and all CI checks passing
41
+
42
+ ### Pull request descriptions
43
+
44
+ - Never include unticked checkboxes in a PR description.
45
+ - Run all relevant tests before opening the PR (or ask the user to run them if that is not possible).
46
+ - Only create the PR once everything passes — with all checklist items already ticked.
47
+
48
+ ## File discipline
49
+
50
+ Never stage or commit files that were not explicitly created as part of the current task or explicitly requested by the user. This includes cache directories, build artifacts, lock files, and any other files that appear as a side effect of running tools.
51
+
52
+ ## Linting
53
+
54
+ Never run `ruff --fix` across the whole project. Only apply it to a specific file, and only when a manual edit has already failed to resolve the issue.
55
+
56
+ ## Docstrings
57
+
58
+ Every public module, class, and method must have a docstring. Use Google style:
59
+
60
+ ```python
61
+ def my_method(self, path: str) -> list[str]:
62
+ """Short one-line summary.
63
+
64
+ Args:
65
+ path: The collection path under /db/.
66
+
67
+ Returns:
68
+ List of item names found at the path.
69
+
70
+ Raises:
71
+ ExistNotFoundError: If the path does not exist.
72
+ ExistAuthError: If authentication fails.
73
+ """
74
+ ```
75
+
76
+ - `Args` section required whenever the method has parameters beyond `self`.
77
+ - `Returns` section required whenever the return type is not `None`.
78
+ - `Raises` section required whenever the method raises documented exceptions.
79
+ - Ruff enforces this via `select = ["D"]` with `convention = "google"`.
80
+
81
+ ## Code conventions
82
+
83
+ - Python 3.11+. Never import from `typing` when a builtin works (`list`, `dict`, `str | None`, etc.).
84
+ - Every function parameter and return type must be explicitly annotated.
85
+ - Use direct Typer option assignment (`x: bool = typer.Option(...)`) over `Annotated` unless reuse across commands justifies it.
86
+ - `ExistClient` is the single HTTP facade — all REST calls go through it. Commands receive a client via `typer.Context.obj`.
87
+ - Shell completion for collection/document paths lives in `completions.py`.
88
+
89
+ ## Testing conventions
90
+
91
+ - Always use `patch.object(module, 'attr')` instead of the string form `patch("module.path.attr")`. Only fall back to string-based `patch()` when `patch.object` is genuinely not applicable (e.g. patching a name in `builtins`).
92
+
93
+ ## Architecture
94
+
95
+ ```
96
+ src/exist_shell/
97
+ main.py # Typer app, global callback, subcommand registration
98
+ client/ # ExistClient — layered mixin package over the eXist REST API
99
+ # _base.py, _queries.py, _collections.py, _documents.py, _users.py
100
+ completions.py # shell completion (hits server at tab time)
101
+ cache.py # TTL cache for completions
102
+ config.py # server/collection config persistence
103
+ exceptions.py # custom exceptions
104
+ models.py # Pydantic API response models
105
+ utils.py # shared utilities
106
+ xquery.py # XQuery preprocessing and validation
107
+ commands/ # one module per subcommand: cat, collection, cp, edit, exec, ls, mkdir, put, rm, server, sync
108
+ ```
@@ -0,0 +1,15 @@
1
+ .PHONY: help ruff ty test checks
2
+
3
+ help: ## Show available targets
4
+ @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " %-8s %s\n", $$1, $$2}'
5
+
6
+ checks: ruff ty test ## Run ruff, ty and tests
7
+
8
+ test: ## Run tests
9
+ uv run pytest
10
+
11
+ ruff: ## Lint with ruff
12
+ uv run ruff check src/
13
+
14
+ ty: ## Type-check with ty
15
+ uv run ty check src/
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: exist-shell
3
+ Version: 0.1.0
4
+ Summary: Command-line tool to interact with eXist-db via REST
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: httpx>=0.27
7
+ Requires-Dist: platformdirs>=4.10.0; sys_platform == 'win32'
8
+ Requires-Dist: pydantic>=2.13.4
9
+ Requires-Dist: tomlkit>=0.15.0
10
+ Requires-Dist: typer>=0.26.7