diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 84a43ed..66b8e13 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -156,6 +156,18 @@ jobs: } " + # Rename each package's `## [Unreleased]` section to `## [X.Y.Z] - ` + # and reopen an empty Unreleased section. Skipped on prereleases and dry + # runs — prereleases would fragment a single logical release into many + # changelog entries, and dry runs must not mutate files. + - name: Finalize CHANGELOGs + if: | + steps.bump.outputs.is_prerelease == 'false' && + github.event.inputs.dry_run != 'true' + env: + GITHUB_REPOSITORY: ${{ github.repository }} + run: node scripts/finalize-changelogs.mjs "${{ steps.bump.outputs.new_version }}" + - name: Build packages run: | npm run build --workspace=packages/core @@ -173,13 +185,18 @@ jobs: package.json packages/core/package.json packages/core/dist/ + packages/core/CHANGELOG.md packages/sdk/typescript/package.json packages/sdk/typescript/dist/ + packages/sdk/typescript/CHANGELOG.md packages/cli/package.json packages/cli/bin/ + packages/cli/CHANGELOG.md packages/file-observer/package.json + packages/file-observer/CHANGELOG.md packages/local-mount/package.json packages/local-mount/dist/ + packages/local-mount/CHANGELOG.md retention-days: 1 # Cross-compile relayfile-mount for every consumer-supported platform. @@ -420,7 +437,13 @@ jobs: git config user.name "GitHub Actions" git config user.email "actions@github.com" - git add package.json packages/core/package.json packages/sdk/typescript/package.json packages/cli/package.json packages/file-observer/package.json packages/local-mount/package.json + git add \ + package.json \ + packages/core/package.json packages/core/CHANGELOG.md \ + packages/sdk/typescript/package.json packages/sdk/typescript/CHANGELOG.md \ + packages/cli/package.json packages/cli/CHANGELOG.md \ + packages/file-observer/package.json packages/file-observer/CHANGELOG.md \ + packages/local-mount/package.json packages/local-mount/CHANGELOG.md if ! git diff --staged --quiet; then git commit -m "chore(release): v${NEW_VERSION}" git push diff --git a/.trajectories/completed/2026-04/traj_iuzm83ogm43k.json b/.trajectories/completed/2026-04/traj_iuzm83ogm43k.json new file mode 100644 index 0000000..bf83ddc --- /dev/null +++ b/.trajectories/completed/2026-04/traj_iuzm83ogm43k.json @@ -0,0 +1,53 @@ +{ + "id": "traj_iuzm83ogm43k", + "version": 1, + "task": { + "title": "Replace chokidar with @parcel/watcher in local-mount" + }, + "status": "completed", + "startedAt": "2026-04-20T20:35:15.759Z", + "completedAt": "2026-04-20T20:58:15.412Z", + "agents": [ + { + "name": "default", + "role": "lead", + "joinedAt": "2026-04-20T20:58:15.094Z" + } + ], + "chapters": [ + { + "id": "chap_2g7q1zwb1j1e", + "title": "Work", + "agentName": "default", + "startedAt": "2026-04-20T20:58:15.094Z", + "endedAt": "2026-04-20T20:58:15.412Z", + "events": [ + { + "ts": 1776718695095, + "type": "decision", + "content": "Switched watcher from chokidar v4 to @parcel/watcher 2.5.6: Switched watcher from chokidar v4 to @parcel/watcher 2.5.6", + "raw": { + "question": "Switched watcher from chokidar v4 to @parcel/watcher 2.5.6", + "chosen": "Switched watcher from chokidar v4 to @parcel/watcher 2.5.6", + "alternatives": [], + "reasoning": "Chokidar v4's awaitWriteFinish polling + fs.watch teardown caused noticeable hangs on exit. @parcel/watcher uses native FSEvents/inotify/ReadDirectoryChangesW with clean async unsubscribe. Replaced awaitWriteFinish with a small per-path setTimeout debounce; reconcile loop + mtime/content check absorb the looser write semantics." + }, + "significance": "high" + } + ] + } + ], + "retrospective": { + "summary": "Replaced chokidar with @parcel/watcher in packages/local-mount. All 22 tests pass. Exit-hang should be gone because unsubscribe() on native-backed watchers resolves promptly instead of draining stability-poll timers.", + "approach": "Standard approach", + "confidence": 0.85 + }, + "commits": [], + "filesChanged": [], + "projectId": "/Users/will/Projects/relayfile", + "tags": [], + "_trace": { + "startRef": "81a329d005404a6f156c837829d5c5d8d47aa05d", + "endRef": "81a329d005404a6f156c837829d5c5d8d47aa05d" + } +} \ No newline at end of file diff --git a/.trajectories/completed/2026-04/traj_iuzm83ogm43k.md b/.trajectories/completed/2026-04/traj_iuzm83ogm43k.md new file mode 100644 index 0000000..f8aa624 --- /dev/null +++ b/.trajectories/completed/2026-04/traj_iuzm83ogm43k.md @@ -0,0 +1,31 @@ +# Trajectory: Replace chokidar with @parcel/watcher in local-mount + +> **Status:** ✅ Completed +> **Confidence:** 85% +> **Started:** April 20, 2026 at 04:35 PM +> **Completed:** April 20, 2026 at 04:58 PM + +--- + +## Summary + +Replaced chokidar with @parcel/watcher in packages/local-mount. All 22 tests pass. Exit-hang should be gone because unsubscribe() on native-backed watchers resolves promptly instead of draining stability-poll timers. + +**Approach:** Standard approach + +--- + +## Key Decisions + +### Switched watcher from chokidar v4 to @parcel/watcher 2.5.6 +- **Chose:** Switched watcher from chokidar v4 to @parcel/watcher 2.5.6 +- **Reasoning:** Chokidar v4's awaitWriteFinish polling + fs.watch teardown caused noticeable hangs on exit. @parcel/watcher uses native FSEvents/inotify/ReadDirectoryChangesW with clean async unsubscribe. Replaced awaitWriteFinish with a small per-path setTimeout debounce; reconcile loop + mtime/content check absorb the looser write semantics. + +--- + +## Chapters + +### 1. Work +*Agent: default* + +- Switched watcher from chokidar v4 to @parcel/watcher 2.5.6: Switched watcher from chokidar v4 to @parcel/watcher 2.5.6 diff --git a/README.md b/README.md index 9c7797b..d9cebdc 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,22 @@ relayfile observer relayfile observer ws_123 --no-open ``` +## Changelogs + +Each publishable package keeps its own `CHANGELOG.md`: + +- [`relayfile`](packages/cli/CHANGELOG.md) — CLI +- [`@relayfile/core`](packages/core/CHANGELOG.md) +- [`@relayfile/sdk`](packages/sdk/typescript/CHANGELOG.md) +- [`@relayfile/local-mount`](packages/local-mount/CHANGELOG.md) +- [`@relayfile/file-observer`](packages/file-observer/CHANGELOG.md) + +**Process** — landed in every PR, finalized at release: + +1. PRs that touch a package add an entry under its `## [Unreleased]` section (Keep a Changelog format: `Added` / `Changed` / `Deprecated` / `Removed` / `Fixed` / `Security`). Include the PR number as a link reference at the bottom of the file. +2. At release, the `Publish Package` workflow runs [`scripts/finalize-changelogs.mjs`](scripts/finalize-changelogs.mjs), which renames `[Unreleased]` to `[x.y.z] - YYYY-MM-DD`, opens a fresh empty `[Unreleased]` above, and rewrites the compare-link references. Prereleases skip this step so their entries accumulate until the final release. +3. Packages without user-visible changes in a given release leave `[Unreleased]` as `_No unreleased changes._`. The finalizer rewrites the dated section's body to `_No user-visible changes in this release._` so the release heading still reads naturally. + ## License MIT diff --git a/package-lock.json b/package-lock.json index 6c8189f..224fee4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { "name": "relayfile", - "version": "0.3.0", + "version": "0.3.2", "lockfileVersion": 3, "requires": true, "packages": { "": { - "version": "0.3.0", + "version": "0.3.2", "workspaces": [ "packages/core", "packages/sdk/typescript", @@ -600,6 +600,301 @@ "dev": true, "license": "MIT" }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/@relaycast/sdk": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@relaycast/sdk/-/sdk-1.1.0.tgz", @@ -1275,21 +1570,6 @@ "node": ">= 16" } }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/chownr": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", @@ -1383,6 +1663,15 @@ "node": ">=6" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/emoji-regex": { "version": "10.6.0", "dev": true, @@ -1547,6 +1836,15 @@ "node": ">= 4" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "5.1.0", "dev": true, @@ -1561,6 +1859,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/js-tokens": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", @@ -1736,6 +2046,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, "node_modules/onetime": { "version": "7.0.0", "dev": true, @@ -1778,7 +2094,6 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -1816,19 +2131,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/relayfile": { "resolved": "packages/cli", "link": true @@ -2434,7 +2736,7 @@ }, "packages/cli": { "name": "relayfile", - "version": "0.3.0", + "version": "0.3.2", "hasInstallScript": true, "license": "MIT", "bin": { @@ -2446,7 +2748,7 @@ }, "packages/core": { "name": "@relayfile/core", - "version": "0.5.0", + "version": "0.3.2", "license": "MIT", "devDependencies": { "@types/node": "^22.0.0", @@ -2459,10 +2761,10 @@ }, "packages/local-mount": { "name": "@relayfile/local-mount", - "version": "0.3.0", + "version": "0.3.2", "license": "MIT", "dependencies": { - "chokidar": "^4.0.3", + "@parcel/watcher": "^2.5.6", "ignore": "^7.0.5" }, "devDependencies": { @@ -2488,10 +2790,10 @@ }, "packages/sdk/typescript": { "name": "@relayfile/sdk", - "version": "0.4.0", + "version": "0.3.2", "license": "MIT", "dependencies": { - "@relayfile/core": "^0.5.0" + "@relayfile/core": "0.3.2" }, "devDependencies": { "typescript": "^5.7.3", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md new file mode 100644 index 0000000..85828fd --- /dev/null +++ b/packages/cli/CHANGELOG.md @@ -0,0 +1,66 @@ +# Changelog + +All notable changes to `relayfile` (CLI) are documented here. +Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +_No unreleased changes._ + +## [0.2.2] - 2026-04-20 + +### Fixed +- Publish workflow now attaches `relayfile-mount` binaries to GitHub releases alongside `relayfile`. ([#51]) + +## [0.2.0] - 2026-04-19 + +### Added +- Ship the `@relayfile/local-mount` package via the monorepo publish workflow. ([#47]) + +## [0.1.13] - 2026-04-17 + +### Added +- `relayfile observer` — launch the file-observer dashboard from the CLI. ([#46]) + +### Fixed +- Default the observer to the live router path. +- Reduce WebSocket polling overhead in mount mode. ([#45]) + +## [0.1.12] - 2026-04-17 + +### Fixed +- Default to the hosted relayfile API so first-run no longer requires `--host`. ([#44]) +- Speed up mount bootstrap by seeding from an export snapshot. ([#44]) + +## [0.1.11] - 2026-04-17 + +### Added +- Support a default workspace selection so repeat invocations skip the picker. ([#43]) + +## [0.1.10] - 2026-04-17 + +### Fixed +- Package native Go binaries into the npm distribution. ([#42]) + +## [0.1.7] - 2026-04-11 + +### Fixed +- Upload raw binaries to GitHub releases. ([#34]) + +[Unreleased]: https://github.com/AgentWorkforce/relayfile/compare/v0.3.2...HEAD +[0.2.2]: https://github.com/AgentWorkforce/relayfile/releases/tag/v0.2.2 +[0.2.0]: https://github.com/AgentWorkforce/relayfile/releases/tag/v0.2.0 +[0.1.13]: https://github.com/AgentWorkforce/relayfile/releases/tag/v0.1.13 +[0.1.12]: https://github.com/AgentWorkforce/relayfile/releases/tag/v0.1.12 +[0.1.11]: https://github.com/AgentWorkforce/relayfile/releases/tag/v0.1.11 +[0.1.10]: https://github.com/AgentWorkforce/relayfile/releases/tag/v0.1.10 +[0.1.7]: https://github.com/AgentWorkforce/relayfile/releases/tag/v0.1.7 +[#34]: https://github.com/AgentWorkforce/relayfile/pull/34 +[#42]: https://github.com/AgentWorkforce/relayfile/pull/42 +[#43]: https://github.com/AgentWorkforce/relayfile/pull/43 +[#44]: https://github.com/AgentWorkforce/relayfile/pull/44 +[#45]: https://github.com/AgentWorkforce/relayfile/pull/45 +[#46]: https://github.com/AgentWorkforce/relayfile/pull/46 +[#47]: https://github.com/AgentWorkforce/relayfile/pull/47 +[#51]: https://github.com/AgentWorkforce/relayfile/pull/51 diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md new file mode 100644 index 0000000..e2f5283 --- /dev/null +++ b/packages/core/CHANGELOG.md @@ -0,0 +1,25 @@ +# Changelog + +All notable changes to `@relayfile/core` are documented here. +Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +_No unreleased changes._ + +## [0.3.2] - 2026-04-21 + +### Added +- `relayfile.fork` API — fork a workspace at a specific commit. ([#58]) + +## [0.3.0] - 2026-04-20 + +### Added +- Optional `contentIdentity` on write operations, enabling server-side deduplication of identical payloads. ([#54]) + +[Unreleased]: https://github.com/AgentWorkforce/relayfile/compare/v0.3.2...HEAD +[0.3.2]: https://github.com/AgentWorkforce/relayfile/releases/tag/v0.3.2 +[0.3.0]: https://github.com/AgentWorkforce/relayfile/releases/tag/v0.3.0 +[#54]: https://github.com/AgentWorkforce/relayfile/pull/54 +[#58]: https://github.com/AgentWorkforce/relayfile/pull/58 diff --git a/packages/file-observer/CHANGELOG.md b/packages/file-observer/CHANGELOG.md new file mode 100644 index 0000000..86ee0d9 --- /dev/null +++ b/packages/file-observer/CHANGELOG.md @@ -0,0 +1,30 @@ +# Changelog + +All notable changes to `@relayfile/file-observer` are documented here. +Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +_No unreleased changes._ + +## [0.1.13] - 2026-04-17 + +### Added +- Launchable from the `relayfile` CLI via `relayfile observer`. ([#46]) + +### Fixed +- Default the observer to the live router path instead of a local stub. + +## [0.1.9] - 2026-04-17 + +Initial release. + +### Added +- Dashboard workflow for visualizing relayfile filesystem activity in real time. ([#40]) + +[Unreleased]: https://github.com/AgentWorkforce/relayfile/compare/v0.3.2...HEAD +[0.1.13]: https://github.com/AgentWorkforce/relayfile/releases/tag/v0.1.13 +[0.1.9]: https://github.com/AgentWorkforce/relayfile/releases/tag/v0.1.9 +[#40]: https://github.com/AgentWorkforce/relayfile/pull/40 +[#46]: https://github.com/AgentWorkforce/relayfile/pull/46 diff --git a/packages/local-mount/CHANGELOG.md b/packages/local-mount/CHANGELOG.md new file mode 100644 index 0000000..e0857ec --- /dev/null +++ b/packages/local-mount/CHANGELOG.md @@ -0,0 +1,47 @@ +# Changelog + +All notable changes to `@relayfile/local-mount` are documented here. +Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Changed +- **BREAKING**: Renamed `AutoSyncOptions.writeFinishMs` → `AutoSyncOptions.debounceMs`. The semantics also shifted — it is now a per-path event coalescing debounce (default `50ms`), not a file-stability threshold (previously `200ms`). Any caller passing `writeFinishMs` will now be ignored silently under TypeScript's structural typing; update the field name. +- Replaced `chokidar` with [`@parcel/watcher`](https://www.npmjs.com/package/@parcel/watcher) for file watching. `autoSync.stop()` no longer hangs on teardown — native FSEvents/inotify/ReadDirectoryChangesW subscriptions unsubscribe promptly instead of draining per-file `awaitWriteFinish` polling timers. + +### Fixed +- `startAutoSync` no longer leaks a subscription when one of the two `@parcel/watcher` subscribes rejects. If mount- or project-side setup fails, the successful side is now unsubscribed before the error surfaces. +- `AutoSyncHandle.stop()` now honors its "stopped means quiesced" contract. A `stopped` flag blocks new debounces from scheduling the moment `stop()` is called, and the `pendingDebounces` map is cleared *after* the watcher unsubscribes resolve. Previously, events delivered during the unsubscribe await could create timers that fired after `stop()` returned, running file ops against a mount `launchOnMount`'s `cleanup()` had already deleted. + +## [0.3.0] - 2026-04-20 + +### Fixed +- Ignore mount-watcher echo writes so a mount→project propagation does not fire a spurious project→mount event. ([#52]) + +## [0.2.1] - 2026-04-20 + +### Fixed +- Preserve the local mount copy when the server denies a mount-sync write, instead of dropping it. ([#50]) + +## [0.2.0] - 2026-04-19 + +Initial release. + +### Added +- `createSymlinkMount(projectDir, mountDir, options)` — copies files into a mount directory, honors `.agentignore` and `.agentreadonly` dotfiles, enforces mode `0o444` on readonly matches, and writes `_MOUNT_README.md` / `.relayfile-local-mount` markers. ([#47]) +- `readAgentDotfiles(projectDir, options?)` — reads project-local `.agentignore`, `.agentreadonly`, and per-agent variants. +- `launchOnMount(options)` — creates a mount, spawns a CLI inside it, forwards `SIGINT` / `SIGTERM`, runs a final sync-back pass, and tears the mount down. +- `startAutoSync()` — bidirectional mount↔project sync with mount-wins conflict resolution, delete propagation, and a periodic full-reconcile safety net. ([#49]) +- Directory-only ignore patterns (e.g. `cache/`) match directories without swallowing like-named files. +- README documenting the mount lifecycle, dotfile semantics, and auto-sync behavior. ([#48]) + +[Unreleased]: https://github.com/AgentWorkforce/relayfile/compare/v0.3.2...HEAD +[0.3.0]: https://github.com/AgentWorkforce/relayfile/releases/tag/v0.3.0 +[0.2.1]: https://github.com/AgentWorkforce/relayfile/releases/tag/v0.2.1 +[0.2.0]: https://github.com/AgentWorkforce/relayfile/releases/tag/v0.2.0 +[#47]: https://github.com/AgentWorkforce/relayfile/pull/47 +[#48]: https://github.com/AgentWorkforce/relayfile/pull/48 +[#49]: https://github.com/AgentWorkforce/relayfile/pull/49 +[#50]: https://github.com/AgentWorkforce/relayfile/pull/50 +[#52]: https://github.com/AgentWorkforce/relayfile/pull/52 diff --git a/packages/local-mount/README.md b/packages/local-mount/README.md index a840931..6370dce 100644 --- a/packages/local-mount/README.md +++ b/packages/local-mount/README.md @@ -70,8 +70,8 @@ By default, `launchOnMount` keeps the mount and project directory in sync contin interface AutoSyncOptions { /** Full-reconcile interval as a safety net. Default: 10_000 ms. */ scanIntervalMs?: number; - /** chokidar `awaitWriteFinish` stability threshold. Default: 200 ms. */ - writeFinishMs?: number; + /** Per-path event debounce in ms. Default: 50 ms. */ + debounceMs?: number; /** Invoked on sync errors. Defaults to swallowing them. */ onError?: (err: Error) => void; } @@ -91,11 +91,11 @@ Control it from `launchOnMount`: launchOnMount({ /* ... */, autoSync: false }); // Tune it. -launchOnMount({ /* ... */, autoSync: { scanIntervalMs: 5_000, writeFinishMs: 100 } }); +launchOnMount({ /* ... */, autoSync: { scanIntervalMs: 5_000, debounceMs: 100 } }); ``` How it works: -- chokidar watches both the mount and the project tree +- [@parcel/watcher](https://www.npmjs.com/package/@parcel/watcher) watches both the mount and the project tree using native FSEvents/inotify/ReadDirectoryChangesW - every `scanIntervalMs`, a full reconcile walks both trees as a safety net for missed events - per-file `mtime` is tracked at the last sync, so the scan skips files that haven't changed diff --git a/packages/local-mount/package.json b/packages/local-mount/package.json index 0ab7983..95de414 100644 --- a/packages/local-mount/package.json +++ b/packages/local-mount/package.json @@ -15,7 +15,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "chokidar": "^4.0.3", + "@parcel/watcher": "^2.5.6", "ignore": "^7.0.5" }, "devDependencies": { diff --git a/packages/local-mount/src/auto-sync.test.ts b/packages/local-mount/src/auto-sync.test.ts index 269c1b4..168f01f 100644 --- a/packages/local-mount/src/auto-sync.test.ts +++ b/packages/local-mount/src/auto-sync.test.ts @@ -23,7 +23,7 @@ function write(file: string, body: string): void { /** * Wait up to `timeoutMs` for `check` to return true. Useful for letting - * chokidar + awaitWriteFinish observe a write and propagate it. + * the watcher observe a write and propagate it. */ async function waitFor(check: () => boolean, timeoutMs = 3000): Promise { const start = Date.now(); @@ -59,9 +59,9 @@ describe('startAutoSync', () => { excludeDirs: [], }); - // Use a short writeFinish so the test runs quickly; still long enough + // Use a short debounce so the test runs quickly; still long enough // to coalesce a single write. - const auto = handle.startAutoSync({ writeFinishMs: 50, scanIntervalMs: 10_000 }); + const auto = handle.startAutoSync({ debounceMs: 50, scanIntervalMs: 10_000 }); await auto.ready(); try { writeFileSync(path.join(handle.mountDir, 'file.txt'), 'edited-in-mount', 'utf8'); @@ -81,7 +81,7 @@ describe('startAutoSync', () => { excludeDirs: [], }); - const auto = handle.startAutoSync({ writeFinishMs: 50, scanIntervalMs: 10_000 }); + const auto = handle.startAutoSync({ debounceMs: 50, scanIntervalMs: 10_000 }); await auto.ready(); try { writeFileSync(path.join(projectDir, 'file.txt'), 'edited-externally', 'utf8'); @@ -103,7 +103,7 @@ describe('startAutoSync', () => { excludeDirs: [], }); - const auto = handle.startAutoSync({ writeFinishMs: 50, scanIntervalMs: 10_000 }); + const auto = handle.startAutoSync({ debounceMs: 50, scanIntervalMs: 10_000 }); await auto.ready(); try { rmSync(path.join(handle.mountDir, 'file.txt')); @@ -123,7 +123,7 @@ describe('startAutoSync', () => { excludeDirs: [], }); - const auto = handle.startAutoSync({ writeFinishMs: 50, scanIntervalMs: 10_000 }); + const auto = handle.startAutoSync({ debounceMs: 50, scanIntervalMs: 10_000 }); await auto.ready(); try { rmSync(path.join(projectDir, 'file.txt')); @@ -143,7 +143,7 @@ describe('startAutoSync', () => { excludeDirs: [], }); - const auto = handle.startAutoSync({ writeFinishMs: 50, scanIntervalMs: 10_000 }); + const auto = handle.startAutoSync({ debounceMs: 50, scanIntervalMs: 10_000 }); await auto.ready(); try { // Bypass the 0o444 permission for the test. @@ -171,7 +171,7 @@ describe('startAutoSync', () => { excludeDirs: [], }); - const auto = handle.startAutoSync({ writeFinishMs: 50, scanIntervalMs: 10_000 }); + const auto = handle.startAutoSync({ debounceMs: 50, scanIntervalMs: 10_000 }); await auto.ready(); try { writeFileSync(path.join(projectDir, 'locked.txt'), 'updated-externally', 'utf8'); @@ -195,11 +195,11 @@ describe('startAutoSync', () => { // Don't start autosync yet — set up the conflict state first, then // trigger a single reconcile so we exercise the resolution rule. - const auto = handle.startAutoSync({ writeFinishMs: 10, scanIntervalMs: 10_000 }); + const auto = handle.startAutoSync({ debounceMs: 10, scanIntervalMs: 10_000 }); // Stop immediately to drain priming; then mutate and reconcile manually. await auto.stop(); - const auto2 = handle.startAutoSync({ writeFinishMs: 10, scanIntervalMs: 10_000 }); + const auto2 = handle.startAutoSync({ debounceMs: 10, scanIntervalMs: 10_000 }); await auto2.ready(); try { writeFileSync(path.join(projectDir, 'file.txt'), 'project-side', 'utf8'); @@ -224,7 +224,7 @@ describe('startAutoSync', () => { excludeDirs: [], }); - const auto = handle.startAutoSync({ writeFinishMs: 50, scanIntervalMs: 10_000 }); + const auto = handle.startAutoSync({ debounceMs: 50, scanIntervalMs: 10_000 }); await auto.ready(); try { // File appearing in project under an ignored path — must NOT appear in mount. @@ -254,7 +254,7 @@ describe('startAutoSync', () => { excludeDirs: [], }); - const auto = handle.startAutoSync({ writeFinishMs: 50, scanIntervalMs: 10_000 }); + const auto = handle.startAutoSync({ debounceMs: 50, scanIntervalMs: 10_000 }); await auto.ready(); try { writeFileSync(path.join(handle.mountDir, 'docs/cache'), 'edited', 'utf8'); @@ -278,7 +278,7 @@ describe('startAutoSync', () => { // Start autosync but we'll rely on the explicit reconcile() call rather // than waiting for the watcher, to simulate a missed event. - const auto = handle.startAutoSync({ writeFinishMs: 50, scanIntervalMs: 100 }); + const auto = handle.startAutoSync({ debounceMs: 50, scanIntervalMs: 100 }); try { writeFileSync(path.join(handle.mountDir, 'file.txt'), 'edited', 'utf8'); // Forcing a reconcile should find the change regardless of whether @@ -298,7 +298,7 @@ describe('startAutoSync', () => { excludeDirs: [], }); - const auto = handle.startAutoSync({ writeFinishMs: 50, scanIntervalMs: 10_000 }); + const auto = handle.startAutoSync({ debounceMs: 50, scanIntervalMs: 10_000 }); await auto.ready(); try { writeFileSync(path.join(handle.mountDir, '_MOUNT_README.md'), 'mutated', 'utf8'); diff --git a/packages/local-mount/src/auto-sync.ts b/packages/local-mount/src/auto-sync.ts index 1cf5e08..9739234 100644 --- a/packages/local-mount/src/auto-sync.ts +++ b/packages/local-mount/src/auto-sync.ts @@ -12,7 +12,7 @@ import { } from 'node:fs'; import type { Stats } from 'node:fs'; import path from 'node:path'; -import chokidar, { type FSWatcher } from 'chokidar'; +import watcher, { type AsyncSubscription } from '@parcel/watcher'; export interface AutoSyncContext { realMountDir: string; @@ -21,7 +21,8 @@ export interface AutoSyncContext { /** * Directory-only ignore patterns (ending in `/`) must only match when the * path is a directory. Callers that know the path's type pass `isDirectory`; - * callers that don't (chokidar's prune filter) should check both forms. + * callers that don't should omit the second argument and fall back to the + * file-form check. */ isIgnored: (relPosix: string, isDirectory?: boolean) => boolean; isReadonly: (relPosix: string) => boolean; @@ -31,8 +32,11 @@ export interface AutoSyncContext { export interface AutoSyncOptions { /** Full-reconcile interval as a safety net. Default: 10_000ms. */ scanIntervalMs?: number; - /** chokidar awaitWriteFinish stabilityThreshold in ms. Default: 200. */ - writeFinishMs?: number; + /** + * Per-path event debounce in ms. Rapid watcher events for the same path + * are coalesced into a single sync. Default: 50. + */ + debounceMs?: number; /** Invoked on errors during sync — logged by default consumer. */ onError?: (err: Error) => void; } @@ -57,7 +61,7 @@ export function startAutoSync( opts: AutoSyncOptions = {} ): AutoSyncHandle { const scanIntervalMs = opts.scanIntervalMs ?? 10_000; - const writeFinishMs = opts.writeFinishMs ?? 200; + const debounceMs = opts.debounceMs ?? 50; const onError = opts.onError ?? (() => { /* ignore by default */ }); const state = new Map(); @@ -66,7 +70,9 @@ export function startAutoSync( let syncing = false; let pending = false; + let stopped = false; let totalChanges = 0; + const pendingDebounces = new Map(); const runReconcile = async (): Promise => { if (syncing) { @@ -107,34 +113,69 @@ export function startAutoSync( } }; - const makeWatcher = (root: string): { watcher: FSWatcher; ready: Promise } => { - const watcher = chokidar.watch(root, { - ignoreInitial: true, - persistent: true, - followSymlinks: false, - awaitWriteFinish: { - stabilityThreshold: writeFinishMs, - pollInterval: 50, - }, - ignored: (candidate: string, stats?: Stats) => - shouldChokidarIgnore(candidate, root, ctx, stats), - }); - const onEvent = (p: string) => syncPathFromRoot(root, p); - watcher.on('add', onEvent); - watcher.on('change', onEvent); - watcher.on('unlink', onEvent); - watcher.on('error', (err) => onError(err as Error)); - const ready = new Promise((resolve) => { - watcher.once('ready', () => resolve()); - }); - return { watcher, ready }; + const schedulePathSync = (root: string, absPath: string): void => { + // Once stop() has begun, refuse to schedule new timers. Watcher + // callbacks can still fire during the unsubscribe await (native + // backends deliver queued events asynchronously); without this guard + // those events would create timers that outlive stop() and do file + // work against a mount the caller may have already cleaned up. + if (stopped) return; + // Coalesce bursts of events for the same path. The reconcile path + // re-checks content via mtime+bytes, so a partial-write event that + // races a later write is harmless. + const existing = pendingDebounces.get(absPath); + if (existing) clearTimeout(existing); + const t = setTimeout(() => { + pendingDebounces.delete(absPath); + syncPathFromRoot(root, absPath); + }, debounceMs); + pendingDebounces.set(absPath, t); }; - const mount = makeWatcher(ctx.realMountDir); - const project = makeWatcher(ctx.realProjectDir); - const mountWatcher = mount.watcher; - const projectWatcher = project.watcher; - const watchersReady = Promise.all([mount.ready, project.ready]); + const ignoreGlobs = buildIgnoreGlobs(ctx); + + const subscribeTo = (root: string): Promise => + watcher.subscribe( + root, + (err, events) => { + if (err) { onError(err); return; } + for (const ev of events) { + schedulePathSync(root, ev.path); + } + }, + { ignore: ignoreGlobs } + ); + + let mountSub: AsyncSubscription | undefined; + let projectSub: AsyncSubscription | undefined; + // Subscribe in parallel but track each outcome independently. With + // Promise.all, a failure on one side would reject before the other's + // assignment ran and leak the succeeded subscription. allSettled lets us + // tear down whichever fulfilled before re-throwing the first failure. + const watchersReady = (async () => { + const [mountResult, projectResult] = await Promise.allSettled([ + subscribeTo(ctx.realMountDir), + subscribeTo(ctx.realProjectDir), + ]); + if (mountResult.status === 'fulfilled') mountSub = mountResult.value; + if (projectResult.status === 'fulfilled') projectSub = projectResult.value; + if (mountResult.status === 'fulfilled' && projectResult.status === 'fulfilled') { + return; + } + await Promise.allSettled([ + mountSub?.unsubscribe(), + projectSub?.unsubscribe(), + ]); + mountSub = undefined; + projectSub = undefined; + throw mountResult.status === 'rejected' + ? mountResult.reason + : (projectResult as PromiseRejectedResult).reason; + })(); + // If subscription setup fails, surface via onError rather than an unhandled + // rejection. stop() still awaits the same promise and will observe the + // rejection after the cleanup above has already run. + watchersReady.catch((err) => onError(err as Error)); const interval = setInterval(() => { void runReconcile(); @@ -144,8 +185,25 @@ export function startAutoSync( return { async stop() { + // Flip the flag first so any watcher callbacks delivered during the + // awaits below refuse to schedule new timers. + stopped = true; clearInterval(interval); - await Promise.all([mountWatcher.close(), projectWatcher.close()]); + try { + await watchersReady; + } catch { + // Setup failed and already cleaned up any partial subscription; + // mountSub / projectSub were reset to undefined before the throw. + } + await Promise.allSettled([ + mountSub?.unsubscribe(), + projectSub?.unsubscribe(), + ]); + // Clear debounces *after* unsubscribe resolves: any timer scheduled + // between stop() being called and the watcher actually quiescing is + // gathered here, so none fire after stop() returns. + for (const t of pendingDebounces.values()) clearTimeout(t); + pendingDebounces.clear(); // Drain any pending work so callers can rely on "stopped means quiesced". await runReconcile(); }, @@ -157,6 +215,24 @@ export function startAutoSync( }; } +function buildIgnoreGlobs(ctx: AutoSyncContext): string[] { + // @parcel/watcher matches globs against absolute paths via globset. For each + // excluded directory name, ignore both the directory itself and everything + // beneath it, anywhere under the watched root. The `isExcluded` predicate is + // driven by a Set of directory names, so we probe a small set of common + // exclusions rather than introspecting it. The in-handler `isSyncCandidate` + // filter is authoritative — this is just a perf hint so the watcher doesn't + // recurse into heavy trees like node_modules or .git. + const globs: string[] = []; + const candidates = ['.git', 'node_modules', 'dist', 'build', '.next', '.cache']; + for (const name of candidates) { + if (ctx.isExcluded(name)) { + globs.push(`**/${name}`, `**/${name}/**`); + } + } + return globs; +} + function primeState(state: Map, ctx: AutoSyncContext): void { // Record current mtimes for every file that exists in both trees with the // same content. Files that differ are left out so the first reconcile sees @@ -527,25 +603,3 @@ function walk( } } -function shouldChokidarIgnore( - candidate: string, - root: string, - ctx: AutoSyncContext, - stats?: Stats -): boolean { - if (candidate === root) return false; - const rel = path.relative(root, candidate); - if (rel === '' || rel.startsWith('..')) return false; - const relPosix = rel.split(path.sep).join('/'); - if (ctx.isExcluded(relPosix)) return true; - if (ctx.isReservedFile(relPosix)) return true; - // chokidar calls this filter twice: first without stats (pre-stat prune), - // then again with stats once it knows the entry type. Only apply the - // directory-form match when we have stats confirming it's a directory, - // otherwise a directory-only pattern like `cache/` would wrongly prune a - // same-named file. - if (stats) { - return ctx.isIgnored(relPosix, stats.isDirectory()); - } - return ctx.isIgnored(relPosix); -} diff --git a/packages/local-mount/src/symlink-mount.ts b/packages/local-mount/src/symlink-mount.ts index 2d19509..96102c0 100644 --- a/packages/local-mount/src/symlink-mount.ts +++ b/packages/local-mount/src/symlink-mount.ts @@ -37,8 +37,8 @@ export interface SymlinkMountHandle { syncBack(): Promise; /** * Start bidirectional auto-sync: watches both the mount and project trees - * with chokidar and runs a full reconcile every `scanIntervalMs` as a - * safety net. Returns a handle you must `stop()` before teardown. + * via @parcel/watcher and runs a full reconcile every `scanIntervalMs` + * as a safety net. Returns a handle you must `stop()` before teardown. */ startAutoSync(opts?: AutoSyncOptions): AutoSyncHandle; cleanup(): void; diff --git a/packages/sdk/typescript/CHANGELOG.md b/packages/sdk/typescript/CHANGELOG.md new file mode 100644 index 0000000..78f29d9 --- /dev/null +++ b/packages/sdk/typescript/CHANGELOG.md @@ -0,0 +1,32 @@ +# Changelog + +All notable changes to `@relayfile/sdk` are documented here. +Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +_No unreleased changes._ + +## [0.3.2] - 2026-04-21 + +### Added +- `relayfile.fork` client method — fork a workspace at a specific commit. ([#58]) + +## [0.3.1] - 2026-04-21 + +### Added +- Expose `ContentIdentity` on write types so callers can opt into server-side deduplication. ([#57]) + +## [0.1.8] - 2026-04-16 + +### Fixed +- Bind `fetch` to `globalThis` so the SDK runs on Cloudflare Workers without `TypeError: Illegal invocation`. ([#41]) + +[Unreleased]: https://github.com/AgentWorkforce/relayfile/compare/v0.3.2...HEAD +[0.3.2]: https://github.com/AgentWorkforce/relayfile/releases/tag/v0.3.2 +[0.3.1]: https://github.com/AgentWorkforce/relayfile/releases/tag/v0.3.1 +[0.1.8]: https://github.com/AgentWorkforce/relayfile/releases/tag/v0.1.8 +[#41]: https://github.com/AgentWorkforce/relayfile/pull/41 +[#57]: https://github.com/AgentWorkforce/relayfile/pull/57 +[#58]: https://github.com/AgentWorkforce/relayfile/pull/58 diff --git a/scripts/finalize-changelogs.mjs b/scripts/finalize-changelogs.mjs new file mode 100644 index 0000000..482069a --- /dev/null +++ b/scripts/finalize-changelogs.mjs @@ -0,0 +1,73 @@ +#!/usr/bin/env node +// Finalize each package's CHANGELOG.md for a release: +// - rename `## [Unreleased]` to `## [X.Y.Z] - YYYY-MM-DD` +// - insert a fresh empty `## [Unreleased]` section above it +// - rewrite the `[Unreleased]: ...compare/...HEAD` link to start from the new tag +// - append a `[X.Y.Z]: ...releases/tag/vX.Y.Z` link reference +// +// Usage: node scripts/finalize-changelogs.mjs +// e.g. node scripts/finalize-changelogs.mjs 0.3.3 + +import fs from 'node:fs'; + +const version = process.argv[2]; +if (!version) { + console.error('Usage: finalize-changelogs.mjs '); + process.exit(1); +} + +const today = new Date().toISOString().slice(0, 10); +const repoSlug = process.env.GITHUB_REPOSITORY || 'AgentWorkforce/relayfile'; + +const changelogs = [ + 'packages/core/CHANGELOG.md', + 'packages/sdk/typescript/CHANGELOG.md', + 'packages/cli/CHANGELOG.md', + 'packages/file-observer/CHANGELOG.md', + 'packages/local-mount/CHANGELOG.md', +]; + +const placeholder = '_No unreleased changes._'; +const unreleasedHeadingRe = /^## \[Unreleased\][^\n]*$/m; +const unreleasedLinkRe = + /^\[Unreleased\]:\s*https:\/\/github\.com\/[^\s/]+\/[^\s/]+\/compare\/\S+?\.\.\.HEAD\s*$/m; + +for (const clPath of changelogs) { + if (!fs.existsSync(clPath)) { + console.log(`skip ${clPath}: missing`); + continue; + } + let text = fs.readFileSync(clPath, 'utf8'); + + if (!unreleasedHeadingRe.test(text)) { + console.log(`skip ${clPath}: no [Unreleased] heading`); + continue; + } + + const versionHeading = `## [${version}] - ${today}`; + text = text.replace( + unreleasedHeadingRe, + ['## [Unreleased]', '', placeholder, '', versionHeading].join('\n') + ); + + // If the newly-created version block inherited only the placeholder, reword + // it so the release entry reads naturally instead of "No unreleased changes" + // under a dated heading. + const inheritedEmpty = `${versionHeading}\n\n${placeholder}`; + const rewritten = `${versionHeading}\n\n_No user-visible changes in this release._`; + if (text.includes(inheritedEmpty)) { + text = text.replace(inheritedEmpty, rewritten); + } + + const newCompare = `[Unreleased]: https://github.com/${repoSlug}/compare/v${version}...HEAD`; + const releaseLink = `[${version}]: https://github.com/${repoSlug}/releases/tag/v${version}`; + + if (unreleasedLinkRe.test(text)) { + text = text.replace(unreleasedLinkRe, `${newCompare}\n${releaseLink}`); + } else { + text = text.replace(/\s*$/, `\n\n${newCompare}\n${releaseLink}\n`); + } + + fs.writeFileSync(clPath, text); + console.log(`updated ${clPath} -> [${version}] ${today}`); +}