diff --git a/.github/workflows/release.yml b/.github/workflows/js.yml similarity index 75% rename from .github/workflows/release.yml rename to .github/workflows/js.yml index eabf806..d526a8e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/js.yml @@ -1,14 +1,19 @@ -name: Checks and release +name: JS Checks and Release on: push: branches: - main + paths: + - 'js/**' + - 'scripts/detect-code-changes.mjs' + - '.github/workflows/js.yml' pull_request: types: [opened, synchronize, reopened] - # Manual release support - consolidated here to work with npm trusted publishing - # npm only allows ONE workflow file as trusted publisher, so all publishing - # must go through this workflow (release.yml) + paths: + - 'js/**' + - 'scripts/detect-code-changes.mjs' + - '.github/workflows/js.yml' workflow_dispatch: inputs: release_mode: @@ -34,12 +39,43 @@ on: concurrency: ${{ github.workflow }}-${{ github.ref }} +defaults: + run: + working-directory: js + jobs: - # Changeset check - only runs on PRs + # === DETECT CHANGES - determines which jobs should run === + detect-changes: + name: Detect Changes + runs-on: ubuntu-latest + if: github.event_name != 'workflow_dispatch' + outputs: + mjs-changed: ${{ steps.changes.outputs.mjs-changed }} + js-changed: ${{ steps.changes.outputs.js-changed }} + package-changed: ${{ steps.changes.outputs.package-changed }} + docs-changed: ${{ steps.changes.outputs.docs-changed }} + workflow-changed: ${{ steps.changes.outputs.workflow-changed }} + any-code-changed: ${{ steps.changes.outputs.any-code-changed }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Detect changes + id: changes + working-directory: . + env: + GITHUB_EVENT_NAME: ${{ github.event_name }} + GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }} + GITHUB_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: node scripts/detect-code-changes.mjs --lang js + + # === CHANGESET CHECK - only runs on PRs with code changes === changeset-check: name: Check for Changesets runs-on: ubuntu-latest - if: github.event_name == 'pull_request' + needs: [detect-changes] + if: github.event_name == 'pull_request' && needs.detect-changes.outputs.any-code-changed == 'true' steps: - uses: actions/checkout@v4 with: @@ -54,6 +90,10 @@ jobs: run: npm install - name: Check for changesets + env: + GITHUB_BASE_REF: ${{ github.base_ref }} + GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }} + GITHUB_HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: | # Skip changeset check for automated version PRs if [[ "${{ github.head_ref }}" == "changeset-release/"* ]]; then @@ -64,12 +104,18 @@ jobs: # Run changeset validation script node scripts/validate-changeset.mjs - # Linting and formatting - runs after changeset check on PRs, immediately on main + # === LINT AND FORMAT CHECK === lint: name: Lint and Format Check runs-on: ubuntu-latest - needs: [changeset-check] - if: always() && (github.event_name == 'push' || needs.changeset-check.result == 'success') + needs: [detect-changes] + if: | + github.event_name == 'push' || + needs.detect-changes.outputs.mjs-changed == 'true' || + needs.detect-changes.outputs.js-changed == 'true' || + needs.detect-changes.outputs.docs-changed == 'true' || + needs.detect-changes.outputs.package-changed == 'true' || + needs.detect-changes.outputs.workflow-changed == 'true' steps: - uses: actions/checkout@v4 @@ -94,8 +140,8 @@ jobs: test: name: Test (${{ matrix.runtime }} on ${{ matrix.os }}) runs-on: ${{ matrix.os }} - needs: [changeset-check] - if: always() && (github.event_name == 'push' || needs.changeset-check.result == 'success') + needs: [detect-changes, changeset-check] + if: always() && (github.event_name == 'push' || needs.changeset-check.result == 'success' || needs.changeset-check.result == 'skipped') strategy: fail-fast: false matrix: @@ -138,6 +184,10 @@ jobs: with: deno-version: v2.x + - name: Install dependencies (Deno) + if: matrix.runtime == 'deno' + run: deno install + - name: Run tests (Deno) if: matrix.runtime == 'deno' run: deno test --allow-read --allow-write @@ -146,11 +196,8 @@ jobs: release: name: Release needs: [lint, test] - # Use always() to ensure this job runs even if changeset-check was skipped - # This is needed because lint/test jobs have a transitive dependency on changeset-check if: always() && github.ref == 'refs/heads/main' && github.event_name == 'push' && needs.lint.result == 'success' && needs.test.result == 'success' runs-on: ubuntu-latest - # Permissions required for npm OIDC trusted publishing permissions: contents: write pull-requests: write @@ -186,7 +233,6 @@ jobs: run: node scripts/version-and-commit.mjs --mode changeset - name: Publish to npm - # Run if version was committed OR if a previous attempt already committed (for re-runs) if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' id: publish run: node scripts/publish-to-npm.mjs --should-pull @@ -203,14 +249,11 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: node scripts/format-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}" --commit-sha "${{ github.sha }}" - # Manual Instant Release - triggered via workflow_dispatch with instant mode - # This job is in release.yml because npm trusted publishing - # only allows one workflow file to be registered as a trusted publisher + # Manual Instant Release instant-release: name: Instant Release if: github.event_name == 'workflow_dispatch' && github.event.inputs.release_mode == 'instant' runs-on: ubuntu-latest - # Permissions required for npm OIDC trusted publishing permissions: contents: write pull-requests: write @@ -237,7 +280,6 @@ jobs: run: node scripts/version-and-commit.mjs --mode instant --bump-type "${{ github.event.inputs.bump_type }}" --description "${{ github.event.inputs.description }}" - name: Publish to npm - # Run if version was committed OR if a previous attempt already committed (for re-runs) if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' id: publish run: node scripts/publish-to-npm.mjs @@ -254,7 +296,7 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: node scripts/format-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}" --commit-sha "${{ github.sha }}" - # Manual Changeset PR - creates a pull request with the changeset for review + # Manual Changeset PR changeset-pr: name: Create Changeset PR if: github.event_name == 'workflow_dispatch' && github.event.inputs.release_mode == 'changeset-pr' @@ -280,23 +322,21 @@ jobs: - name: Format changeset with Prettier run: | - # Run Prettier on the changeset file to ensure it matches project style npx prettier --write ".changeset/*.md" || true - echo "Formatted changeset files" - name: Create Pull Request uses: peter-evans/create-pull-request@v7 with: token: ${{ secrets.GITHUB_TOKEN }} - commit-message: 'chore: add changeset for manual ${{ github.event.inputs.bump_type }} release' - branch: changeset-manual-release-${{ github.run_id }} + commit-message: 'chore(js): add changeset for manual ${{ github.event.inputs.bump_type }} release' + branch: changeset-manual-js-release-${{ github.run_id }} delete-branch: true - title: 'chore: manual ${{ github.event.inputs.bump_type }} release' + title: 'chore(js): manual ${{ github.event.inputs.bump_type }} release' body: | - ## Manual Release Request + ## Manual Release Request (JavaScript) - This PR was created by a manual workflow trigger to prepare a **${{ github.event.inputs.bump_type }}** release. + This PR was created by a manual workflow trigger to prepare a **${{ github.event.inputs.bump_type }}** release for the JavaScript package. ### Release Details - **Type:** ${{ github.event.inputs.bump_type }} @@ -306,5 +346,4 @@ jobs: ### Next Steps 1. Review the changeset in this PR 2. Merge this PR to main - 3. The automated release workflow will create a version PR - 4. Merge the version PR to publish to npm and create a GitHub release + 3. The automated release workflow will publish to npm and create a GitHub release diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..09ad7a3 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,352 @@ +name: Rust CI/CD Pipeline + +on: + push: + branches: + - main + paths: + - 'rust/**' + - 'scripts/detect-code-changes.mjs' + - '.github/workflows/rust.yml' + pull_request: + types: [opened, synchronize, reopened] + paths: + - 'rust/**' + - 'scripts/detect-code-changes.mjs' + - '.github/workflows/rust.yml' + workflow_dispatch: + inputs: + bump_type: + description: 'Version bump type' + required: true + type: choice + options: + - patch + - minor + - major + description: + description: 'Release description (optional)' + required: false + type: string + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + RUSTFLAGS: -Dwarnings + +defaults: + run: + working-directory: rust + +jobs: + # === DETECT CHANGES - determines which jobs should run === + detect-changes: + name: Detect Changes + runs-on: ubuntu-latest + if: github.event_name != 'workflow_dispatch' + outputs: + rs-changed: ${{ steps.changes.outputs.rs-changed }} + toml-changed: ${{ steps.changes.outputs.toml-changed }} + mjs-changed: ${{ steps.changes.outputs.mjs-changed }} + docs-changed: ${{ steps.changes.outputs.docs-changed }} + workflow-changed: ${{ steps.changes.outputs.workflow-changed }} + any-code-changed: ${{ steps.changes.outputs.any-code-changed }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + + - name: Detect changes + id: changes + working-directory: . + env: + GITHUB_EVENT_NAME: ${{ github.event_name }} + GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }} + GITHUB_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: node scripts/detect-code-changes.mjs --lang rust + + # === CHANGELOG CHECK - only runs on PRs with code changes === + changelog: + name: Changelog Fragment Check + runs-on: ubuntu-latest + needs: [detect-changes] + if: github.event_name == 'pull_request' && needs.detect-changes.outputs.any-code-changed == 'true' + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check for changelog fragments + run: | + # Get list of fragment files (excluding README and template) + FRAGMENTS=$(find changelog.d -name "*.md" ! -name "README.md" 2>/dev/null | wc -l) + + # Get changed files in PR + CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD) + + # Check if any source files changed (excluding docs and config) + SOURCE_CHANGED=$(echo "$CHANGED_FILES" | grep -E "^rust/(src/|tests/|scripts/|Cargo\.toml)" | wc -l) + + if [ "$SOURCE_CHANGED" -gt 0 ] && [ "$FRAGMENTS" -eq 0 ]; then + echo "::warning::No changelog fragment found. Please add a changelog entry in rust/changelog.d/" + echo "" + echo "To create a changelog fragment:" + echo " Create a new .md file in rust/changelog.d/ with your changes" + echo "" + echo "See rust/changelog.d/README.md for more information." + exit 0 + fi + + echo "Changelog check passed" + + # === LINT AND FORMAT CHECK === + lint: + name: Lint and Format Check + runs-on: ubuntu-latest + needs: [detect-changes] + if: | + github.event_name == 'push' || + github.event_name == 'workflow_dispatch' || + needs.detect-changes.outputs.rs-changed == 'true' || + needs.detect-changes.outputs.toml-changed == 'true' || + needs.detect-changes.outputs.mjs-changed == 'true' || + needs.detect-changes.outputs.docs-changed == 'true' || + needs.detect-changes.outputs.workflow-changed == 'true' + steps: + - uses: actions/checkout@v4 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + rust/target + key: ${{ runner.os }}-cargo-${{ hashFiles('rust/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Check formatting + run: cargo fmt --all -- --check + + - name: Run Clippy + run: cargo clippy --all-targets --all-features + + - name: Check file size limit + run: node scripts/check-file-size.mjs + + # === TEST === + test: + name: Test (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + needs: [detect-changes, changelog] + if: always() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || needs.changelog.result == 'success' || needs.changelog.result == 'skipped') + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + steps: + - uses: actions/checkout@v4 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + rust/target + key: ${{ runner.os }}-cargo-${{ hashFiles('rust/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Run tests + run: cargo test --all-features --verbose + + - name: Run doc tests + run: cargo test --doc --verbose + + # === BUILD === + build: + name: Build Package + runs-on: ubuntu-latest + needs: [lint, test] + if: always() && needs.lint.result == 'success' && needs.test.result == 'success' + steps: + - uses: actions/checkout@v4 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + rust/target + key: ${{ runner.os }}-cargo-build-${{ hashFiles('rust/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-build- + + - name: Build release + run: cargo build --release --verbose + + - name: Check package + run: cargo package --list + + # === AUTO RELEASE === + auto-release: + name: Auto Release + needs: [lint, test, build] + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Determine bump type from changelog fragments + id: bump_type + run: node scripts/get-bump-type.mjs + + - name: Check if version already released or no fragments + id: check + run: | + if [ "${{ steps.bump_type.outputs.has_fragments }}" != "true" ]; then + CURRENT_VERSION=$(grep -Po '(?<=^version = ")[^"]*' Cargo.toml) + if git rev-parse "rust-v$CURRENT_VERSION" >/dev/null 2>&1; then + echo "No changelog fragments and rust-v$CURRENT_VERSION already released" + echo "should_release=false" >> $GITHUB_OUTPUT + else + echo "No changelog fragments but rust-v$CURRENT_VERSION not yet released" + echo "should_release=true" >> $GITHUB_OUTPUT + echo "skip_bump=true" >> $GITHUB_OUTPUT + fi + else + echo "Found changelog fragments, proceeding with release" + echo "should_release=true" >> $GITHUB_OUTPUT + echo "skip_bump=false" >> $GITHUB_OUTPUT + fi + + - name: Collect changelog and bump version + id: version + if: steps.check.outputs.should_release == 'true' && steps.check.outputs.skip_bump != 'true' + run: | + node scripts/version-and-commit.mjs \ + --bump-type "${{ steps.bump_type.outputs.bump_type }}" + + - name: Get current version + id: current_version + if: steps.check.outputs.should_release == 'true' + run: | + CURRENT_VERSION=$(grep -Po '(?<=^version = ")[^"]*' Cargo.toml) + echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT + + - name: Build release + if: steps.check.outputs.should_release == 'true' + run: cargo build --release + + - name: Create GitHub Release + if: steps.check.outputs.should_release == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + node scripts/create-github-release.mjs \ + --release-version "${{ steps.current_version.outputs.version }}" \ + --repository "${{ github.repository }}" \ + --tag-prefix "rust-v" + + # === MANUAL RELEASE === + manual-release: + name: Manual Release + needs: [lint, test, build] + if: github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Collect changelog fragments + run: | + FRAGMENTS=$(find changelog.d -name "*.md" ! -name "README.md" 2>/dev/null | wc -l) + if [ "$FRAGMENTS" -gt 0 ]; then + echo "Found $FRAGMENTS changelog fragment(s), collecting..." + node scripts/collect-changelog.mjs + else + echo "No changelog fragments found, skipping collection" + fi + + - name: Version and commit + id: version + run: | + node scripts/version-and-commit.mjs \ + --bump-type "${{ github.event.inputs.bump_type }}" \ + --description "${{ github.event.inputs.description }}" + + - name: Build release + if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' + run: cargo build --release + + - name: Create GitHub Release + if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + node scripts/create-github-release.mjs \ + --release-version "${{ steps.version.outputs.new_version }}" \ + --repository "${{ github.repository }}" \ + --tag-prefix "rust-v" diff --git a/.gitignore b/.gitignore index 4bbcce4..dd0a1fd 100644 --- a/.gitignore +++ b/.gitignore @@ -142,3 +142,13 @@ dist # Vite logs files vite.config.js.timestamp-* vite.config.ts.timestamp-* + +# Rust +rust/target/ +rust/Cargo.lock +*.rs.bk +*.rlib +*.rmeta +*.so +*.dylib +*.dll diff --git a/README.md b/README.md index 442a300..5328a87 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # lino-env -A JavaScript library to operate .lenv files - an alternative to .env files that uses `: ` (colon-space) instead of `=` for key-value separation and supports duplicate keys. +A library to operate .lenv files - an alternative to .env files that uses `: ` (colon-space) instead of `=` for key-value separation and supports duplicate keys. + +**Available in both JavaScript and Rust!** ## What are .lenv files? @@ -18,13 +20,22 @@ API_KEY: abc123 The key difference is the use of `: ` separator, which aligns with [links-notation](https://github.com/link-foundation/links-notation) format. Additionally, .lenv files support duplicate keys, where multiple instances of the same key can exist. -## Installation +## Packages + +| Package | Language | Directory | Status | +| ------------------ | ---------- | --------- | --------------------------------------------------------------------------------------- | +| [lino-env](./js) | JavaScript | `./js` | [![npm](https://img.shields.io/npm/v/lino-env)](https://www.npmjs.com/package/lino-env) | +| [lino-env](./rust) | Rust | `./rust` | Coming soon | + +## JavaScript Package + +### Installation ```bash npm install lino-env ``` -## Quick Start +### Quick Start ```bash # create .lenv file @@ -43,9 +54,7 @@ Output: Hello World ``` -## Usage - -### ESM (import) +### Usage ```javascript import linoenv from 'lino-env'; @@ -54,294 +63,41 @@ linoenv.config(); console.log(`Hello ${process.env.HELLO}`); ``` -### CommonJS (require) - -```javascript -require('lino-env').config(); - -console.log(`Hello ${process.env.HELLO}`); -``` - -## API Reference - -### Dotenvx-like API - -These functions provide a simple API similar to dotenvx for common use cases: - -#### `config(options)` - -Load .lenv file and inject into process.env - -```javascript -import linoenv from 'lino-env'; - -linoenv.config(); // loads .lenv - -// or specify a custom path -linoenv.config({ path: '.lenv.production' }); -``` - -Returns: `{ parsed: Object }` - Object containing parsed key-value pairs - -#### `get(key, options)` - -Get a value from the loaded .lenv file - -```javascript -import linoenv from 'lino-env'; - -const value = linoenv.get('API_KEY'); - -// or from a specific file -const value = linoenv.get('API_KEY', { path: '.lenv.production' }); -``` - -Returns: `string | undefined` - -#### `set(key, value, options)` - -Set a value in a .lenv file - -```javascript -import linoenv from 'lino-env'; - -linoenv.set('API_KEY', 'new_value'); - -// or to a specific file -linoenv.set('API_KEY', 'new_value', { path: '.lenv.production' }); -``` - -### Named Exports +For full API documentation, see the [JavaScript README](./js/README.md) (coming soon) or the source code. -You can also use named exports for tree-shaking: +## Rust Package -```javascript -import { config, get, set } from 'lino-env'; - -config(); -console.log(get('HELLO')); -set('HELLO', 'Universe'); -``` - -### Class: LinoEnv - -The main class for reading and writing .lenv files. - -#### Constructor - -```javascript -import { LinoEnv } from 'lino-env'; - -const env = new LinoEnv('.lenv'); -``` - -#### Methods - -##### `read()` - -Reads and parses the .lenv file. If the file doesn't exist, initializes with empty data. - -```javascript -const env = new LinoEnv('.lenv'); -env.read(); -``` - -Returns: `this` (for method chaining) - -##### `write()` - -Writes the current data back to the .lenv file. - -```javascript -env.set('API_KEY', 'value').write(); -``` - -Returns: `this` (for method chaining) - -##### `get(reference)` - -Gets the last instance of a reference (key). - -```javascript -env.add('API_KEY', 'value1'); -env.add('API_KEY', 'value2'); -console.log(env.get('API_KEY')); // 'value2' -``` - -Returns: `string | undefined` - -##### `getAll(reference)` +### Installation -Gets all instances of a reference (key). +Add this to your `Cargo.toml`: -```javascript -env.add('API_KEY', 'value1'); -env.add('API_KEY', 'value2'); -console.log(env.getAll('API_KEY')); // ['value1', 'value2'] -``` - -Returns: `string[]` - -##### `set(reference, value)` - -Sets a reference to a single value, replacing all existing instances. - -```javascript -env.set('API_KEY', 'new_value'); +```toml +[dependencies] +lino-env = "0.1" ``` -Returns: `this` (for method chaining) - -##### `add(reference, value)` +### Quick Start -Adds a new instance of a reference, allowing duplicates. +```rust +use lino_env::LinoEnv; -```javascript -env.add('API_KEY', 'value1'); -env.add('API_KEY', 'value2'); // Now there are 2 instances -``` +// Create and write a new .lenv file +let mut env = LinoEnv::new(".lenv"); +env.set("GITHUB_TOKEN", "gh_abc123"); +env.set("API_KEY", "my_api_key"); +env.write().unwrap(); -Returns: `this` (for method chaining) +// Read an existing .lenv file +let mut env = LinoEnv::new(".lenv"); +env.read().unwrap(); -##### `has(reference)` - -Checks if a reference exists. - -```javascript -if (env.has('API_KEY')) { - console.log('API_KEY exists'); +// Get a value +if let Some(token) = env.get("GITHUB_TOKEN") { + println!("Token: {}", token); } ``` -Returns: `boolean` - -##### `delete(reference)` - -Deletes all instances of a reference. - -```javascript -env.delete('API_KEY'); -``` - -Returns: `this` (for method chaining) - -##### `keys()` - -Gets all keys. - -```javascript -console.log(env.keys()); // ['GITHUB_TOKEN', 'API_KEY', ...] -``` - -Returns: `string[]` - -##### `toObject()` - -Converts to a plain object with the last instance of each key. - -```javascript -env.add('KEY1', 'value1a'); -env.add('KEY1', 'value1b'); -env.set('KEY2', 'value2'); - -console.log(env.toObject()); -// { KEY1: 'value1b', KEY2: 'value2' } -``` - -Returns: `Object` - -### Convenience Functions - -#### `readLinoEnv(filePath)` - -Convenience function to read a .lenv file. - -```javascript -import { readLinoEnv } from 'lino-env'; - -const env = readLinoEnv('.lenv'); -console.log(env.get('GITHUB_TOKEN')); -``` - -Returns: `LinoEnv` - -#### `writeLinoEnv(filePath, data)` - -Convenience function to create and write a .lenv file from an object. - -```javascript -import { writeLinoEnv } from 'lino-env'; - -writeLinoEnv('.lenv', { - API_KEY: 'test_key', - SECRET: 'test_secret', -}); -``` - -Returns: `LinoEnv` - -## Usage Examples - -### Basic Usage - -```javascript -import { LinoEnv } from 'lino-env'; - -// Create and write -const env = new LinoEnv('.lenv'); -env.set('GITHUB_TOKEN', 'gh_test123'); -env.set('TELEGRAM_TOKEN', '054test456'); -env.write(); - -// Read -const env2 = new LinoEnv('.lenv'); -env2.read(); -console.log(env2.get('GITHUB_TOKEN')); // 'gh_test123' -``` - -### Working with Duplicates - -```javascript -import { LinoEnv } from 'lino-env'; - -const env = new LinoEnv('.lenv'); - -// Add multiple instances of the same key -env.add('SERVER', 'server1.example.com'); -env.add('SERVER', 'server2.example.com'); -env.add('SERVER', 'server3.example.com'); - -// Get the last one -console.log(env.get('SERVER')); // 'server3.example.com' - -// Get all instances -console.log(env.getAll('SERVER')); -// ['server1.example.com', 'server2.example.com', 'server3.example.com'] - -env.write(); // Persist to file -``` - -### Method Chaining - -```javascript -import { LinoEnv } from 'lino-env'; - -new LinoEnv('.lenv').set('API_KEY', 'abc123').set('SECRET', 'xyz789').write(); -``` - -### Handling Special Characters - -The library correctly handles values with colons, spaces, and other special characters: - -```javascript -env.set('URL', 'https://example.com:8080'); -env.set('MESSAGE', 'Hello World'); -env.write(); - -const env2 = readLinoEnv('.lenv'); -console.log(env2.get('URL')); // 'https://example.com:8080' -console.log(env2.get('MESSAGE')); // 'Hello World' -``` +For full API documentation, see the [Rust README](./rust/README.md). ## File Format @@ -369,6 +125,55 @@ URL: https://example.com:8080 MESSAGE: Hello World ``` +## Repository Structure + +``` +lino-env/ +├── js/ # JavaScript package +│ ├── src/ # Source code +│ ├── tests/ # Tests +│ ├── .changeset/ # Changeset configuration +│ ├── package.json # Package manifest +│ └── ... +├── rust/ # Rust package +│ ├── src/ # Source code +│ ├── tests/ # Tests (integration) +│ ├── changelog.d/ # Changelog fragments +│ ├── Cargo.toml # Package manifest +│ └── ... +├── scripts/ # Shared scripts +└── .github/workflows/ # CI/CD workflows + ├── js.yml # JavaScript CI/CD + └── rust.yml # Rust CI/CD +``` + +## Development + +### Prerequisites + +- Node.js 20.x for JavaScript development +- Rust 1.70+ for Rust development + +### Running Tests + +```bash +# JavaScript tests +cd js && npm test + +# Rust tests +cd rust && cargo test +``` + +### Linting and Formatting + +```bash +# JavaScript +cd js && npm run lint && npm run format:check + +# Rust +cd rust && cargo fmt --check && cargo clippy +``` + ## License This project is released into the public domain under [The Unlicense](http://unlicense.org). diff --git a/.changeset/README.md b/js/.changeset/README.md similarity index 100% rename from .changeset/README.md rename to js/.changeset/README.md diff --git a/js/.changeset/add-rust-restructure.md b/js/.changeset/add-rust-restructure.md new file mode 100644 index 0000000..607ffff --- /dev/null +++ b/js/.changeset/add-rust-restructure.md @@ -0,0 +1,12 @@ +--- +'lino-env': minor +--- + +Add Rust version of the code and restructure repository as monorepo + +This release restructures the repository to support both JavaScript and Rust implementations: + +- Moved all JS-specific code to ./js folder +- Added new Rust implementation in ./rust folder +- Created separate CI/CD workflows for each language (js.yml and rust.yml) +- The npm package functionality remains unchanged diff --git a/.changeset/config.json b/js/.changeset/config.json similarity index 100% rename from .changeset/config.json rename to js/.changeset/config.json diff --git a/.husky/pre-commit b/js/.husky/pre-commit similarity index 100% rename from .husky/pre-commit rename to js/.husky/pre-commit diff --git a/.prettierignore b/js/.prettierignore similarity index 100% rename from .prettierignore rename to js/.prettierignore diff --git a/.prettierrc b/js/.prettierrc similarity index 100% rename from .prettierrc rename to js/.prettierrc diff --git a/CHANGELOG.md b/js/CHANGELOG.md similarity index 100% rename from CHANGELOG.md rename to js/CHANGELOG.md diff --git a/js/README.md b/js/README.md new file mode 100644 index 0000000..bda1ebd --- /dev/null +++ b/js/README.md @@ -0,0 +1,299 @@ +# lino-env (JavaScript) + +A JavaScript library to operate .lenv files - an alternative to .env files that uses `: ` (colon-space) instead of `=` for key-value separation and supports duplicate keys. + +## What are .lenv files? + +.lenv files are configuration files similar to .env files, but with a different syntax: + +``` +# .env format (traditional) +GITHUB_TOKEN=gh_... +API_KEY=abc123 + +# .lenv format (this library) +GITHUB_TOKEN: gh_... +API_KEY: abc123 +``` + +The key difference is the use of `: ` separator, which aligns with [links-notation](https://github.com/link-foundation/links-notation) format. Additionally, .lenv files support duplicate keys, where multiple instances of the same key can exist. + +## Installation + +```bash +npm install lino-env +``` + +## Quick Start + +```bash +# create .lenv file +echo "HELLO: World" > .lenv + +# create index.js +echo "import linoenv from 'lino-env'; linoenv.config(); console.log('Hello ' + process.env.HELLO)" > index.js + +# run +node index.js +``` + +Output: + +``` +Hello World +``` + +## Usage + +### ESM (import) + +```javascript +import linoenv from 'lino-env'; +linoenv.config(); + +console.log(`Hello ${process.env.HELLO}`); +``` + +### CommonJS (require) + +```javascript +require('lino-env').config(); + +console.log(`Hello ${process.env.HELLO}`); +``` + +## API Reference + +### Dotenvx-like API + +These functions provide a simple API similar to dotenvx for common use cases: + +#### `config(options)` + +Load .lenv file and inject into process.env + +```javascript +import linoenv from 'lino-env'; + +linoenv.config(); // loads .lenv + +// or specify a custom path +linoenv.config({ path: '.lenv.production' }); +``` + +Returns: `{ parsed: Object }` - Object containing parsed key-value pairs + +#### `get(key, options)` + +Get a value from the loaded .lenv file + +```javascript +import linoenv from 'lino-env'; + +const value = linoenv.get('API_KEY'); + +// or from a specific file +const value = linoenv.get('API_KEY', { path: '.lenv.production' }); +``` + +Returns: `string | undefined` + +#### `set(key, value, options)` + +Set a value in a .lenv file + +```javascript +import linoenv from 'lino-env'; + +linoenv.set('API_KEY', 'new_value'); + +// or to a specific file +linoenv.set('API_KEY', 'new_value', { path: '.lenv.production' }); +``` + +### Class: LinoEnv + +The main class for reading and writing .lenv files. + +#### Constructor + +```javascript +import { LinoEnv } from 'lino-env'; + +const env = new LinoEnv('.lenv'); +``` + +#### Methods + +##### `read()` + +Reads and parses the .lenv file. If the file doesn't exist, initializes with empty data. + +```javascript +const env = new LinoEnv('.lenv'); +env.read(); +``` + +Returns: `this` (for method chaining) + +##### `write()` + +Writes the current data back to the .lenv file. + +```javascript +env.set('API_KEY', 'value').write(); +``` + +Returns: `this` (for method chaining) + +##### `get(reference)` + +Gets the last instance of a reference (key). + +```javascript +env.add('API_KEY', 'value1'); +env.add('API_KEY', 'value2'); +console.log(env.get('API_KEY')); // 'value2' +``` + +Returns: `string | undefined` + +##### `getAll(reference)` + +Gets all instances of a reference (key). + +```javascript +env.add('API_KEY', 'value1'); +env.add('API_KEY', 'value2'); +console.log(env.getAll('API_KEY')); // ['value1', 'value2'] +``` + +Returns: `string[]` + +##### `set(reference, value)` + +Sets a reference to a single value, replacing all existing instances. + +```javascript +env.set('API_KEY', 'new_value'); +``` + +Returns: `this` (for method chaining) + +##### `add(reference, value)` + +Adds a new instance of a reference, allowing duplicates. + +```javascript +env.add('API_KEY', 'value1'); +env.add('API_KEY', 'value2'); // Now there are 2 instances +``` + +Returns: `this` (for method chaining) + +##### `has(reference)` + +Checks if a reference exists. + +```javascript +if (env.has('API_KEY')) { + console.log('API_KEY exists'); +} +``` + +Returns: `boolean` + +##### `delete(reference)` + +Deletes all instances of a reference. + +```javascript +env.delete('API_KEY'); +``` + +Returns: `this` (for method chaining) + +##### `keys()` + +Gets all keys. + +```javascript +console.log(env.keys()); // ['GITHUB_TOKEN', 'API_KEY', ...] +``` + +Returns: `string[]` + +##### `toObject()` + +Converts to a plain object with the last instance of each key. + +```javascript +env.add('KEY1', 'value1a'); +env.add('KEY1', 'value1b'); +env.set('KEY2', 'value2'); + +console.log(env.toObject()); +// { KEY1: 'value1b', KEY2: 'value2' } +``` + +Returns: `Object` + +### Convenience Functions + +#### `readLinoEnv(filePath)` + +Convenience function to read a .lenv file. + +```javascript +import { readLinoEnv } from 'lino-env'; + +const env = readLinoEnv('.lenv'); +console.log(env.get('GITHUB_TOKEN')); +``` + +Returns: `LinoEnv` + +#### `writeLinoEnv(filePath, data)` + +Convenience function to create and write a .lenv file from an object. + +```javascript +import { writeLinoEnv } from 'lino-env'; + +writeLinoEnv('.lenv', { + API_KEY: 'test_key', + SECRET: 'test_secret', +}); +``` + +Returns: `LinoEnv` + +## File Format + +.lenv files use the following format: + +- Key-value separator: `: ` (colon followed by space) +- One key-value pair per line +- Empty lines and lines starting with `#` are ignored +- Duplicate keys are allowed +- Values can contain spaces, colons, and other special characters + +Example `.lenv` file: + +``` +# Configuration file +GITHUB_TOKEN: gh_abc123xyz +TELEGRAM_TOKEN: 054test456 + +# Multiple servers +SERVER: server1.example.com +SERVER: server2.example.com + +# Values with special characters +URL: https://example.com:8080 +MESSAGE: Hello World +``` + +## License + +This project is released into the public domain under [The Unlicense](http://unlicense.org). diff --git a/bunfig.toml b/js/bunfig.toml similarity index 100% rename from bunfig.toml rename to js/bunfig.toml diff --git a/deno.json b/js/deno.json similarity index 100% rename from deno.json rename to js/deno.json diff --git a/eslint.config.js b/js/eslint.config.js similarity index 100% rename from eslint.config.js rename to js/eslint.config.js diff --git a/examples/basic-usage.mjs b/js/examples/basic-usage.mjs similarity index 100% rename from examples/basic-usage.mjs rename to js/examples/basic-usage.mjs diff --git a/examples/dotenvx-style.mjs b/js/examples/dotenvx-style.mjs similarity index 100% rename from examples/dotenvx-style.mjs rename to js/examples/dotenvx-style.mjs diff --git a/examples/duplicates.lenv b/js/examples/duplicates.lenv similarity index 100% rename from examples/duplicates.lenv rename to js/examples/duplicates.lenv diff --git a/examples/example.lenv b/js/examples/example.lenv similarity index 100% rename from examples/example.lenv rename to js/examples/example.lenv diff --git a/examples/quick.lenv b/js/examples/quick.lenv similarity index 100% rename from examples/quick.lenv rename to js/examples/quick.lenv diff --git a/examples/test.lenv b/js/examples/test.lenv similarity index 100% rename from examples/test.lenv rename to js/examples/test.lenv diff --git a/experiments/npm-publishing-investigation.md b/js/experiments/npm-publishing-investigation.md similarity index 100% rename from experiments/npm-publishing-investigation.md rename to js/experiments/npm-publishing-investigation.md diff --git a/package-lock.json b/js/package-lock.json similarity index 91% rename from package-lock.json rename to js/package-lock.json index ac4aee1..ac87047 100644 --- a/package-lock.json +++ b/js/package-lock.json @@ -34,13 +34,13 @@ } }, "node_modules/@changesets/apply-release-plan": { - "version": "7.0.13", - "resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-7.0.13.tgz", - "integrity": "sha512-BIW7bofD2yAWoE8H4V40FikC+1nNFEKBisMECccS16W1rt6qqhNTBDmIw5HaqmMgtLNz9e7oiALiEUuKrQ4oHg==", + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-7.0.14.tgz", + "integrity": "sha512-ddBvf9PHdy2YY0OUiEl3TV78mH9sckndJR14QAt87KLEbIov81XO0q0QAmvooBxXlqRRP8I9B7XOzZwQG7JkWA==", "dev": true, "license": "MIT", "dependencies": { - "@changesets/config": "^3.1.1", + "@changesets/config": "^3.1.2", "@changesets/get-version-range-type": "^0.4.0", "@changesets/git": "^3.0.4", "@changesets/should-skip-package": "^0.1.2", @@ -71,19 +71,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/@changesets/apply-release-plan/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@changesets/assemble-release-plan": { "version": "6.0.9", "resolved": "https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-6.0.9.tgz", @@ -99,19 +86,6 @@ "semver": "^7.5.3" } }, - "node_modules/@changesets/assemble-release-plan/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@changesets/changelog-git": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/@changesets/changelog-git/-/changelog-git-0.2.1.tgz", @@ -123,27 +97,27 @@ } }, "node_modules/@changesets/cli": { - "version": "2.29.7", - "resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-2.29.7.tgz", - "integrity": "sha512-R7RqWoaksyyKXbKXBTbT4REdy22yH81mcFK6sWtqSanxUCbUi9Uf+6aqxZtDQouIqPdem2W56CdxXgsxdq7FLQ==", + "version": "2.29.8", + "resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-2.29.8.tgz", + "integrity": "sha512-1weuGZpP63YWUYjay/E84qqwcnt5yJMM0tep10Up7Q5cS/DGe2IZ0Uj3HNMxGhCINZuR7aO9WBMdKnPit5ZDPA==", "dev": true, "license": "MIT", "dependencies": { - "@changesets/apply-release-plan": "^7.0.13", + "@changesets/apply-release-plan": "^7.0.14", "@changesets/assemble-release-plan": "^6.0.9", "@changesets/changelog-git": "^0.2.1", - "@changesets/config": "^3.1.1", + "@changesets/config": "^3.1.2", "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.3", - "@changesets/get-release-plan": "^4.0.13", + "@changesets/get-release-plan": "^4.0.14", "@changesets/git": "^3.0.4", "@changesets/logger": "^0.1.1", "@changesets/pre": "^2.0.2", - "@changesets/read": "^0.6.5", + "@changesets/read": "^0.6.6", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@changesets/write": "^0.4.0", - "@inquirer/external-editor": "^1.0.0", + "@inquirer/external-editor": "^1.0.2", "@manypkg/get-packages": "^1.1.3", "ansi-colors": "^4.1.3", "ci-info": "^3.7.0", @@ -162,39 +136,10 @@ "changeset": "bin.js" } }, - "node_modules/@changesets/cli/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@changesets/cli/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@changesets/config": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@changesets/config/-/config-3.1.1.tgz", - "integrity": "sha512-bd+3Ap2TKXxljCggI0mKPfzCQKeV/TU4yO2h2C6vAihIo8tzseAn2e7klSuiyYYXvgu53zMN1OeYMIQkaQoWnA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@changesets/config/-/config-3.1.2.tgz", + "integrity": "sha512-CYiRhA4bWKemdYi/uwImjPxqWNpqGPNbEBdX1BdONALFIDK7MCUj6FPkzD+z9gJcvDFUQJn9aDVf4UG7OT6Kog==", "dev": true, "license": "MIT", "dependencies": { @@ -230,30 +175,17 @@ "semver": "^7.5.3" } }, - "node_modules/@changesets/get-dependents-graph/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@changesets/get-release-plan": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/@changesets/get-release-plan/-/get-release-plan-4.0.13.tgz", - "integrity": "sha512-DWG1pus72FcNeXkM12tx+xtExyH/c9I1z+2aXlObH3i9YA7+WZEVaiHzHl03thpvAgWTRaH64MpfHxozfF7Dvg==", + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@changesets/get-release-plan/-/get-release-plan-4.0.14.tgz", + "integrity": "sha512-yjZMHpUHgl4Xl5gRlolVuxDkm4HgSJqT93Ri1Uz8kGrQb+5iJ8dkXJ20M2j/Y4iV5QzS2c5SeTxVSKX+2eMI0g==", "dev": true, "license": "MIT", "dependencies": { "@changesets/assemble-release-plan": "^6.0.9", - "@changesets/config": "^3.1.1", + "@changesets/config": "^3.1.2", "@changesets/pre": "^2.0.2", - "@changesets/read": "^0.6.5", + "@changesets/read": "^0.6.6", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3" } @@ -290,14 +222,14 @@ } }, "node_modules/@changesets/parse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@changesets/parse/-/parse-0.4.1.tgz", - "integrity": "sha512-iwksMs5Bf/wUItfcg+OXrEpravm5rEd9Bf4oyIPL4kVTmJQ7PNDSd6MDYkpSJR1pn7tz/k8Zf2DhTCqX08Ou+Q==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@changesets/parse/-/parse-0.4.2.tgz", + "integrity": "sha512-Uo5MC5mfg4OM0jU3up66fmSn6/NE9INK+8/Vn/7sMVcdWg46zfbvvUSjD9EMonVqPi9fbrJH9SXHn48Tr1f2yA==", "dev": true, "license": "MIT", "dependencies": { "@changesets/types": "^6.1.0", - "js-yaml": "^3.13.1" + "js-yaml": "^4.1.1" } }, "node_modules/@changesets/pre": { @@ -314,15 +246,15 @@ } }, "node_modules/@changesets/read": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/@changesets/read/-/read-0.6.5.tgz", - "integrity": "sha512-UPzNGhsSjHD3Veb0xO/MwvasGe8eMyNrR/sT9gR8Q3DhOQZirgKhhXv/8hVsI0QpPjR004Z9iFxoJU6in3uGMg==", + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/@changesets/read/-/read-0.6.6.tgz", + "integrity": "sha512-P5QaN9hJSQQKJShzzpBT13FzOSPyHbqdoIBUd2DJdgvnECCyO6LmAOWSV+O8se2TaZJVwSXjL+v9yhb+a9JeJg==", "dev": true, "license": "MIT", "dependencies": { "@changesets/git": "^3.0.4", "@changesets/logger": "^0.1.1", - "@changesets/parse": "^0.4.1", + "@changesets/parse": "^0.4.2", "@changesets/types": "^6.1.0", "fs-extra": "^7.0.1", "p-filter": "^2.1.0", @@ -460,9 +392,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", "dev": true, "license": "MIT", "dependencies": { @@ -472,7 +404,7 @@ "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", + "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" }, @@ -483,30 +415,10 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/eslintrc/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/@eslint/js": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", - "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", "dev": true, "license": "MIT", "engines": { @@ -801,6 +713,22 @@ "node": ">=6" } }, + "node_modules/ansi-escapes": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz", + "integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -828,14 +756,11 @@ } }, "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } + "license": "Python-2.0" }, "node_modules/array-union": { "version": "2.1.0", @@ -974,52 +899,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cli-truncate/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/cli-truncate/node_modules/string-width": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", - "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1127,6 +1006,13 @@ "node": ">=8" } }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, "node_modules/enquirer": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", @@ -1154,10 +1040,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", - "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", "dependencies": { @@ -1167,7 +1066,7 @@ "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.1", + "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -1291,19 +1190,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/eslint/node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -1321,27 +1207,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "is-glob": "^4.0.3" + "p-locate": "^5.0.0" }, "engines": { - "node": ">=10.13.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "node_modules/eslint/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "license": "MIT", "dependencies": { - "p-locate": "^5.0.0" + "yocto-queue": "^0.1.0" }, "engines": { "node": ">=10" @@ -1489,6 +1378,19 @@ "node": ">=8.6.0" } }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -1504,9 +1406,9 @@ "license": "MIT" }, "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "dev": true, "license": "ISC", "dependencies": { @@ -1603,16 +1505,16 @@ } }, "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", "dependencies": { - "is-glob": "^4.0.1" + "is-glob": "^4.0.3" }, "engines": { - "node": ">= 6" + "node": ">=10.13.0" } }, "node_modules/globals": { @@ -1667,9 +1569,9 @@ } }, "node_modules/human-id": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/human-id/-/human-id-4.1.2.tgz", - "integrity": "sha512-v/J+4Z/1eIJovEBdlV5TYj1IR+ZiohcYGRY+qN/oC9dAfKzVT023N/Bgw37hrKCoVRBvk3bqyzpr2PP5YeTMSg==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/human-id/-/human-id-4.1.3.tgz", + "integrity": "sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==", "dev": true, "license": "MIT", "bin": { @@ -1693,9 +1595,9 @@ } }, "node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz", + "integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==", "dev": true, "license": "MIT", "dependencies": { @@ -1766,6 +1668,22 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "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", @@ -1820,14 +1738,13 @@ "license": "ISC" }, "node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" @@ -1895,13 +1812,13 @@ "license": "Unlicense" }, "node_modules/lint-staged": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.6.tgz", - "integrity": "sha512-s1gphtDbV4bmW1eylXpVMk2u7is7YsrLl8hzrtvC70h4ByhcMLZFY01Fx05ZUDNuv1H8HO4E+e2zgejV1jVwNw==", + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.7.tgz", + "integrity": "sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow==", "dev": true, "license": "MIT", "dependencies": { - "commander": "^14.0.1", + "commander": "^14.0.2", "listr2": "^9.0.5", "micromatch": "^4.0.8", "nano-spawn": "^2.0.0", @@ -1937,91 +1854,6 @@ "node": ">=20.0.0" } }, - "node_modules/listr2/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/listr2/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/listr2/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true, - "license": "MIT" - }, - "node_modules/listr2/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/listr2/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/listr2/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -2069,22 +1901,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-update/node_modules/ansi-escapes": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz", - "integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "environment": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/log-update/node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", @@ -2098,44 +1914,6 @@ "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/log-update/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/log-update/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true, - "license": "MIT" - }, - "node_modules/log-update/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/log-update/node_modules/strip-ansi": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", @@ -2152,24 +1930,6 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/log-update/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -2257,6 +2017,22 @@ "dev": true, "license": "MIT" }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -2296,16 +2072,16 @@ } }, "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, "license": "MIT", "dependencies": { - "yocto-queue": "^0.1.0" + "p-try": "^2.0.0" }, "engines": { - "node": ">=10" + "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -2324,22 +2100,6 @@ "node": ">=8" } }, - "node_modules/p-locate/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/p-map": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", @@ -2467,9 +2227,9 @@ } }, "node_modules/prettier": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", + "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", "dev": true, "license": "MIT", "bin": { @@ -2483,9 +2243,9 @@ } }, "node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", "dev": true, "license": "MIT", "dependencies": { @@ -2559,14 +2319,28 @@ "node": ">=6" } }, - "node_modules/read-yaml-file/node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "node_modules/read-yaml-file/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=4" + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/read-yaml-file/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, "node_modules/resolve-from": { @@ -2596,35 +2370,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/restore-cursor/node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/restore-cursor/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -2674,6 +2419,19 @@ "dev": true, "license": "MIT" }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2697,6 +2455,19 @@ "node": ">=8" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -2737,22 +2508,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", - "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/spawndamnit": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/spawndamnit/-/spawndamnit-3.0.1.tgz", @@ -2764,19 +2519,6 @@ "signal-exit": "^4.0.1" } }, - "node_modules/spawndamnit/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -2794,6 +2536,52 @@ "node": ">=0.6.19" } }, + "node_modules/string-width": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", + "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -2807,6 +2595,16 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -2863,9 +2661,9 @@ } }, "node_modules/test-anywhere": { - "version": "0.8.48", - "resolved": "https://registry.npmjs.org/test-anywhere/-/test-anywhere-0.8.48.tgz", - "integrity": "sha512-9fq3Qu5wCC6d46k9ibOD2DMlyuF/GqMTEzKWOavlbmRvsCdxP+u5iQmpE3cGeIx8/JS8niHAcfDM3SvuYxD42w==", + "version": "0.8.50", + "resolved": "https://registry.npmjs.org/test-anywhere/-/test-anywhere-0.8.50.tgz", + "integrity": "sha512-zsqOAJ38rSBUJPzgtV8v6v8FrTrQM+OiINBvrfaSwwVSiuS/vE1C5t2BROy13P+SCJDefzi9j60kZRQGYp7HzA==", "dev": true, "license": "Unlicense", "engines": { @@ -2944,10 +2742,88 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", "dev": true, "license": "ISC", "bin": { @@ -2955,6 +2831,9 @@ }, "engines": { "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" } }, "node_modules/yocto-queue": { diff --git a/package.json b/js/package.json similarity index 98% rename from package.json rename to js/package.json index 7c99d73..7a9acb0 100644 --- a/package.json +++ b/js/package.json @@ -23,7 +23,8 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/link-foundation/lino-env.git" + "url": "git+https://github.com/link-foundation/lino-env.git", + "directory": "js" }, "bugs": { "url": "https://github.com/link-foundation/lino-env/issues" diff --git a/scripts/changeset-version.mjs b/js/scripts/changeset-version.mjs similarity index 100% rename from scripts/changeset-version.mjs rename to js/scripts/changeset-version.mjs diff --git a/scripts/check-file-size.mjs b/js/scripts/check-file-size.mjs similarity index 100% rename from scripts/check-file-size.mjs rename to js/scripts/check-file-size.mjs diff --git a/scripts/create-github-release.mjs b/js/scripts/create-github-release.mjs similarity index 100% rename from scripts/create-github-release.mjs rename to js/scripts/create-github-release.mjs diff --git a/scripts/create-manual-changeset.mjs b/js/scripts/create-manual-changeset.mjs similarity index 100% rename from scripts/create-manual-changeset.mjs rename to js/scripts/create-manual-changeset.mjs diff --git a/scripts/format-github-release.mjs b/js/scripts/format-github-release.mjs similarity index 100% rename from scripts/format-github-release.mjs rename to js/scripts/format-github-release.mjs diff --git a/scripts/format-release-notes.mjs b/js/scripts/format-release-notes.mjs similarity index 100% rename from scripts/format-release-notes.mjs rename to js/scripts/format-release-notes.mjs diff --git a/scripts/instant-version-bump.mjs b/js/scripts/instant-version-bump.mjs similarity index 100% rename from scripts/instant-version-bump.mjs rename to js/scripts/instant-version-bump.mjs diff --git a/scripts/publish-to-npm.mjs b/js/scripts/publish-to-npm.mjs similarity index 100% rename from scripts/publish-to-npm.mjs rename to js/scripts/publish-to-npm.mjs diff --git a/scripts/setup-npm.mjs b/js/scripts/setup-npm.mjs similarity index 100% rename from scripts/setup-npm.mjs rename to js/scripts/setup-npm.mjs diff --git a/scripts/validate-changeset.mjs b/js/scripts/validate-changeset.mjs similarity index 100% rename from scripts/validate-changeset.mjs rename to js/scripts/validate-changeset.mjs diff --git a/scripts/version-and-commit.mjs b/js/scripts/version-and-commit.mjs similarity index 100% rename from scripts/version-and-commit.mjs rename to js/scripts/version-and-commit.mjs diff --git a/src/lino-env.mjs b/js/src/lino-env.mjs similarity index 100% rename from src/lino-env.mjs rename to js/src/lino-env.mjs diff --git a/tests/lino-env.test.mjs b/js/tests/lino-env.test.mjs similarity index 100% rename from tests/lino-env.test.mjs rename to js/tests/lino-env.test.mjs diff --git a/rust/.gitignore b/rust/.gitignore new file mode 100644 index 0000000..5289aa5 --- /dev/null +++ b/rust/.gitignore @@ -0,0 +1,56 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# See https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +# Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# Generated by cargo mutants +# Contains mutation testing data +**/mutants.out*/ + +# IDE and editor files +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS files +.DS_Store +Thumbs.db + +# Python virtual environments (for scripts) +.venv/ +venv/ +__pycache__/ +*.pyc +*.pyo + +# Coverage reports +*.lcov +coverage/ +tarpaulin-report.html + +# Benchmark results +criterion/ + +# Documentation build output +doc/ + +# Local development files +.env +.env.local +*.local + +# Log files +*.log +logs/ diff --git a/rust/.pre-commit-config.yaml b/rust/.pre-commit-config.yaml new file mode 100644 index 0000000..ce4da86 --- /dev/null +++ b/rust/.pre-commit-config.yaml @@ -0,0 +1,34 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - id: check-merge-conflict + - id: check-toml + - id: debug-statements + + - repo: local + hooks: + - id: cargo-fmt + name: cargo fmt + entry: cargo fmt --all -- + language: system + types: [rust] + pass_filenames: false + + - id: cargo-clippy + name: cargo clippy + entry: cargo clippy --all-targets --all-features -- -D warnings + language: system + types: [rust] + pass_filenames: false + + - id: cargo-test + name: cargo test + entry: cargo test + language: system + types: [rust] + pass_filenames: false diff --git a/rust/CHANGELOG.md b/rust/CHANGELOG.md new file mode 100644 index 0000000..4a421c3 --- /dev/null +++ b/rust/CHANGELOG.md @@ -0,0 +1,16 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [Unreleased] + +## [0.1.0] - Initial Release + +### Added + +- Initial implementation of LinoEnv struct for reading and writing .lenv files +- Support for `: ` (colon-space) separator as per links-notation specification +- Support for multiple values per key (duplicates allowed) +- Convenience functions `read_lino_env` and `write_lino_env` +- Full test coverage +- Documentation with examples diff --git a/rust/Cargo.toml b/rust/Cargo.toml new file mode 100644 index 0000000..4516337 --- /dev/null +++ b/rust/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "lino-env" +version = "0.1.0" +edition = "2021" +description = "A Rust library to read and write .lenv files" +readme = "README.md" +license = "Unlicense" +keywords = ["lino", "env", "environment", "links-notation", "config"] +categories = ["config", "parser-implementations"] +repository = "https://github.com/link-foundation/lino-env" +documentation = "https://github.com/link-foundation/lino-env" +rust-version = "1.70" + +[lib] +name = "lino_env" +path = "src/lib.rs" + +[dependencies] + +[dev-dependencies] + +[lints.rust] +unsafe_code = "forbid" + +[lints.clippy] +all = { level = "warn", priority = -1 } +pedantic = { level = "warn", priority = -1 } +nursery = { level = "warn", priority = -1 } + +# Allow some common patterns +module_name_repetitions = "allow" +too_many_lines = "allow" +missing_errors_doc = "allow" +missing_panics_doc = "allow" + +[profile.release] +lto = true +codegen-units = 1 +strip = true diff --git a/rust/README.md b/rust/README.md new file mode 100644 index 0000000..6b5fa49 --- /dev/null +++ b/rust/README.md @@ -0,0 +1,106 @@ +# lino-env (Rust) + +A Rust library to read and write `.lenv` files. + +## What is .lenv? + +`.lenv` files are environment configuration files that use `: ` (colon-space) instead of `=` for key-value separation. This format is part of the links-notation specification. + +Example `.lenv` file: + +``` +GITHUB_TOKEN: gh_abc123 +TELEGRAM_TOKEN: 054xyz789 +API_URL: https://api.example.com:8080 +``` + +## Installation + +Add this to your `Cargo.toml`: + +```toml +[dependencies] +lino-env = "0.1" +``` + +## Usage + +### Basic Usage + +```rust +use lino_env::LinoEnv; + +// Create and write a new .lenv file +let mut env = LinoEnv::new(".lenv"); +env.set("GITHUB_TOKEN", "gh_abc123"); +env.set("API_KEY", "my_api_key"); +env.write().unwrap(); + +// Read an existing .lenv file +let mut env = LinoEnv::new(".lenv"); +env.read().unwrap(); + +// Get a value +if let Some(token) = env.get("GITHUB_TOKEN") { + println!("Token: {}", token); +} +``` + +### Multiple Values per Key + +`.lenv` files support multiple values for the same key: + +```rust +use lino_env::LinoEnv; + +let mut env = LinoEnv::new(".lenv"); + +// Add multiple values for the same key +env.add("ALLOWED_HOST", "localhost"); +env.add("ALLOWED_HOST", "example.com"); +env.add("ALLOWED_HOST", "api.example.com"); + +// Get the last value +assert_eq!(env.get("ALLOWED_HOST"), Some("api.example.com".to_string())); + +// Get all values +let hosts = env.get_all("ALLOWED_HOST"); +assert_eq!(hosts, vec!["localhost", "example.com", "api.example.com"]); +``` + +### Convenience Functions + +```rust +use lino_env::{read_lino_env, write_lino_env}; +use std::collections::HashMap; + +// Write using a HashMap +let mut data = HashMap::new(); +data.insert("KEY1".to_string(), "value1".to_string()); +data.insert("KEY2".to_string(), "value2".to_string()); +write_lino_env(".lenv", &data).unwrap(); + +// Read into a LinoEnv instance +let env = read_lino_env(".lenv").unwrap(); +println!("{:?}", env.get("KEY1")); +``` + +## API Reference + +### LinoEnv + +- `new(file_path)` - Create a new LinoEnv instance +- `read()` - Read and parse the .lenv file +- `write()` - Write the current data to the file +- `get(key)` - Get the last value for a key +- `get_all(key)` - Get all values for a key +- `set(key, value)` - Set a key to a single value (replaces all) +- `add(key, value)` - Add a value to a key (allows duplicates) +- `has(key)` - Check if a key exists +- `delete(key)` - Delete all values for a key +- `keys()` - Get all keys +- `to_hash_map()` - Convert to HashMap with last values + +## License + +Unlicense diff --git a/rust/changelog.d/README.md b/rust/changelog.d/README.md new file mode 100644 index 0000000..38d4a02 --- /dev/null +++ b/rust/changelog.d/README.md @@ -0,0 +1,146 @@ +# Changelog Fragments + +This directory contains changelog fragments that will be collected into `CHANGELOG.md` during releases. + +## How to Add a Changelog Fragment + +When making changes that should be documented in the changelog, create a fragment file: + +```bash +# Create a new fragment with timestamp +touch changelog.d/$(date +%Y%m%d_%H%M%S)_description.md + +# Or manually create a file matching the pattern: YYYYMMDD_HHMMSS_description.md +``` + +## Fragment Format + +Each fragment should include a **frontmatter section** specifying the version bump type: + +```markdown +--- +bump: patch +--- + +### Fixed + +- Description of bug fix +``` + +### Bump Types + +Use semantic versioning bump types in the frontmatter: + +- **`major`**: Breaking changes (incompatible API changes) +- **`minor`**: New features (backward compatible) +- **`patch`**: Bug fixes (backward compatible) + +### Content Categories + +Use these categories in your fragment content: + +```markdown +--- +bump: minor +--- + +### Added + +- Description of new feature + +### Changed + +- Description of change to existing functionality + +### Fixed + +- Description of bug fix + +### Removed + +- Description of removed feature + +### Deprecated + +- Description of deprecated feature + +### Security + +- Description of security fix +``` + +## Examples + +### Adding a new feature (minor bump) + +```markdown +--- +bump: minor +--- + +### Added + +- New async processing mode for batch operations +``` + +### Fixing a bug (patch bump) + +```markdown +--- +bump: patch +--- + +### Fixed + +- Fixed memory leak in connection pool handling +``` + +### Breaking change (major bump) + +```markdown +--- +bump: major +--- + +### Changed + +- Renamed `process()` to `process_async()` - this is a breaking change + +### Removed + +- Removed deprecated `legacy_mode` option +``` + +## Why Fragments? + +Using changelog fragments (similar to [Changesets](https://github.com/changesets/changesets) in JavaScript and [Scriv](https://scriv.readthedocs.io/) in Python): + +1. **No merge conflicts**: Multiple PRs can add fragments without conflicts +2. **Per-PR documentation**: Each PR documents its own changes +3. **Automated version bumping**: Version bump type is specified per-change +4. **Automated collection**: Fragments are automatically collected during release +5. **Consistent format**: Template ensures consistent changelog entries + +## How It Works + +1. **During PR**: Add a fragment file with your changes and bump type +2. **On merge to main**: The release workflow automatically: + - Reads all fragment files and determines the highest bump type + - Bumps the version in `Cargo.toml` accordingly + - Collects fragments into `CHANGELOG.md` + - Creates a git tag and GitHub release + - Removes processed fragment files + +## Multiple PRs and Bump Priority + +When multiple PRs are merged before a release, all pending fragments are processed together. The **highest** bump type wins: + +- If any fragment specifies `major`, the release is a major version bump +- Otherwise, if any specifies `minor`, the release is a minor version bump +- Otherwise, the release is a patch version bump + +This ensures that breaking changes are never missed, even when combined with smaller changes. + +## Default Behavior + +If a fragment doesn't include a bump type in the frontmatter, it defaults to `patch`. diff --git a/rust/scripts/bump-version.mjs b/rust/scripts/bump-version.mjs new file mode 100644 index 0000000..579aafc --- /dev/null +++ b/rust/scripts/bump-version.mjs @@ -0,0 +1,119 @@ +#!/usr/bin/env node + +/** + * Bump version in Cargo.toml + * Usage: node scripts/bump-version.mjs --bump-type [--dry-run] + * + * Uses link-foundation libraries: + * - use-m: Dynamic package loading without package.json dependencies + * - lino-arguments: Unified configuration from CLI args, env vars, and .lenv files + */ + +import { readFileSync, writeFileSync } from 'fs'; + +// Load use-m dynamically +const { use } = eval( + await (await fetch('https://unpkg.com/use-m/use.js')).text() +); + +// Import lino-arguments for CLI argument parsing +const { makeConfig } = await use('lino-arguments'); + +// Parse CLI arguments +const config = makeConfig({ + yargs: ({ yargs, getenv }) => + yargs + .option('bump-type', { + type: 'string', + default: getenv('BUMP_TYPE', ''), + describe: 'Version bump type: major, minor, or patch', + choices: ['major', 'minor', 'patch'], + }) + .option('dry-run', { + type: 'boolean', + default: false, + describe: 'Show what would be done without making changes', + }), +}); + +const { bumpType, dryRun } = config; + +if (!bumpType || !['major', 'minor', 'patch'].includes(bumpType)) { + console.error( + 'Usage: node scripts/bump-version.mjs --bump-type [--dry-run]' + ); + process.exit(1); +} + +/** + * Get current version from Cargo.toml + * @returns {{major: number, minor: number, patch: number}} + */ +function getCurrentVersion() { + const cargoToml = readFileSync('Cargo.toml', 'utf-8'); + const match = cargoToml.match(/^version\s*=\s*"(\d+)\.(\d+)\.(\d+)"/m); + + if (!match) { + console.error('Error: Could not parse version from Cargo.toml'); + process.exit(1); + } + + return { + major: parseInt(match[1], 10), + minor: parseInt(match[2], 10), + patch: parseInt(match[3], 10), + }; +} + +/** + * Calculate new version based on bump type + * @param {{major: number, minor: number, patch: number}} current + * @param {string} bumpType + * @returns {string} + */ +function calculateNewVersion(current, bumpType) { + const { major, minor, patch } = current; + + switch (bumpType) { + case 'major': + return `${major + 1}.0.0`; + case 'minor': + return `${major}.${minor + 1}.0`; + case 'patch': + return `${major}.${minor}.${patch + 1}`; + default: + throw new Error(`Invalid bump type: ${bumpType}`); + } +} + +/** + * Update version in Cargo.toml + * @param {string} newVersion + */ +function updateCargoToml(newVersion) { + let cargoToml = readFileSync('Cargo.toml', 'utf-8'); + cargoToml = cargoToml.replace( + /^(version\s*=\s*")[^"]+(")/m, + `$1${newVersion}$2` + ); + writeFileSync('Cargo.toml', cargoToml, 'utf-8'); +} + +try { + const current = getCurrentVersion(); + const currentStr = `${current.major}.${current.minor}.${current.patch}`; + const newVersion = calculateNewVersion(current, bumpType); + + console.log(`Current version: ${currentStr}`); + console.log(`New version: ${newVersion}`); + + if (dryRun) { + console.log('Dry run - no changes made'); + } else { + updateCargoToml(newVersion); + console.log('Updated Cargo.toml'); + } +} catch (error) { + console.error('Error:', error.message); + process.exit(1); +} diff --git a/rust/scripts/check-file-size.mjs b/rust/scripts/check-file-size.mjs new file mode 100644 index 0000000..4f2aedc --- /dev/null +++ b/rust/scripts/check-file-size.mjs @@ -0,0 +1,100 @@ +#!/usr/bin/env node + +/** + * Check for files exceeding the maximum allowed line count + * Exits with error code 1 if any files exceed the limit + * + * Uses link-foundation libraries: + * - use-m: Dynamic package loading without package.json dependencies + */ + +import { readFileSync, readdirSync, statSync } from 'fs'; +import { join, relative, extname } from 'path'; + +const MAX_LINES = 1000; +const FILE_EXTENSIONS = ['.rs']; +const EXCLUDE_PATTERNS = ['target', '.git', 'node_modules']; + +/** + * Check if a path should be excluded + * @param {string} path + * @returns {boolean} + */ +function shouldExclude(path) { + return EXCLUDE_PATTERNS.some((pattern) => path.includes(pattern)); +} + +/** + * Recursively find all Rust files in a directory + * @param {string} directory + * @returns {string[]} + */ +function findRustFiles(directory) { + const files = []; + + function walkDir(dir) { + const entries = readdirSync(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = join(dir, entry.name); + + if (shouldExclude(fullPath)) { + continue; + } + + if (entry.isDirectory()) { + walkDir(fullPath); + } else if (entry.isFile() && FILE_EXTENSIONS.includes(extname(entry.name))) { + files.push(fullPath); + } + } + } + + walkDir(directory); + return files; +} + +/** + * Count lines in a file + * @param {string} filePath + * @returns {number} + */ +function countLines(filePath) { + const content = readFileSync(filePath, 'utf-8'); + return content.split('\n').length; +} + +try { + const cwd = process.cwd(); + console.log(`\nChecking Rust files for maximum ${MAX_LINES} lines...\n`); + + const files = findRustFiles(cwd); + const violations = []; + + for (const file of files) { + const lineCount = countLines(file); + if (lineCount > MAX_LINES) { + violations.push({ + file: relative(cwd, file), + lines: lineCount, + }); + } + } + + if (violations.length === 0) { + console.log('All files are within the line limit\n'); + process.exit(0); + } else { + console.log('Found files exceeding the line limit:\n'); + for (const violation of violations) { + console.log( + ` ${violation.file}: ${violation.lines} lines (exceeds ${MAX_LINES})` + ); + } + console.log(`\nPlease refactor these files to be under ${MAX_LINES} lines\n`); + process.exit(1); + } +} catch (error) { + console.error('Error:', error.message); + process.exit(1); +} diff --git a/rust/scripts/collect-changelog.mjs b/rust/scripts/collect-changelog.mjs new file mode 100644 index 0000000..a8c59ac --- /dev/null +++ b/rust/scripts/collect-changelog.mjs @@ -0,0 +1,170 @@ +#!/usr/bin/env node + +/** + * Collect changelog fragments into CHANGELOG.md + * This script collects all .md files from changelog.d/ (except README.md) + * and prepends them to CHANGELOG.md, then removes the processed fragments. + * + * Uses link-foundation libraries: + * - use-m: Dynamic package loading without package.json dependencies + */ + +import { + readFileSync, + writeFileSync, + readdirSync, + unlinkSync, + existsSync, +} from 'fs'; +import { join } from 'path'; + +const CHANGELOG_DIR = 'changelog.d'; +const CHANGELOG_FILE = 'CHANGELOG.md'; +const INSERT_MARKER = ''; + +/** + * Get version from Cargo.toml + * @returns {string} + */ +function getVersionFromCargo() { + const cargoToml = readFileSync('Cargo.toml', 'utf-8'); + const match = cargoToml.match(/^version\s*=\s*"([^"]+)"/m); + + if (!match) { + console.error('Error: Could not find version in Cargo.toml'); + process.exit(1); + } + + return match[1]; +} + +/** + * Strip frontmatter from markdown content + * @param {string} content - Markdown content potentially with frontmatter + * @returns {string} - Content without frontmatter + */ +function stripFrontmatter(content) { + const frontmatterMatch = content.match(/^---\s*\n[\s\S]*?\n---\s*\n([\s\S]*)$/); + if (frontmatterMatch) { + return frontmatterMatch[1].trim(); + } + return content.trim(); +} + +/** + * Collect all changelog fragments + * @returns {string} + */ +function collectFragments() { + if (!existsSync(CHANGELOG_DIR)) { + return ''; + } + + const files = readdirSync(CHANGELOG_DIR) + .filter((f) => f.endsWith('.md') && f !== 'README.md') + .sort(); + + const fragments = []; + for (const file of files) { + const rawContent = readFileSync(join(CHANGELOG_DIR, file), 'utf-8'); + // Strip frontmatter (which contains bump type metadata) + const content = stripFrontmatter(rawContent); + if (content) { + fragments.push(content); + } + } + + return fragments.join('\n\n'); +} + +/** + * Update CHANGELOG.md with collected fragments + * @param {string} version + * @param {string} fragments + */ +function updateChangelog(version, fragments) { + const dateStr = new Date().toISOString().split('T')[0]; + const newEntry = `\n## [${version}] - ${dateStr}\n\n${fragments}\n`; + + if (existsSync(CHANGELOG_FILE)) { + let content = readFileSync(CHANGELOG_FILE, 'utf-8'); + + if (content.includes(INSERT_MARKER)) { + content = content.replace(INSERT_MARKER, `${INSERT_MARKER}${newEntry}`); + } else { + // Insert after the first ## heading + const lines = content.split('\n'); + let insertIndex = -1; + + for (let i = 0; i < lines.length; i++) { + if (lines[i].startsWith('## [')) { + insertIndex = i; + break; + } + } + + if (insertIndex >= 0) { + lines.splice(insertIndex, 0, newEntry); + content = lines.join('\n'); + } else { + // Append after the main heading + content += newEntry; + } + } + + writeFileSync(CHANGELOG_FILE, content, 'utf-8'); + } else { + const content = `# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +${INSERT_MARKER} +${newEntry} +`; + writeFileSync(CHANGELOG_FILE, content, 'utf-8'); + } + + console.log(`Updated CHANGELOG.md with version ${version}`); +} + +/** + * Remove processed changelog fragments + */ +function removeFragments() { + if (!existsSync(CHANGELOG_DIR)) { + return; + } + + const files = readdirSync(CHANGELOG_DIR).filter( + (f) => f.endsWith('.md') && f !== 'README.md' + ); + + for (const file of files) { + const filePath = join(CHANGELOG_DIR, file); + unlinkSync(filePath); + console.log(`Removed ${filePath}`); + } +} + +try { + const version = getVersionFromCargo(); + console.log(`Collecting changelog fragments for version ${version}`); + + const fragments = collectFragments(); + + if (!fragments) { + console.log('No changelog fragments found'); + process.exit(0); + } + + updateChangelog(version, fragments); + removeFragments(); + + console.log('Changelog collection complete'); +} catch (error) { + console.error('Error:', error.message); + process.exit(1); +} diff --git a/rust/scripts/create-github-release.mjs b/rust/scripts/create-github-release.mjs new file mode 100644 index 0000000..1d82c96 --- /dev/null +++ b/rust/scripts/create-github-release.mjs @@ -0,0 +1,110 @@ +#!/usr/bin/env node + +/** + * Create GitHub Release from CHANGELOG.md + * Usage: node scripts/create-github-release.mjs --release-version --repository + * + * Uses link-foundation libraries: + * - use-m: Dynamic package loading without package.json dependencies + * - command-stream: Modern shell command execution with streaming support + * - lino-arguments: Unified configuration from CLI args, env vars, and .lenv files + */ + +import { readFileSync, existsSync } from 'fs'; + +// Load use-m dynamically +const { use } = eval( + await (await fetch('https://unpkg.com/use-m/use.js')).text() +); + +// Import link-foundation libraries +const { $ } = await use('command-stream'); +const { makeConfig } = await use('lino-arguments'); + +// Parse CLI arguments +// Note: Using --release-version instead of --version to avoid conflict with yargs' built-in --version flag +const config = makeConfig({ + yargs: ({ yargs, getenv }) => + yargs + .option('release-version', { + type: 'string', + default: getenv('VERSION', ''), + describe: 'Version number (e.g., 1.0.0)', + }) + .option('repository', { + type: 'string', + default: getenv('REPOSITORY', ''), + describe: 'GitHub repository (e.g., owner/repo)', + }), +}); + +const { releaseVersion: version, repository } = config; + +if (!version || !repository) { + console.error('Error: Missing required arguments'); + console.error( + 'Usage: node scripts/create-github-release.mjs --release-version --repository ' + ); + process.exit(1); +} + +const tag = `v${version}`; + +console.log(`Creating GitHub release for ${tag}...`); + +/** + * Extract changelog content for a specific version + * @param {string} version + * @returns {string} + */ +function getChangelogForVersion(version) { + const changelogPath = 'CHANGELOG.md'; + + if (!existsSync(changelogPath)) { + return `Release v${version}`; + } + + const content = readFileSync(changelogPath, 'utf-8'); + + // Find the section for this version + const escapedVersion = version.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const pattern = new RegExp( + `## \\[${escapedVersion}\\].*?\\n([\\s\\S]*?)(?=\\n## \\[|$)` + ); + const match = content.match(pattern); + + if (match) { + return match[1].trim(); + } + + return `Release v${version}`; +} + +try { + const releaseNotes = getChangelogForVersion(version); + + // Create release using GitHub API with JSON input + // This avoids shell escaping issues + const payload = JSON.stringify({ + tag_name: tag, + name: `v${version}`, + body: releaseNotes, + }); + + try { + await $`gh api repos/${repository}/releases -X POST --input -`.run({ + stdin: payload, + }); + console.log(`Created GitHub release: ${tag}`); + } catch (error) { + // Check if release already exists + if (error.message && error.message.includes('already exists')) { + console.log(`Release ${tag} already exists, skipping`); + } else { + throw error; + } + } +} catch (error) { + console.error('Error creating release:', error.message); + process.exit(1); +} diff --git a/rust/scripts/get-bump-type.mjs b/rust/scripts/get-bump-type.mjs new file mode 100644 index 0000000..ff9f77c --- /dev/null +++ b/rust/scripts/get-bump-type.mjs @@ -0,0 +1,152 @@ +#!/usr/bin/env node + +/** + * Parse changelog fragments and determine version bump type + * + * This script reads changeset fragments from changelog.d/ and determines + * the version bump type based on the frontmatter in each fragment. + * + * Fragment format: + * --- + * bump: patch|minor|major + * --- + * + * ### Added + * - Your changes here + * + * Usage: node scripts/get-bump-type.mjs [--default ] + * + * Uses link-foundation libraries: + * - use-m: Dynamic package loading without package.json dependencies + * - lino-arguments: Unified configuration from CLI args, env vars, and .lenv files + */ + +import { readFileSync, readdirSync, existsSync, appendFileSync } from 'fs'; +import { join } from 'path'; + +// Load use-m dynamically +const { use } = eval( + await (await fetch('https://unpkg.com/use-m/use.js')).text() +); + +// Import lino-arguments for CLI argument parsing +const { makeConfig } = await use('lino-arguments'); + +// Parse CLI arguments +const config = makeConfig({ + yargs: ({ yargs, getenv }) => + yargs + .option('default', { + type: 'string', + default: getenv('DEFAULT_BUMP', 'patch'), + describe: 'Default bump type if no fragments specify one', + choices: ['major', 'minor', 'patch'], + }), +}); + +const { default: defaultBump } = config; + +const CHANGELOG_DIR = 'changelog.d'; + +// Bump type priority (higher = more significant) +const BUMP_PRIORITY = { + patch: 1, + minor: 2, + major: 3, +}; + +/** + * Parse frontmatter from a markdown file + * @param {string} content - File content + * @returns {{bump?: string, content: string}} + */ +function parseFrontmatter(content) { + const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/); + + if (!frontmatterMatch) { + return { content }; + } + + const frontmatter = frontmatterMatch[1]; + const body = frontmatterMatch[2]; + + // Parse YAML-like frontmatter (simple key: value format) + const data = {}; + for (const line of frontmatter.split('\n')) { + const match = line.match(/^\s*(\w+)\s*:\s*(.+?)\s*$/); + if (match) { + data[match[1]] = match[2]; + } + } + + return { ...data, content: body }; +} + +/** + * Get all changelog fragments and determine bump type + * @returns {{bumpType: string, fragmentCount: number}} + */ +function determineBumpType() { + if (!existsSync(CHANGELOG_DIR)) { + console.log(`No ${CHANGELOG_DIR} directory found`); + return { bumpType: defaultBump, fragmentCount: 0 }; + } + + const files = readdirSync(CHANGELOG_DIR) + .filter((f) => f.endsWith('.md') && f !== 'README.md') + .sort(); + + if (files.length === 0) { + console.log('No changelog fragments found'); + return { bumpType: defaultBump, fragmentCount: 0 }; + } + + let highestPriority = 0; + let highestBumpType = defaultBump; + + for (const file of files) { + const content = readFileSync(join(CHANGELOG_DIR, file), 'utf-8'); + const { bump } = parseFrontmatter(content); + + if (bump && BUMP_PRIORITY[bump]) { + const priority = BUMP_PRIORITY[bump]; + if (priority > highestPriority) { + highestPriority = priority; + highestBumpType = bump; + } + console.log(`Fragment ${file}: bump=${bump}`); + } else { + console.log(`Fragment ${file}: no bump specified, using default`); + } + } + + return { bumpType: highestBumpType, fragmentCount: files.length }; +} + +/** + * Append to GitHub Actions output file + * @param {string} key + * @param {string} value + */ +function setOutput(key, value) { + const outputFile = process.env.GITHUB_OUTPUT; + if (outputFile) { + appendFileSync(outputFile, `${key}=${value}\n`); + } + // Also log for visibility + console.log(`Output: ${key}=${value}`); +} + +try { + const { bumpType, fragmentCount } = determineBumpType(); + + console.log(`\nDetermined bump type: ${bumpType} (from ${fragmentCount} fragment(s))`); + + setOutput('bump_type', bumpType); + setOutput('fragment_count', String(fragmentCount)); + setOutput('has_fragments', fragmentCount > 0 ? 'true' : 'false'); + +} catch (error) { + console.error('Error:', error.message); + process.exit(1); +} diff --git a/rust/scripts/version-and-commit.mjs b/rust/scripts/version-and-commit.mjs new file mode 100644 index 0000000..4e66286 --- /dev/null +++ b/rust/scripts/version-and-commit.mjs @@ -0,0 +1,276 @@ +#!/usr/bin/env node + +/** + * Bump version in Cargo.toml and commit changes + * Used by the CI/CD pipeline for releases + * + * Usage: node scripts/version-and-commit.mjs --bump-type [--description ] + * + * Uses link-foundation libraries: + * - use-m: Dynamic package loading without package.json dependencies + * - command-stream: Modern shell command execution with streaming support + * - lino-arguments: Unified configuration from CLI args, env vars, and .lenv files + */ + +import { readFileSync, writeFileSync, appendFileSync, readdirSync, existsSync } from 'fs'; +import { join } from 'path'; + +// Load use-m dynamically +const { use } = eval( + await (await fetch('https://unpkg.com/use-m/use.js')).text() +); + +// Import link-foundation libraries +const { $ } = await use('command-stream'); +const { makeConfig } = await use('lino-arguments'); + +// Parse CLI arguments +const config = makeConfig({ + yargs: ({ yargs, getenv }) => + yargs + .option('bump-type', { + type: 'string', + default: getenv('BUMP_TYPE', ''), + describe: 'Version bump type: major, minor, or patch', + choices: ['major', 'minor', 'patch'], + }) + .option('description', { + type: 'string', + default: getenv('DESCRIPTION', ''), + describe: 'Release description', + }), +}); + +const { bumpType, description } = config; + +if (!bumpType || !['major', 'minor', 'patch'].includes(bumpType)) { + console.error( + 'Usage: node scripts/version-and-commit.mjs --bump-type [--description ]' + ); + process.exit(1); +} + +/** + * Append to GitHub Actions output file + * @param {string} key + * @param {string} value + */ +function setOutput(key, value) { + const outputFile = process.env.GITHUB_OUTPUT; + if (outputFile) { + appendFileSync(outputFile, `${key}=${value}\n`); + } + // Also log for visibility + console.log(`::set-output name=${key}::${value}`); +} + +/** + * Get current version from Cargo.toml + * @returns {{major: number, minor: number, patch: number}} + */ +function getCurrentVersion() { + const cargoToml = readFileSync('Cargo.toml', 'utf-8'); + const match = cargoToml.match(/^version\s*=\s*"(\d+)\.(\d+)\.(\d+)"/m); + + if (!match) { + console.error('Error: Could not parse version from Cargo.toml'); + process.exit(1); + } + + return { + major: parseInt(match[1], 10), + minor: parseInt(match[2], 10), + patch: parseInt(match[3], 10), + }; +} + +/** + * Calculate new version based on bump type + * @param {{major: number, minor: number, patch: number}} current + * @param {string} bumpType + * @returns {string} + */ +function calculateNewVersion(current, bumpType) { + const { major, minor, patch } = current; + + switch (bumpType) { + case 'major': + return `${major + 1}.0.0`; + case 'minor': + return `${major}.${minor + 1}.0`; + case 'patch': + return `${major}.${minor}.${patch + 1}`; + default: + throw new Error(`Invalid bump type: ${bumpType}`); + } +} + +/** + * Update version in Cargo.toml + * @param {string} newVersion + */ +function updateCargoToml(newVersion) { + let cargoToml = readFileSync('Cargo.toml', 'utf-8'); + cargoToml = cargoToml.replace( + /^(version\s*=\s*")[^"]+(")/m, + `$1${newVersion}$2` + ); + writeFileSync('Cargo.toml', cargoToml, 'utf-8'); + console.log(`Updated Cargo.toml to version ${newVersion}`); +} + +/** + * Check if a git tag exists for this version + * @param {string} version + * @returns {Promise} + */ +async function checkTagExists(version) { + try { + await $`git rev-parse v${version}`.run({ capture: true }); + return true; + } catch { + return false; + } +} + +/** + * Strip frontmatter from markdown content + * @param {string} content - Markdown content potentially with frontmatter + * @returns {string} - Content without frontmatter + */ +function stripFrontmatter(content) { + const frontmatterMatch = content.match(/^---\s*\n[\s\S]*?\n---\s*\n([\s\S]*)$/); + if (frontmatterMatch) { + return frontmatterMatch[1].trim(); + } + return content.trim(); +} + +/** + * Collect changelog fragments and update CHANGELOG.md + * @param {string} version + */ +function collectChangelog(version) { + const changelogDir = 'changelog.d'; + const changelogFile = 'CHANGELOG.md'; + + if (!existsSync(changelogDir)) { + return; + } + + const files = readdirSync(changelogDir).filter( + (f) => f.endsWith('.md') && f !== 'README.md' + ); + + if (files.length === 0) { + return; + } + + const fragments = files + .sort() + .map((f) => { + const rawContent = readFileSync(join(changelogDir, f), 'utf-8'); + // Strip frontmatter (which contains bump type metadata) + return stripFrontmatter(rawContent); + }) + .filter(Boolean) + .join('\n\n'); + + if (!fragments) { + return; + } + + const dateStr = new Date().toISOString().split('T')[0]; + const newEntry = `\n## [${version}] - ${dateStr}\n\n${fragments}\n`; + + if (existsSync(changelogFile)) { + let content = readFileSync(changelogFile, 'utf-8'); + const lines = content.split('\n'); + let insertIndex = -1; + + for (let i = 0; i < lines.length; i++) { + if (lines[i].startsWith('## [')) { + insertIndex = i; + break; + } + } + + if (insertIndex >= 0) { + lines.splice(insertIndex, 0, newEntry); + content = lines.join('\n'); + } else { + content += newEntry; + } + + writeFileSync(changelogFile, content, 'utf-8'); + } + + console.log(`Collected ${files.length} changelog fragment(s)`); +} + +async function main() { + try { + // Configure git + await $`git config user.name "github-actions[bot]"`; + await $`git config user.email "github-actions[bot]@users.noreply.github.com"`; + + const current = getCurrentVersion(); + const newVersion = calculateNewVersion(current, bumpType); + + // Check if this version was already released + if (await checkTagExists(newVersion)) { + console.log(`Tag v${newVersion} already exists`); + setOutput('already_released', 'true'); + setOutput('new_version', newVersion); + return; + } + + // Update version in Cargo.toml + updateCargoToml(newVersion); + + // Collect changelog fragments + collectChangelog(newVersion); + + // Stage Cargo.toml and CHANGELOG.md + await $`git add Cargo.toml CHANGELOG.md`; + + // Check if there are changes to commit + try { + await $`git diff --cached --quiet`.run({ capture: true }); + // No changes to commit + console.log('No changes to commit'); + setOutput('version_committed', 'false'); + setOutput('new_version', newVersion); + return; + } catch { + // There are changes to commit (git diff exits with 1 when there are differences) + } + + // Commit changes + const commitMsg = description + ? `chore: release v${newVersion}\n\n${description}` + : `chore: release v${newVersion}`; + await $`git commit -m ${commitMsg}`; + console.log(`Committed version ${newVersion}`); + + // Create tag + const tagMsg = description + ? `Release v${newVersion}\n\n${description}` + : `Release v${newVersion}`; + await $`git tag -a v${newVersion} -m ${tagMsg}`; + console.log(`Created tag v${newVersion}`); + + // Push changes and tag + await $`git push`; + await $`git push --tags`; + console.log('Pushed changes and tags'); + + setOutput('version_committed', 'true'); + setOutput('new_version', newVersion); + } catch (error) { + console.error('Error:', error.message); + process.exit(1); + } +} + +main(); diff --git a/rust/src/lib.rs b/rust/src/lib.rs new file mode 100644 index 0000000..c603af7 --- /dev/null +++ b/rust/src/lib.rs @@ -0,0 +1,742 @@ +//! `LinoEnv` - A Rust library to read and write `.lenv` files. +//! +//! `.lenv` files use `: ` instead of `=` for key-value separation. +//! Example: `GITHUB_TOKEN: gh_....` + +use std::collections::HashMap; +use std::fs; +use std::io::{self, BufRead, BufReader, Write}; +use std::path::Path; + +/// Package version (matches Cargo.toml version). +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); + +/// `LinoEnv` - A struct to read and write `.lenv` files. +/// +/// `.lenv` files use `: ` instead of `=` for key-value separation. +/// +/// # Examples +/// +/// ``` +/// use lino_env::LinoEnv; +/// use std::fs; +/// +/// // Create a temporary file for testing +/// let path = std::env::temp_dir().join("test_lino_env_example.lenv"); +/// let path = path.to_str().unwrap(); +/// +/// let mut env = LinoEnv::new(path); +/// env.set("GITHUB_TOKEN", "gh_test123"); +/// env.set("TELEGRAM_TOKEN", "054test456"); +/// env.write().unwrap(); +/// +/// // Read it back +/// let mut env2 = LinoEnv::new(path); +/// env2.read().unwrap(); +/// assert_eq!(env2.get("GITHUB_TOKEN"), Some("gh_test123".to_string())); +/// +/// // Clean up +/// fs::remove_file(path).ok(); +/// ``` +#[derive(Debug, Clone)] +pub struct LinoEnv { + file_path: String, + data: HashMap>, +} + +impl LinoEnv { + /// Create a new `LinoEnv` instance. + /// + /// # Arguments + /// + /// * `file_path` - Path to the .lenv file + /// + /// # Examples + /// + /// ``` + /// use lino_env::LinoEnv; + /// let env = LinoEnv::new(".lenv"); + /// ``` + #[must_use] + pub fn new>(file_path: P) -> Self { + Self { + file_path: file_path.as_ref().to_string(), + data: HashMap::new(), + } + } + + /// Read and parse the .lenv file. + /// + /// Stores all instances of each key (duplicates are allowed). + /// + /// # Errors + /// + /// Returns an error if the file cannot be read. + /// + /// # Examples + /// + /// ``` + /// use lino_env::LinoEnv; + /// let mut env = LinoEnv::new(".lenv"); + /// // Will return Ok even if file doesn't exist (data will be empty) + /// let _ = env.read(); + /// ``` + pub fn read(&mut self) -> io::Result<&mut Self> { + self.data.clear(); + + let path = Path::new(&self.file_path); + if !path.exists() { + return Ok(self); + } + + let file = fs::File::open(path)?; + let reader = BufReader::new(file); + + for line in reader.lines() { + let line = line?; + let trimmed = line.trim(); + + // Skip empty lines and comments + if trimmed.is_empty() || trimmed.starts_with('#') { + continue; + } + + // Parse line with `: ` separator + if let Some(separator_index) = line.find(": ") { + let key = line[..separator_index].trim().to_string(); + let value = line[separator_index + 2..].to_string(); // Don't trim value to preserve spaces + + self.data.entry(key).or_default().push(value); + } + } + + Ok(self) + } + + /// Get the last instance of a reference (key). + /// + /// # Arguments + /// + /// * `reference` - The key to look up + /// + /// # Returns + /// + /// The last value associated with the key, or None if not found. + /// + /// # Examples + /// + /// ``` + /// use lino_env::LinoEnv; + /// let mut env = LinoEnv::new(".lenv"); + /// env.set("KEY", "value"); + /// assert_eq!(env.get("KEY"), Some("value".to_string())); + /// assert_eq!(env.get("NONEXISTENT"), None); + /// ``` + #[must_use] + pub fn get(&self, reference: &str) -> Option { + self.data + .get(reference) + .and_then(|values| values.last().cloned()) + } + + /// Get all instances of a reference (key). + /// + /// # Arguments + /// + /// * `reference` - The key to look up + /// + /// # Returns + /// + /// All values associated with the key, or an empty vector if not found. + /// + /// # Examples + /// + /// ``` + /// use lino_env::LinoEnv; + /// let mut env = LinoEnv::new(".lenv"); + /// env.add("KEY", "value1"); + /// env.add("KEY", "value2"); + /// assert_eq!(env.get_all("KEY"), vec!["value1", "value2"]); + /// ``` + #[must_use] + pub fn get_all(&self, reference: &str) -> Vec { + self.data.get(reference).cloned().unwrap_or_default() + } + + /// Set all instances of a reference to a new value. + /// + /// Replaces all existing instances with a single new value. + /// + /// # Arguments + /// + /// * `reference` - The key to set + /// * `value` - The new value + /// + /// # Examples + /// + /// ``` + /// use lino_env::LinoEnv; + /// let mut env = LinoEnv::new(".lenv"); + /// env.add("KEY", "old1"); + /// env.add("KEY", "old2"); + /// env.set("KEY", "new_value"); + /// assert_eq!(env.get_all("KEY"), vec!["new_value"]); + /// ``` + pub fn set(&mut self, reference: &str, value: &str) -> &mut Self { + self.data + .insert(reference.to_string(), vec![value.to_string()]); + self + } + + /// Add a new instance of a reference (allows duplicates). + /// + /// # Arguments + /// + /// * `reference` - The key to add + /// * `value` - The value to add + /// + /// # Examples + /// + /// ``` + /// use lino_env::LinoEnv; + /// let mut env = LinoEnv::new(".lenv"); + /// env.add("KEY", "value1"); + /// env.add("KEY", "value2"); + /// assert_eq!(env.get_all("KEY"), vec!["value1", "value2"]); + /// ``` + pub fn add(&mut self, reference: &str, value: &str) -> &mut Self { + self.data + .entry(reference.to_string()) + .or_default() + .push(value.to_string()); + self + } + + /// Write the current data back to the .lenv file. + /// + /// # Errors + /// + /// Returns an error if the file cannot be written. + /// + /// # Examples + /// + /// ``` + /// use lino_env::LinoEnv; + /// use std::fs; + /// + /// let path = std::env::temp_dir().join("test_lino_env_write.lenv"); + /// let path = path.to_str().unwrap(); + /// let mut env = LinoEnv::new(path); + /// env.set("KEY", "value"); + /// env.write().unwrap(); + /// + /// // Clean up + /// fs::remove_file(path).ok(); + /// ``` + pub fn write(&self) -> io::Result<&Self> { + let mut file = fs::File::create(&self.file_path)?; + + for (key, values) in &self.data { + for value in values { + writeln!(file, "{key}: {value}")?; + } + } + + Ok(self) + } + + /// Check if a reference exists. + /// + /// # Arguments + /// + /// * `reference` - The key to check + /// + /// # Returns + /// + /// `true` if the key exists and has at least one value. + /// + /// # Examples + /// + /// ``` + /// use lino_env::LinoEnv; + /// let mut env = LinoEnv::new(".lenv"); + /// env.set("KEY", "value"); + /// assert!(env.has("KEY")); + /// assert!(!env.has("NONEXISTENT")); + /// ``` + #[must_use] + pub fn has(&self, reference: &str) -> bool { + self.data + .get(reference) + .is_some_and(|values| !values.is_empty()) + } + + /// Delete all instances of a reference. + /// + /// # Arguments + /// + /// * `reference` - The key to delete + /// + /// # Examples + /// + /// ``` + /// use lino_env::LinoEnv; + /// let mut env = LinoEnv::new(".lenv"); + /// env.set("KEY", "value"); + /// env.delete("KEY"); + /// assert!(!env.has("KEY")); + /// ``` + pub fn delete(&mut self, reference: &str) -> &mut Self { + self.data.remove(reference); + self + } + + /// Get all keys. + /// + /// # Returns + /// + /// A vector of all keys in the environment. + /// + /// # Examples + /// + /// ``` + /// use lino_env::LinoEnv; + /// let mut env = LinoEnv::new(".lenv"); + /// env.set("KEY1", "value1"); + /// env.set("KEY2", "value2"); + /// let keys = env.keys(); + /// assert!(keys.contains(&"KEY1".to_string())); + /// assert!(keys.contains(&"KEY2".to_string())); + /// ``` + #[must_use] + pub fn keys(&self) -> Vec { + self.data.keys().cloned().collect() + } + + /// Get all entries as a `HashMap` (with last instance of each key). + /// + /// # Returns + /// + /// A `HashMap` with each key mapped to its last value. + /// + /// # Examples + /// + /// ``` + /// use lino_env::LinoEnv; + /// let mut env = LinoEnv::new(".lenv"); + /// env.add("KEY1", "value1a"); + /// env.add("KEY1", "value1b"); + /// env.set("KEY2", "value2"); + /// let obj = env.to_hash_map(); + /// assert_eq!(obj.get("KEY1"), Some(&"value1b".to_string())); + /// ``` + #[must_use] + pub fn to_hash_map(&self) -> HashMap { + let mut result = HashMap::new(); + for (key, values) in &self.data { + if let Some(last_value) = values.last() { + result.insert(key.clone(), last_value.clone()); + } + } + result + } +} + +/// Convenience function to read a .lenv file. +/// +/// # Arguments +/// +/// * `file_path` - Path to the .lenv file +/// +/// # Errors +/// +/// Returns an error if the file cannot be read. +/// +/// # Examples +/// +/// ``` +/// use lino_env::read_lino_env; +/// // Will work even if file doesn't exist +/// let env = read_lino_env(".lenv"); +/// ``` +pub fn read_lino_env>(file_path: P) -> io::Result { + let mut env = LinoEnv::new(file_path); + env.read()?; + Ok(env) +} + +/// Convenience function to create and write a .lenv file. +/// +/// # Arguments +/// +/// * `file_path` - Path to the .lenv file +/// * `data` - Key-value pairs to write +/// +/// # Errors +/// +/// Returns an error if the file cannot be written. +/// +/// # Examples +/// +/// ``` +/// use lino_env::write_lino_env; +/// use std::collections::HashMap; +/// use std::fs; +/// +/// let path = std::env::temp_dir().join("test_write_lino_env.lenv"); +/// let path = path.to_str().unwrap(); +/// let mut data = HashMap::new(); +/// data.insert("KEY".to_string(), "value".to_string()); +/// write_lino_env(path, &data).unwrap(); +/// +/// // Clean up +/// fs::remove_file(path).ok(); +/// ``` +#[allow(clippy::implicit_hasher)] +pub fn write_lino_env>( + file_path: P, + data: &HashMap, +) -> io::Result { + let mut env = LinoEnv::new(file_path); + for (key, value) in data { + env.set(key, value); + } + env.write()?; + Ok(env) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + + fn cleanup(path: &str) { + fs::remove_file(path).ok(); + } + + fn test_file(name: &str) -> String { + std::env::temp_dir() + .join(format!("lino_env_test_{name}.lenv")) + .to_string_lossy() + .to_string() + } + + mod basic_tests { + use super::*; + + #[test] + fn test_create_and_write() { + let test_file = test_file("basic_create_write"); + cleanup(&test_file); + let mut env = LinoEnv::new(&test_file); + env.set("GITHUB_TOKEN", "gh_test123"); + env.set("TELEGRAM_TOKEN", "054test456"); + env.write().unwrap(); + + assert!(Path::new(&test_file).exists()); + cleanup(&test_file); + } + + #[test] + fn test_read() { + let test_file = test_file("basic_read"); + cleanup(&test_file); + // First create a file + let mut env1 = LinoEnv::new(&test_file); + env1.set("GITHUB_TOKEN", "gh_test123"); + env1.set("TELEGRAM_TOKEN", "054test456"); + env1.write().unwrap(); + + // Then read it + let mut env2 = LinoEnv::new(&test_file); + env2.read().unwrap(); + + assert_eq!(env2.get("GITHUB_TOKEN"), Some("gh_test123".to_string())); + assert_eq!(env2.get("TELEGRAM_TOKEN"), Some("054test456".to_string())); + cleanup(&test_file); + } + } + + mod get_tests { + use super::*; + + #[test] + fn test_get_last_instance() { + let test_file = test_file("get_last_instance"); + cleanup(&test_file); + let mut env = LinoEnv::new(&test_file); + env.add("API_KEY", "value1"); + env.add("API_KEY", "value2"); + env.add("API_KEY", "value3"); + + assert_eq!(env.get("API_KEY"), Some("value3".to_string())); + cleanup(&test_file); + } + + #[test] + fn test_get_nonexistent() { + let test_file = test_file("get_nonexistent"); + let env = LinoEnv::new(&test_file); + assert_eq!(env.get("NON_EXISTENT"), None); + } + } + + mod get_all_tests { + use super::*; + + #[test] + fn test_get_all_instances() { + let test_file = test_file("get_all_instances"); + cleanup(&test_file); + let mut env = LinoEnv::new(&test_file); + env.add("API_KEY", "value1"); + env.add("API_KEY", "value2"); + env.add("API_KEY", "value3"); + + assert_eq!(env.get_all("API_KEY"), vec!["value1", "value2", "value3"]); + cleanup(&test_file); + } + + #[test] + fn test_get_all_nonexistent() { + let test_file = test_file("get_all_nonexistent"); + let env = LinoEnv::new(&test_file); + assert!(env.get_all("NON_EXISTENT").is_empty()); + } + } + + mod set_tests { + use super::*; + + #[test] + fn test_set_replaces_all() { + let test_file = test_file("set_replaces_all"); + cleanup(&test_file); + let mut env = LinoEnv::new(&test_file); + env.add("API_KEY", "value1"); + env.add("API_KEY", "value2"); + env.set("API_KEY", "new_value"); + + assert_eq!(env.get("API_KEY"), Some("new_value".to_string())); + assert_eq!(env.get_all("API_KEY"), vec!["new_value"]); + cleanup(&test_file); + } + } + + mod add_tests { + use super::*; + + #[test] + fn test_add_duplicates() { + let test_file = test_file("add_duplicates"); + cleanup(&test_file); + let mut env = LinoEnv::new(&test_file); + env.add("KEY", "value1"); + env.add("KEY", "value2"); + env.add("KEY", "value3"); + + assert_eq!(env.get_all("KEY"), vec!["value1", "value2", "value3"]); + cleanup(&test_file); + } + } + + mod has_tests { + use super::*; + + #[test] + fn test_has_existing() { + let test_file = test_file("has_existing"); + cleanup(&test_file); + let mut env = LinoEnv::new(&test_file); + env.set("KEY", "value"); + + assert!(env.has("KEY")); + cleanup(&test_file); + } + + #[test] + fn test_has_nonexistent() { + let test_file = test_file("has_nonexistent"); + let env = LinoEnv::new(&test_file); + assert!(!env.has("NON_EXISTENT")); + } + } + + mod delete_tests { + use super::*; + + #[test] + fn test_delete_all_instances() { + let test_file = test_file("delete_all_instances"); + cleanup(&test_file); + let mut env = LinoEnv::new(&test_file); + env.add("KEY", "value1"); + env.add("KEY", "value2"); + env.delete("KEY"); + + assert!(!env.has("KEY")); + assert_eq!(env.get("KEY"), None); + cleanup(&test_file); + } + } + + mod keys_tests { + use super::*; + + #[test] + fn test_keys() { + let test_file = test_file("keys"); + cleanup(&test_file); + let mut env = LinoEnv::new(&test_file); + env.set("KEY1", "value1"); + env.set("KEY2", "value2"); + env.set("KEY3", "value3"); + + let keys = env.keys(); + assert!(keys.contains(&"KEY1".to_string())); + assert!(keys.contains(&"KEY2".to_string())); + assert!(keys.contains(&"KEY3".to_string())); + assert_eq!(keys.len(), 3); + cleanup(&test_file); + } + } + + mod to_hash_map_tests { + use super::*; + + #[test] + fn test_to_hash_map() { + let test_file = test_file("to_hash_map"); + cleanup(&test_file); + let mut env = LinoEnv::new(&test_file); + env.add("KEY1", "value1a"); + env.add("KEY1", "value1b"); + env.set("KEY2", "value2"); + + let obj = env.to_hash_map(); + assert_eq!(obj.get("KEY1"), Some(&"value1b".to_string())); + assert_eq!(obj.get("KEY2"), Some(&"value2".to_string())); + cleanup(&test_file); + } + } + + mod persistence_tests { + use super::*; + + #[test] + fn test_persist_duplicates() { + let test_file = test_file("persist_duplicates"); + cleanup(&test_file); + let mut env1 = LinoEnv::new(&test_file); + env1.add("KEY", "value1"); + env1.add("KEY", "value2"); + env1.add("KEY", "value3"); + env1.write().unwrap(); + + let mut env2 = LinoEnv::new(&test_file); + env2.read().unwrap(); + + assert_eq!(env2.get_all("KEY"), vec!["value1", "value2", "value3"]); + assert_eq!(env2.get("KEY"), Some("value3".to_string())); + cleanup(&test_file); + } + } + + mod convenience_function_tests { + use super::*; + + #[test] + fn test_read_lino_env() { + let test_file_path = test_file("convenience_read"); + cleanup(&test_file_path); + let mut data = HashMap::new(); + data.insert("GITHUB_TOKEN".to_string(), "gh_test".to_string()); + data.insert("TELEGRAM_TOKEN".to_string(), "054test".to_string()); + write_lino_env(&test_file_path, &data).unwrap(); + + let env = read_lino_env(&test_file_path).unwrap(); + assert_eq!(env.get("GITHUB_TOKEN"), Some("gh_test".to_string())); + assert_eq!(env.get("TELEGRAM_TOKEN"), Some("054test".to_string())); + cleanup(&test_file_path); + } + + #[test] + fn test_write_lino_env() { + let test_file_path = test_file("convenience_write"); + cleanup(&test_file_path); + let mut data = HashMap::new(); + data.insert("API_KEY".to_string(), "test_key".to_string()); + data.insert("SECRET".to_string(), "test_secret".to_string()); + write_lino_env(&test_file_path, &data).unwrap(); + + let env = read_lino_env(&test_file_path).unwrap(); + assert_eq!(env.get("API_KEY"), Some("test_key".to_string())); + assert_eq!(env.get("SECRET"), Some("test_secret".to_string())); + cleanup(&test_file_path); + } + } + + mod format_tests { + use super::*; + + #[test] + fn test_values_with_colons() { + let test_file_path = test_file("format_colons"); + cleanup(&test_file_path); + let mut env = LinoEnv::new(&test_file_path); + env.set("URL", "https://example.com:8080"); + env.write().unwrap(); + + let mut env2 = LinoEnv::new(&test_file_path); + env2.read().unwrap(); + assert_eq!( + env2.get("URL"), + Some("https://example.com:8080".to_string()) + ); + cleanup(&test_file_path); + } + + #[test] + fn test_values_with_spaces() { + let test_file_path = test_file("format_spaces"); + cleanup(&test_file_path); + let mut env = LinoEnv::new(&test_file_path); + env.set("MESSAGE", "Hello World"); + env.write().unwrap(); + + let mut env2 = LinoEnv::new(&test_file_path); + env2.read().unwrap(); + assert_eq!(env2.get("MESSAGE"), Some("Hello World".to_string())); + cleanup(&test_file_path); + } + } + + mod edge_case_tests { + use super::*; + + #[test] + fn test_nonexistent_file() { + let test_file_path = test_file("nonexistent"); + cleanup(&test_file_path); + let mut env = LinoEnv::new(&test_file_path); + env.read().unwrap(); + + assert_eq!(env.get("ANY_KEY"), None); + assert!(env.keys().is_empty()); + } + + #[test] + fn test_empty_values() { + let test_file_path = test_file("empty_values"); + cleanup(&test_file_path); + let mut env = LinoEnv::new(&test_file_path); + env.set("EMPTY_KEY", ""); + env.write().unwrap(); + + let mut env2 = LinoEnv::new(&test_file_path); + env2.read().unwrap(); + assert_eq!(env2.get("EMPTY_KEY"), Some(String::new())); + cleanup(&test_file_path); + } + } +} diff --git a/scripts/detect-code-changes.mjs b/scripts/detect-code-changes.mjs new file mode 100644 index 0000000..e3ae289 --- /dev/null +++ b/scripts/detect-code-changes.mjs @@ -0,0 +1,245 @@ +#!/usr/bin/env node + +/** + * Detect code changes for CI/CD pipeline + * + * This script detects what types of files have changed between two commits + * and outputs the results for use in GitHub Actions workflow conditions. + * + * Key behavior: + * - For PRs: compares PR head against base branch + * - For pushes: compares HEAD against HEAD^ + * - Excludes certain folders and file types from "code changes" detection + * + * Excluded from code changes (don't require changesets/changelog fragments): + * - Markdown files (*.md) in any folder + * - .changeset/ folder (JS changeset metadata) + * - changelog.d/ folder (Rust changelog fragments) + * - docs/ folder (documentation) + * - experiments/ folder (experimental scripts) + * - examples/ folder (example scripts) + * + * Usage: + * node scripts/detect-code-changes.mjs + * node scripts/detect-code-changes.mjs --lang js + * node scripts/detect-code-changes.mjs --lang rust + * + * Environment variables (set by GitHub Actions): + * - GITHUB_EVENT_NAME: 'pull_request' or 'push' + * - GITHUB_BASE_SHA: Base commit SHA for PR + * - GITHUB_HEAD_SHA: Head commit SHA for PR + * + * Outputs (written to GITHUB_OUTPUT): + * - mjs-changed: 'true' if any .mjs files changed + * - js-changed: 'true' if any .js files changed + * - package-changed: 'true' if package.json changed + * - rs-changed: 'true' if any .rs files changed + * - toml-changed: 'true' if any .toml files changed + * - docs-changed: 'true' if any .md files changed + * - workflow-changed: 'true' if any .github/workflows/ files changed + * - any-code-changed: 'true' if any code files changed (excludes docs, changesets, experiments, examples) + */ + +import { execSync } from 'child_process'; +import { appendFileSync } from 'fs'; + +// Parse command line arguments +const args = process.argv.slice(2); +const langIndex = args.indexOf('--lang'); +const lang = langIndex !== -1 ? args[langIndex + 1] : 'all'; + +/** + * Execute a shell command and return trimmed output + * @param {string} command - The command to execute + * @returns {string} - The trimmed command output + */ +function exec(command) { + try { + return execSync(command, { encoding: 'utf-8' }).trim(); + } catch (error) { + console.error(`Error executing command: ${command}`); + console.error(error.message); + return ''; + } +} + +/** + * Write output to GitHub Actions output file + * @param {string} name - Output name + * @param {string} value - Output value + */ +function setOutput(name, value) { + const outputFile = process.env.GITHUB_OUTPUT; + if (outputFile) { + appendFileSync(outputFile, `${name}=${value}\n`); + } + console.log(`${name}=${value}`); +} + +/** + * Get the list of changed files between two commits + * @returns {string[]} Array of changed file paths + */ +function getChangedFiles() { + const eventName = process.env.GITHUB_EVENT_NAME || 'local'; + + if (eventName === 'pull_request') { + const baseSha = process.env.GITHUB_BASE_SHA; + const headSha = process.env.GITHUB_HEAD_SHA; + + if (baseSha && headSha) { + console.log(`Comparing PR: ${baseSha}...${headSha}`); + try { + // Ensure we have the base commit + try { + execSync(`git cat-file -e ${baseSha}`, { stdio: 'ignore' }); + } catch { + console.log('Base commit not available locally, attempting fetch...'); + execSync(`git fetch origin ${baseSha}`, { stdio: 'inherit' }); + } + const output = exec(`git diff --name-only ${baseSha} ${headSha}`); + return output ? output.split('\n').filter(Boolean) : []; + } catch (error) { + console.error(`Git diff failed: ${error.message}`); + } + } + } + + // For push events or fallback + console.log('Comparing HEAD^ to HEAD'); + try { + const output = exec('git diff --name-only HEAD^ HEAD'); + return output ? output.split('\n').filter(Boolean) : []; + } catch { + // If HEAD^ doesn't exist (first commit), list all files in HEAD + console.log('HEAD^ not available, listing all files in HEAD'); + const output = exec('git ls-tree --name-only -r HEAD'); + return output ? output.split('\n').filter(Boolean) : []; + } +} + +/** + * Check if a file should be excluded from code changes detection + * @param {string} filePath - The file path to check + * @returns {boolean} True if the file should be excluded + */ +function isExcludedFromCodeChanges(filePath) { + // Exclude markdown files in any folder + if (filePath.endsWith('.md')) { + return true; + } + + // Exclude specific folders from code changes + const excludedFolders = [ + '.changeset/', + 'js/.changeset/', + 'changelog.d/', + 'rust/changelog.d/', + 'docs/', + 'experiments/', + 'examples/', + 'js/experiments/', + 'js/examples/', + 'rust/experiments/', + 'rust/examples/', + ]; + + for (const folder of excludedFolders) { + if (filePath.startsWith(folder)) { + return true; + } + } + + return false; +} + +/** + * Filter files by language folder + * @param {string[]} files - Array of file paths + * @param {string} language - 'js', 'rust', or 'all' + * @returns {string[]} Filtered files + */ +function filterByLanguage(files, language) { + if (language === 'all') { + return files; + } + + const prefix = language === 'js' ? 'js/' : 'rust/'; + return files.filter((file) => file.startsWith(prefix)); +} + +/** + * Main function to detect changes + */ +function detectChanges() { + console.log(`Detecting file changes for CI/CD (language filter: ${lang})...\n`); + + const allChangedFiles = getChangedFiles(); + const changedFiles = filterByLanguage(allChangedFiles, lang); + + console.log('Changed files:'); + if (changedFiles.length === 0) { + console.log(' (none)'); + } else { + changedFiles.forEach((file) => console.log(` ${file}`)); + } + console.log(''); + + // Detect .mjs file changes (JS) + const mjsChanged = changedFiles.some((file) => file.endsWith('.mjs')); + setOutput('mjs-changed', mjsChanged ? 'true' : 'false'); + + // Detect .js file changes (JS) + const jsChanged = changedFiles.some((file) => file.endsWith('.js')); + setOutput('js-changed', jsChanged ? 'true' : 'false'); + + // Detect package.json changes (JS) + const packageChanged = changedFiles.some((file) => file.endsWith('package.json')); + setOutput('package-changed', packageChanged ? 'true' : 'false'); + + // Detect .rs file changes (Rust) + const rsChanged = changedFiles.some((file) => file.endsWith('.rs')); + setOutput('rs-changed', rsChanged ? 'true' : 'false'); + + // Detect .toml file changes (Rust) + const tomlChanged = changedFiles.some((file) => file.endsWith('.toml')); + setOutput('toml-changed', tomlChanged ? 'true' : 'false'); + + // Detect documentation changes (any .md file) + const docsChanged = changedFiles.some((file) => file.endsWith('.md')); + setOutput('docs-changed', docsChanged ? 'true' : 'false'); + + // Detect workflow changes + const workflowChanged = changedFiles.some((file) => + file.startsWith('.github/workflows/') + ); + setOutput('workflow-changed', workflowChanged ? 'true' : 'false'); + + // Detect code changes (excluding docs, changesets, experiments, examples folders, and markdown files) + const codeChangedFiles = changedFiles.filter( + (file) => !isExcludedFromCodeChanges(file) + ); + + console.log('\nFiles considered as code changes:'); + if (codeChangedFiles.length === 0) { + console.log(' (none)'); + } else { + codeChangedFiles.forEach((file) => console.log(` ${file}`)); + } + console.log(''); + + // Check if any code files changed + const codePattern = + lang === 'rust' + ? /\.(rs|toml|mjs|js|yml|yaml)$|\.github\/workflows\// + : lang === 'js' + ? /\.(mjs|js|json|yml|yaml)$|\.github\/workflows\// + : /\.(mjs|js|json|rs|toml|yml|yaml)$|\.github\/workflows\//; + const codeChanged = codeChangedFiles.some((file) => codePattern.test(file)); + setOutput('any-code-changed', codeChanged ? 'true' : 'false'); + + console.log('\nChange detection completed.'); +} + +// Run the detection +detectChanges();