tidemark 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.
- tidemark-0.1.0/.github/workflows/ci.yml +67 -0
- tidemark-0.1.0/.github/workflows/release.yml +393 -0
- tidemark-0.1.0/.gitignore +6 -0
- tidemark-0.1.0/AGENTS.md +94 -0
- tidemark-0.1.0/CHANGELOG.md +3 -0
- tidemark-0.1.0/CLAUDE.md +73 -0
- tidemark-0.1.0/Cargo.lock +968 -0
- tidemark-0.1.0/Cargo.toml +40 -0
- tidemark-0.1.0/LICENSE +21 -0
- tidemark-0.1.0/Makefile +38 -0
- tidemark-0.1.0/PKG-INFO +142 -0
- tidemark-0.1.0/README.md +119 -0
- tidemark-0.1.0/prek.toml +36 -0
- tidemark-0.1.0/pyproject.toml +36 -0
- tidemark-0.1.0/src/builder.rs +182 -0
- tidemark-0.1.0/src/cli.rs +528 -0
- tidemark-0.1.0/src/diff.rs +316 -0
- tidemark-0.1.0/src/error.rs +73 -0
- tidemark-0.1.0/src/hash.rs +43 -0
- tidemark-0.1.0/src/lib.rs +11 -0
- tidemark-0.1.0/src/main.rs +11 -0
- tidemark-0.1.0/src/manifest.rs +148 -0
- tidemark-0.1.0/src/output.rs +117 -0
- tidemark-0.1.0/src/refs.rs +112 -0
- tidemark-0.1.0/src/schema.rs +204 -0
- tidemark-0.1.0/src/store.rs +226 -0
- tidemark-0.1.0/src/walk.rs +167 -0
- tidemark-0.1.0/tests/cli.rs +331 -0
- tidemark-0.1.0/tidemark_py/__init__.py +10 -0
- tidemark-0.1.0/tidemark_py/__main__.py +52 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
lint:
|
|
15
|
+
name: Lint
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
21
|
+
with:
|
|
22
|
+
components: rustfmt, clippy
|
|
23
|
+
|
|
24
|
+
- name: Run linting
|
|
25
|
+
run: make lint
|
|
26
|
+
|
|
27
|
+
test:
|
|
28
|
+
name: Test
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/checkout@v4
|
|
32
|
+
|
|
33
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
34
|
+
|
|
35
|
+
- uses: taiki-e/install-action@nextest
|
|
36
|
+
|
|
37
|
+
- name: Run tests
|
|
38
|
+
run: make test
|
|
39
|
+
|
|
40
|
+
coverage:
|
|
41
|
+
name: Coverage
|
|
42
|
+
runs-on: ubuntu-latest
|
|
43
|
+
steps:
|
|
44
|
+
- uses: actions/checkout@v4
|
|
45
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
46
|
+
- uses: taiki-e/install-action@cargo-tarpaulin
|
|
47
|
+
- name: Generate coverage
|
|
48
|
+
run: cargo tarpaulin --out xml
|
|
49
|
+
- name: Upload to Codecov
|
|
50
|
+
uses: codecov/codecov-action@v5
|
|
51
|
+
with:
|
|
52
|
+
files: cobertura.xml
|
|
53
|
+
token: ${{ secrets.CODECOV_TOKEN }}
|
|
54
|
+
|
|
55
|
+
all-checks-passed:
|
|
56
|
+
name: All checks passed
|
|
57
|
+
runs-on: ubuntu-latest
|
|
58
|
+
needs: [lint, test]
|
|
59
|
+
if: always()
|
|
60
|
+
steps:
|
|
61
|
+
- name: Verify all checks passed
|
|
62
|
+
run: |
|
|
63
|
+
if [ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" == "true" ]; then
|
|
64
|
+
echo "Some checks failed or were cancelled"
|
|
65
|
+
exit 1
|
|
66
|
+
fi
|
|
67
|
+
echo "All checks passed"
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v[0-9]*.[0-9]*.[0-9]*'
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
inputs:
|
|
9
|
+
dry_run:
|
|
10
|
+
description: 'Dry run mode (skip actual publishing)'
|
|
11
|
+
required: false
|
|
12
|
+
default: true
|
|
13
|
+
type: boolean
|
|
14
|
+
skip_crates_io:
|
|
15
|
+
description: 'Skip publishing to crates.io (if already published)'
|
|
16
|
+
required: false
|
|
17
|
+
default: false
|
|
18
|
+
type: boolean
|
|
19
|
+
skip_pypi:
|
|
20
|
+
description: 'Skip publishing to PyPI (if already published)'
|
|
21
|
+
required: false
|
|
22
|
+
default: false
|
|
23
|
+
type: boolean
|
|
24
|
+
|
|
25
|
+
permissions:
|
|
26
|
+
contents: write
|
|
27
|
+
|
|
28
|
+
env:
|
|
29
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
30
|
+
|
|
31
|
+
jobs:
|
|
32
|
+
test:
|
|
33
|
+
name: Run tests
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/checkout@v4
|
|
37
|
+
|
|
38
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
39
|
+
|
|
40
|
+
- uses: taiki-e/install-action@nextest
|
|
41
|
+
|
|
42
|
+
- name: Verify Cargo.lock is up to date
|
|
43
|
+
run: cargo check --locked
|
|
44
|
+
|
|
45
|
+
- name: Run tests
|
|
46
|
+
run: make test
|
|
47
|
+
|
|
48
|
+
build:
|
|
49
|
+
name: Build ${{ matrix.target }}
|
|
50
|
+
needs: test
|
|
51
|
+
timeout-minutes: 30
|
|
52
|
+
# Guard: self-hosted runners only execute on trusted events (tag push,
|
|
53
|
+
# workflow_dispatch). Fork PRs must never dispatch jobs to self-hosted.
|
|
54
|
+
if: github.event_name != 'pull_request'
|
|
55
|
+
strategy:
|
|
56
|
+
matrix:
|
|
57
|
+
include:
|
|
58
|
+
- os: ubuntu-latest
|
|
59
|
+
target: x86_64-unknown-linux-gnu
|
|
60
|
+
- os: [self-hosted, Linux, ARM64]
|
|
61
|
+
target: aarch64-unknown-linux-gnu
|
|
62
|
+
- os: windows-latest
|
|
63
|
+
target: x86_64-pc-windows-msvc
|
|
64
|
+
- os: [self-hosted, macOS, ARM64]
|
|
65
|
+
target: x86_64-apple-darwin
|
|
66
|
+
- os: [self-hosted, macOS, ARM64]
|
|
67
|
+
target: aarch64-apple-darwin
|
|
68
|
+
runs-on: ${{ matrix.os }}
|
|
69
|
+
steps:
|
|
70
|
+
- uses: actions/checkout@v4
|
|
71
|
+
with:
|
|
72
|
+
# Preserve target/ between runs on self-hosted runners for cargo
|
|
73
|
+
# incremental rebuild. GitHub-hosted runners start clean each job
|
|
74
|
+
# regardless of this setting, so this only affects self-hosted.
|
|
75
|
+
clean: false
|
|
76
|
+
|
|
77
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
78
|
+
with:
|
|
79
|
+
targets: ${{ matrix.target }}
|
|
80
|
+
|
|
81
|
+
- name: Prepare cargo cache dirs (arm64 linux)
|
|
82
|
+
if: matrix.target == 'aarch64-unknown-linux-gnu'
|
|
83
|
+
run: mkdir -p "${RUNNER_TOOL_CACHE}/cargo-cache/registry" "${RUNNER_TOOL_CACHE}/cargo-cache/git"
|
|
84
|
+
|
|
85
|
+
- name: Clean stale dist
|
|
86
|
+
# Keep dist/ fresh each run so previous builds cannot leak artifacts
|
|
87
|
+
# into the upload. Self-hosted runners do not clean automatically.
|
|
88
|
+
# On the arm64 linux runner, maturin-action runs as root inside the
|
|
89
|
+
# manylinux container and leaves root-owned files in dist/; clean
|
|
90
|
+
# via docker so the unprivileged runner user can proceed.
|
|
91
|
+
shell: bash
|
|
92
|
+
run: |
|
|
93
|
+
if [[ "${{ matrix.target }}" == "aarch64-unknown-linux-gnu" ]]; then
|
|
94
|
+
docker run --rm -v "${GITHUB_WORKSPACE}:/io" alpine rm -rf /io/dist /io/target/wheels
|
|
95
|
+
else
|
|
96
|
+
rm -rf dist target/wheels
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
- name: Install cross-compilation tools
|
|
100
|
+
# Only needed on GitHub-hosted linux where the arm64 toolchain must
|
|
101
|
+
# be cross-installed. The self-hosted arm64 runner is native.
|
|
102
|
+
if: matrix.target == 'aarch64-unknown-linux-gnu' && runner.arch != 'ARM64'
|
|
103
|
+
run: sudo apt-get update && sudo apt-get install -y gcc-aarch64-linux-gnu
|
|
104
|
+
|
|
105
|
+
- name: Install uv
|
|
106
|
+
if: matrix.target != 'aarch64-unknown-linux-gnu'
|
|
107
|
+
uses: astral-sh/setup-uv@v6
|
|
108
|
+
|
|
109
|
+
- name: Install maturin and zig
|
|
110
|
+
# maturin-action provides its own maturin inside the manylinux
|
|
111
|
+
# container for the arm64 linux build, so skip the host install
|
|
112
|
+
# on the self-hosted runner.
|
|
113
|
+
if: matrix.target != 'aarch64-unknown-linux-gnu'
|
|
114
|
+
shell: bash
|
|
115
|
+
run: |
|
|
116
|
+
uv venv "${RUNNER_TEMP}/build-venv"
|
|
117
|
+
VENV_BIN="${RUNNER_TEMP}/build-venv/bin"
|
|
118
|
+
[ -d "$VENV_BIN" ] || VENV_BIN="${RUNNER_TEMP}/build-venv/Scripts"
|
|
119
|
+
uv pip install --python "$VENV_BIN/python" maturin ziglang
|
|
120
|
+
echo "$VENV_BIN" >> "$GITHUB_PATH"
|
|
121
|
+
|
|
122
|
+
- name: Build wheel (arm64 linux via maturin-action)
|
|
123
|
+
if: matrix.target == 'aarch64-unknown-linux-gnu'
|
|
124
|
+
uses: PyO3/maturin-action@v1
|
|
125
|
+
with:
|
|
126
|
+
target: aarch64-unknown-linux-gnu
|
|
127
|
+
args: --release --out dist
|
|
128
|
+
manylinux: manylinux_2_28
|
|
129
|
+
# Mount persistent cargo cache into the manylinux container so
|
|
130
|
+
# dependency downloads survive between jobs on the self-hosted
|
|
131
|
+
# runner.
|
|
132
|
+
docker-options: -v ${{ runner.tool_cache }}/cargo-cache/registry:/root/.cargo/registry -v ${{ runner.tool_cache }}/cargo-cache/git:/root/.cargo/git
|
|
133
|
+
|
|
134
|
+
- name: Build wheel
|
|
135
|
+
if: matrix.target != 'aarch64-unknown-linux-gnu'
|
|
136
|
+
shell: bash
|
|
137
|
+
run: |
|
|
138
|
+
case "${{ matrix.target }}" in
|
|
139
|
+
*-gnu)
|
|
140
|
+
maturin build --release --target ${{ matrix.target }} --compatibility manylinux_2_28 --zig --out dist
|
|
141
|
+
;;
|
|
142
|
+
*)
|
|
143
|
+
maturin build --release --target ${{ matrix.target }} --out dist
|
|
144
|
+
;;
|
|
145
|
+
esac
|
|
146
|
+
|
|
147
|
+
- name: Build binary (arm64 linux via cargo-zigbuild)
|
|
148
|
+
# On the Pi runner build natively with cargo-zigbuild targeting
|
|
149
|
+
# glibc 2.28 for wide distro compatibility. Use a separate target
|
|
150
|
+
# directory so host cargo does not conflict with root-owned files
|
|
151
|
+
# left behind by the maturin-action container build in target/.
|
|
152
|
+
if: matrix.target == 'aarch64-unknown-linux-gnu'
|
|
153
|
+
env:
|
|
154
|
+
CARGO_TARGET_DIR: target-host
|
|
155
|
+
run: cargo zigbuild --release --target aarch64-unknown-linux-gnu.2.28
|
|
156
|
+
|
|
157
|
+
- name: Build binary
|
|
158
|
+
if: matrix.target != 'aarch64-unknown-linux-gnu'
|
|
159
|
+
env:
|
|
160
|
+
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
|
|
161
|
+
run: cargo build --release --target ${{ matrix.target }}
|
|
162
|
+
|
|
163
|
+
- name: Verify binary
|
|
164
|
+
if: ${{ !contains(matrix.target, 'aarch64-unknown-linux') }}
|
|
165
|
+
shell: bash
|
|
166
|
+
run: |
|
|
167
|
+
if [[ "${{ runner.os }}" == "Windows" ]]; then
|
|
168
|
+
./target/${{ matrix.target }}/release/tidemark.exe --version
|
|
169
|
+
else
|
|
170
|
+
./target/${{ matrix.target }}/release/tidemark --version
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
- name: Create release archive
|
|
174
|
+
shell: bash
|
|
175
|
+
run: |
|
|
176
|
+
VERSION=${GITHUB_REF#refs/tags/}
|
|
177
|
+
ARCHIVE_NAME="tidemark-${VERSION}-${{ matrix.target }}"
|
|
178
|
+
|
|
179
|
+
mkdir -p release-package
|
|
180
|
+
|
|
181
|
+
if [[ "${{ runner.os }}" == "Windows" ]]; then
|
|
182
|
+
cp "target/${{ matrix.target }}/release/tidemark.exe" release-package/tidemark.exe
|
|
183
|
+
cd release-package
|
|
184
|
+
powershell -command "Compress-Archive -Path tidemark.exe -DestinationPath ../${ARCHIVE_NAME}.zip"
|
|
185
|
+
cd ..
|
|
186
|
+
powershell -command "Get-FileHash -Path '${ARCHIVE_NAME}.zip' -Algorithm SHA256 | Select-Object -ExpandProperty Hash" > "${ARCHIVE_NAME}.zip.sha256"
|
|
187
|
+
else
|
|
188
|
+
if [[ "${{ matrix.target }}" == "aarch64-unknown-linux-gnu" ]]; then
|
|
189
|
+
BIN_PATH="target-host/aarch64-unknown-linux-gnu/release/tidemark"
|
|
190
|
+
else
|
|
191
|
+
BIN_PATH="target/${{ matrix.target }}/release/tidemark"
|
|
192
|
+
fi
|
|
193
|
+
cp "$BIN_PATH" release-package/tidemark
|
|
194
|
+
tar -czf "${ARCHIVE_NAME}.tar.gz" -C release-package tidemark
|
|
195
|
+
|
|
196
|
+
if [[ "${{ runner.os }}" == "macOS" ]]; then
|
|
197
|
+
shasum -a 256 "${ARCHIVE_NAME}.tar.gz" > "${ARCHIVE_NAME}.tar.gz.sha256"
|
|
198
|
+
else
|
|
199
|
+
sha256sum "${ARCHIVE_NAME}.tar.gz" > "${ARCHIVE_NAME}.tar.gz.sha256"
|
|
200
|
+
fi
|
|
201
|
+
fi
|
|
202
|
+
|
|
203
|
+
rm -rf release-package
|
|
204
|
+
|
|
205
|
+
- name: Upload wheel
|
|
206
|
+
uses: actions/upload-artifact@v4
|
|
207
|
+
with:
|
|
208
|
+
path: dist/*.whl
|
|
209
|
+
name: wheel-${{ matrix.target }}
|
|
210
|
+
|
|
211
|
+
- name: Upload release archives
|
|
212
|
+
uses: actions/upload-artifact@v4
|
|
213
|
+
with:
|
|
214
|
+
path: |
|
|
215
|
+
tidemark-*-${{ matrix.target }}.tar.gz*
|
|
216
|
+
tidemark-*-${{ matrix.target }}.zip*
|
|
217
|
+
name: release-${{ matrix.target }}
|
|
218
|
+
|
|
219
|
+
sdist:
|
|
220
|
+
name: Build source distribution
|
|
221
|
+
runs-on: ubuntu-latest
|
|
222
|
+
needs: test
|
|
223
|
+
steps:
|
|
224
|
+
- uses: actions/checkout@v4
|
|
225
|
+
|
|
226
|
+
- uses: astral-sh/setup-uv@v6
|
|
227
|
+
|
|
228
|
+
- name: Install maturin
|
|
229
|
+
shell: bash
|
|
230
|
+
run: |
|
|
231
|
+
uv venv "${RUNNER_TEMP}/build-venv"
|
|
232
|
+
uv pip install --python "${RUNNER_TEMP}/build-venv/bin/python" maturin
|
|
233
|
+
echo "${RUNNER_TEMP}/build-venv/bin" >> "$GITHUB_PATH"
|
|
234
|
+
|
|
235
|
+
- name: Build sdist
|
|
236
|
+
run: maturin sdist
|
|
237
|
+
|
|
238
|
+
- uses: actions/upload-artifact@v4
|
|
239
|
+
with:
|
|
240
|
+
path: target/wheels/*.tar.gz
|
|
241
|
+
name: sdist
|
|
242
|
+
|
|
243
|
+
release:
|
|
244
|
+
runs-on: ubuntu-latest
|
|
245
|
+
needs: [build, sdist]
|
|
246
|
+
steps:
|
|
247
|
+
- uses: actions/checkout@v4
|
|
248
|
+
|
|
249
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
250
|
+
|
|
251
|
+
- name: Install uv
|
|
252
|
+
uses: astral-sh/setup-uv@v5
|
|
253
|
+
with:
|
|
254
|
+
enable-cache: false
|
|
255
|
+
|
|
256
|
+
- name: Publish to crates.io
|
|
257
|
+
if: ${{ inputs.dry_run != true && inputs.skip_crates_io != true }}
|
|
258
|
+
env:
|
|
259
|
+
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
|
260
|
+
run: cargo publish --locked
|
|
261
|
+
|
|
262
|
+
- name: Test crates.io publish (dry run)
|
|
263
|
+
if: ${{ inputs.dry_run == true && inputs.skip_crates_io != true }}
|
|
264
|
+
run: |
|
|
265
|
+
echo "DRY RUN: Would publish to crates.io"
|
|
266
|
+
cargo publish --dry-run --locked
|
|
267
|
+
|
|
268
|
+
- name: Skip crates.io publishing
|
|
269
|
+
if: ${{ inputs.skip_crates_io == true }}
|
|
270
|
+
run: echo "Skipping crates.io publishing as requested"
|
|
271
|
+
|
|
272
|
+
- name: Download all artifacts
|
|
273
|
+
uses: actions/download-artifact@v4
|
|
274
|
+
with:
|
|
275
|
+
path: artifacts
|
|
276
|
+
|
|
277
|
+
- name: Publish to PyPI
|
|
278
|
+
if: ${{ inputs.dry_run != true && inputs.skip_pypi != true }}
|
|
279
|
+
env:
|
|
280
|
+
TWINE_USERNAME: __token__
|
|
281
|
+
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
|
|
282
|
+
run: |
|
|
283
|
+
uv tool run twine upload artifacts/wheel-*/*.whl artifacts/sdist/*.tar.gz
|
|
284
|
+
|
|
285
|
+
- name: Test PyPI upload (dry run)
|
|
286
|
+
if: ${{ inputs.dry_run == true && inputs.skip_pypi != true }}
|
|
287
|
+
run: |
|
|
288
|
+
echo "DRY RUN: Would upload to PyPI:"
|
|
289
|
+
find artifacts/wheel-* -name "*.whl" -type f | sort
|
|
290
|
+
find artifacts/sdist -name "*.tar.gz" -type f | sort
|
|
291
|
+
uv tool run twine check artifacts/wheel-*/*.whl artifacts/sdist/*.tar.gz
|
|
292
|
+
|
|
293
|
+
- name: Skip PyPI publishing
|
|
294
|
+
if: ${{ inputs.skip_pypi == true }}
|
|
295
|
+
run: echo "Skipping PyPI publishing as requested"
|
|
296
|
+
|
|
297
|
+
- name: Create Release
|
|
298
|
+
if: ${{ inputs.dry_run != true }}
|
|
299
|
+
uses: softprops/action-gh-release@v2
|
|
300
|
+
with:
|
|
301
|
+
generate_release_notes: true
|
|
302
|
+
files: |
|
|
303
|
+
artifacts/release-*/tidemark-*.tar.gz
|
|
304
|
+
artifacts/release-*/tidemark-*.tar.gz.sha256
|
|
305
|
+
artifacts/release-*/tidemark-*.zip
|
|
306
|
+
artifacts/release-*/tidemark-*.zip.sha256
|
|
307
|
+
|
|
308
|
+
- name: Dry Run Summary
|
|
309
|
+
if: ${{ inputs.dry_run == true }}
|
|
310
|
+
run: |
|
|
311
|
+
echo "Dry run complete. Artifacts built but nothing published."
|
|
312
|
+
echo "Archives:"
|
|
313
|
+
find artifacts/release-* -type f | sort
|
|
314
|
+
|
|
315
|
+
update-homebrew:
|
|
316
|
+
needs: release
|
|
317
|
+
if: ${{ !inputs.dry_run }}
|
|
318
|
+
runs-on: ubuntu-latest
|
|
319
|
+
steps:
|
|
320
|
+
- uses: actions/download-artifact@v4
|
|
321
|
+
with:
|
|
322
|
+
path: /tmp/artifacts
|
|
323
|
+
|
|
324
|
+
- name: Compute SHA256 hashes
|
|
325
|
+
id: hashes
|
|
326
|
+
run: |
|
|
327
|
+
for target in x86_64-apple-darwin aarch64-apple-darwin x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu; do
|
|
328
|
+
sha=$(cat /tmp/artifacts/release-${target}/tidemark-${{ github.ref_name }}-${target}.tar.gz.sha256 | awk '{print $1}')
|
|
329
|
+
key=$(echo "${target}" | tr '-' '_')
|
|
330
|
+
echo "${key}=${sha}" >> "$GITHUB_OUTPUT"
|
|
331
|
+
done
|
|
332
|
+
|
|
333
|
+
- name: Update Homebrew formula
|
|
334
|
+
env:
|
|
335
|
+
GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
|
|
336
|
+
run: |
|
|
337
|
+
VERSION="${{ github.ref_name }}"
|
|
338
|
+
VERSION_NUM="${VERSION#v}"
|
|
339
|
+
|
|
340
|
+
cat > /tmp/tidemark.rb << 'FORMULA'
|
|
341
|
+
class Tidemark < Formula
|
|
342
|
+
desc "Snapshot a directory tree and diff what changed - no git required"
|
|
343
|
+
homepage "https://github.com/rvben/tidemark"
|
|
344
|
+
version "VERSION_NUM"
|
|
345
|
+
license "MIT"
|
|
346
|
+
|
|
347
|
+
on_macos do
|
|
348
|
+
if Hardware::CPU.arm?
|
|
349
|
+
url "https://github.com/rvben/tidemark/releases/download/VERSION/tidemark-VERSION-aarch64-apple-darwin.tar.gz"
|
|
350
|
+
sha256 "SHA_AARCH64_APPLE_DARWIN"
|
|
351
|
+
else
|
|
352
|
+
url "https://github.com/rvben/tidemark/releases/download/VERSION/tidemark-VERSION-x86_64-apple-darwin.tar.gz"
|
|
353
|
+
sha256 "SHA_X86_64_APPLE_DARWIN"
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
on_linux do
|
|
358
|
+
if Hardware::CPU.arm?
|
|
359
|
+
url "https://github.com/rvben/tidemark/releases/download/VERSION/tidemark-VERSION-aarch64-unknown-linux-gnu.tar.gz"
|
|
360
|
+
sha256 "SHA_AARCH64_UNKNOWN_LINUX_GNU"
|
|
361
|
+
else
|
|
362
|
+
url "https://github.com/rvben/tidemark/releases/download/VERSION/tidemark-VERSION-x86_64-unknown-linux-gnu.tar.gz"
|
|
363
|
+
sha256 "SHA_X86_64_UNKNOWN_LINUX_GNU"
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def install
|
|
368
|
+
bin.install "tidemark"
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
test do
|
|
372
|
+
system "#{bin}/tidemark", "--version"
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
FORMULA
|
|
376
|
+
|
|
377
|
+
sed -i "s/VERSION_NUM/${VERSION_NUM}/g" /tmp/tidemark.rb
|
|
378
|
+
sed -i "s/VERSION/${VERSION}/g" /tmp/tidemark.rb
|
|
379
|
+
sed -i "s/SHA_AARCH64_APPLE_DARWIN/${{ steps.hashes.outputs.aarch64_apple_darwin }}/g" /tmp/tidemark.rb
|
|
380
|
+
sed -i "s/SHA_X86_64_APPLE_DARWIN/${{ steps.hashes.outputs.x86_64_apple_darwin }}/g" /tmp/tidemark.rb
|
|
381
|
+
sed -i "s/SHA_AARCH64_UNKNOWN_LINUX_GNU/${{ steps.hashes.outputs.aarch64_unknown_linux_gnu }}/g" /tmp/tidemark.rb
|
|
382
|
+
sed -i "s/SHA_X86_64_UNKNOWN_LINUX_GNU/${{ steps.hashes.outputs.x86_64_unknown_linux_gnu }}/g" /tmp/tidemark.rb
|
|
383
|
+
|
|
384
|
+
# Clone tap repo, update formula, push
|
|
385
|
+
git clone https://x-access-token:${GH_TOKEN}@github.com/rvben/homebrew-tap.git /tmp/tap
|
|
386
|
+
mkdir -p /tmp/tap/Formula
|
|
387
|
+
cp /tmp/tidemark.rb /tmp/tap/Formula/tidemark.rb
|
|
388
|
+
cd /tmp/tap
|
|
389
|
+
git config user.name "github-actions[bot]"
|
|
390
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
391
|
+
git add Formula/tidemark.rb
|
|
392
|
+
git diff --cached --quiet || git commit -m "Update tidemark to ${VERSION}"
|
|
393
|
+
git push
|
tidemark-0.1.0/AGENTS.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# tidemark for agents
|
|
2
|
+
|
|
3
|
+
`tidemark` is a deterministic filesystem snapshot/diff tool designed to be driven
|
|
4
|
+
by AI agents. This document is the operational contract.
|
|
5
|
+
|
|
6
|
+
## Discover capabilities
|
|
7
|
+
|
|
8
|
+
Run `tidemark schema` first. It returns a clispec v0.1 document listing every
|
|
9
|
+
command, its arguments (name, type, required, default), output fields, error
|
|
10
|
+
kinds (each with a `retryable` flag), exit-code semantics, and which commands
|
|
11
|
+
mutate state (`mutating: true`). The schema is the source of truth; prefer it
|
|
12
|
+
over parsing help text.
|
|
13
|
+
|
|
14
|
+
## Output contract
|
|
15
|
+
|
|
16
|
+
- Output is **JSON by default when stdout is not a TTY**. You never need a flag,
|
|
17
|
+
but you may pass `--json` (or `--output json`) to be explicit.
|
|
18
|
+
- **stdout carries data only.** All diagnostics and the human summary line go to
|
|
19
|
+
stderr. Piping stdout to a JSON parser is always safe.
|
|
20
|
+
- `diff` returns a flat object:
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"changes": [ { "kind": "modified", "path": "src/a.rs",
|
|
25
|
+
"old_hash": "blake3:...", "new_hash": "blake3:...",
|
|
26
|
+
"size_delta": 12 } ],
|
|
27
|
+
"added": 0, "modified": 1, "deleted": 0, "renamed": 0,
|
|
28
|
+
"total": 1, "limit": null, "offset": 0
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- `list` uses the envelope `{"items": [...], "total": N, "limit": L, "offset": O}`.
|
|
33
|
+
- Bound large results with `--limit` / `--offset`, and trim fields with
|
|
34
|
+
`--fields path,kind`. These flags are global (accepted on any command).
|
|
35
|
+
- Running `tidemark` with no command lists stored snapshots.
|
|
36
|
+
|
|
37
|
+
## Recommended pattern
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
tidemark snap -o pre.tidemark # portable manifest (or: tidemark snap before)
|
|
41
|
+
<run the operation under test>
|
|
42
|
+
tidemark diff pre.tidemark @ --json # @ means "the current tree"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
A ref (the `A`/`B` of `diff`) is one of: a store label, a path to a manifest
|
|
46
|
+
file, or `@` for the live tree. `tidemark diff before` is shorthand for
|
|
47
|
+
`tidemark diff before @`. A bare ref resolves to a store label first, falling
|
|
48
|
+
back to a same-named file only if no such label exists. `tidemark init` creates
|
|
49
|
+
the `.tidemark/` store explicitly (idempotent).
|
|
50
|
+
|
|
51
|
+
## Determinism and idempotency
|
|
52
|
+
|
|
53
|
+
- `tree_digest` is a BLAKE3 Merkle root over the sorted entries. Two snapshots of
|
|
54
|
+
an unchanged tree are byte-for-byte identical in digest.
|
|
55
|
+
- `mtime` never affects change detection, so rebuilds and `touch` do not produce
|
|
56
|
+
spurious diffs.
|
|
57
|
+
- `tidemark snap LABEL` on an unchanged tree is a success no-op (exit 0).
|
|
58
|
+
Re-using a label for a *different* tree returns the `conflict` error kind
|
|
59
|
+
(exit 2) unless you pass `--force`.
|
|
60
|
+
- A file that vanishes between the directory walk and its read is skipped, so a
|
|
61
|
+
concurrent deletion mid-snapshot does not abort the whole snapshot.
|
|
62
|
+
|
|
63
|
+
## Exit codes
|
|
64
|
+
|
|
65
|
+
- Default: `0` success, `2` error.
|
|
66
|
+
- `diff --exit-code`: `0` no changes, `1` changes found, `2` error. Use this when
|
|
67
|
+
you only need a yes/no "did anything change" signal.
|
|
68
|
+
|
|
69
|
+
## Errors
|
|
70
|
+
|
|
71
|
+
Always to stderr, shape:
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{"error": {"kind": "not_found", "message": "...", "retryable": false}}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Kinds: `not_found`, `conflict`, `invalid_input`, `unsupported`, and `io`
|
|
78
|
+
(the only `retryable: true` kind - safe to retry transient filesystem errors).
|
|
79
|
+
Argument-parse failures are also reported as `invalid_input` JSON on stderr.
|
|
80
|
+
|
|
81
|
+
## Content diffs
|
|
82
|
+
|
|
83
|
+
`diff --content` emits a real unified line diff in each modified change's
|
|
84
|
+
`content_preview` field, reconstructed from text content stored in both
|
|
85
|
+
manifests. Because every snapshot stores inline text content (UTF-8 files under
|
|
86
|
+
256 KiB), this works even between two stored manifests, not just against the live
|
|
87
|
+
tree. Binary, oversized, or `--no-content` files report that the content was
|
|
88
|
+
unavailable instead.
|
|
89
|
+
|
|
90
|
+
## Safety
|
|
91
|
+
|
|
92
|
+
- Destructive `rm` refuses to run without `--yes` when stdin is not a TTY.
|
|
93
|
+
- tidemark never records its own `.tidemark/` store directory in a snapshot.
|
|
94
|
+
- Labels are validated against path traversal and control characters.
|
tidemark-0.1.0/CLAUDE.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Build & Test Commands
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
make check # lint + test (CI runs this)
|
|
9
|
+
make build # cargo build --release
|
|
10
|
+
make test # cargo nextest run + cargo test --doc
|
|
11
|
+
make lint # cargo fmt --check + cargo clippy -D warnings
|
|
12
|
+
make fmt # auto-format
|
|
13
|
+
make score # build release + clispec score ./target/release/tidemark
|
|
14
|
+
make ci # check + score
|
|
15
|
+
make install # cargo install --path .
|
|
16
|
+
cargo nextest run <name> # run a single test by name
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Architecture
|
|
20
|
+
|
|
21
|
+
tidemark is a Rust CLI that snapshots a directory tree into a deterministic
|
|
22
|
+
BLAKE3 manifest and diffs two snapshots (added/modified/deleted/renamed) with no
|
|
23
|
+
git required. Binary name: `tidemark`. Also published to PyPI (`uvx tidemark`)
|
|
24
|
+
and Homebrew (`brew install rvben/tap/tidemark`). Scores 100/100 against
|
|
25
|
+
[The CLI Spec](https://clispec.dev).
|
|
26
|
+
|
|
27
|
+
### Pure-core + thin-shell layout
|
|
28
|
+
|
|
29
|
+
- **Pure (no I/O, unit-tested directly):** `manifest` (`Manifest`/`Entry`, Merkle
|
|
30
|
+
`tree_digest`), `diff` (classification, rename detection, `unified_diff`).
|
|
31
|
+
- **I/O wrappers:** `walk` (directory traversal via the `ignore` crate,
|
|
32
|
+
`require_git(false)`), `hash` (`hash_bytes`), `builder` (`build_manifest` +
|
|
33
|
+
`SnapOptions`), `store` (`.tidemark/` labeled snapshot store), `refs` (resolve a
|
|
34
|
+
ref: store label | manifest file | `@` current tree).
|
|
35
|
+
- **Shell:** `output` (JSON/table, pagination, field selection), `schema` (the
|
|
36
|
+
clispec v0.1 document), `cli` (clap tree, dispatch, exit codes), `error`
|
|
37
|
+
(`TidemarkError` + clispec error kinds).
|
|
38
|
+
|
|
39
|
+
### Key patterns
|
|
40
|
+
|
|
41
|
+
- **clispec scorer probes the first non-mutating command** for structured-output
|
|
42
|
+
and stream checks, so `list` MUST stay ordered before `diff` in `schema.rs`
|
|
43
|
+
(`tidemark diff` errors with no snapshot; `list` always exits 0 with an
|
|
44
|
+
envelope). Reordering drops the score.
|
|
45
|
+
- **Schema matches `clispec.dev/schema/v0.1.json`:** top-level
|
|
46
|
+
`name`/`version`/`commands`/`errors`; per-command `mutating` boolean +
|
|
47
|
+
`output_fields`; `errors` is a top-level array of `{kind, retryable}`.
|
|
48
|
+
- **`mtime` is informational only** - excluded from `tree_digest` and change
|
|
49
|
+
detection, so `touch`/rebuilds never create false positives.
|
|
50
|
+
- **Rename detection only on unambiguous `(hash, mode, kind)` signatures**
|
|
51
|
+
(exactly one deleted + one added share it); duplicate-content files stay as
|
|
52
|
+
add/delete.
|
|
53
|
+
- **Inline content** stored for UTF-8 files <= 256 KiB (`CONTENT_CAP_BYTES`)
|
|
54
|
+
enables real two-sided `diff --content`, even between two stored manifests.
|
|
55
|
+
- **stdout/stderr flushed before `process::exit`** in `main.rs` (exit skips
|
|
56
|
+
destructors, so a buffered pipe could otherwise truncate).
|
|
57
|
+
- Destructive `rm` requires `--yes` when stdin is not a TTY. tidemark never
|
|
58
|
+
records its own `.tidemark/` store in a snapshot. Labels are validated against
|
|
59
|
+
path traversal and control characters.
|
|
60
|
+
|
|
61
|
+
### CI / release
|
|
62
|
+
|
|
63
|
+
All CI steps are make targets; the pipeline only runs make. `ci.yml` runs
|
|
64
|
+
lint/test/coverage on push and PR. `release.yml` triggers on a `v*` tag (or
|
|
65
|
+
manual dispatch, dry-run by default) and publishes to crates.io + PyPI + a
|
|
66
|
+
GitHub release, then updates the Homebrew tap formula. Release is driven by
|
|
67
|
+
`make release-patch|minor|major` (vership).
|
|
68
|
+
|
|
69
|
+
## Documentation
|
|
70
|
+
|
|
71
|
+
- `README.md` - human-facing usage.
|
|
72
|
+
- `AGENTS.md` - the agent-facing operational contract.
|
|
73
|
+
- `docs/superpowers/` - design spec and implementation plan (gitignored).
|