From 82b78058a5b080b3d1e7c2d99f95abbca0563fb5 Mon Sep 17 00:00:00 2001 From: Danny Smith Date: Tue, 30 Dec 2025 22:35:07 +0000 Subject: [PATCH 01/14] Add prep release script --- .../task-x-cli-distribution-and-releases.md | 70 ++++----- tdn-cli/package.json | 3 +- tdn-cli/scripts/prepare-release.js | 148 ++++++++++++++++++ 3 files changed, 183 insertions(+), 38 deletions(-) create mode 100755 tdn-cli/scripts/prepare-release.js diff --git a/docs/tasks-todo/task-x-cli-distribution-and-releases.md b/docs/tasks-todo/task-x-cli-distribution-and-releases.md index 56e12b12..e3cfb6fe 100644 --- a/docs/tasks-todo/task-x-cli-distribution-and-releases.md +++ b/docs/tasks-todo/task-x-cli-distribution-and-releases.md @@ -66,10 +66,13 @@ Create build scripts and verify binaries work on each platform. tdn-cli/ ├── dist/ # Built binaries (gitignored) ├── scripts/ -│ ├── build-release.sh # Script for local builds -│ └── install.sh # User install script +│ ├── build-release.sh # Script for local builds +│ ├── install.sh # User install script +│ └── prepare-release.js # Version bump + release prep (already created) ``` +**Note:** `prepare-release.js` already exists. Run with `bun run release:prepare 1.0.0`. + **1.3: Create build-release.sh** ```bash @@ -297,7 +300,12 @@ jobs: run: | cd dist tar -czvf tdn-${{ matrix.target }}.tar.gz tdn - sha256sum tdn-${{ matrix.target }}.tar.gz > tdn-${{ matrix.target }}.tar.gz.sha256 + # macOS uses shasum, Linux uses sha256sum + if command -v sha256sum &> /dev/null; then + sha256sum tdn-${{ matrix.target }}.tar.gz > tdn-${{ matrix.target }}.tar.gz.sha256 + else + shasum -a 256 tdn-${{ matrix.target }}.tar.gz > tdn-${{ matrix.target }}.tar.gz.sha256 + fi rm tdn - name: Create archive (Windows) @@ -432,36 +440,29 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Download archives and calculate checksums + - name: Download checksums and update formula run: | BASE_URL="https://github.com/taskdn/taskdn/releases/download/tdn-cli-v${VERSION}" - # Download each archive and get checksum + # Download checksums and export as env vars for target in darwin-arm64 darwin-x64 linux-arm64 linux-x64; do - echo "Downloading ${target}..." - curl -sL "${BASE_URL}/tdn-${target}.tar.gz.sha256" -o "${target}.sha256" - # Extract just the hash (first field) - HASH=$(cut -d' ' -f1 "${target}.sha256") - echo "${target}=${HASH}" >> checksums.env + echo "Fetching ${target} checksum..." + HASH=$(curl -sL "${BASE_URL}/tdn-${target}.tar.gz.sha256" | cut -d' ' -f1) + # Export with underscores (bash doesn't like hyphens in var names) + VAR_NAME=$(echo "$target" | tr '-' '_') + export "$VAR_NAME=$HASH" echo " ${target}: ${HASH}" done - - name: Update formula - run: | - # Load checksums - source checksums.env - # Update version sed -i "s/version \".*\"/version \"${VERSION}\"/" Formula/tdn.rb - # Update each sha256 placeholder - sed -i "s/PLACEHOLDER_DARWIN_ARM64/${darwin_arm64:-PLACEHOLDER_DARWIN_ARM64}/" Formula/tdn.rb - sed -i "s/PLACEHOLDER_DARWIN_X64/${darwin_x64:-PLACEHOLDER_DARWIN_X64}/" Formula/tdn.rb - sed -i "s/PLACEHOLDER_LINUX_ARM64/${linux_arm64:-PLACEHOLDER_LINUX_ARM64}/" Formula/tdn.rb - sed -i "s/PLACEHOLDER_LINUX_X64/${linux_x64:-PLACEHOLDER_LINUX_X64}/" Formula/tdn.rb - - # For subsequent updates, replace existing hashes (64 hex chars) - sed -i "s/sha256 \"[a-f0-9]\{64\}\"/sha256 \"${darwin_arm64}\"/" Formula/tdn.rb || true + # Update checksums - use perl for multi-line regex (more reliable than sed) + # Each sha256 is updated based on its preceding url line + perl -i -0pe "s|(url.*tdn-darwin-arm64.*\n\s*)sha256 \"[^\"]*\"|\1sha256 \"$darwin_arm64\"|g" Formula/tdn.rb + perl -i -0pe "s|(url.*tdn-darwin-x64.*\n\s*)sha256 \"[^\"]*\"|\1sha256 \"$darwin_x64\"|g" Formula/tdn.rb + perl -i -0pe "s|(url.*tdn-linux-arm64.*\n\s*)sha256 \"[^\"]*\"|\1sha256 \"$linux_arm64\"|g" Formula/tdn.rb + perl -i -0pe "s|(url.*tdn-linux-x64.*\n\s*)sha256 \"[^\"]*\"|\1sha256 \"$linux_x64\"|g" Formula/tdn.rb echo "Updated Formula/tdn.rb:" cat Formula/tdn.rb @@ -476,15 +477,9 @@ jobs: Automated update triggered by new release. **Version:** ${{ env.VERSION }} - - **Checksums:** - - darwin-arm64: `${{ env.darwin_arm64 }}` - - darwin-x64: `${{ env.darwin_x64 }}` - - linux-arm64: `${{ env.linux_arm64 }}` - - linux-x64: `${{ env.linux_x64 }}` ``` -**Note:** The Homebrew formula update workflow above is simplified. For production, consider using a more robust approach like a dedicated script that properly parses and updates the Ruby formula. +**Note:** The perl regex matches each sha256 line based on its preceding url line, so it works for both initial placeholders and subsequent hash updates. Test on first release. ### Phase 5: Install Script @@ -636,9 +631,9 @@ echo "" info "Run 'tdn --help' to get started" ``` -### Phase 6: Release Automation (Optional) +### Phase 6: Release Automation (Optional - Skip for Manual Releases) -Set up release-please for automated versioning and changelogs. +Set up release-please for automated versioning and changelogs. **Skip this phase if you prefer manual releases** — just use `bun run release:prepare VERSION` and push the tag yourself. **6.1: Create release-please configuration** @@ -825,9 +820,9 @@ The project requires Rust 1.85+ (for edition 2024). The workflows pin to `dtolna NAPI-RS binaries cannot be cross-compiled. The Rust code must be compiled on the target platform. This is why we use platform-specific runners instead of cross-compiling from Linux. -### Conventional Commits +### Conventional Commits (Only if using Phase 6) -For release-please to work, use conventional commit format: +For release-please to work, use conventional commit format. **If doing manual releases, skip this.** ``` feat(cli): add new filter option @@ -840,10 +835,11 @@ chore(cli): update dependencies Version appears in multiple places: - `tdn-cli/package.json` — Source of truth -- `.release-please-manifest.json` — Updated by release-please -- Git tag — Created by release-please +- `tdn-cli/crates/core/Cargo.toml` — Rust crate version +- `.release-please-manifest.json` — Updated by release-please (if using) +- Git tag — Created manually or by release-please -release-please keeps these in sync automatically. For manual releases, update `package.json` first. +**For manual releases:** Use `bun run release:prepare 1.0.0` which updates both `package.json` and `Cargo.toml`, runs checks, and optionally creates the git commit/tag. ### Future Enhancements diff --git a/tdn-cli/package.json b/tdn-cli/package.json index d0e1a09d..70b88ce8 100644 --- a/tdn-cli/package.json +++ b/tdn-cli/package.json @@ -22,7 +22,8 @@ "fix": "bun run format && bun run lint:fix && cargo fmt --manifest-path crates/core/Cargo.toml", "test": "bun run test:ts && bun run test:rust", "test:ts": "bun test", - "test:rust": "cargo test --manifest-path crates/core/Cargo.toml" + "test:rust": "cargo test --manifest-path crates/core/Cargo.toml", + "release:prepare": "node scripts/prepare-release.js" }, "devDependencies": { "@eslint/js": "^9.39.2", diff --git a/tdn-cli/scripts/prepare-release.js b/tdn-cli/scripts/prepare-release.js new file mode 100755 index 00000000..24c6e14f --- /dev/null +++ b/tdn-cli/scripts/prepare-release.js @@ -0,0 +1,148 @@ +#!/usr/bin/env node + +import fs from 'fs' +import { execSync } from 'child_process' +import readline from 'readline' + +function exec(command, options = {}) { + try { + return execSync(command, { + encoding: 'utf8', + stdio: options.silent ? 'pipe' : 'inherit', + ...options, + }) + } catch (error) { + throw new Error(`Command failed: ${command}\n${error.message}`) + } +} + +function askQuestion(question) { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }) + + return new Promise(resolve => { + rl.question(question, answer => { + rl.close() + resolve(answer.trim()) + }) + }) +} + +async function prepareRelease() { + const version = process.argv[2] + + if (!version || !version.match(/^(tdn-cli-)?v?\d+\.\d+\.\d+$/)) { + console.error('Usage: node scripts/prepare-release.js 1.0.0') + console.error(' or: bun run release:prepare 1.0.0') + process.exit(1) + } + + const cleanVersion = version.replace(/^(tdn-cli-)?v?/, '') + const tagVersion = `tdn-cli-v${cleanVersion}` + + console.log(`Preparing release ${tagVersion}...\n`) + + try { + // Check git status + console.log('Checking git status...') + const gitStatus = exec('git status --porcelain', { silent: true }) + if (gitStatus.trim()) { + console.error( + 'Working directory is not clean. Please commit or stash changes first.' + ) + console.log('Uncommitted changes:') + console.log(gitStatus) + process.exit(1) + } + console.log('Working directory is clean') + + // Run all checks first + console.log('\nRunning pre-release checks...') + exec('bun run check') + console.log('All checks passed') + + // Update package.json + console.log('\nUpdating package.json...') + const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')) + const oldPkgVersion = pkg.version + pkg.version = cleanVersion + fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n') + console.log(` ${oldPkgVersion} -> ${cleanVersion}`) + + // Update Cargo.toml + console.log('Updating crates/core/Cargo.toml...') + const cargoPath = 'crates/core/Cargo.toml' + const cargoToml = fs.readFileSync(cargoPath, 'utf8') + const oldCargoVersion = cargoToml.match(/version = "([^"]*)"/) + const updatedCargo = cargoToml.replace( + /version = "[^"]*"/, + `version = "${cleanVersion}"` + ) + fs.writeFileSync(cargoPath, updatedCargo) + console.log( + ` ${oldCargoVersion ? oldCargoVersion[1] : 'unknown'} -> ${cleanVersion}` + ) + + // Run bun install to update lock files + console.log('\nUpdating lock files...') + exec('bun install', { silent: true }) + console.log('Lock files updated') + + // Final check that Rust code compiles + console.log('\nRunning final compilation check...') + exec('cargo check --manifest-path crates/core/Cargo.toml') + console.log('Rust compilation check passed') + + console.log(`\nSuccessfully prepared release ${tagVersion}!`) + console.log('\nGit commands to execute:') + console.log(` git add .`) + console.log(` git commit -m "chore(cli): release ${tagVersion}"`) + console.log(` git tag ${tagVersion}`) + console.log(` git push && git push origin ${tagVersion}`) + + console.log('\nAfter pushing:') + console.log(' - GitHub Actions will automatically build the release') + console.log(' - Binaries for all platforms will be uploaded to GitHub Releases') + console.log(' - Homebrew formula update will be triggered (if configured)') + + // Interactive execution option + const answer = await askQuestion( + '\nWould you like me to execute these git commands? (y/N): ' + ) + + if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') { + console.log('\nExecuting git commands...') + + console.log('Adding changes...') + exec('git add .') + + console.log('Creating commit...') + exec(`git commit -m "chore(cli): release ${tagVersion}"`) + + console.log('Creating tag...') + exec(`git tag ${tagVersion}`) + + console.log('Pushing to remote...') + exec('git push') + exec(`git push origin ${tagVersion}`) + + console.log(`\nRelease ${tagVersion} has been published!`) + console.log( + 'Check GitHub Actions: https://github.com/taskdn/taskdn/actions' + ) + console.log( + 'Release will appear at: https://github.com/taskdn/taskdn/releases' + ) + } else { + console.log('\nGit commands saved for manual execution.') + console.log(" Run them when you're ready to release.") + } + } catch (error) { + console.error('\nPre-release preparation failed:', error.message) + process.exit(1) + } +} + +prepareRelease() From 6fdfbc2a0005e26bc7339b8e94617404616188ed Mon Sep 17 00:00:00 2001 From: Danny Smith Date: Tue, 30 Dec 2025 22:44:50 +0000 Subject: [PATCH 02/14] add build scripts for standalone binary compilation - Add build:binary and build:binary:release scripts to package.json - Create scripts/build-release.sh for platform-specific local builds - Add *.bun-build to gitignore --- tdn-cli/.gitignore | 1 + tdn-cli/package.json | 2 ++ tdn-cli/scripts/build-release.sh | 28 ++++++++++++++++++++++++++++ 3 files changed, 31 insertions(+) create mode 100755 tdn-cli/scripts/build-release.sh diff --git a/tdn-cli/.gitignore b/tdn-cli/.gitignore index 05a37b57..8dbaf876 100644 --- a/tdn-cli/.gitignore +++ b/tdn-cli/.gitignore @@ -6,6 +6,7 @@ node_modules out dist *.tgz +*.bun-build # code coverage coverage diff --git a/tdn-cli/package.json b/tdn-cli/package.json index 70b88ce8..e4005959 100644 --- a/tdn-cli/package.json +++ b/tdn-cli/package.json @@ -23,6 +23,8 @@ "test": "bun run test:ts && bun run test:rust", "test:ts": "bun test", "test:rust": "cargo test --manifest-path crates/core/Cargo.toml", + "build:binary": "bun build --compile --minify src/index.ts --outfile dist/tdn", + "build:binary:release": "bun build --compile --minify --sourcemap src/index.ts --outfile dist/tdn", "release:prepare": "node scripts/prepare-release.js" }, "devDependencies": { diff --git a/tdn-cli/scripts/build-release.sh b/tdn-cli/scripts/build-release.sh new file mode 100755 index 00000000..a7a12edc --- /dev/null +++ b/tdn-cli/scripts/build-release.sh @@ -0,0 +1,28 @@ +#!/bin/bash +set -euo pipefail + +# Build release binary for current platform (macOS/Linux only) +# Usage: ./scripts/build-release.sh + +VERSION="${VERSION:-dev}" +PLATFORM="$(uname -s | tr '[:upper:]' '[:lower:]')" +ARCH="$(uname -m)" + +# Normalize architecture names +case "$ARCH" in + x86_64) ARCH="x64" ;; + aarch64|arm64) ARCH="arm64" ;; +esac + +OUTPUT_NAME="tdn-${PLATFORM}-${ARCH}" + +echo "Building tdn v${VERSION} for ${PLATFORM}-${ARCH}..." + +# Ensure bindings are built +bun run build + +# Build standalone binary +bun build --compile --minify src/index.ts --outfile "dist/${OUTPUT_NAME}" + +echo "Built: dist/${OUTPUT_NAME}" +ls -lh "dist/${OUTPUT_NAME}" From b44bca89da3f24f94b1e5fd257c6e83c6936681e Mon Sep 17 00:00:00 2001 From: Danny Smith Date: Tue, 30 Dec 2025 22:47:30 +0000 Subject: [PATCH 03/14] add CI workflow for lint, test, and binary builds - Lint & typecheck on ubuntu - Tests on ubuntu, macos, windows - Binary builds for all 5 platform targets - Uploads artifacts for each platform --- .github/workflows/ci-cli.yml | 98 ++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 .github/workflows/ci-cli.yml diff --git a/.github/workflows/ci-cli.yml b/.github/workflows/ci-cli.yml new file mode 100644 index 00000000..794e5a4f --- /dev/null +++ b/.github/workflows/ci-cli.yml @@ -0,0 +1,98 @@ +name: CI - CLI + +on: + push: + branches: [main] + paths: + - 'tdn-cli/**' + - '.github/workflows/ci-cli.yml' + pull_request: + branches: [main] + paths: + - 'tdn-cli/**' + - '.github/workflows/ci-cli.yml' + +defaults: + run: + working-directory: tdn-cli + +env: + CARGO_TERM_COLOR: always + +jobs: + lint: + name: Lint & Typecheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + - uses: dtolnay/rust-toolchain@1.85 + - run: bun install + - run: bun run build # Build NAPI bindings + - run: bun run check + + test: + name: Test - ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + - uses: dtolnay/rust-toolchain@1.85 + - run: bun install + - run: bun run build + - run: bun run test + + build: + name: Build Binary - ${{ matrix.target }} + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - target: darwin-arm64 + runner: macos-latest + - target: darwin-x64 + runner: macos-13 + - target: linux-x64 + runner: ubuntu-latest + - target: linux-arm64 + runner: ubuntu-24.04-arm + - target: windows-x64 + runner: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + - uses: dtolnay/rust-toolchain@1.85 + + - name: Install dependencies + run: bun install + + - name: Build NAPI bindings + run: bun run build + + - name: Build standalone binary (Unix) + if: matrix.target != 'windows-x64' + run: bun build --compile --minify src/index.ts --outfile dist/tdn-${{ matrix.target }} + + - name: Build standalone binary (Windows) + if: matrix.target == 'windows-x64' + run: bun build --compile --minify src/index.ts --outfile dist/tdn-${{ matrix.target }}.exe + + - name: Test binary (Unix) + if: matrix.target != 'windows-x64' + run: ./dist/tdn-${{ matrix.target }} --version + + - name: Test binary (Windows) + if: matrix.target == 'windows-x64' + run: .\dist\tdn-${{ matrix.target }}.exe --version + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: tdn-${{ matrix.target }} + path: tdn-cli/dist/tdn-${{ matrix.target }}* + if-no-files-found: error From dbf25201a104dbeed81a855cff73b1a10336d919 Mon Sep 17 00:00:00 2001 From: Danny Smith Date: Tue, 30 Dec 2025 23:00:00 +0000 Subject: [PATCH 04/14] ci(cli): add release workflow and install script - Release workflow triggered by tdn-cli-v* tags - Builds archives with checksums for all 5 platforms - Creates GitHub Release with auto-generated notes - Triggers Homebrew tap update for stable releases - Add curl-installable install.sh script --- .github/workflows/release-cli.yml | 127 +++++++++++++++++++++++++++ tdn-cli/scripts/install.sh | 141 ++++++++++++++++++++++++++++++ 2 files changed, 268 insertions(+) create mode 100644 .github/workflows/release-cli.yml create mode 100755 tdn-cli/scripts/install.sh diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml new file mode 100644 index 00000000..f02fd90c --- /dev/null +++ b/.github/workflows/release-cli.yml @@ -0,0 +1,127 @@ +name: Release - CLI + +on: + push: + tags: + - 'tdn-cli-v*' + +defaults: + run: + working-directory: tdn-cli + +permissions: + contents: write + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + name: Build - ${{ matrix.target }} + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - target: darwin-arm64 + runner: macos-latest + - target: darwin-x64 + runner: macos-13 + - target: linux-x64 + runner: ubuntu-latest + - target: linux-arm64 + runner: ubuntu-24.04-arm + - target: windows-x64 + runner: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + - uses: dtolnay/rust-toolchain@1.85 + + - name: Install dependencies + run: bun install + + - name: Build NAPI bindings + run: bun run build + + - name: Build standalone binary (Unix) + if: matrix.target != 'windows-x64' + run: bun build --compile --minify src/index.ts --outfile dist/tdn + + - name: Build standalone binary (Windows) + if: matrix.target == 'windows-x64' + run: bun build --compile --minify src/index.ts --outfile dist/tdn.exe + + - name: Create archive (Unix) + if: matrix.target != 'windows-x64' + run: | + cd dist + tar -czvf tdn-${{ matrix.target }}.tar.gz tdn + # macOS uses shasum, Linux uses sha256sum + if command -v sha256sum &> /dev/null; then + sha256sum tdn-${{ matrix.target }}.tar.gz > tdn-${{ matrix.target }}.tar.gz.sha256 + else + shasum -a 256 tdn-${{ matrix.target }}.tar.gz > tdn-${{ matrix.target }}.tar.gz.sha256 + fi + rm tdn + + - name: Create archive (Windows) + if: matrix.target == 'windows-x64' + shell: bash + run: | + cd dist + 7z a -tzip tdn-${{ matrix.target }}.zip tdn.exe + sha256sum tdn-${{ matrix.target }}.zip > tdn-${{ matrix.target }}.zip.sha256 + rm tdn.exe + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: tdn-${{ matrix.target }} + path: | + tdn-cli/dist/tdn-${{ matrix.target }}.tar.gz + tdn-cli/dist/tdn-${{ matrix.target }}.tar.gz.sha256 + tdn-cli/dist/tdn-${{ matrix.target }}.zip + tdn-cli/dist/tdn-${{ matrix.target }}.zip.sha256 + if-no-files-found: error + + release: + name: Create GitHub Release + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + merge-multiple: true + + - name: Copy install script + run: cp tdn-cli/scripts/install.sh artifacts/install.sh + + - name: List artifacts + run: ls -la artifacts/ + + - name: Extract version + id: version + run: echo "version=${GITHUB_REF#refs/tags/tdn-cli-v}" >> $GITHUB_OUTPUT + + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + name: tdn-cli v${{ steps.version.outputs.version }} + generate_release_notes: true + files: artifacts/* + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Trigger Homebrew update + if: ${{ !contains(github.ref, '-') }} # Skip for pre-releases (e.g., v1.0.0-beta) + uses: peter-evans/repository-dispatch@v3 + with: + token: ${{ secrets.HOMEBREW_TAP_TOKEN }} + repository: taskdn/homebrew-tdn + event-type: update-formula + client-payload: '{"version": "${{ steps.version.outputs.version }}"}' diff --git a/tdn-cli/scripts/install.sh b/tdn-cli/scripts/install.sh new file mode 100755 index 00000000..4629a41c --- /dev/null +++ b/tdn-cli/scripts/install.sh @@ -0,0 +1,141 @@ +#!/bin/bash +set -euo pipefail + +# tdn-cli installer +# Usage: curl -fsSL https://github.com/taskdn/taskdn/releases/latest/download/install.sh | bash +# +# Environment variables: +# TDN_VERSION - Version to install (default: latest) +# TDN_INSTALL_DIR - Installation directory (default: ~/.local/bin) +# TDN_SKIP_VERIFY - Set to 1 to skip checksum verification + +VERSION="${TDN_VERSION:-latest}" +INSTALL_DIR="${TDN_INSTALL_DIR:-$HOME/.local/bin}" +SKIP_VERIFY="${TDN_SKIP_VERIFY:-0}" + +REPO="taskdn/taskdn" + +# Colors (disabled if not a terminal) +if [ -t 1 ]; then + RED='\033[0;31m' + GREEN='\033[0;32m' + YELLOW='\033[1;33m' + NC='\033[0m' +else + RED='' + GREEN='' + YELLOW='' + NC='' +fi + +info() { echo -e "${GREEN}==>${NC} $1"; } +warn() { echo -e "${YELLOW}Warning:${NC} $1"; } +error() { echo -e "${RED}Error:${NC} $1" >&2; exit 1; } + +# Check for required commands +command -v curl >/dev/null 2>&1 || error "curl is required but not installed" +command -v tar >/dev/null 2>&1 || error "tar is required but not installed" + +# Detect platform +OS="$(uname -s)" +ARCH="$(uname -m)" + +case "$OS" in + Darwin) PLATFORM="darwin" ;; + Linux) PLATFORM="linux" ;; + *) error "Unsupported operating system: $OS (only macOS and Linux are supported)" ;; +esac + +case "$ARCH" in + x86_64|amd64) ARCH="x64" ;; + aarch64|arm64) ARCH="arm64" ;; + *) error "Unsupported architecture: $ARCH" ;; +esac + +TARGET="${PLATFORM}-${ARCH}" +info "Detected platform: ${TARGET}" + +# Get version +if [ "$VERSION" = "latest" ]; then + info "Fetching latest version..." + # Filter for tdn-cli releases specifically + VERSION=$(curl -sL "https://api.github.com/repos/${REPO}/releases" | \ + grep '"tag_name":' | \ + grep 'tdn-cli-v' | \ + head -1 | \ + sed -E 's/.*"tdn-cli-v([^"]+)".*/\1/') + if [ -z "$VERSION" ]; then + error "Failed to fetch latest version. Check your internet connection." + fi +fi +info "Version: ${VERSION}" + +# Construct download URLs +ARCHIVE="tdn-${TARGET}.tar.gz" +BASE_URL="https://github.com/${REPO}/releases/download/tdn-cli-v${VERSION}" +ARCHIVE_URL="${BASE_URL}/${ARCHIVE}" +CHECKSUM_URL="${BASE_URL}/${ARCHIVE}.sha256" + +# Create temp directory +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +# Download archive +info "Downloading ${ARCHIVE}..." +if ! curl -fsSL "$ARCHIVE_URL" -o "$TMPDIR/$ARCHIVE"; then + error "Failed to download ${ARCHIVE_URL}" +fi + +# Verify checksum +if [ "$SKIP_VERIFY" != "1" ]; then + info "Verifying checksum..." + if curl -fsSL "$CHECKSUM_URL" -o "$TMPDIR/checksum.sha256" 2>/dev/null; then + EXPECTED=$(cut -d' ' -f1 "$TMPDIR/checksum.sha256") + if command -v sha256sum >/dev/null 2>&1; then + ACTUAL=$(sha256sum "$TMPDIR/$ARCHIVE" | cut -d' ' -f1) + elif command -v shasum >/dev/null 2>&1; then + ACTUAL=$(shasum -a 256 "$TMPDIR/$ARCHIVE" | cut -d' ' -f1) + else + warn "Neither sha256sum nor shasum found, skipping verification" + ACTUAL="$EXPECTED" + fi + if [ "$EXPECTED" != "$ACTUAL" ]; then + error "Checksum mismatch!\n Expected: ${EXPECTED}\n Actual: ${ACTUAL}" + fi + info "Checksum verified" + else + warn "Could not download checksum file, skipping verification" + fi +fi + +# Extract +info "Extracting..." +tar -xzf "$TMPDIR/$ARCHIVE" -C "$TMPDIR" + +# Install +mkdir -p "$INSTALL_DIR" +mv "$TMPDIR/tdn" "$INSTALL_DIR/" +chmod +x "$INSTALL_DIR/tdn" + +# Verify installation +if [ -x "$INSTALL_DIR/tdn" ]; then + INSTALLED_VERSION=$("$INSTALL_DIR/tdn" --version 2>/dev/null || echo "unknown") + info "Successfully installed tdn v${INSTALLED_VERSION} to ${INSTALL_DIR}/tdn" +else + error "Installation failed - binary not executable" +fi + +# Check if in PATH +if ! command -v tdn >/dev/null 2>&1; then + echo "" + warn "${INSTALL_DIR} is not in your PATH" + echo "" + echo "Add this to your shell profile (~/.bashrc, ~/.zshrc, etc.):" + echo "" + echo " export PATH=\"\$PATH:${INSTALL_DIR}\"" + echo "" + echo "Then restart your shell or run: source ~/.bashrc" +fi + +echo "" +info "Run 'tdn --help' to get started" From a4af50e4d66b480f7082f0ef49c3ad0c063bd148 Mon Sep 17 00:00:00 2001 From: Danny Smith Date: Tue, 30 Dec 2025 23:21:42 +0000 Subject: [PATCH 05/14] Release process --- .github/workflows/release-cli.yml | 4 ++-- .../task-x-cli-distribution-and-releases.md | 20 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index f02fd90c..0e95d913 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -122,6 +122,6 @@ jobs: uses: peter-evans/repository-dispatch@v3 with: token: ${{ secrets.HOMEBREW_TAP_TOKEN }} - repository: taskdn/homebrew-tdn - event-type: update-formula + repository: dannysmith/homebrew-taproom + event-type: update-tdn-formula client-payload: '{"version": "${{ steps.version.outputs.version }}"}' diff --git a/docs/tasks-todo/task-x-cli-distribution-and-releases.md b/docs/tasks-todo/task-x-cli-distribution-and-releases.md index e3cfb6fe..b4452249 100644 --- a/docs/tasks-todo/task-x-cli-distribution-and-releases.md +++ b/docs/tasks-todo/task-x-cli-distribution-and-releases.md @@ -8,7 +8,7 @@ Set up a production-ready distribution system for `tdn-cli` using Bun's standalo ```bash # Homebrew (macOS/Linux) -brew install taskdn/tdn/tdn +brew install dannysmith/taproom/tdn # Direct download curl -fsSL https://github.com/taskdn/taskdn/releases/latest/download/install.sh | bash @@ -39,7 +39,7 @@ We tested both approaches: Before starting: -1. Create GitHub repository: `taskdn/homebrew-tdn` for Homebrew tap +1. Create GitHub repository: `dannysmith/homebrew-taproom` for Homebrew tap 2. Create GitHub PAT for Homebrew updates (see "GitHub Secrets Required" section) 3. (Optional) Set up domain for install script hosting @@ -374,9 +374,9 @@ jobs: Set up Homebrew distribution. -**4.1: Create homebrew-tdn repository** +**4.1: Create homebrew-taproom repository** -Create `taskdn/homebrew-tdn` repository with: +Create `dannysmith/homebrew-taproom` repository with: `Formula/tdn.rb`: ```ruby @@ -418,15 +418,15 @@ class Tdn < Formula end ``` -**4.2: Create auto-update workflow in homebrew-tdn repo** +**4.2: Create auto-update workflow in homebrew-taproom repo** -`.github/workflows/update-formula.yml`: +`.github/workflows/update-tdn-formula.yml`: ```yaml name: Update Formula on: repository_dispatch: - types: [update-formula] + types: [update-tdn-formula] permissions: contents: write @@ -748,7 +748,7 @@ tdn --version - [ ] Homebrew update is triggered (for non-pre-release tags) ### Phase 4: Homebrew -- [ ] `brew tap taskdn/tdn` works +- [ ] `brew tap dannysmith/taproom` works - [ ] `brew install tdn` downloads correct binary for platform - [ ] `tdn --version` works after install - [ ] Formula auto-update PR is created on new releases @@ -774,12 +774,12 @@ tdn --version | Secret | Purpose | How to Create | |--------|---------|---------------| -| `HOMEBREW_TAP_TOKEN` | Trigger updates in homebrew-tdn repo | Create a fine-grained PAT with `repo` scope for `taskdn/homebrew-tdn` only | +| `HOMEBREW_TAP_TOKEN` | Trigger updates in homebrew-taproom repo | Create a fine-grained PAT with `repo` scope for `dannysmith/homebrew-taproom` only | **Creating HOMEBREW_TAP_TOKEN:** 1. Go to GitHub Settings → Developer settings → Personal access tokens → Fine-grained tokens 2. Create new token with: - - Repository access: Only select repositories → `taskdn/homebrew-tdn` + - Repository access: Only select repositories → `dannysmith/homebrew-taproom` - Permissions: Contents (read/write), Pull requests (read/write) 3. Add as secret in `taskdn/taskdn` repo: Settings → Secrets → Actions → New repository secret From aa3256439f952e404f50ba56e7dbda5f38f26342 Mon Sep 17 00:00:00 2001 From: Danny Smith Date: Tue, 30 Dec 2025 23:24:46 +0000 Subject: [PATCH 06/14] add release process documentation --- tdn-cli/docs/developer/releases.md | 134 +++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 tdn-cli/docs/developer/releases.md diff --git a/tdn-cli/docs/developer/releases.md b/tdn-cli/docs/developer/releases.md new file mode 100644 index 00000000..b038ad00 --- /dev/null +++ b/tdn-cli/docs/developer/releases.md @@ -0,0 +1,134 @@ +# Releasing tdn-cli + +This document covers how to release new versions of the CLI. + +## Overview + +Releases are triggered by pushing a git tag in the format `tdn-cli-v`. The GitHub Actions workflow then: + +1. Builds binaries for all 5 platforms +2. Creates archives with SHA256 checksums +3. Publishes a GitHub Release with all assets +4. Triggers an auto-update PR in the Homebrew tap + +## Prerequisites + +Before your first release, ensure: + +1. **Homebrew tap exists**: `dannysmith/homebrew-taproom` with the formula and update workflow +2. **GitHub secret configured**: `HOMEBREW_TAP_TOKEN` in `taskdn/taskdn` repo settings + +## Release Process + +### 1. Prepare the release + +From the `tdn-cli/` directory: + +```bash +bun run release:prepare +``` + +For example: +```bash +bun run release:prepare 1.0.0 +``` + +This script: +- Updates version in `package.json` +- Updates version in `crates/core/Cargo.toml` +- Runs all checks (`bun run check`) +- Prompts to create a git commit and tag + +### 2. Push the tag + +If you didn't let the script create the commit/tag, do it manually: + +```bash +git add -A +git commit -m "chore(cli): release v1.0.0" +git tag tdn-cli-v1.0.0 +``` + +Then push: + +```bash +git push origin main +git push origin tdn-cli-v1.0.0 +``` + +### 3. Monitor the release + +1. Go to [Actions](https://github.com/taskdn/taskdn/actions) and watch the "Release - CLI" workflow +2. Once complete, check the [Releases](https://github.com/taskdn/taskdn/releases) page +3. Verify all assets are present: + - `tdn-darwin-arm64.tar.gz` + `.sha256` + - `tdn-darwin-x64.tar.gz` + `.sha256` + - `tdn-linux-arm64.tar.gz` + `.sha256` + - `tdn-linux-x64.tar.gz` + `.sha256` + - `tdn-windows-x64.zip` + `.sha256` + - `install.sh` + +### 4. Merge the Homebrew PR + +1. Go to [homebrew-taproom PRs](https://github.com/dannysmith/homebrew-taproom/pulls) +2. Review the auto-generated PR (version and checksums should be updated) +3. Merge it + +### 5. Verify installation + +Test that users can install: + +```bash +# Homebrew (if tap already added) +brew upgrade tdn + +# Or fresh install +brew install dannysmith/taproom/tdn + +# Verify +tdn --version +``` + +## Pre-releases + +For beta/RC releases, use a pre-release version format: + +```bash +bun run release:prepare 1.0.0-beta.1 +git tag tdn-cli-v1.0.0-beta.1 +git push origin tdn-cli-v1.0.0-beta.1 +``` + +Pre-releases (versions containing `-`) will: +- Create a GitHub Release (marked as pre-release) +- **Not** trigger the Homebrew formula update + +## Troubleshooting + +### Build fails on a specific platform + +Check the Actions logs. Common issues: +- Rust version mismatch (requires 1.85+) +- Missing dependencies on Linux ARM64 + +### Homebrew update not triggered + +- Verify `HOMEBREW_TAP_TOKEN` secret exists and hasn't expired +- Check it's not a pre-release version (contains `-`) +- Check the workflow logs for dispatch errors + +### Checksum mismatch in Homebrew + +The formula update workflow fetches checksums from the release. If they don't match: +1. Check the `.sha256` files in the GitHub Release +2. Manually update `Formula/tdn.rb` if needed + +## Local testing + +To build a release binary locally: + +```bash +./scripts/build-release.sh +``` + +This creates `dist/tdn--` for your current platform. From a60940279a699568d9f1181960cc4cf30d7947cd Mon Sep 17 00:00:00 2001 From: Danny Smith Date: Tue, 30 Dec 2025 23:33:36 +0000 Subject: [PATCH 07/14] Fix Builds --- .github/workflows/ci-cli.yml | 6 +++--- .github/workflows/release-cli.yml | 2 +- docs/tasks-todo/task-x-cli-distribution-and-releases.md | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-cli.yml b/.github/workflows/ci-cli.yml index 794e5a4f..7b3be5b2 100644 --- a/.github/workflows/ci-cli.yml +++ b/.github/workflows/ci-cli.yml @@ -26,7 +26,7 @@ jobs: steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 - - uses: dtolnay/rust-toolchain@1.85 + - uses: dtolnay/rust-toolchain@stable - run: bun install - run: bun run build # Build NAPI bindings - run: bun run check @@ -41,7 +41,7 @@ jobs: steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 - - uses: dtolnay/rust-toolchain@1.85 + - uses: dtolnay/rust-toolchain@stable - run: bun install - run: bun run build - run: bun run test @@ -66,7 +66,7 @@ jobs: steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 - - uses: dtolnay/rust-toolchain@1.85 + - uses: dtolnay/rust-toolchain@stable - name: Install dependencies run: bun install diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index 0e95d913..218bb405 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -36,7 +36,7 @@ jobs: steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 - - uses: dtolnay/rust-toolchain@1.85 + - uses: dtolnay/rust-toolchain@stable - name: Install dependencies run: bun install diff --git a/docs/tasks-todo/task-x-cli-distribution-and-releases.md b/docs/tasks-todo/task-x-cli-distribution-and-releases.md index b4452249..5abc1ef6 100644 --- a/docs/tasks-todo/task-x-cli-distribution-and-releases.md +++ b/docs/tasks-todo/task-x-cli-distribution-and-releases.md @@ -814,7 +814,7 @@ This is acceptable because: ### Rust Version -The project requires Rust 1.85+ (for edition 2024). The workflows pin to `dtolnay/rust-toolchain@1.85` to ensure consistent builds. Update this when bumping the MSRV. +The project requires Rust stable. The workflows use `dtolnay/rust-toolchain@stable` to ensure compatibility with NAPI-RS dependencies. ### Cross-Compilation Limitation From 7fddb648c76a1f575ca277b5fb423858aade7f9f Mon Sep 17 00:00:00 2001 From: Danny Smith Date: Tue, 30 Dec 2025 23:35:28 +0000 Subject: [PATCH 08/14] fix(ci): address CodeRabbit review feedback - Split upload artifact step for Unix/Windows file types - Fix working directory for release job steps - Improve checksum verification to not falsely report success - Quote variable expansion in version extraction --- .github/workflows/release-cli.yml | 15 +++++++++++++-- tdn-cli/scripts/install.sh | 11 ++++++----- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index 218bb405..896c607e 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -74,13 +74,22 @@ jobs: sha256sum tdn-${{ matrix.target }}.zip > tdn-${{ matrix.target }}.zip.sha256 rm tdn.exe - - name: Upload artifact + - name: Upload artifact (Unix) + if: matrix.target != 'windows-x64' uses: actions/upload-artifact@v4 with: name: tdn-${{ matrix.target }} path: | tdn-cli/dist/tdn-${{ matrix.target }}.tar.gz tdn-cli/dist/tdn-${{ matrix.target }}.tar.gz.sha256 + if-no-files-found: error + + - name: Upload artifact (Windows) + if: matrix.target == 'windows-x64' + uses: actions/upload-artifact@v4 + with: + name: tdn-${{ matrix.target }} + path: | tdn-cli/dist/tdn-${{ matrix.target }}.zip tdn-cli/dist/tdn-${{ matrix.target }}.zip.sha256 if-no-files-found: error @@ -99,14 +108,16 @@ jobs: merge-multiple: true - name: Copy install script + working-directory: . run: cp tdn-cli/scripts/install.sh artifacts/install.sh - name: List artifacts + working-directory: . run: ls -la artifacts/ - name: Extract version id: version - run: echo "version=${GITHUB_REF#refs/tags/tdn-cli-v}" >> $GITHUB_OUTPUT + run: echo "version=\"${GITHUB_REF#refs/tags/tdn-cli-v}\"" >> $GITHUB_OUTPUT - name: Create Release uses: softprops/action-gh-release@v2 diff --git a/tdn-cli/scripts/install.sh b/tdn-cli/scripts/install.sh index 4629a41c..b08bf205 100755 --- a/tdn-cli/scripts/install.sh +++ b/tdn-cli/scripts/install.sh @@ -91,18 +91,19 @@ if [ "$SKIP_VERIFY" != "1" ]; then info "Verifying checksum..." if curl -fsSL "$CHECKSUM_URL" -o "$TMPDIR/checksum.sha256" 2>/dev/null; then EXPECTED=$(cut -d' ' -f1 "$TMPDIR/checksum.sha256") + ACTUAL="" if command -v sha256sum >/dev/null 2>&1; then ACTUAL=$(sha256sum "$TMPDIR/$ARCHIVE" | cut -d' ' -f1) elif command -v shasum >/dev/null 2>&1; then ACTUAL=$(shasum -a 256 "$TMPDIR/$ARCHIVE" | cut -d' ' -f1) - else - warn "Neither sha256sum nor shasum found, skipping verification" - ACTUAL="$EXPECTED" fi - if [ "$EXPECTED" != "$ACTUAL" ]; then + if [ -z "$ACTUAL" ]; then + warn "No hash tool available (sha256sum or shasum), skipping verification" + elif [ "$EXPECTED" != "$ACTUAL" ]; then error "Checksum mismatch!\n Expected: ${EXPECTED}\n Actual: ${ACTUAL}" + else + info "Checksum verified" fi - info "Checksum verified" else warn "Could not download checksum file, skipping verification" fi From 52c5a08c51cf1ef8617478e11d74d27645a119a0 Mon Sep 17 00:00:00 2001 From: Danny Smith Date: Tue, 30 Dec 2025 23:46:01 +0000 Subject: [PATCH 09/14] Fixes --- .github/workflows/ci-cli.yml | 2 +- .github/workflows/release-cli.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-cli.yml b/.github/workflows/ci-cli.yml index 7b3be5b2..0c153db0 100644 --- a/.github/workflows/ci-cli.yml +++ b/.github/workflows/ci-cli.yml @@ -56,7 +56,7 @@ jobs: - target: darwin-arm64 runner: macos-latest - target: darwin-x64 - runner: macos-13 + runner: macos-15-intel - target: linux-x64 runner: ubuntu-latest - target: linux-arm64 diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index 896c607e..38abeea0 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -26,7 +26,7 @@ jobs: - target: darwin-arm64 runner: macos-latest - target: darwin-x64 - runner: macos-13 + runner: macos-15-intel - target: linux-x64 runner: ubuntu-latest - target: linux-arm64 From 0576d224ef9e2bfaea3f13079843be1d32498334 Mon Sep 17 00:00:00 2001 From: Danny Smith Date: Wed, 31 Dec 2025 00:01:28 +0000 Subject: [PATCH 10/14] Fixes --- .github/workflows/release-cli.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index 38abeea0..8f6551fc 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -117,7 +117,7 @@ jobs: - name: Extract version id: version - run: echo "version=\"${GITHUB_REF#refs/tags/tdn-cli-v}\"" >> $GITHUB_OUTPUT + run: echo "version=${GITHUB_REF#refs/tags/tdn-cli-v}" >> "$GITHUB_OUTPUT" - name: Create Release uses: softprops/action-gh-release@v2 From 0c8d93ffb06b397b0ec8eb15f08cf43051e551f3 Mon Sep 17 00:00:00 2001 From: Danny Smith Date: Wed, 31 Dec 2025 00:06:45 +0000 Subject: [PATCH 11/14] Fix test issues --- tdn-cli/tests/unit/config-security.test.ts | 24 ++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/tdn-cli/tests/unit/config-security.test.ts b/tdn-cli/tests/unit/config-security.test.ts index b70c4c82..428d100b 100644 --- a/tdn-cli/tests/unit/config-security.test.ts +++ b/tdn-cli/tests/unit/config-security.test.ts @@ -1,6 +1,7 @@ import { describe, test, expect, beforeEach, afterEach } from 'bun:test'; import { validateVaultPath } from '@/config/index.ts'; -import { platform } from 'os'; +import { platform, homedir } from 'os'; +import { join, sep } from 'path'; describe('config security', () => { describe('validateVaultPath', () => { @@ -33,10 +34,10 @@ describe('config security', () => { }); test('accepts absolute paths in home directory', () => { - const { homedir } = require('os'); const home = homedir(); - const result = validateVaultPath(`${home}/Documents/tasks`, 'tasksDir'); - expect(result).toBe(`${home}/Documents/tasks`); + const testPath = join(home, 'Documents', 'tasks'); + const result = validateVaultPath(testPath, 'tasksDir'); + expect(result).toBe(testPath); expect(warnCalls).toHaveLength(0); }); @@ -140,7 +141,13 @@ describe('config security', () => { test('resolves relative paths to absolute', () => { const result = validateVaultPath('./tasks', 'tasksDir'); - expect(result).toMatch(/^\/.*tasks$/); + // On Windows, absolute paths start with drive letter (e.g., C:\) + // On Unix, they start with / + if (platform() === 'win32') { + expect(result).toMatch(/^[A-Z]:\\.*tasks$/i); + } else { + expect(result).toMatch(/^\/.*tasks$/); + } }); test('includes pathType in error messages', () => { @@ -154,11 +161,12 @@ describe('config security', () => { }); test('handles paths with .. in the middle correctly', () => { - const { homedir } = require('os'); const home = homedir(); // A path like ~/foo/../bar should resolve to ~/bar, which is safe - const result = validateVaultPath(`${home}/foo/../bar`, 'tasksDir'); - expect(result).toBe(`${home}/bar`); + const testPath = join(home, 'foo', '..', 'bar'); + const expectedPath = join(home, 'bar'); + const result = validateVaultPath(testPath, 'tasksDir'); + expect(result).toBe(expectedPath); expect(warnCalls).toHaveLength(0); }); }); From a715acde1f9b4937c18fd6b74152fd255e2f50b1 Mon Sep 17 00:00:00 2001 From: Danny Smith Date: Wed, 31 Dec 2025 00:08:59 +0000 Subject: [PATCH 12/14] Tweaks --- .github/workflows/ci-cli.yml | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-cli.yml b/.github/workflows/ci-cli.yml index 0c153db0..629f9d2f 100644 --- a/.github/workflows/ci-cli.yml +++ b/.github/workflows/ci-cli.yml @@ -27,6 +27,9 @@ jobs: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 - uses: dtolnay/rust-toolchain@stable + - uses: swatinem/rust-cache@v2 + with: + workspaces: './tdn-cli/crates/core -> target' - run: bun install - run: bun run build # Build NAPI bindings - run: bun run check @@ -42,6 +45,9 @@ jobs: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 - uses: dtolnay/rust-toolchain@stable + - uses: swatinem/rust-cache@v2 + with: + workspaces: './tdn-cli/crates/core -> target' - run: bun install - run: bun run build - run: bun run test @@ -67,6 +73,9 @@ jobs: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 - uses: dtolnay/rust-toolchain@stable + - uses: swatinem/rust-cache@v2 + with: + workspaces: './tdn-cli/crates/core -> target' - name: Install dependencies run: bun install @@ -82,13 +91,17 @@ jobs: if: matrix.target == 'windows-x64' run: bun build --compile --minify src/index.ts --outfile dist/tdn-${{ matrix.target }}.exe - - name: Test binary (Unix) + - name: Smoke test binary (Unix) if: matrix.target != 'windows-x64' - run: ./dist/tdn-${{ matrix.target }} --version + run: | + ./dist/tdn-${{ matrix.target }} --version + ./dist/tdn-${{ matrix.target }} --help - - name: Test binary (Windows) + - name: Smoke test binary (Windows) if: matrix.target == 'windows-x64' - run: .\dist\tdn-${{ matrix.target }}.exe --version + run: | + .\dist\tdn-${{ matrix.target }}.exe --version + .\dist\tdn-${{ matrix.target }}.exe --help - name: Upload artifact uses: actions/upload-artifact@v4 From 548c31f3233685a3ba50555a04932b2b603f622f Mon Sep 17 00:00:00 2001 From: Danny Smith Date: Wed, 31 Dec 2025 00:25:51 +0000 Subject: [PATCH 13/14] Fix failing tests on Windows and Linux. --- tdn-cli/crates/core/src/writer.rs | 30 +++++++++++++++------- tdn-cli/src/config/index.ts | 5 ++-- tdn-cli/tests/e2e/list.test.ts | 8 +++--- tdn-cli/tests/e2e/modify.test.ts | 6 +++-- tdn-cli/tests/helpers/cli.ts | 9 +++++++ tdn-cli/tests/unit/bindings.test.ts | 4 +-- tdn-cli/tests/unit/config-security.test.ts | 11 ++++++-- 7 files changed, 52 insertions(+), 21 deletions(-) diff --git a/tdn-cli/crates/core/src/writer.rs b/tdn-cli/crates/core/src/writer.rs index f3895dc9..40967f4b 100644 --- a/tdn-cli/crates/core/src/writer.rs +++ b/tdn-cli/crates/core/src/writer.rs @@ -191,15 +191,27 @@ pub fn atomic_write(path: &Path, content: &str) -> Result<()> { })?; // Sync to disk (fsync) to ensure durability before rename - let file = fs::File::open(&temp_path).map_err(|e| { - TdnError::write_error( - &path_str, - format!("Failed to open temp file for sync: {}", e), - ) - })?; - file.sync_all().map_err(|e| { - TdnError::write_error(&path_str, format!("Failed to sync file to disk: {}", e)) - })?; + // On Windows, sync_all can fail with "Access is denied" in certain directories + // (especially temp directories). We make this non-fatal since the atomic rename + // is the primary durability mechanism. + if let Ok(file) = fs::File::open(&temp_path) { + if let Err(e) = file.sync_all() { + // On Windows, log but continue - the rename will still be atomic + #[cfg(target_os = "windows")] + { + // Silently ignore sync errors on Windows + let _ = e; + } + #[cfg(not(target_os = "windows"))] + { + return Err(TdnError::write_error( + &path_str, + format!("Failed to sync file to disk: {}", e), + ) + .into()); + } + } + } // Rename temp file to target (atomic on most filesystems) fs::rename(&temp_path, path).map_err(|e| { diff --git a/tdn-cli/src/config/index.ts b/tdn-cli/src/config/index.ts index 91902919..235304da 100644 --- a/tdn-cli/src/config/index.ts +++ b/tdn-cli/src/config/index.ts @@ -68,9 +68,10 @@ export function validateVaultPath(path: string, pathType: string = 'vault path') } // Warn if outside home directory (informational, not blocking) - // Exception: /var/folders is allowed (macOS temp directory) + // Exceptions: temp directories on macOS (/var/folders) and Linux (/tmp) const home = homedir(); - if (!absolutePath.startsWith(home) && !absolutePath.startsWith('/var/folders/')) { + const isTempDir = absolutePath.startsWith('/var/folders/') || absolutePath.startsWith('/tmp/'); + if (!absolutePath.startsWith(home) && !isTempDir) { console.warn( `Warning: ${pathType} is outside your home directory: ${absolutePath}\n` + `This may cause permission issues or affect system files.` diff --git a/tdn-cli/tests/e2e/list.test.ts b/tdn-cli/tests/e2e/list.test.ts index 17685103..928ead4f 100644 --- a/tdn-cli/tests/e2e/list.test.ts +++ b/tdn-cli/tests/e2e/list.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect } from 'bun:test'; -import { runCli } from '../helpers/cli'; +import { runCli, isArchivePath } from '../helpers/cli'; describe('tdn list', () => { describe('default behavior (active tasks)', () => { @@ -901,13 +901,13 @@ describe('tdn list --include-archived flag', () => { const { stdout, exitCode } = await runCli(['list', '--include-archived', '--json']); expect(exitCode).toBe(0); const output = JSON.parse(stdout); - expect(output.tasks.some((t: { path: string }) => t.path.includes('archive/'))).toBe(true); + expect(output.tasks.some((t: { path: string }) => isArchivePath(t.path))).toBe(true); }); test('does not include archived tasks by default', async () => { const { stdout } = await runCli(['list', '--json']); const output = JSON.parse(stdout); - expect(output.tasks.every((t: { path: string }) => !t.path.includes('archive/'))).toBe(true); + expect(output.tasks.every((t: { path: string }) => !isArchivePath(t.path))).toBe(true); }); }); @@ -917,7 +917,7 @@ describe('tdn list --only-archived flag', () => { expect(exitCode).toBe(0); const output = JSON.parse(stdout); expect(output.tasks.length).toBeGreaterThan(0); - expect(output.tasks.every((t: { path: string }) => t.path.includes('archive/'))).toBe(true); + expect(output.tasks.every((t: { path: string }) => isArchivePath(t.path))).toBe(true); }); }); diff --git a/tdn-cli/tests/e2e/modify.test.ts b/tdn-cli/tests/e2e/modify.test.ts index 1107792f..06f4f685 100644 --- a/tdn-cli/tests/e2e/modify.test.ts +++ b/tdn-cli/tests/e2e/modify.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect, beforeEach, afterEach } from 'bun:test'; -import { runCli } from '../helpers/cli'; +import { runCli, isArchivePath } from '../helpers/cli'; import { mkdtempSync, rmSync, existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'; import { tmpdir } from 'os'; import { join } from 'path'; @@ -382,7 +382,9 @@ describe('tdn archive', () => { expect(exitCode).toBe(0); const output = JSON.parse(stdout); - expect(output.to).toContain('archive/test-task.md'); + // Check archive path in cross-platform way (Windows uses backslashes) + expect(isArchivePath(output.to)).toBe(true); + expect(output.to).toMatch(/test-task\.md$/); // Original file should not exist expect(existsSync(taskPath)).toBe(false); diff --git a/tdn-cli/tests/helpers/cli.ts b/tdn-cli/tests/helpers/cli.ts index a93f9141..0fa44ab6 100644 --- a/tdn-cli/tests/helpers/cli.ts +++ b/tdn-cli/tests/helpers/cli.ts @@ -93,3 +93,12 @@ export async function runCli( export function fixturePath(relativePath: string): string { return resolve(import.meta.dir, '../fixtures', relativePath); } + +/** + * Check if a path contains an archive directory segment. + * Works on both Windows (backslash) and Unix (forward slash) paths. + */ +export function isArchivePath(path: string): boolean { + // Match 'archive/' or 'archive\' in the path + return /archive[/\\]/.test(path); +} diff --git a/tdn-cli/tests/unit/bindings.test.ts b/tdn-cli/tests/unit/bindings.test.ts index 3c194bed..d831c852 100644 --- a/tdn-cli/tests/unit/bindings.test.ts +++ b/tdn-cli/tests/unit/bindings.test.ts @@ -7,7 +7,7 @@ import { scanAreas, } from '@bindings'; import type { VaultConfig } from '@bindings'; -import { fixturePath } from '../helpers/cli'; +import { fixturePath, isArchivePath } from '../helpers/cli'; describe('NAPI bindings', () => { describe('parseTaskFile', () => { @@ -138,7 +138,7 @@ describe('NAPI bindings', () => { test('does not include files from subdirectories', () => { // The archive/ subdirectory is not scanned const tasks = scanTasks(config); - const archiveTasks = tasks.filter((t) => t.path.includes('archive/')); + const archiveTasks = tasks.filter((t) => isArchivePath(t.path)); expect(archiveTasks.length).toBe(0); }); }); diff --git a/tdn-cli/tests/unit/config-security.test.ts b/tdn-cli/tests/unit/config-security.test.ts index 428d100b..c0a4b157 100644 --- a/tdn-cli/tests/unit/config-security.test.ts +++ b/tdn-cli/tests/unit/config-security.test.ts @@ -130,10 +130,17 @@ describe('config security', () => { ).toThrow('system directory'); }); - test('warns when path is outside home directory but not system dir', () => { - // /tmp is outside home but not a protected system directory + test('accepts /tmp without warning (Linux temp directory)', () => { + // /tmp is the standard temp directory on Linux, should be allowed like /var/folders on macOS const result = validateVaultPath('/tmp/tasks', 'tasksDir'); expect(result).toBe('/tmp/tasks'); + expect(warnCalls).toHaveLength(0); + }); + + test('warns when path is outside home directory and not a temp dir', () => { + // /opt is outside home and not a temp directory + const result = validateVaultPath('/opt/tasks', 'tasksDir'); + expect(result).toBe('/opt/tasks'); expect(warnCalls.length).toBeGreaterThan(0); expect(warnCalls[0]).toContain('outside your home directory'); }); From 85000c86154e8b0fd172ad39e7fbf0926ec4115f Mon Sep 17 00:00:00 2001 From: Danny Smith Date: Wed, 31 Dec 2025 00:43:39 +0000 Subject: [PATCH 14/14] Fix Windows CI: use Path::join() for cross-platform path construction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rust tests were using format!("{}/file.md", dir) which creates mixed path separators on Windows (e.g., C:\Users\...\tasks/task1.md). Changed to use Path::new().join() which uses the correct separator for each platform. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- tdn-cli/crates/core/src/vault_index.rs | 30 +++++++++++++------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/tdn-cli/crates/core/src/vault_index.rs b/tdn-cli/crates/core/src/vault_index.rs index 5f23cb02..d3037eb6 100644 --- a/tdn-cli/crates/core/src/vault_index.rs +++ b/tdn-cli/crates/core/src/vault_index.rs @@ -1083,7 +1083,7 @@ mod tests { "q1.md", "---\ntitle: Q1\narea: \"[[Work]]\"\n---\n", ); - let task_path = format!("{}/task1.md", config.tasks_dir); + let task_path = Path::new(&config.tasks_dir).join("task1.md"); write_file( Path::new(&config.tasks_dir), "task1.md", @@ -1091,7 +1091,7 @@ mod tests { ); let session = create_vault_session(config.clone()); - let result = get_task_context(&session, task_path); + let result = get_task_context(&session, task_path.to_string_lossy().to_string()); assert!(result.task.is_some()); assert_eq!(result.task.unwrap().title, "Task One"); @@ -1125,7 +1125,7 @@ mod tests { "work.md", "---\ntitle: Work\n---\n", ); - let task_path = format!("{}/task1.md", config.tasks_dir); + let task_path = Path::new(&config.tasks_dir).join("task1.md"); write_file( Path::new(&config.tasks_dir), "task1.md", @@ -1133,7 +1133,7 @@ mod tests { ); let session = create_vault_session(config.clone()); - let result = get_task_context(&session, task_path); + let result = get_task_context(&session, task_path.to_string_lossy().to_string()); assert!(result.task.is_some()); assert!(result.project.is_none()); // No project reference @@ -1146,7 +1146,7 @@ mod tests { let temp_dir = create_temp_vault(); let config = create_vault_config(&temp_dir); - let task_path = format!("{}/task1.md", config.tasks_dir); + let task_path = Path::new(&config.tasks_dir).join("task1.md"); write_file( Path::new(&config.tasks_dir), "task1.md", @@ -1154,7 +1154,7 @@ mod tests { ); let session = create_vault_session(config.clone()); - let result = get_task_context(&session, task_path); + let result = get_task_context(&session, task_path.to_string_lossy().to_string()); assert!(result.task.is_some()); assert!(result.project.is_none()); @@ -1167,7 +1167,7 @@ mod tests { let temp_dir = create_temp_vault(); let config = create_vault_config(&temp_dir); - let task_path = format!("{}/task1.md", config.tasks_dir); + let task_path = Path::new(&config.tasks_dir).join("task1.md"); write_file( Path::new(&config.tasks_dir), "task1.md", @@ -1175,7 +1175,7 @@ mod tests { ); let session = create_vault_session(config.clone()); - let result = get_task_context(&session, task_path); + let result = get_task_context(&session, task_path.to_string_lossy().to_string()); assert!(result.task.is_some()); assert!(result.project.is_none()); @@ -1188,7 +1188,7 @@ mod tests { let temp_dir = create_temp_vault(); let config = create_vault_config(&temp_dir); - let task_path = format!("{}/task1.md", config.tasks_dir); + let task_path = Path::new(&config.tasks_dir).join("task1.md"); write_file( Path::new(&config.tasks_dir), "task1.md", @@ -1196,7 +1196,7 @@ mod tests { ); let session = create_vault_session(config.clone()); - let result = get_task_context(&session, task_path); + let result = get_task_context(&session, task_path.to_string_lossy().to_string()); assert!(result.task.is_some()); assert!(result.area.is_none()); @@ -1224,7 +1224,7 @@ mod tests { "q1.md", "---\ntitle: Q1\narea: \"[[Work]]\"\n---\n", ); - let task_path = format!("{}/task1.md", config.tasks_dir); + let task_path = Path::new(&config.tasks_dir).join("task1.md"); // Task has direct area "Personal" but project is in "Work" write_file( Path::new(&config.tasks_dir), @@ -1233,7 +1233,7 @@ mod tests { ); let session = create_vault_session(config.clone()); - let result = get_task_context(&session, task_path); + let result = get_task_context(&session, task_path.to_string_lossy().to_string()); assert!(result.task.is_some()); assert!(result.project.is_some()); @@ -1315,16 +1315,16 @@ mod tests { let temp_dir = create_temp_vault(); let config = create_vault_config(&temp_dir); - let task_path = format!("{}/my-task.md", config.tasks_dir); + let task_path = Path::new(&config.tasks_dir).join("my-task.md"); write_file( Path::new(&config.tasks_dir), "my-task.md", "---\ntitle: My Task\nstatus: ready\n---\n", ); - // Look up by absolute path (starts with /) + // Look up by absolute path (starts with / on Unix, drive letter on Windows) let session = create_vault_session(config.clone()); - let result = get_task_context(&session, task_path); + let result = get_task_context(&session, task_path.to_string_lossy().to_string()); assert!(result.task.is_some()); assert_eq!(result.task.unwrap().title, "My Task");