diff --git a/.claude/skills/ci-prep/SKILL.md b/.claude/skills/ci-prep/SKILL.md new file mode 100644 index 0000000..89a0bcb --- /dev/null +++ b/.claude/skills/ci-prep/SKILL.md @@ -0,0 +1,121 @@ +--- +name: ci-prep +description: Prepares the current branch for CI by running the exact same steps locally and fixing issues. If CI is already failing, fetches the GH Actions logs first to diagnose. Use before pushing, when CI is red, or when the user says "fix ci". +argument-hint: "[--failing] [optional job name to focus on]" +--- + + +# CI Prep + +Prepare the current state for CI. If CI is already failing, fetch and analyze the logs first. + +## Arguments + +- `--failing` — Indicates a GitHub Actions run is already failing. When present, you MUST execute **Step 1** before doing anything else. +- Any other argument is treated as a job name to focus on (but all failures are still reported). + +If `--failing` is NOT passed, skip directly to **Step 2**. + +## Step 1 — Fetch failed CI logs (only when `--failing`) + +You MUST do this before any other work. + +```bash +BRANCH=$(git branch --show-current) +PR_JSON=$(gh pr list --head "$BRANCH" --state open --json number,title,url --limit 1) +``` + +If the JSON array is empty, **stop immediately**: +> No open PR found for branch `$BRANCH`. Create a PR first. + +Otherwise fetch the logs: + +```bash +PR_NUMBER=$(echo "$PR_JSON" | jq -r '.[0].number') +gh pr checks "$PR_NUMBER" +RUN_ID=$(gh run list --branch "$BRANCH" --limit 1 --json databaseId --jq '.[0].databaseId') +gh run view "$RUN_ID" +gh run view "$RUN_ID" --log-failed +``` + +Read **every line** of `--log-failed` output. For each failure note the exact file, line, and error message. If a job name argument was provided, prioritize that job but still report all failures. + +## Step 2 — Analyze the CI workflow + +1. Find the CI workflow file. Look in `.github/workflows/` for `ci.yml`, `build.yml`, `test.yml`, `checks.yml`, `main.yml`, `pull_request.yml`, or any workflow triggered on `pull_request` or `push`. +2. Read the workflow file completely. Parse every job and every step. +3. Extract the ordered list of commands the CI actually runs. In a spec-compliant repo this is `make lint → make test → make build` (REPO-STANDARDS-SPEC [MAKE-TARGETS]), but the actual CI may use `npm`, `cargo`, `dotnet`, raw shell commands, or anything else. Extract what is *actually there*. +4. Note any environment variables, matrix strategies, or conditional steps that affect execution. + +**Do NOT assume the steps are `make lint`, `make test`, `make build`.** The actual CI may run different commands, in a different order. Extract what the CI *actually does*. If you find extra targets beyond the 7 in [MAKE-TARGETS] (e.g. `make fmt-check`, `make coverage-check`), flag them in your final report — they should be consolidated by the agent-pmo skill. + +### Release workflow blocker scan + +If `.github/workflows/release.yml` exists, scan it before broad local CI. These are critical blockers +and must be fixed before release work is considered CI-ready: + +- Tag-triggered jobs checking out `ref: main` instead of the tagged SHA. +- Any `git commit`, `git push`, branch mutation, or tag mutation during release. +- Version bump commits after the tag already exists. +- Ad hoc `sed` version stamping of structured files instead of a first-class stamper/build input. +- Missing tests that pass a test version into the same stamper used by release. +- Native VSIX releases without Node `22.x`, `npx vsce package --target `, one VSIX per + target, target-suffixed filenames, and package-content verification. +- VS Code native-binary activation that reads or mutates PATH, uses package-manager/global installs + as normal startup sources, or copies bundled VSIX binaries after install. + +## Step 3 — Run each CI step locally, in order + +Work through failures in this priority order: + +1. **Formatting** — run auto-formatters first to clear noise +2. **Compilation errors** — must compile before lint/test +3. **Lint violations** — fix the code pattern +4. **Runtime / test failures** — fix source code to satisfy the test + +For each command extracted from the CI workflow: + +1. Run the command exactly as CI would run it (adjusting only for local environment differences like not needing `actions/checkout`). +2. If the step fails, **stop and fix the issues** before continuing to the next step. +3. After fixing, re-run the same step to confirm it passes. +4. Move to the next step only after the current one succeeds. + +### Hard constraints + +- **NEVER modify test files** — fix the source code, not the tests +- **NEVER add suppressions** (`// eslint-disable`, `// @ts-ignore`, `// @ts-nocheck`) +- **NEVER use `any` in TypeScript** to silence type errors +- **NEVER delete or ignore failing tests** +- **NEVER remove assertions** + +If stuck on the same failure after 5 attempts, ask the user for help. + +## Step 4 — Report + +- List every step that was run and its result (pass/fail/fixed). +- If any step could not be fixed, report what failed and why. +- Confirm whether the branch is ready to push. + +## Step 5 — Remote CI follow-up (only when `--failing`) + +Once all CI steps pass locally: + +1. Report the local fixes and exact commands that now pass. +2. Do not commit or push. The user owns source-control writes. +3. If the user pushes, monitor the new run until completion or failure. +4. Upon failure, go back to Step 1. + +## Rules + +- **Always read the CI workflow first.** Never assume what commands CI runs. +- Do not commit or push from this skill. +- Fix issues found in each step before moving to the next +- Never skip steps or suppress errors +- If the CI workflow has multiple jobs, run all of them (respecting dependency order) +- Skip steps that are CI-infrastructure-only (checkout, setup-node/python/rust actions, cache steps, artifact uploads) — focus on the actual build/test/lint commands + +## Success criteria + +- Every command that CI runs has been executed locally and passed +- All fixes are applied to the working tree +- The CI passes successfully (if you are correcting and existing failure) diff --git a/.claude/skills/code-dedup/SKILL.md b/.claude/skills/code-dedup/SKILL.md new file mode 100644 index 0000000..1b1c593 --- /dev/null +++ b/.claude/skills/code-dedup/SKILL.md @@ -0,0 +1,108 @@ +--- +name: code-dedup +description: Searches for duplicate code, duplicate tests, and dead code, then safely merges or removes them. Use when the user says "deduplicate", "find duplicates", "remove dead code", "DRY up", or "code dedup". Requires test coverage — refuses to touch untested code. +--- + + +# Code Dedup + +Carefully search for duplicate code, duplicate tests, and dead code across the Diffy repo. Merge duplicates and delete dead code — but only when test coverage proves the change is safe. + +## Prerequisites — hard gate + +Before touching ANY code, verify these conditions. If any fail, stop and report why. + +1. Run `make test` — all tests must pass. If tests fail, stop. Do not dedup a broken codebase. +2. Run `make test` — tests are fail-fast AND enforce the coverage threshold from `coverage-thresholds.json`. If anything fails, stop and fix it before deduping. +3. Verify the project uses **static typing**. TypeScript with `tsconfig.json` `"strict": true` is required (which Diffy enforces — see CLAUDE.md). If `strict` is off, STOP and refuse. + +## Steps + +Copy this checklist and track progress: + +``` +Dedup Progress: +- [ ] Step 1: Prerequisites passed (tests green, coverage met, strict TS) +- [ ] Step 2: Dead code scan complete +- [ ] Step 3: Duplicate code scan complete +- [ ] Step 4: Duplicate test scan complete +- [ ] Step 5: Changes applied +- [ ] Step 6: Verification passed (tests green, coverage stable) +``` + +### Step 1 — Inventory test coverage + +Before deciding what to touch, understand what is tested. + +1. Run `make test` to confirm green baseline. `make test` is fail-fast AND enforces the coverage threshold from `coverage-thresholds.json` (REPO-STANDARDS-SPEC [TEST-RULES], [COVERAGE-THRESHOLDS-JSON]). It exits non-zero on any test failure OR coverage shortfall. +2. Note the current coverage percentage — this is the floor. It must not drop. +3. Identify which files/modules have coverage and which do not. Only files WITH coverage are candidates for dedup. + +### Step 2 — Scan for dead code + +Search for code that is never called, never imported, never referenced. + +1. Look for unused exports, unused functions, unused classes, unused variables. +2. Check TypeScript: `noUnusedLocals`/`noUnusedParameters` are already enabled in Diffy's `tsconfig.json`. Look for unexported helpers with zero references. +3. For each candidate: **grep the entire codebase** (including `src/test/`, `package.json` `contributes`, `scripts/`, configs) for references. Only mark as dead if truly zero references. +4. List all dead code found with file paths and line numbers. Do NOT delete yet. + +### Step 3 — Scan for duplicate code + +Search for code blocks that do the same thing in multiple places. + +1. Look for functions/methods with identical or near-identical logic. +2. Look for copy-pasted blocks (same structure, maybe different variable names). +3. Look for multiple implementations of the same algorithm or pattern. +4. Check across module boundaries — Diffy's layered architecture (`src/git/`, `src/ui/`, `src/providers/`, `src/commands/`) is a natural place for accidental duplication. +5. For each duplicate pair: note both locations, what they do, and how they differ (if at all). +6. List all duplicates found. Do NOT merge yet. + +### Step 4 — Scan for duplicate tests + +Search for tests that verify the same behavior. + +1. Look for test functions with identical assertions against the same code paths. +2. Look for test fixtures/helpers that are duplicated across `src/test/unit/` and `src/test/suite/`. +3. Look for E2E tests that fully cover what a unit test also covers (per CLAUDE.md, E2E is preferred — keep the E2E, mark the unit test as redundant only if E2E genuinely subsumes its coverage). +4. List all duplicate tests found. Do NOT delete yet. + +### Step 5 — Apply changes (one at a time) + +For each change, follow this cycle: **change → test → verify coverage → continue or revert**. + +#### 5a. Remove dead code +- Delete dead code identified in Step 2 +- After each deletion: run `make test` (fail-fast + coverage + threshold all in one) +- If `make test` exits non-zero (test failure OR coverage drop): **revert immediately** and investigate +- Dead code removal should never break tests or drop coverage + +#### 5b. Merge duplicate code +- For each duplicate pair: extract the shared logic into a single function/module +- Update all call sites to use the shared version +- After each merge: run `make test` +- If tests fail: **revert immediately**. The duplicates may have subtle differences you missed. +- If coverage drops: the shared code must have equivalent test coverage. Add tests if needed before proceeding. + +#### 5c. Remove duplicate tests +- Delete the redundant test (keep the more thorough one) +- After each deletion: run `make test` +- If coverage drops below threshold, `make test` exits non-zero — **revert immediately**. The "duplicate" test was covering something the other wasn't. + +### Step 6 — Final verification + +1. Run `make lint` — ESLint + tsc --noEmit must pass clean. +2. Run `make test` — tests must pass AND coverage must remain ≥ the baseline from Step 1. +3. Report: what was removed, what was merged, final coverage vs baseline. + +(Only the 7 standard targets exist — `make lint` and `make test` cover linting and coverage checks respectively.) + +## Rules + +- **No test coverage = do not touch.** If a file has no tests covering it, leave it alone entirely. You cannot safely dedup what you cannot verify. +- **Coverage must not drop.** If removing or merging code causes coverage to decrease, revert and investigate. The coverage floor from Step 1 is sacred. +- **Strict TypeScript only.** Diffy enforces `strict: true`. If that ever changes, refuse to dedup until it's re-enabled. +- **One change at a time.** Make one dedup change, run tests, verify coverage. Never batch multiple dedup changes before testing. +- **When in doubt, leave it.** If two code blocks look similar but you're not 100% sure they're functionally identical, leave both. False dedup is worse than duplication. +- **Preserve public API surface.** Do not change exported function signatures, command IDs, URI scheme, or `package.json` `contributes` keys that the extension exposes. +- **Three similar lines is fine.** Do not create abstractions for trivial duplication. Per CLAUDE.md: three similar lines is better than a premature abstraction. Only dedup when the shared logic is substantial (>10 lines) or when there are 3+ copies. diff --git a/.claude/skills/fix-bug/SKILL.md b/.claude/skills/fix-bug/SKILL.md new file mode 100644 index 0000000..0f0b4a4 --- /dev/null +++ b/.claude/skills/fix-bug/SKILL.md @@ -0,0 +1,67 @@ +--- +name: fix-bug +description: Fix a bug using test-driven development. Use when the user reports a bug, describes unexpected behavior, wants to fix a defect, or says something is broken. Enforces a strict test-first workflow where a failing test must be written and verified before any fix is attempted. +argument-hint: "[bug description]" +allowed-tools: Read, Grep, Glob, Edit, Write, Bash +--- + + +# Bug Fix Skill — Test-First Workflow + +You MUST follow this exact workflow. Do NOT skip steps. Do NOT fix the bug before writing a failing test. + +## Step 1: Understand the Bug + +- Read the bug description: $ARGUMENTS +- Investigate the codebase to understand the relevant code +- Identify the root cause (or narrow down candidates) +- Summarize your understanding of the bug to the user before proceeding + +## Step 2: Write a Failing Test + +- Write a test that **directly exercises the buggy behavior** +- The test must assert the **correct/expected** behavior — so it FAILS against the current broken code +- The test name should clearly describe the bug (e.g., `test_orange_color_not_applied_to_head`) +- Use the project's existing test framework and conventions (Diffy uses mocha for unit and `@vscode/test-electron` for E2E — pick the right tier per CLAUDE.md) + +## Step 3: Run the Test — Confirm It FAILS + +- Run ONLY the new test (not the full suite) +- **Verify the test FAILS** and that it fails **because of the bug**, not for some other reason (typo, import error, wrong selector, etc.) +- If the test passes: your test does not capture the bug. Go back to Step 2 +- If the test fails for the wrong reason: fix the test, not the code. Go back to Step 2 +- **Repeat until the test fails specifically because of the bug** + +## Step 4: Show Failure to User + +- Show the user the test code and the failure output +- Explicitly ask: "This test fails because of the bug. Can you confirm this captures the issue before I fix it?" +- **STOP and WAIT for user acknowledgment before proceeding** +- Do NOT continue to Step 5 until the user confirms + +## Step 5: Fix the Bug + +- Make the **minimum change** needed to fix the bug +- Do not refactor, clean up, or "improve" surrounding code +- Do not change the test + +## Step 6: Run the Test — Confirm It PASSES + +- Run the new test again +- **Verify it PASSES** +- If it fails: go back to Step 5 and adjust the fix +- **Repeat until the test passes** + +## Step 7: Run the Full Test Suite + +- Run `make test` to make sure nothing else broke (fail-fast + coverage threshold per CLAUDE.md) +- If other tests fail: fix the regression without breaking the new test +- Report the final result to the user + +## Rules + +- NEVER fix the bug before the failing test is written and confirmed +- NEVER skip asking the user to acknowledge the test failure +- NEVER modify the test to make it pass — modify the source code +- If you cannot write a test for the bug, explain why and ask the user how to proceed +- Keep the fix minimal — one bug, one fix, one test diff --git a/.claude/skills/spec-check/SKILL.md b/.claude/skills/spec-check/SKILL.md new file mode 100644 index 0000000..bf296df --- /dev/null +++ b/.claude/skills/spec-check/SKILL.md @@ -0,0 +1,331 @@ +--- +name: spec-check +description: Audit spec/plan documents against the codebase. Ensures every spec section has implementing code, tests, and matching logic. Use when the user says "check specs", "spec audit", or "verify specs". +argument-hint: "[optional spec ID or filename filter]" +--- + + +# spec-check + +> **Portable skill.** This skill adapts to the current repository. The agent MUST inspect the repo structure and use judgment to apply these instructions appropriately. + +Audit spec/plan documents against the codebase. Ensures every spec section has implementing code, tests, and that the code logic matches the spec. + +In Diffy, the primary spec lives at [docs/specs/spec.md](../../../docs/specs/spec.md) and the live plan/TODO list at [docs/plans/plan.md](../../../docs/plans/plan.md). + +## Arguments + +- `$ARGUMENTS` — optional spec name or ID to check (e.g., `AUTH-TOKEN-VERIFY` or `repo-standards`). If empty, check ALL specs. Spec IDs are descriptive slugs, NEVER numbered (see Step 1). + +## Instructions + +Follow these steps exactly. Be strict and pedantic. Stop on the first failure. + +--- + +### Step 1: Validate spec ID structure + +Before checking code/test references, verify that the specs themselves are well-formed. + +1. Find all spec documents (see locations in Step 2). +2. Extract every section ID using the regex `\[([A-Z][A-Z0-9]*(-[A-Z0-9]+)+)\]`. +3. **Flag invalid IDs:** + - Numbered IDs (`[SPEC-001]`, `[REQ-003]`, `[CI-004]`) — must be renamed to descriptive hierarchical slugs. + - Single-word IDs (`[TIMEOUT]`) — must have a group prefix. + - IDs with trailing numbers (`[FEAT-AUTH-01]`) — the number is meaningless, remove it. +4. **Check group clustering:** The first word of each ID is its group. All sections in the same group MUST appear together (adjacent) in the document. If they're scattered, flag it. +5. **Check for missing IDs:** Any heading that defines a requirement or behavior should have an ID. Flag headings in spec files that look like they define behavior but lack an ID. + +If any ID violations are found, report them all and **STOP**: +``` +SPEC ID VIOLATIONS: + +- docs/specs/AUTH-SPEC.md line 12: [SPEC-001] → rename to descriptive ID (e.g., [AUTH-LOGIN]) +- docs/specs/AUTH-SPEC.md line 30: [AUTH-TOKEN-VERIFY] and [AUTH-LOGIN] are not adjacent (scattered group) +- docs/specs/CI-SPEC.md line 5: "## Coverage thresholds" has no spec ID + +Fix spec IDs first, then re-run spec-check. +``` + +If all IDs are valid, proceed to Step 2. + +--- + +### Step 2: Find all spec/plan documents + +Search for markdown files that contain spec sections with IDs. Look in these locations: + +- `docs/*.md` +- `docs/**/*.md` +- `SPEC.md` +- `PLAN.md` +- `specs/*.md` + +Use Glob to find candidate files, then use Grep to confirm they contain spec IDs. + +**Spec ID patterns** — IDs appear in square brackets, typically at the start of a heading or section line. Match this regex pattern: + +``` +\[([A-Z][A-Z0-9]*(-[A-Z0-9]+)+)\] +``` + +Spec IDs are **hierarchical descriptive slugs, NEVER numbered.** The format is `[GROUP-TOPIC]` or `[GROUP-TOPIC-DETAIL]`. The first word is the **group** — all sections sharing the same group MUST appear together in the spec's table of contents. IDs are uppercase, hyphen-separated, unique across the repo, and MUST NOT contain sequential numbers. + +The hierarchy depth varies by repo: two words for simple repos (`[AUTH-LOGIN]`), three for most (`[AUTH-TOKEN-VERIFY]`), four for complex domains (`[AUTH-OAUTH-REFRESH-FLOW]`). The hierarchy mirrors the spec document's heading structure. + +Examples of valid spec IDs (note how groups cluster): +- `[AUTH-LOGIN]`, `[AUTH-TOKEN-VERIFY]`, `[AUTH-TOKEN-REFRESH]` — all in the AUTH group +- `[CI-TIMEOUT]`, `[CI-LINT]`, `[CI-RELEASE]` — all in the CI group +- `[LINT-ESLINT]`, `[LINT-RUFF]` — all in the LINT group +- `[FEAT-DARK-MODE]`, `[FEAT-SEARCH-FILTER]` — all in the FEAT group + +Examples of INVALID spec IDs: +- `[SPEC-001]` — numbered, meaningless +- `[FEAT-AUTH-01]` — trailing number +- `[REQ-003]` — sequential index, no group hierarchy +- `[CI-004]` — numbered, tells the reader nothing +- `[TIMEOUT]` — no group prefix, ungrouped + +For each file, extract every spec ID and its associated section title (the heading text after the ID) and the full section content (everything until the next heading of equal or higher level). + +--- + +### Step 3: Filter specs + +- If `$ARGUMENTS` is non-empty, filter the discovered specs: + - If it matches a spec ID exactly (e.g., `AUTH-TOKEN-VERIFY`), check only that spec. + - If it matches a partial name (e.g., `repo-standards`), check all specs in files whose path contains that string. +- If `$ARGUMENTS` is empty, process ALL discovered specs. + +If filtering produces zero specs, report an error: +``` +ERROR: No specs found matching "$ARGUMENTS". Discovered spec files: [list them] +``` + +--- + +### Step 4: Check each spec section + +For EACH spec section that has an ID, perform checks A, B, and C below. **Stop on the first failure.** + +#### Check A: Code references the spec ID + +Search the entire codebase for the spec ID string, **excluding** these directories: +- `docs/` +- `node_modules/` +- `.git/` +- `*.md` files (markdown is docs, not code) + +Use Grep with the literal spec ID (e.g., `[AUTH-TOKEN-VERIFY]`) to find references in code files. + +Code files should contain comments referencing the spec ID. The search must catch **all** comment styles across languages: + +**C-style `//` comments** (JavaScript, TypeScript, Rust, C#, F#, Java, Kotlin, Go, Swift, Dart): +- `// Implements [AUTH-TOKEN-VERIFY]` +- `// [AUTH-TOKEN-VERIFY]` +- `// Tests [AUTH-TOKEN-VERIFY]` (also counts as a code reference) +- `/// Implements [AUTH-TOKEN-VERIFY]` (doc comments) + +**Hash `#` comments** (Python, Ruby, Shell/Bash, YAML, TOML): +- `# Implements [AUTH-TOKEN-VERIFY]` +- `# [AUTH-TOKEN-VERIFY]` +- `# Tests [AUTH-TOKEN-VERIFY]` + +**HTML/XML comments** (HTML, CSS, SVG, XML, XAML, JSX templates): +- `` +- `` + +**ML-style comments** (F#, OCaml): +- `(* Implements [AUTH-TOKEN-VERIFY] *)` + +**Lua comments:** +- `-- Implements [AUTH-TOKEN-VERIFY]` + +**CSS comments:** +- `/* Implements [AUTH-TOKEN-VERIFY] */` + +**The key rule:** any comment in any language containing the exact spec ID string (e.g., `[AUTH-TOKEN-VERIFY]`) counts as a valid code reference. The Grep search uses the literal spec ID string, so it naturally matches all comment styles. Do NOT restrict the search to specific comment prefixes — just search for the spec ID string itself. + +**If NO code files reference the spec ID:** + +``` +SPEC VIOLATION: [AUTH-TOKEN-VERIFY] "Section Title" has no implementing code. + +Every spec section must have at least one code file that references it via a comment +containing the spec ID (e.g., `// Implements [AUTH-TOKEN-VERIFY]`). + +ACTION REQUIRED: Add a comment referencing [AUTH-TOKEN-VERIFY] in the file(s) that implement +this spec section, then re-run spec-check. +``` + +**STOP HERE. Do not continue to other checks.** + +#### Check B: Tests reference the spec ID + +Search test files for the spec ID. Test files are found in: +- `test/` +- `tests/` +- `**/*.test.*` +- `**/*.spec.*` +- `**/*_test.*` +- `**/test_*.*` +- `**/*Tests.*` +- `**/*Test.*` + +Use Grep to search these locations for the literal spec ID string. + +Tests should contain the spec ID in comments, test names, or annotations. The search must catch **all** test frameworks across languages: + +**JavaScript/TypeScript** (Jest, Mocha, Vitest, Playwright): +- `// Tests [AUTH-TOKEN-VERIFY]` +- `describe('[AUTH-TOKEN-VERIFY] Authentication flow', () => ...)` +- `test('[AUTH-TOKEN-VERIFY] should verify token', () => ...)` +- `it('[AUTH-TOKEN-VERIFY] verifies token', () => ...)` + +**Python** (pytest, unittest): +- `# Tests [AUTH-TOKEN-VERIFY]` +- `def test_auth_token_verify_flow():` +- `class TestAuthTokenVerify:` + +**Rust:** +- `// Tests [AUTH-TOKEN-VERIFY]` +- `#[test] // Tests [AUTH-TOKEN-VERIFY]` + +**C#** (xUnit, NUnit, MSTest): +- `// Tests [AUTH-TOKEN-VERIFY]` +- `[Fact] // Tests [AUTH-TOKEN-VERIFY]` +- `[Test] // Tests [AUTH-TOKEN-VERIFY]` +- `[TestMethod] // Tests [AUTH-TOKEN-VERIFY]` + +**F#** (xUnit, Expecto): +- `// Tests [AUTH-TOKEN-VERIFY]` +- `[] // Tests [AUTH-TOKEN-VERIFY]` +- `testCase "[AUTH-TOKEN-VERIFY] description" <| fun () ->` + +**Java/Kotlin** (JUnit, TestNG): +- `// Tests [AUTH-TOKEN-VERIFY]` +- `@Test // Tests [AUTH-TOKEN-VERIFY]` + +**Go:** +- `// Tests [AUTH-TOKEN-VERIFY]` +- `func TestAuthTokenVerify(t *testing.T) { // Tests [AUTH-TOKEN-VERIFY]` + +**Swift** (XCTest): +- `// Tests [AUTH-TOKEN-VERIFY]` +- `func testAuthTokenVerify() { // Tests [AUTH-TOKEN-VERIFY]` + +**Dart** (flutter_test): +- `// Tests [AUTH-TOKEN-VERIFY]` +- `test('[AUTH-TOKEN-VERIFY] description', () { ... });` + +**Ruby** (RSpec, Minitest): +- `# Tests [AUTH-TOKEN-VERIFY]` +- `describe '[AUTH-TOKEN-VERIFY] Authentication' do` +- `it '[AUTH-TOKEN-VERIFY] verifies token' do` + +**Shell** (bats, shunit2): +- `# Tests [AUTH-TOKEN-VERIFY]` +- `@test "[AUTH-TOKEN-VERIFY] description" {` + +**The key rule:** same as Check A — search for the literal spec ID string in test files. Any occurrence of the exact spec ID in a test file counts. Do NOT restrict to specific patterns — just search for the spec ID string itself. + +**If NO test files reference the spec ID:** + +``` +SPEC VIOLATION: [AUTH-TOKEN-VERIFY] "Section Title" has no tests. + +Every spec section must have corresponding tests that reference the spec ID. + +ACTION REQUIRED: Add tests for [AUTH-TOKEN-VERIFY] with a comment or test name containing +the spec ID, then re-run spec-check. +``` + +**STOP HERE. Do not continue to other checks.** + +#### Check C: Code logic matches the spec + +This is the most critical check. You must: + +1. **Read the spec section content carefully.** Understand exactly what behavior, logic, ordering, conditions, and constraints the spec describes. + +2. **Read the implementing code.** Use the references found in Check A to locate the implementing files. Read the relevant functions/sections. + +3. **Compare spec vs. code.** Be SENSITIVE and PEDANTIC. Check for: + - **Ordering violations** — If the spec says A happens before B, the code must do A before B. + - **Missing conditions** — If the spec says "only when X", the code must have that condition. + - **Extra behavior** — If the code does something the spec doesn't mention, flag it only if it contradicts the spec. + - **Wrong logic** — If the spec says "greater than" but code uses "greater than or equal", that's a violation. + - **Missing steps** — If the spec describes 5 steps but code only implements 3, that's a violation. + - **Wrong defaults** — If the spec says "default to X" but code defaults to Y, that's a violation. + +4. **If the code deviates from the spec**, report a detailed error: + +``` +SPEC VIOLATION: [AUTH-TOKEN-VERIFY] Code does not match spec. + +SPEC SAYS: +> "The authentication flow must verify the token expiry before checking permissions" +> (from docs/specs/AUTH-SPEC.md, line 42) + +CODE DOES: +> `if (hasPermission(user)) { verifyToken(token); }` (src/auth.ts:42) + +DEVIATION: The code checks permissions BEFORE verifying token expiry. +The spec explicitly requires token expiry verification FIRST. + +ACTION REQUIRED: Reorder the logic in src/auth.ts to verify token expiry +before checking permissions, as specified in [AUTH-TOKEN-VERIFY]. +``` + +**STOP HERE. Do not continue to other specs.** + +5. **If the code matches the spec**, this check passes. Move to the next spec. + +--- + +### Step 5: Report results + +#### On failure (any check fails): + +Output ONLY the first violation found. Use the exact error format shown above. Do not summarize other specs. Do not offer to fix the code. Just report the violation. + +End with: +``` +spec-check FAILED. Fix the violation above and re-run. +``` + +#### On success (all specs pass): + +Output a summary table: + +``` +spec-check PASSED. All specs verified. + +| Spec ID | Title | Code References | Test References | Logic Match | +|----------------|--------------------------|-----------------|-----------------|-------------| +| [AUTH-TOKEN-VERIFY] | Authentication flow | src/auth.ts | tests/auth.test.ts | PASS | +| [RATE-LIMIT-CONFIG] | Rate limiting | src/rate.ts | tests/rate.test.ts | PASS | +| ... | ... | ... | ... | ... | + +Checked N spec sections across M files. All have implementing code, tests, and matching logic. +``` + +--- + +## Search strategy summary + +1. **Validate spec IDs:** Check all IDs are hierarchical, descriptive, grouped, and non-numbered +2. **Find spec files:** Glob for `docs/**/*.md`, `SPEC.md`, `PLAN.md`, `specs/**/*.md` +3. **Extract spec IDs:** Grep for `\[[A-Z][A-Z0-9]*(-[A-Z0-9]+)+\]` in those files +4. **Find code refs:** Grep for the literal spec ID in all files, excluding `docs/`, `node_modules/`, `.git/`, `*.md` +5. **Find test refs:** Grep for the literal spec ID in test directories and test file patterns +6. **Read and compare:** Read the spec section content and the implementing code, compare logic + +## Key principles + +- **Fail fast.** Stop on the first violation. One fix at a time. +- **Be pedantic.** If the spec says it, the code must do it. No "close enough". +- **Quote everything.** Always quote the spec text and the code in error messages so the developer sees exactly what's wrong. +- **Be actionable.** Every error must tell the developer what file to change and what to do. +- **Exclude docs from code search.** Markdown files are documentation, not implementation. Only search actual code files for spec references. +- **No numbered IDs.** Spec IDs are hierarchical descriptive slugs (`[AUTH-TOKEN-VERIFY]`), NEVER sequential numbers (`[SPEC-001]`). The first word is the group — sections sharing a group must be adjacent in the TOC. If you encounter numbered or ungrouped IDs, flag them as a violation. diff --git a/.claude/skills/submit-pr/SKILL.md b/.claude/skills/submit-pr/SKILL.md new file mode 100644 index 0000000..63d336d --- /dev/null +++ b/.claude/skills/submit-pr/SKILL.md @@ -0,0 +1,39 @@ +--- +name: submit-pr +description: Creates a pull request with a well-structured description after verifying CI passes. Use when the user asks to submit, create, or open a pull request. +disable-model-invocation: true +--- + + +# Submit PR + +Create a pull request for the current branch with a well-structured description. + +## Steps + +*NOTE: if you already ran make ci in this session and it passed, you can skip step 1.* + +1. Run `make ci` — must pass completely before creating PR +2. **Generate the diff against main.** Run `git diff main...HEAD > /tmp/pr-diff.txt` to capture the full diff between the current branch and the head of main. This is the ONLY source of truth for what the PR contains. **Warning:** the diff can be very large. If the diff file exceeds context limits, process it in chunks (e.g., read sections with `head`/`tail` or split by file) rather than trying to load it all at once. +3. **Derive the PR title and description SOLELY from the diff.** Read the diff output and summarize what changed. Ignore commit messages, branch names, and any other metadata — only the actual code/content diff matters. +4. Write PR body using the template in `.github/pull_request_template.md` +5. Fill in (based on the diff analysis from step 3): + - TLDR: one sentence + - What Was Added: new files, features, deps + - What Was Changed/Deleted: modified behaviour + - How Tests Prove It Works: specific test names or output + - Spec/Doc Changes: if any + - Breaking Changes: yes/no + description +6. Use `gh pr create` with the filled template + +## Rules + +- Never create a PR if `make ci` fails +- PR description must be specific and tight — no vague placeholders +- Link to the relevant GitHub issue if one exists + +## Success criteria + +- `make ci` passed +- PR created with `gh pr create` +- PR URL returned to user diff --git a/.claude/skills/upgrade-packages/SKILL.md b/.claude/skills/upgrade-packages/SKILL.md new file mode 100644 index 0000000..05a5043 --- /dev/null +++ b/.claude/skills/upgrade-packages/SKILL.md @@ -0,0 +1,95 @@ +--- +name: upgrade-packages +description: Upgrade all dependencies/packages to their latest versions. Diffy is TypeScript/Node — use when the user says "upgrade packages", "update dependencies", "bump versions", "update packages", or "upgrade deps". +argument-hint: "[--check-only] [--major] [package-name]" +--- + + +# Upgrade Packages + +Upgrade Diffy's npm dependencies to their latest compatible (or latest major, if `--major`) versions. + +## Arguments + +- `--check-only` — List outdated packages without upgrading. Stop after Step 2. +- `--major` — Include major version bumps (breaking changes). Without this flag, stay within semver-compatible ranges. +- Any other argument is treated as a specific package name to upgrade (instead of all packages). + +## Step 1 — Detect package manager + +Diffy uses **npm** (package manifest: `package.json`, lockfile: `package-lock.json`). If for some reason the repo has migrated to yarn or pnpm, adapt accordingly (`yarn outdated`/`yarn up` or `pnpm outdated`/`pnpm update`). + +If `package.json` is missing, stop and tell the user. + +## Step 2 — List outdated packages + +Run BEFORE upgrading anything. Show the user what will change. + +```bash +npm outdated +``` + +**Read the docs:** https://docs.npmjs.com/cli/v10/commands/npm-update + +If `--check-only` was passed, **stop here** and report the outdated list. + +## Step 3 — Read the official upgrade docs + +**Before running any upgrade command, you MUST fetch and read the official documentation URL above.** Use WebFetch to retrieve the page. This ensures you use the correct flags and understand the behavior. Do not guess at flags or options from memory. + +## Step 4 — Upgrade packages + +Run the upgrade. If a specific package name was given as an argument, upgrade only that package. + +```bash +npm update # semver-compatible (within package.json ranges) +# --major flag: +npx npm-check-updates -u && npm install # bump package.json to latest majors +``` + +### Diffy-specific cautions + +- `@types/vscode` and `@types/node` must remain compatible with the `engines.vscode` minimum declared in `package.json`. A major bump that requires a newer VSCode runtime is a breaking change to consumers. +- `@vscode/test-electron`, `mocha`, `c8`, and `vsce` (`@vscode/vsce`) are tightly coupled to the extension test/release pipeline — review their changelogs before bumping. +- `typescript`, `eslint`, `typescript-eslint`, `prettier` — major bumps frequently change rule defaults; rerun `make lint` and `make fmt CHECK=1` after the upgrade. + +## Step 5 — Verify the upgrade + +After upgrading, run the project's build and test suite to confirm nothing broke: + +```bash +make ci +``` + +If tests fail: +1. Read the failure output carefully +2. Check the changelog / migration guide for the upgraded packages (fetch the release notes URL if available) +3. Fix breaking changes in the code +4. Re-run tests +5. If stuck after 3 attempts on the same failure, report it to the user with the error details and the package that caused it + +## Step 6 — Report + +Provide a summary: + +- Packages upgraded (old version -> new version) +- Packages skipped (and why, e.g., major version bump without `--major` flag) +- Build/test result after upgrade +- Any breaking changes that were fixed +- Any packages that could not be upgraded (with error details) + +## Rules + +- **Always list outdated packages first** before upgrading anything +- **Always read the official docs** for the package manager before running upgrade commands +- **Always run `make ci` after upgrading** to catch breakage immediately +- **Never remove packages** unless they were explicitly deprecated and replaced +- **Never downgrade packages** unless rolling back a broken upgrade +- **Never modify `package-lock.json` manually** — let npm regenerate it +- **Commit nothing** — leave changes in the working tree for the user to review + +## Success criteria + +- All outdated packages upgraded to latest compatible (or latest major if `--major`) +- `make ci` passes +- User has a clear summary of what changed diff --git a/.claude/skills/website-audit/SKILL.md b/.claude/skills/website-audit/SKILL.md new file mode 100644 index 0000000..520714e --- /dev/null +++ b/.claude/skills/website-audit/SKILL.md @@ -0,0 +1,180 @@ +--- +name: website-audit +description: Audits a website for SEO, AI search performance, structured data, mobile usability, broken links, and social media cards. Fixes issues found. Use when the user mentions "audit website", "SEO", "fix search ranking", "AI search", "structured data", "social media cards", or "website performance". +--- + + +# Website Audit + +Performs a comprehensive website audit and fixes issues affecting search visibility and AI discoverability. + +Copy this checklist and track your progress: + +``` +Audit Progress: +- [ ] Step 1: Read guidelines +- [ ] Step 2: Audit AI search readiness +- [ ] Step 3: Audit SEO and keywords +- [ ] Step 4: Audit crawling and indexing +- [ ] Step 5: Audit broken links and canonicalization +- [ ] Step 6: Audit mobile usability +- [ ] Step 7: Audit structured data +- [ ] Step 8: Audit social media cards +- [ ] Step 9: Audit For Unsubstantiated Claims +- [ ] Step 10: Audit Design Compliance +- [ ] Step 11: Test with Playwright +- [ ] Step 12: Report findings +``` + +- Check the outputted HTML/CSS/JavaScript AFTER the website is generated by the static content generator. - Don't just check the static content before the website is generated. +- Fix issues at the core where the static content templates are stored - not in the outputted HTML (e.g. _site) +- Never manually edit the generated website content directly +- ENSURE THE FOOTER HAS A copyright link to nimblesite.co + +## Step 1 — Read guidelines + +Fetch and read each of these before auditing. These are the authoritative references for every step that follows. + +- [Google's guidance on using generative AI content](https://developers.google.com/search/docs/fundamentals/using-gen-ai-content) +- [Top ways to ensure content performs well in Google's AI experiences](https://developers.google.com/search/blog/2025/05/succeeding-in-ai-search) +- [SEO Starter Guide](https://developers.google.com/search/docs/fundamentals/seo-starter-guide) + +If the repo has a business plan doc, take it into account + +Identify the website source files in the repo. Determine the framework (static site generator, Next.js, Hugo, etc.) so you know where to find templates, metadata, and content. + +## Step 2 — Audit AI search readiness + +Apply the guidance from the AI search article. Check: + +1. **Content quality** — Is content original, expert-level, and comprehensive? Flag thin or duplicated pages. +2. **Clear structure** — Do pages use descriptive headings, lists, and concise answers to likely questions? +3. **Entity clarity** — Are key terms, products, and concepts defined clearly so AI can extract them? +4. **Freshness signals** — Are dates, update timestamps, and authorship present? + +Fix issues directly in the source files. For each fix, note what changed and why. + +## Step 3 — Audit SEO and keywords + +1. Search [Google Trends](https://trends.google.com/home) for trending keywords related to the website's content. +2. Review each page's ``, `<meta name="description">`, and `<h1>` tags. +3. Check for keyword opportunities — can trending terms be naturally inserted into headings, descriptions, or body content? +4. Verify each page has a unique, descriptive title (50-60 chars) and meta description (150-160 chars). +5. Check image `alt` attributes describe the image content and include relevant keywords where natural. + +Apply the [SEO Starter Guide](https://developers.google.com/search/docs/fundamentals/seo-starter-guide) principles. Fix issues directly. + +## Step 4 — Audit crawling and indexing + +Reference: [Overview of crawling and indexing topics](https://developers.google.com/search/docs/crawling-indexing) + +1. **robots.txt** — Locate and review it. Verify it doesn't block important pages. Reference: [robots.txt spec](https://developers.google.com/search/docs/crawling-indexing/robots-txt) +2. **Sitemap** — Locate the sitemap (or sitemap index). Verify all important pages are listed and no dead URLs are included. Reference: [Sitemap guidelines](https://developers.google.com/search/docs/crawling-indexing/sitemaps/large-sitemaps) +3. **Meta robots tags** — Check for unintended `noindex` or `nofollow` directives on pages that should be indexed. + +Note: robots.txt and sitemaps are often auto-generated. If so, check the generator config rather than the output file. + +## Step 5 — Audit broken links and canonicalization + +Reference: [What is canonicalization](https://developers.google.com/search/docs/crawling-indexing/canonicalization) + +1. Check all internal links resolve to valid pages (no 404s). +2. Verify `<link rel="canonical">` tags are present and point to the correct URL. +3. Check for duplicate content accessible via multiple URLs (with/without trailing slash, www vs non-www). +4. Verify redirects use 301 (permanent) not 302 (temporary) where appropriate. + +## Step 6 — Audit mobile usability + +Reference: [Mobile-first indexing best practices](https://developers.google.com/search/docs/crawling-indexing/mobile/mobile-sites-mobile-first-indexing) + +1. Verify the `<meta name="viewport">` tag is present and correctly configured. +2. Check that content is identical between mobile and desktop (mobile-first indexing requires this). +3. Verify touch targets are adequately sized (min 48x48px). +4. Check font sizes are readable without zooming (min 16px body text). + +## Step 7 — Audit structured data + +Reference: [Structured data guidelines](https://developers.google.com/search/docs/appearance/structured-data/sd-policies) + +1. Check for existing JSON-LD `<script type="application/ld+json">` blocks. +2. Verify the structured data matches the page content (no misleading markup). +3. Add missing structured data where appropriate: + - **Organization/Person** on the homepage + - **Article/BlogPosting** on blog posts (with author, datePublished, dateModified) + - **BreadcrumbList** for navigation + - **FAQ** for pages with question/answer content +4. Validate JSON-LD syntax is correct. + +## Step 8 — Audit social media cards + +Reference: [Implementing Social Media Preview Cards](https://documentation.platformos.com/use-cases/implementing-social-media-preview-cards) + +Check every page template includes: + +**Open Graph (Facebook/LinkedIn):** +- `og:title`, `og:description`, `og:image`, `og:url`, `og:type` + +**Twitter Card:** +- `twitter:card`, `twitter:title`, `twitter:description`, `twitter:image` + +Verify `og:image` dimensions are at least 1200x630px. Fix missing or incomplete tags. + +## Step 9 - Audit For Unsubstantiated Claims + +Ensure that all claims are backed up with a link to a reputable source. As an example, this claim isn't valid as content unless it links to an authority that found this through research + +> Research shows teams with strong DevEx perform 4-5x better across speed, quality, and engagement + +Search for the authoritative URL and add a link to the URL. If it is not available, change the claim to something that can be substatiated. + +## Step 10 — Audit Design Compliance + +Read the design system docs and view the design screens in the designsystem folder. + +## Step 11 — Test with Playwright + +Build and run the website locally using `make website-run` (or the project's equivalent dev server command). + +**Desktop tests (1280x720):** + +1. Navigate to the homepage — take a screenshot. +2. Navigate to each major section — verify pages load without errors. +3. Check the browser console for JavaScript errors. +4. Verify all navigation links work. + +**Mobile tests (375x667, iPhone SE):** + +1. Resize the browser to mobile dimensions. +2. Navigate to the homepage — take a screenshot. +3. Verify the layout is responsive (no horizontal overflow, readable text). +4. Test navigation menu (hamburger menu if applicable). + +If any page fails to load or has console errors, fix the issue and retest. + +## Step 12 — Report findings + +Summarize the audit results: + +``` +## Website Audit Report + +### Fixed +- [List each issue fixed with file and line reference] + +### Warnings (manual review needed) +- [Issues that need human judgment] + +### Passed +- [Areas that passed audit with no issues] + +### Screenshots +- [Reference Playwright screenshots taken] +``` + +## Rules + +- **Fix issues directly** — don't just report them. Only flag issues as warnings when they require human judgment (e.g., content tone, keyword selection). +- **One step at a time** — complete each step before moving to the next. +- **Preserve existing content** — improve structure and metadata without rewriting the author's voice. +- **No keyword stuffing** — keywords must read naturally in context. +- **Respect the framework** — edit templates/configs, not generated output files. diff --git a/.clinerules/00-read-instructions.md b/.clinerules/00-read-instructions.md new file mode 100644 index 0000000..48aaceb --- /dev/null +++ b/.clinerules/00-read-instructions.md @@ -0,0 +1,2 @@ +<!-- agent-pmo:74cf183 --> +@CLAUDE.md diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 0000000..043e911 --- /dev/null +++ b/.cursorrules @@ -0,0 +1,2 @@ +# agent-pmo:74cf183 +@CLAUDE.md diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..68df27c --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,25 @@ +{ + "_agent_pmo": "74cf183", + "name": "Diffy (Node.js)", + "image": "mcr.microsoft.com/devcontainers/typescript-node:1-20", + "remoteUser": "vscode", + "postCreateCommand": "make setup", + "customizations": { + "vscode": { + "extensions": [ + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "usernamehw.errorlens", + "ms-vscode.vscode-typescript-next" + ], + "settings": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "eslint.validate": ["typescript", "javascript"], + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + } + } + } + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..40a8c4c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +* text=auto eol=lf + +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.vsix binary diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..48aaceb --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,2 @@ +<!-- agent-pmo:74cf183 --> +@CLAUDE.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..9734233 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,10 @@ +<!-- agent-pmo:74cf183 --> +## TLDR +<!-- One sentence: what does this PR do? --> + +## Details +<!-- New functionality, new files, new dependencies. What changed? --> + +## How Do The Automated Tests Prove It Works? +<!-- Name specific tests or describe what the test output demonstrates. --> +<!-- "Tests pass" is not acceptable. Be specific. --> diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2e7bd9d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,60 @@ +# agent-pmo:74cf183 +name: CI + +on: + pull_request: + branches: [main] + push: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + ci: + name: CI + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - run: npm ci + + - name: Format check + run: make fmt CHECK=1 + + - name: Lint (includes shipwright.json schema check) + run: make lint + + - name: Dry-run release stamp (proves the script still runs) + run: node scripts/stamp-release-version.mjs 0.0.0 --dry + + # E2E suite needs xvfb so @vscode/test-electron can launch a headless Electron. + - name: Test + run: xvfb-run -a make test + + - name: Build + run: make build + + - name: Upload coverage + uses: actions/upload-artifact@v4 + if: always() + with: + name: coverage-report + path: | + coverage/ + lcov.info + retention-days: 7 + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: build + path: out/ + retention-days: 7 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..882f1b6 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,184 @@ +# agent-pmo:74cf183 +# Shipwright SWR-REL-* release pipeline for Diffy. +# +# 1. version — derive semver from the v* tag, capture source SHA +# 2. ci-gate — run `make ci` on the tagged commit; publish is blocked unless this passes +# 3. validate-manifest — schema-check shipwright.json before any artifact is built +# 4. build-vsix matrix — stamp the runner working tree, verify, build, package per platform +# 5. release — assemble the GitHub Release with every VSIX +# 6. publish — push every VSIX to the VS Code Marketplace in one vsce call +# +# Source-controlled versions are never mutated. Stamping happens in the runner +# working tree only, per SWR-VERSION-BUILD-STAMPING. + +name: Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + +jobs: + version: + name: Extract release version + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + version: ${{ steps.extract.outputs.version }} + source_sha: ${{ steps.extract.outputs.source_sha }} + steps: + - name: Extract version from tag + id: extract + run: | + echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" + echo "source_sha=${GITHUB_SHA}" >> "$GITHUB_OUTPUT" + + ci-gate: + name: CI gate (make ci) + needs: version + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ needs.version.outputs.source_sha }} + - uses: actions/setup-node@v4 + with: + node-version: '22.x' + cache: 'npm' + - run: npm ci + - name: Format check + run: make fmt CHECK=1 + - name: Lint (includes shipwright.json schema check) + run: make lint + - name: Test (xvfb for @vscode/test-electron) + run: xvfb-run -a make test + - name: Build + run: make build + + validate-manifest: + name: Validate shipwright.json + needs: version + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ needs.version.outputs.source_sha }} + - uses: actions/setup-node@v4 + with: + node-version: '22.x' + cache: 'npm' + - run: npm ci + - name: Validate shipwright.json against schema + run: npm run shipwright:validate + - name: Dry-run stamp (proves the script runs against this tag) + run: node scripts/stamp-release-version.mjs "${{ needs.version.outputs.version }}" --dry + + build-vsix: + name: Build VSIX (${{ matrix.artifact_name }}) + needs: [version, ci-gate, validate-manifest] + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + vsce_target: linux-x64 + artifact_name: linux-x64 + - os: ubuntu-latest + vsce_target: linux-arm64 + artifact_name: linux-arm64 + - os: macos-latest + vsce_target: darwin-x64 + artifact_name: darwin-x64 + - os: macos-latest + vsce_target: darwin-arm64 + artifact_name: darwin-arm64 + - os: windows-latest + vsce_target: win32-x64 + artifact_name: win32-x64 + - os: windows-latest + vsce_target: win32-arm64 + artifact_name: win32-arm64 + runs-on: ${{ matrix.os }} + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ needs.version.outputs.source_sha }} + + # VSIX packaging requires Node 22.x per [CI-VSIX-PLATFORM]. + - uses: actions/setup-node@v4 + with: + node-version: '22.x' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Stamp release version (runner tree only) + run: node scripts/stamp-release-version.mjs "${{ needs.version.outputs.version }}" + + - name: Verify every version carrier + run: node scripts/verify-versions.mjs "${{ needs.version.outputs.version }}" + + - name: Build + run: npm run build + + - name: Package VSIX + run: npx vsce package --target ${{ matrix.vsce_target }} --out diffy-${{ needs.version.outputs.version }}-${{ matrix.vsce_target }}.vsix + + - name: Verify VSIX bundles shipwright.json + shell: bash + run: unzip -l diffy-${{ needs.version.outputs.version }}-${{ matrix.vsce_target }}.vsix | grep -F "extension/shipwright.json" + + - name: Upload VSIX + uses: actions/upload-artifact@v4 + with: + name: vsix-${{ matrix.artifact_name }} + path: diffy-${{ needs.version.outputs.version }}-${{ matrix.vsce_target }}.vsix + + release: + name: Create release + needs: [version, build-vsix] + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ needs.version.outputs.source_sha }} + - uses: actions/download-artifact@v4 + with: + path: artifacts/ + + - name: Create GitHub release + uses: softprops/action-gh-release@v2 + with: + files: artifacts/**/*.vsix + generate_release_notes: true + + publish: + name: Publish to VS Code Marketplace + needs: [version, release] + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ needs.version.outputs.source_sha }} + - uses: actions/download-artifact@v4 + with: + path: artifacts/ + + - uses: actions/setup-node@v4 + with: + node-version: '22.x' + + # Publish all platform-specific VSIX files in a single vsce invocation. + - name: Publish to Marketplace + run: npx vsce publish --packagePath $(find artifacts -name '*.vsix') + env: + VSCE_PAT: ${{ secrets.VSCE_PAT }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e437c3c --- /dev/null +++ b/.gitignore @@ -0,0 +1,80 @@ +# agent-pmo:74cf183 +# ============================================================================= +# UNIVERSAL — applies to every repo in this portfolio +# ============================================================================= + +# OS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +Thumbs.db +ehthumbs.db +Desktop.ini + +# Editor swap/temp files only — never ignore the config dirs themselves +*.swp +*.swo +*~ +*.sublime-project +*.sublime-workspace + +# IDE config dirs MUST be committed — they carry VS Code settings, extensions, +# launch configs, and title-bar colorization that the team shares. +# AI agent config dirs MUST also be committed — they carry skills and instructions. +# DO NOT add .vscode/, .idea/, .claude/, .codex/, .agents/, .cline/, .opencode/, +# .github/copilot-instructions.md, .cursorrules, .windsurfrules, or similar. + +# Portfolio-wide tooling +.too_many_cooks/ +.commandtree/ +.playwright-mcp/ +.deslop-cache/ +.ghissues/ +coordination/ +logs/ +nohup.out + +# Coverage artifacts (all languages) +coverage/ +lcov.info +htmlcov/ +.coverage +coverage.xml +coverage.out +coverage-summary.json +TestResults/ + +# Secrets / local overrides +.env +.env.local +.env.*.local +*.local +*.secret +*.pem +*.key +!*.pub.key + +# Temporary +tmp/ +temp/ +scratch/ + +# ============================================================================= +# TypeScript / Node.js / VSCode extension +# ============================================================================= +node_modules/ +dist/ +out/ +build/ +*.vsix +*.tgz +.npm/ +.cache/ +.vscode-test/ +.vscode-test-web/ +.nyc_output/ + +# Shipwright SWR-VERSION-BUILD-STAMPING — release-time artifact, never committed +build-info.json diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..19bbb42 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,12 @@ +{ + "tabWidth": 2, + "useTabs": false, + "printWidth": 120, + "semi": true, + "singleQuote": false, + "trailingComma": "es5", + "bracketSpacing": true, + "arrowParens": "always", + "endOfLine": "lf", + "bracketSameLine": false +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..ada5174 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,11 @@ +{ + "_agent_pmo": "74cf183", + "recommendations": [ + "nimblesite.commandtree", + "nimblesite.too-many-cooks", + "nimblesite.typeDiagram", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "usernamehw.errorlens" + ] +} diff --git a/.vscodeignore b/.vscodeignore new file mode 100644 index 0000000..fa1dc30 --- /dev/null +++ b/.vscodeignore @@ -0,0 +1,33 @@ +.deslop-cache/** +.ghissues/** +.commandtree/** +.claude/** +.clinerules/** +.devcontainer/** +.github/** +.vscode/** +.vscode-test/** +.cursorrules +.windsurfrules +opencode.json +AGENTS.md +CLAUDE.md +.gitignore +.prettierrc.json +eslint.config.mjs +tsconfig.json +Makefile +coverage/** +coverage-thresholds.json +docs/** +scripts/** +src/** +test-fixtures/** +out/test/** +out/scripts/** +**/*.map +**/*.ts +!out/**/*.d.ts +**/tsconfig*.json +**/.eslintrc* +node_modules/.cache/** diff --git a/.windsurfrules b/.windsurfrules new file mode 100644 index 0000000..043e911 --- /dev/null +++ b/.windsurfrules @@ -0,0 +1,2 @@ +# agent-pmo:74cf183 +@CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..34b118b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,3 @@ +<!-- agent-pmo:74cf183 --> + +@CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md index 57b2736..4ea4630 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,5 +1,7 @@ # Diffy — Agent Instructions +<!-- agent-pmo:74cf183 --> + ⚠️ KILLING A VSCODE PROCESS — EVEN IN THE BROWSER — WILL BE MET WITH INSTANT, EXTREME VIOLENCE! > ⚠️ **TOKEN DISCIPLINE.** Check file size first. `Grep` over `Read`. Use `offset`/`limit`. @@ -17,7 +19,7 @@ Full design + execution plan: [spec.md](spec.md). **Diffy** is a VSCode extension that does exactly one thing: **pick two things and diff them** against a git repository. Side A is a commit; Side B is another commit, the working copy, the index, or a branch/tag (resolved to a commit). It shells out to `git`, hands two URIs to VSCode's built-in `vscode.diff`, and uses a multi-step QuickPick for browsing many changed files. No custom renderer, no custom view. -**Primary language:** TypeScript (pure — Rust LSP was considered and rejected; LSP is for *language* semantics, not diffing) +**Primary language:** TypeScript (pure — Rust LSP was considered and rejected; LSP is for _language_ semantics, not diffing) **Build command:** `make ci` **Test command:** `make test` **Lint command:** `make lint` @@ -43,7 +45,7 @@ context-menu / palette command ## Hard Rules (no exceptions, NON-NEGOTIABLE) -- **NO git commands from the agent.** No `git add`, `commit`, `push`, `checkout`, `merge`, `rebase`. CI and GitHub Actions handle git. (Diffy itself shells out to `git` at runtime — that's the product. The *agent* doesn't drive git in the dev loop.) +- **NO git commands from the agent.** No `git add`, `commit`, `push`, `checkout`, `merge`, `rebase`. CI and GitHub Actions handle git. (Diffy itself shells out to `git` at runtime — that's the product. The _agent_ doesn't drive git in the dev loop.) - **NO new views, sidebars, activity-bar icons, tree providers, or webviews.** Context menus + palette commands only. Browsing many files is a QuickPick, not a panel. - **NO THROWING EXCEPTIONS for control flow.** Return `Result<T,E>` via a discriminated union. Panics are bugs. - **NO REGEX on structured data.** Git porcelain output is parsed via NUL-delimited splits (`-z` flag everywhere). Never regex over JSON, YAML, source code, or git output. @@ -100,15 +102,17 @@ context-menu / palette command Do not write assertions that guard against AI / taxonomy strings. Assert on **positive, human-readable values** that the user would actually see. ⛔️ BAD + ```typescript assert.doesNotMatch(label, /\[diffy-internal-tag\]/); ``` ✅ GOOD + ```typescript -const tabs = vscode.window.tabGroups.all.flatMap(g => g.tabs); -const diffTab = tabs.find(t => t.input instanceof vscode.TabInputTextDiff); -assert.ok(diffTab, 'a diff tab opened'); +const tabs = vscode.window.tabGroups.all.flatMap((g) => g.tabs); +const diffTab = tabs.find((t) => t.input instanceof vscode.TabInputTextDiff); +assert.ok(diffTab, "a diff tab opened"); assert.match(diffTab.label, /a1b2c3 ↔ Working Copy/); ``` diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1ccc31c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 NIMBLESITE PTY LTD + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5c52545 --- /dev/null +++ b/Makefile @@ -0,0 +1,107 @@ +# agent-pmo:74cf183 +# ============================================================================= +# Standard Makefile — Diffy (VSCode extension, TypeScript) +# Cross-platform: Linux, macOS, Windows (via GNU Make) +# ============================================================================= + +.PHONY: build test lint fmt clean ci setup package help + +# --------------------------------------------------------------------------- +# OS Detection +# --------------------------------------------------------------------------- +ifeq ($(OS),Windows_NT) + SHELL := powershell.exe + .SHELLFLAGS := -NoProfile -Command + RM = Remove-Item -Recurse -Force -ErrorAction SilentlyContinue + MKDIR = New-Item -ItemType Directory -Force + HOME ?= $(USERPROFILE) +else + RM = rm -rf + MKDIR = mkdir -p +endif + +# --------------------------------------------------------------------------- +# Coverage — single source of truth is coverage-thresholds.json +# See REPO-STANDARDS-SPEC [COVERAGE-THRESHOLDS-JSON]. +# --------------------------------------------------------------------------- +COVERAGE_THRESHOLDS_FILE := coverage-thresholds.json + +# ============================================================================= +# Standard Targets +# +# These 7 targets are portfolio-wide and identical across every repo. +# Do NOT add extra public targets here — put them in the Repo-Specific +# Targets section at the bottom of this file. +# See REPO-STANDARDS-SPEC [MAKE-TARGETS]. +# ============================================================================= + +## build: Compile TypeScript to out/ +build: + @echo "==> Building..." + npm run build + +## test: Fail-fast tests + coverage + threshold enforcement. +## See REPO-STANDARDS-SPEC [TEST-RULES] and [COVERAGE-THRESHOLDS-JSON]. +test: + @echo "==> Testing (fail-fast + coverage + threshold)..." + npm run test:coverage + $(MAKE) _coverage_check + +## lint: ESLint + tsc --noEmit + shipwright.json schema check (read-only). No formatting. +lint: + @echo "==> Linting..." + npm run lint + npm run typecheck + npm run shipwright:validate + +## fmt: Prettier in-place. Pass CHECK=1 for read-only check (CI use). +fmt: + @echo "==> Formatting$(if $(CHECK), (check mode),)..." + npm run fmt$(if $(CHECK),:check,) + +## clean: Remove all build artifacts +clean: + @echo "==> Cleaning..." + $(RM) out dist coverage .vscode-test .nyc_output *.vsix build-info.json + +## ci: lint + test + build (full CI simulation) +ci: lint test build + +## setup: Post-create dev environment setup (used by devcontainer) +setup: + @echo "==> Setting up development environment..." + npm ci + @echo "==> Setup complete. Run 'make ci' to validate." + +# --------------------------------------------------------------------------- +# Internal sub-recipes — underscore-prefixed, NOT in .PHONY, NOT public. +# --------------------------------------------------------------------------- + +_coverage_check: + @node scripts/check-coverage-threshold.mjs + +## help: List all available targets +help: + @echo "Standard targets:" + @echo " build - Compile TypeScript to out/" + @echo " test - Fail-fast tests + coverage + threshold enforcement" + @echo " lint - ESLint + tsc --noEmit (read-only, no formatting)" + @echo " fmt - Format code in-place (CHECK=1 for read-only CI check)" + @echo " clean - Remove build artifacts and VSIX files" + @echo " ci - lint + test + build (full CI simulation)" + @echo " setup - Install npm dependencies" + @echo "" + @echo "Repo-specific targets:" + @echo " package - Build the .vsix bundle via vsce" + +# ============================================================================= +# Repo-Specific Targets +# +# Targets below this line are specific to this repo and are NOT part of the +# standard 7-target interface. +# ============================================================================= + +## package: Build the .vsix bundle via vsce +package: build + @echo "==> Packaging VSIX..." + npx vsce package diff --git a/README.md b/README.md new file mode 100644 index 0000000..80ca8c0 --- /dev/null +++ b/README.md @@ -0,0 +1,127 @@ +# Diffy + +Pick two things and diff them — context-menu git diffing in VSCode. + +Diffy adds **no panels, no sidebars, no activity-bar icons**. Every feature hangs off the menus VSCode already has (SCM history, SCM changes, editor tab, file explorer) and a handful of palette commands. Pick a commit on the left, pick anything (another commit, a branch/tag, the index, the working copy) on the right, then drill through changed files with a QuickPick that stays open for as long as you need it. + +--- + +## How to access every command + +There are **seven** commands. Three are reachable from the Command Palette, four are reachable only from a right-click menu (because they need a target — a commit, a file — that the menu provides). + +### From the SCM **History view** (right-click a commit) + +Open the **Source Control** view (Ctrl/Cmd+Shift+G), expand a repository's **History** section, and **right-click any commit**. Three Diffy entries appear: + +| Menu label | Command ID | What it does | +| ------------------------------------ | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Diffy: Compare with…** | `diffy.compareWith` | The right-clicked commit is Side A. A QuickPick asks what Side B is: **Working Copy**, **Index**, **Pick a commit…**, or **Pick a branch or tag…**. Then a file QuickPick lists every changed file; selecting one opens `vscode.diff`. The picker stays open so you can open many files in a row. | +| **Diffy: Compare with Working Copy** | `diffy.compareWithWorkingCopy` | Same as above, but Side B is hardcoded to your on-disk working copy. Skips the Side B prompt. | +| **Diffy: Compare with Previous** | `diffy.compareWithPrevious` | Side A is the right-clicked commit, Side B is its first parent (`<sha>^1`). Use this for a classic "what changed in this commit?" view. | + +> These three are intentionally **hidden from the Command Palette** — they only make sense when invoked against a specific commit, which the right-click target provides. + +### From the SCM **Changes** view (right-click a changed file) + +In the Source Control view, **right-click any file** under **Changes**, **Staged Changes**, or **Merge Changes**: + +| Menu label | Command ID | What it does | +| ------------------------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| **Diffy: Compare with Commit…** | `diffy.compareFileWithCommit` | Pick a commit from a log QuickPick. Opens a single-file diff: that file at the chosen commit ↔ your working copy. | + +### From the **editor tab title** (right-click the file's tab) + +**Right-click the tab** of any open text file: + +| Menu label | Command ID | What it does | +| ------------------------------- | ----------------------------- | -------------------------------------------------------------------------- | +| **Diffy: Compare with Commit…** | `diffy.compareFileWithCommit` | Same as above, but the target file is the one whose tab you right-clicked. | + +### From the **File Explorer** (right-click a file in the tree) + +**Right-click any file** (not a folder) in the Explorer: + +| Menu label | Command ID | What it does | +| ------------------------------- | ----------------------------- | ------------------------------------------------------------ | +| **Diffy: Compare with Commit…** | `diffy.compareFileWithCommit` | Same again — the target is the file you clicked in the tree. | + +### From the **Command Palette** (Ctrl/Cmd+Shift+P) + +Type `Diffy:` to filter. Three entries are listed: + +| Palette label | Command ID | What it does | +| --------------------------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Diffy: Compare Two Commits** | `diffy.compareTwoCommits` | No target needed. QuickPick chain: pick repo → pick Side A commit → pick Side B (working copy / index / commit / ref) → pick files. | +| **Diffy: Compare with Commit…** | `diffy.compareFileWithCommit` | Uses the **currently focused editor's file** as the target. Same flow as the right-click version. If no editor is focused, Diffy will tell you to open a file first. | +| **Diffy: Reopen Last Comparison** | `diffy.reopenLast` | Reopens the file picker for the last A↔B comparison you made (stored per-workspace). Handy after closing the picker mid-review. | + +> **`Diffy: Show Logs`** (`diffy.showLogs`) exists but is hidden from the palette; it's reserved for the extension to surface its OutputChannel programmatically. Open it manually via **View → Output → "Diffy"**. + +--- + +## Quick visual reference + +``` +Source Control (Ctrl/Cmd+Shift+G) +└── History + └── <right-click a commit> + ├── Diffy: Compare with… ← pick anything for Side B + ├── Diffy: Compare with Working Copy ← Side B = on-disk + └── Diffy: Compare with Previous ← Side B = parent commit + +Source Control → Changes / Staged Changes +└── <right-click a file> + └── Diffy: Compare with Commit… ← pick commit; diff vs working copy + +Editor tab (right-click) ──┐ +File Explorer (right-click file) ──┼──► Diffy: Compare with Commit… + └── (uses that file as the target) + +Command Palette (Ctrl/Cmd+Shift+P) +├── Diffy: Compare Two Commits +├── Diffy: Compare with Commit… ← uses focused editor's file +└── Diffy: Reopen Last Comparison +``` + +--- + +## What Side B can be + +Whenever Diffy asks you to pick Side B, you get four choices: + +- **Working Copy** — the on-disk files in the repo (uncommitted changes included). +- **Index** — the staging area (what `git diff --cached` would compare against). +- **Pick a commit…** — choose from a QuickPick of recent log entries. +- **Pick a branch or tag…** — choose from all refs; resolved to its tip commit. + +## Diff titles + +Tabs that Diffy opens are titled human-first, e.g. `a1b2c3d ↔ Working Copy — src/foo.ts` or `a1b2c3d ↔ 9f8e7d6 — src/foo.ts`. No internal labels, no debug strings. + +## Requirements + +- VSCode `^1.85.0` +- Node `>=20` (for local development) +- Git on `PATH` +- The built-in **Git** extension (Diffy depends on it via `extensionDependencies`) + +## Development + +Standard make targets: + +```sh +make setup # install deps +make build # tsc +make test # fail-fast unit + e2e, enforces coverage threshold +make lint +make fmt +make ci # what CI runs +make package # build .vsix +``` + +See [CLAUDE.md](CLAUDE.md) for the full architecture and contributor rules. + +## License + +MIT diff --git a/coverage-thresholds.json b/coverage-thresholds.json new file mode 100644 index 0000000..3b711ee --- /dev/null +++ b/coverage-thresholds.json @@ -0,0 +1,5 @@ +{ + "_agent_pmo": "74cf183", + "_doc": "Single source of truth for code coverage thresholds. See REPO-STANDARDS-SPEC [COVERAGE-THRESHOLDS-JSON]. NO GitHub repo variables. NO env vars. NO public `make coverage-check` target. This file is read by the internal `_coverage_check` recipe inside `make test`. `make test` exits non-zero if measured coverage < threshold. Thresholds are monotonically increasing — only ratchet UP, never down.", + "default_threshold": 95 +} diff --git a/docs/assets/diffy-icon-primary-1024.png b/docs/assets/diffy-icon-primary-1024.png new file mode 100644 index 0000000..f180ebf Binary files /dev/null and b/docs/assets/diffy-icon-primary-1024.png differ diff --git a/docs/assets/diffy-icon-primary-128.png b/docs/assets/diffy-icon-primary-128.png new file mode 100644 index 0000000..6ed9db4 Binary files /dev/null and b/docs/assets/diffy-icon-primary-128.png differ diff --git a/docs/assets/diffy-icon-primary-256.png b/docs/assets/diffy-icon-primary-256.png new file mode 100644 index 0000000..265ceb8 Binary files /dev/null and b/docs/assets/diffy-icon-primary-256.png differ diff --git a/docs/assets/diffy-icon-primary-512.png b/docs/assets/diffy-icon-primary-512.png new file mode 100644 index 0000000..731c7c8 Binary files /dev/null and b/docs/assets/diffy-icon-primary-512.png differ diff --git a/docs/assets/diffy-icon-primary.webp b/docs/assets/diffy-icon-primary.webp new file mode 100644 index 0000000..ea77662 Binary files /dev/null and b/docs/assets/diffy-icon-primary.webp differ diff --git a/docs/design-system.md b/docs/design-system.md new file mode 100644 index 0000000..a99612a --- /dev/null +++ b/docs/design-system.md @@ -0,0 +1,614 @@ +# Diffy Design System + +This design system defines the visual and content standards for the future Diffy website, documentation pages, marketplace graphics, release notes, and adjacent product collateral. + +It does not define in-extension UI. Diffy remains a native VS Code extension that uses context menus, Command Palette entries, QuickPick, OutputChannel, and `vscode.diff`. + +## Product Position + +Diffy is a developer tool for comparing two git states without leaving the workflow the developer already uses. + +Core promise: + +> Pick two things and diff them. + +Design principles: + +- **Native first:** Present Diffy as a small, precise addition to VS Code, not a replacement shell. +- **Fast orientation:** Help developers understand entry points, command scope, and resulting diffs quickly. +- **Quiet confidence:** Use restrained visual emphasis, clear hierarchy, and practical examples. +- **Diff literacy:** Visual language should make "left vs right", changed files, additions, removals, and revisions immediately legible. +- **No novelty tax:** Avoid playful abstractions when a direct git, file, or VS Code concept would be clearer. + +Audience: + +- Developers reviewing commits, branches, staged work, and working-copy changes. +- Maintainers who need a lightweight extension with no custom panels or persistent UI. +- Teams evaluating the extension from the marketplace, README, or project website. + +## Brand Voice + +Diffy's voice is direct, concise, and work-focused. + +Use: + +- Short verbs: pick, compare, open, reopen, review. +- Concrete nouns: commit, branch, tag, index, working copy, file. +- Plain explanations of scope and constraints. +- Human-readable examples such as `a1b2c3d -> Working Copy - src/foo.ts`. + +Avoid: + +- Marketing claims that imply AI, automation, or code review intelligence. +- Heavy productivity language such as "10x", "revolutionary", or "magic". +- Internal implementation labels in user-facing copy. +- Explaining VS Code basics unless the page is explicitly instructional. + +Example headlines: + +- "Pick two git states and open the diff." +- "Context-menu diffing for VS Code." +- "Compare commits, branches, tags, the index, and your working copy." + +Example body copy: + +> Diffy adds focused compare commands to the VS Code surfaces you already use: SCM history, SCM changes, editor tabs, Explorer, and the Command Palette. + +## Logo Direction + +The Diffy mark should communicate comparison, pairing, and code review without looking like a separate IDE. + +The current logo set is raster-only. There is no SVG source for these assets. Each icon is built from flat geometric shapes, hard color boundaries, and no text, so a future vector conversion can trace the shapes cleanly. + +The whole set is rendered from the same primary mark — there are no alternate concepts in the current shipping set. Pick the size closest to the target rendering surface; the root `icon.png` is byte-identical to the 128 px export. + +Current assets: + +| Asset | Format | Use | +| -------------------------------------------------- | ------ | ---------------------------- | +| [Primary 128](assets/diffy-icon-primary-128.png) | PNG | Root extension icon source | +| [Primary 256](assets/diffy-icon-primary-256.png) | PNG | Marketplace and docs | +| [Primary 512](assets/diffy-icon-primary-512.png) | PNG | High-resolution export | +| [Primary 1024](assets/diffy-icon-primary-1024.png) | PNG | Print, hero, and zoom assets | +| [Primary WebP](assets/diffy-icon-primary.webp) | WebP | Compressed website asset | +| Root extension icon: `icon.png` | PNG | VS Code package icon | + +Preferred concepts: + +- Opposing panes, brackets, or angled halves that imply Side A vs Side B. +- A clear central gutter or compare rail. +- Flat shapes with enough negative space to survive at 16px. +- One primary brand accent plus optional semantic diff accents. + +Do: + +- Keep icons readable at 16px, 32px, 128px, and 256px. +- Use flat fills, crisp geometry, and limited color counts. +- Test against light, dark, and marketplace backgrounds. +- Keep icon variants text-free. + +Do not: + +- Use VS Code product marks or GitHub marks as part of the logo. +- Build the mark around a custom sidebar, panel, activity icon, or webview concept. +- Use gradients, shadows, texture, tiny line art, or photo-like detail in extension icons. + +## Color + +The palette balances a neutral developer-tool foundation with semantic diff colors. Blue is used as the primary action color, but the system should not become a single-hue blue interface. + +### Core Palette + +| Token | Hex | Use | +| ------------ | --------- | ------------------------------------ | +| `ink.950` | `#14161a` | Primary text, dark UI foundation | +| `ink.800` | `#272c33` | Secondary text on light surfaces | +| `ink.600` | `#5b6470` | Muted text, metadata | +| `ink.300` | `#b8c0ca` | Borders on dark surfaces | +| `ink.100` | `#e6e9ee` | Borders on light surfaces | +| `paper.000` | `#ffffff` | Primary page surface | +| `paper.050` | `#f7f8fa` | Alternate section background | +| `paper.100` | `#eef1f5` | Code-block and table backgrounds | +| `blue.600` | `#2563eb` | Primary links and actions | +| `blue.700` | `#1d4ed8` | Action hover state | +| `cyan.500` | `#0891b2` | Secondary accent, command highlights | +| `green.600` | `#16a34a` | Additions, success | +| `red.600` | `#dc2626` | Deletions, errors | +| `amber.500` | `#d97706` | Warnings, changed-state emphasis | +| `violet.600` | `#7c3aed` | Rare accent for branch/tag callouts | + +### Semantic Tokens + +| Token | Hex | Use | +| ---------------- | --------- | --------------------------------- | +| `text.primary` | `#14161a` | Default body text | +| `text.secondary` | `#5b6470` | Metadata and helper text | +| `surface.page` | `#ffffff` | Page background | +| `surface.subtle` | `#f7f8fa` | Alternating content bands | +| `surface.code` | `#eef1f5` | Inline code and examples | +| `border.default` | `#e6e9ee` | Default border | +| `action.primary` | `#2563eb` | Primary buttons and links | +| `diff.addition` | `#16a34a` | Added lines and `+N` indicators | +| `diff.deletion` | `#dc2626` | Deleted lines and `-N` indicators | +| `diff.modified` | `#d97706` | Modified files | +| `diff.rename` | `#0891b2` | Renamed or copied files | + +### Usage Rules + +- Body surfaces should stay mostly white or near-white. +- Use dark surfaces sparingly for code, terminal-style examples, or first-viewport contrast. +- Use green and red only when the meaning is addition/deletion, pass/fail, or success/error. +- Never rely on color alone for diff status. Pair color with text, symbols, or position. +- Maintain WCAG AA contrast for text and interactive states. + +## Typography + +### Font Stack + +Website UI: + +```css +font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + sans-serif; +``` + +Code, command IDs, SHAs, file paths: + +```css +font-family: "JetBrains Mono", "SFMono-Regular", Consolas, "Liberation Mono", monospace; +``` + +### Type Scale + +| Role | Size | Line height | Weight | Use | +| ------- | ---: | ----------: | -----: | ------------------------------ | +| Display | 48px | 56px | 700 | Homepage H1 only | +| H1 | 40px | 48px | 700 | Major page title | +| H2 | 30px | 38px | 650 | Major sections | +| H3 | 22px | 30px | 650 | Subsections and feature groups | +| Body | 16px | 26px | 400 | Default reading text | +| Small | 14px | 22px | 400 | Metadata and helper text | +| Code | 14px | 22px | 500 | Paths, command IDs, examples | +| Caption | 12px | 18px | 500 | Labels, badges, status chips | + +Rules: + +- Do not scale type with viewport width. +- Use `letter-spacing: 0`. +- Keep display type for true hero areas only. +- Keep docs content readable with a maximum line length near 72 characters. + +## Layout + +### Page Widths + +| Token | Value | Use | +| ------------------- | -------: | ------------------------------------ | +| `container.reading` | `760px` | Docs, changelog, long-form content | +| `container.content` | `1120px` | Marketing sections and feature grids | +| `container.wide` | `1280px` | Screenshots, comparison diagrams | +| `gutter.mobile` | `20px` | Mobile horizontal padding | +| `gutter.desktop` | `32px` | Desktop horizontal padding | + +### Spacing Scale + +Use an 8px spacing base. + +| Token | Value | +| --------- | -----: | +| `space.1` | `4px` | +| `space.2` | `8px` | +| `space.3` | `12px` | +| `space.4` | `16px` | +| `space.5` | `24px` | +| `space.6` | `32px` | +| `space.7` | `48px` | +| `space.8` | `64px` | +| `space.9` | `96px` | + +### Layout Rules + +- Use full-width page bands with constrained inner content. +- Do not put page sections inside floating cards. +- Cards are for repeated items, command references, release entries, and framed examples. +- Keep card radius at `8px` or less. +- Every fixed-format UI mock should have stable dimensions to prevent layout shift. +- On the homepage, the first viewport should show the product name, the promise, a real product-oriented visual, and a hint of the next section. + +## Imagery + +Website imagery should show the actual developer workflow. + +Preferred assets: + +- Clean screenshots of VS Code SCM history context menus with Diffy commands. +- File QuickPick screenshots showing changed files and stats. +- `vscode.diff` screenshots with readable left/right labels. +- Simple diagrams showing Side A, Side B, and file selection flow. +- Marketplace graphics that pair the Diffy mark with a real diff or command surface. + +Avoid: + +- Abstract gradient backgrounds without product context. +- Decorative code rain, fake dashboards, or invented web app panels. +- Blurred screenshots that hide the actual command labels. +- Cropped imagery where the user cannot identify VS Code, SCM history, QuickPick, or diff tabs. + +Screenshot treatment: + +- Use native VS Code themes for product screenshots. +- Prefer a light VS Code screenshot on a white or subtle surface. +- Provide dark-theme variants only when needed. +- Annotate sparingly with small numbered callouts or simple labels. + +## Icons + +Use a consistent outline icon set for the website, such as Lucide, when building the web UI. + +Recommended icon mappings: + +| Concept | Icon | +| ------------- | --------------------- | +| Compare | `GitCompare` | +| Commit | `GitCommitHorizontal` | +| Branch | `GitBranch` | +| Tag | `Tag` | +| File | `FileText` | +| Working copy | `FolderGit2` | +| Index/staging | `ListChecks` | +| Reopen | `History` | +| Logs | `ScrollText` | +| External link | `ExternalLink` | + +Rules: + +- Icons support recognition; they do not replace precise labels for commands. +- Icon buttons need accessible names and hover tooltips. +- Keep icon stroke width visually aligned with text weight. +- Do not introduce custom symbols for standard git concepts when a common icon exists. + +## Components + +### Header + +Purpose: + +- Orient users. +- Provide links to docs, GitHub, marketplace, releases, and install instructions. + +Structure: + +- Left: Diffy mark and wordmark. +- Center or right: Docs, GitHub, Releases. +- Right: primary install action. + +Behavior: + +- Header should be compact and sticky only on docs pages. +- On mobile, collapse links into a menu with clear labels. + +### Hero + +Purpose: + +- State what Diffy does and show it in context. + +Required content: + +- H1: `Diffy`. +- Supporting copy that includes "Pick two things and diff them." +- Primary action: install or marketplace link. +- Secondary action: view docs or GitHub. +- Product visual: screenshot or interaction mock of context-menu -> QuickPick -> diff. + +Rules: + +- Do not put the hero text in a card. +- Avoid split layouts where the screenshot is an ornamental side panel. +- The hero should leave a hint of the next section visible on common desktop and mobile viewports. + +### Command Matrix + +Purpose: + +- Make every entry point scannable. + +Fields: + +- Surface: SCM history, SCM changes, editor tab, Explorer, Command Palette. +- Command label. +- Side A. +- Side B. +- Result. + +Example: + +| Surface | Command | Side A | Side B | Result | +| ----------- | ------------------------------- | ------------- | ------------------------------------------- | ---------------------- | +| SCM history | `Diffy: Compare with...` | Picked commit | Commit, branch, tag, index, or working copy | File picker, then diff | +| Editor tab | `Diffy: Compare with Commit...` | Picked commit | Current file | Single-file diff | + +### Workflow Diagram + +Purpose: + +- Explain the flow without adding a fake product UI. + +Preferred shape: + +```text +Choose Side A -> Choose Side B -> Pick changed file -> Open VS Code diff +``` + +Rules: + +- Use real command labels and real git concepts. +- Keep diagrams horizontal on desktop and stacked on mobile. +- Avoid swimlanes unless the page needs implementation depth. + +### Feature Cards + +Use cards for repeated feature summaries only. + +Card anatomy: + +- Icon. +- Short title. +- One sentence of body text. +- Optional command label or screenshot thumbnail. + +Feature card examples: + +- "Context-menu first" +- "Compare against working copy" +- "Review changed files without leaving QuickPick" +- "Reopen the last comparison" + +### Code And Command Blocks + +Use code blocks for commands, command IDs, and examples. + +Rules: + +- Keep examples copyable. +- Do not use terminal prompts unless needed. +- Annotate command examples outside the block rather than inside comments. + +Example: + +```sh +make package +``` + +### Status And Diff Badges + +Use small badges for file states and diff stats. + +| Status | Label | Token | +| -------- | ----- | --------------- | +| Added | `A` | `diff.addition` | +| Modified | `M` | `diff.modified` | +| Deleted | `D` | `diff.deletion` | +| Renamed | `R` | `diff.rename` | +| Copied | `C` | `diff.rename` | + +Stats format: + +```text ++24 -8 +``` + +Rules: + +- Pair `+N` with addition color and `-N` with deletion color. +- Preserve a readable text form for accessibility. +- Keep badges small and stable in width for compact tables. + +### Tables + +Use tables for command references and capability matrices. + +Rules: + +- Prefer direct labels over explanatory prose inside cells. +- Use monospace for command IDs, SHAs, and file paths. +- Keep columns narrow enough for mobile wrapping. + +### Callouts + +Use callouts for important constraints, not decoration. + +Types: + +- Note: neutral guidance. +- Warning: compatibility, installation, or setup caveats. +- Constraint: product boundaries such as "No panels or webviews." + +Constraint callout example: + +> Diffy does not add a sidebar, activity-bar icon, tree view, or webview. It uses existing VS Code surfaces. + +## Motion + +Motion should clarify cause and effect. + +Use: + +- 120ms to 180ms hover and focus transitions. +- Short reveal animations for diagrams or command flows. +- Reduced-motion fallbacks for all nonessential movement. + +Avoid: + +- Long looping hero animations. +- Animated backgrounds behind reading text. +- Motion that implies the extension has a custom persistent UI. + +## Accessibility + +Baseline: + +- WCAG 2.2 AA for website pages. +- Keyboard-accessible navigation and controls. +- Visible focus rings on every interactive element. +- Alt text for screenshots that identifies the VS Code surface and command shown. +- Text alternatives for diagrams. +- Color is never the only indicator of diff status. + +Focus ring: + +```css +outline: 2px solid #2563eb; +outline-offset: 2px; +``` + +Reduced motion: + +```css +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + scroll-behavior: auto !important; + transition-duration: 0.01ms !important; + } +} +``` + +## Content Patterns + +### Product One-Liner + +Use this where space is limited: + +> Context-menu git diffing in VS Code. + +### Short Description + +Use this for marketplace and social metadata: + +> Diffy lets you pick two git states and open file diffs through VS Code's native compare experience. + +### Longer Description + +Use this for the website intro: + +> Diffy adds focused compare commands to VS Code's existing SCM history, SCM changes, editor tab, Explorer, and Command Palette surfaces. Pick a commit, branch, tag, index, or working copy target, then open changed files in VS Code's built-in diff editor. + +### SEO Title Pattern + +```text +Diffy - Context-menu git diffing for VS Code +``` + +### Social Description Pattern + +```text +Pick two git states and open native VS Code diffs from SCM history, Explorer, editor tabs, or the Command Palette. +``` + +## Website Information Architecture + +Recommended initial website: + +- Home: product promise, command flow, install action, screenshots. +- Docs: command reference, workflows, requirements, troubleshooting. +- Changelog: release notes and migration notes. +- Privacy: no telemetry statement for v1 unless this changes. +- GitHub/Marketplace links: external destinations, not duplicated pages. + +Homepage section order: + +1. Hero with product visual. +2. Command surfaces overview. +3. Side A / Side B comparison flow. +4. Screenshots of real VS Code surfaces. +5. Installation and requirements. +6. Links to docs, changelog, and GitHub. + +Docs page groups: + +- Getting started. +- Command reference. +- Comparing commits. +- Comparing files. +- Reopening the last comparison. +- Troubleshooting. +- Development links. + +## CSS Token Starter + +Use these as the first pass for a future web implementation. + +```css +:root { + --diffy-ink-950: #14161a; + --diffy-ink-800: #272c33; + --diffy-ink-600: #5b6470; + --diffy-ink-300: #b8c0ca; + --diffy-ink-100: #e6e9ee; + + --diffy-paper-000: #ffffff; + --diffy-paper-050: #f7f8fa; + --diffy-paper-100: #eef1f5; + + --diffy-blue-600: #2563eb; + --diffy-blue-700: #1d4ed8; + --diffy-cyan-500: #0891b2; + --diffy-green-600: #16a34a; + --diffy-red-600: #dc2626; + --diffy-amber-500: #d97706; + --diffy-violet-600: #7c3aed; + + --diffy-text-primary: var(--diffy-ink-950); + --diffy-text-secondary: var(--diffy-ink-600); + --diffy-surface-page: var(--diffy-paper-000); + --diffy-surface-subtle: var(--diffy-paper-050); + --diffy-surface-code: var(--diffy-paper-100); + --diffy-border-default: var(--diffy-ink-100); + --diffy-action-primary: var(--diffy-blue-600); + --diffy-action-primary-hover: var(--diffy-blue-700); + --diffy-diff-addition: var(--diffy-green-600); + --diffy-diff-deletion: var(--diffy-red-600); + --diffy-diff-modified: var(--diffy-amber-500); + --diffy-diff-rename: var(--diffy-cyan-500); + + --diffy-radius-sm: 4px; + --diffy-radius-md: 8px; + --diffy-shadow-soft: 0 12px 30px rgb(20 22 26 / 10%); + + --diffy-container-reading: 760px; + --diffy-container-content: 1120px; + --diffy-container-wide: 1280px; + + --diffy-space-1: 4px; + --diffy-space-2: 8px; + --diffy-space-3: 12px; + --diffy-space-4: 16px; + --diffy-space-5: 24px; + --diffy-space-6: 32px; + --diffy-space-7: 48px; + --diffy-space-8: 64px; + --diffy-space-9: 96px; +} +``` + +## Governance + +Before publishing a new website page or asset, check: + +- The page reinforces the core promise: "Pick two things and diff them." +- Product screenshots show real VS Code surfaces. +- The page does not imply a custom panel, sidebar, webview, AI feature, or standalone app. +- Diff colors are semantic and accessible. +- Command labels match the extension manifest and README. +- Installation, requirements, and GitHub links are current. +- Copy stays direct, concise, and developer-facing. diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..0824483 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,139 @@ +// agent-pmo:74cf183 +// @ts-check +// +// MAX-STRICTNESS ESLint config: +// - tseslint strictTypeChecked + stylisticTypeChecked baselines +// - every safe extra rule promoted to "error" (no warns) +// - no exceptions, no per-file disables + +import eslint from "@eslint/js"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + eslint.configs.recommended, + ...tseslint.configs.strictTypeChecked, + ...tseslint.configs.stylisticTypeChecked, + { + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + rules: { + // Type-safety hard rules (no `any`, no unsafe ops, no non-null !) + "@typescript-eslint/no-explicit-any": "error", + "@typescript-eslint/no-unsafe-assignment": "error", + "@typescript-eslint/no-unsafe-call": "error", + "@typescript-eslint/no-unsafe-member-access": "error", + "@typescript-eslint/no-unsafe-return": "error", + "@typescript-eslint/no-unsafe-argument": "error", + "@typescript-eslint/no-unsafe-enum-comparison": "error", + "@typescript-eslint/no-non-null-assertion": "error", + "@typescript-eslint/no-confusing-non-null-assertion": "error", + "@typescript-eslint/no-unnecessary-type-assertion": "error", + "@typescript-eslint/no-unnecessary-condition": "error", + "@typescript-eslint/no-unnecessary-boolean-literal-compare": "error", + "@typescript-eslint/no-unnecessary-template-expression": "error", + "@typescript-eslint/no-unnecessary-type-arguments": "error", + "@typescript-eslint/no-redundant-type-constituents": "error", + "@typescript-eslint/no-duplicate-type-constituents": "error", + "@typescript-eslint/no-duplicate-enum-values": "error", + "@typescript-eslint/no-mixed-enums": "error", + "@typescript-eslint/no-meaningless-void-operator": "error", + "@typescript-eslint/no-invalid-void-type": "error", + "@typescript-eslint/no-confusing-void-expression": "error", + "@typescript-eslint/no-base-to-string": "error", + "@typescript-eslint/no-dynamic-delete": "error", + "@typescript-eslint/no-extraneous-class": "error", + "@typescript-eslint/no-for-in-array": "error", + "@typescript-eslint/no-useless-empty-export": "error", + "@typescript-eslint/no-misused-spread": "error", + "@typescript-eslint/no-unsafe-unary-minus": "error", + "@typescript-eslint/no-array-delete": "error", + + // Async / Promise safety + "@typescript-eslint/no-floating-promises": "error", + "@typescript-eslint/no-misused-promises": "error", + "@typescript-eslint/await-thenable": "error", + "@typescript-eslint/promise-function-async": "error", + "@typescript-eslint/return-await": ["error", "always"], + "@typescript-eslint/require-await": "error", + + // Boolean / nullish clarity + "@typescript-eslint/strict-boolean-expressions": "error", + "@typescript-eslint/prefer-nullish-coalescing": "error", + "@typescript-eslint/prefer-optional-chain": "error", + + // Idiomatic patterns + "@typescript-eslint/prefer-as-const": "error", + "@typescript-eslint/prefer-find": "error", + "@typescript-eslint/prefer-includes": "error", + "@typescript-eslint/prefer-readonly": "error", + "@typescript-eslint/prefer-reduce-type-parameter": "error", + "@typescript-eslint/prefer-return-this-type": "error", + "@typescript-eslint/prefer-string-starts-ends-with": "error", + "@typescript-eslint/prefer-for-of": "error", + "@typescript-eslint/prefer-function-type": "error", + "@typescript-eslint/prefer-literal-enum-member": "error", + "@typescript-eslint/unified-signatures": "error", + "@typescript-eslint/require-array-sort-compare": "error", + "@typescript-eslint/switch-exhaustiveness-check": "error", + "@typescript-eslint/restrict-plus-operands": "error", + "@typescript-eslint/restrict-template-expressions": "error", + + // Imports + naming + "@typescript-eslint/consistent-type-imports": [ + "error", + { prefer: "type-imports" }, + ], + "@typescript-eslint/consistent-type-exports": "error", + "@typescript-eslint/consistent-type-definitions": ["error", "interface"], + "@typescript-eslint/no-shadow": "error", + "@typescript-eslint/no-unused-vars": [ + "error", + { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, + ], + "@typescript-eslint/no-import-type-side-effects": "error", + "@typescript-eslint/method-signature-style": ["error", "property"], + + // Errors are typed, not strings + "no-throw-literal": "off", + "@typescript-eslint/only-throw-error": "error", + + // Plain JS hygiene + eqeqeq: ["error", "always"], + curly: ["error", "all"], + "no-console": "error", + "no-var": "error", + "prefer-const": "error", + "no-debugger": "error", + "no-alert": "error", + "no-param-reassign": ["error", { props: true }], + "no-eval": "error", + "no-implied-eval": "off", + "@typescript-eslint/no-implied-eval": "error", + "no-return-assign": "error", + "no-self-compare": "error", + "no-unmodified-loop-condition": "error", + "no-unreachable-loop": "error", + "no-useless-concat": "error", + "no-useless-return": "error", + "prefer-template": "error", + "object-shorthand": "error", + }, + }, + { + // Scripts and bundler config are JS — they don't get TS rules. + ignores: [ + "out/", + "dist/", + "**/*.d.ts", + "node_modules/", + "coverage/", + ".vscode-test/", + "**/*.js", + "**/*.mjs", + ], + }, +); diff --git a/icon.png b/icon.png new file mode 100644 index 0000000..6ed9db4 Binary files /dev/null and b/icon.png differ diff --git a/opencode.json b/opencode.json new file mode 100644 index 0000000..0c8379c --- /dev/null +++ b/opencode.json @@ -0,0 +1,5 @@ +{ + "_agent_pmo": "74cf183", + "$schema": "https://opencode.ai/config.json", + "instructions": ["CLAUDE.md"] +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..41b3e0c --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6849 @@ +{ + "name": "diffy", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "diffy", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "pino": "^9.5.0" + }, + "devDependencies": { + "@eslint/js": "^9.13.0", + "@types/mocha": "^10.0.9", + "@types/node": "^20.16.0", + "@types/vscode": "^1.85.0", + "@vscode/test-electron": "^2.4.1", + "@vscode/vsce": "^3.2.0", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "c8": "^10.1.2", + "eslint": "^9.13.0", + "glob": "^11.0.0", + "mocha": "^10.8.2", + "prettier": "^3.3.3", + "typescript": "^5.6.3", + "typescript-eslint": "^8.12.2" + }, + "engines": { + "node": ">=20", + "vscode": "^1.85.0" + } + }, + "node_modules/@azu/format-text": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@azu/format-text/-/format-text-1.0.2.tgz", + "integrity": "sha512-Swi4N7Edy1Eqq82GxgEECXSSLyn6GOb5htRFPzBDdUkECGXtlf12ynO5oJSpWKPwCaUssOu7NfhDcCWpIC6Ywg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@azu/style-format": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@azu/style-format/-/style-format-1.0.1.tgz", + "integrity": "sha512-AHcTojlNBdD/3/KxIKlg8sxIWHfOtQszLvOpagLTO+bjC3u7SAszu1lf//u7JJC50aUSH+BVWDD/KvaA6Gfn5g==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "@azu/format-text": "^1.0.1" + } + }, + "node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz", + "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz", + "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.23.0.tgz", + "integrity": "sha512-Evs1INHo+jUjwHi1T6SG6Ua/LHOQBCLuKEEE6efIpt4ZOoNonaT1kP32GoOcdNDbfqsD2445CPri3MubBy5DEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz", + "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz", + "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.1.tgz", + "integrity": "sha512-5C/2WD5Vb1lHnZS16dNQRPMjN6oV/Upba+C9nBIs15PmOi6A3ZGs4Lr2u60zw4S04gi+u3cEXiqTVP7M4Pz3kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^5.5.0", + "@azure/msal-node": "^5.1.0", + "open": "^10.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz", + "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.11.0.tgz", + "integrity": "sha512-zkGNYS3TwY8lUpPIafAmsFCYZbgFixY9y/LZB9GUg0IILoHTqpN26j5OrkL1AQThh/YdZsawe4iWXfp85lFVxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.6.2" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "16.6.2", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.6.2.tgz", + "integrity": "sha512-hQjjsekAjB00cM1EmatWJlzhEoK2Qhz7Rj5gvM6tYf8iL7RM3tkxlpU9fG0+ofkulzg9AEEA6dIEnSmDr5ZqUA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.2.2.tgz", + "integrity": "sha512-toS+2AePxqyzb0YOKttDOOiSl3jrkK9aiqIvpurpis0O34QcIS5gToqrgT39p04Dpxw3YoUU0lxJKTpSFFfA6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.6.2", + "jsonwebtoken": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@secretlint/config-creator": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/config-creator/-/config-creator-10.2.2.tgz", + "integrity": "sha512-BynOBe7Hn3LJjb3CqCHZjeNB09s/vgf0baBaHVw67w7gHF0d25c3ZsZ5+vv8TgwSchRdUCRrbbcq5i2B1fJ2QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/types": "^10.2.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/config-loader": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/config-loader/-/config-loader-10.2.2.tgz", + "integrity": "sha512-ndjjQNgLg4DIcMJp4iaRD6xb9ijWQZVbd9694Ol2IszBIbGPPkwZHzJYKICbTBmh6AH/pLr0CiCaWdGJU7RbpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/profiler": "^10.2.2", + "@secretlint/resolver": "^10.2.2", + "@secretlint/types": "^10.2.2", + "ajv": "^8.17.1", + "debug": "^4.4.1", + "rc-config-loader": "^4.1.3" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/core": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/core/-/core-10.2.2.tgz", + "integrity": "sha512-6rdwBwLP9+TO3rRjMVW1tX+lQeo5gBbxl1I5F8nh8bgGtKwdlCMhMKsBWzWg1ostxx/tIG7OjZI0/BxsP8bUgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/profiler": "^10.2.2", + "@secretlint/types": "^10.2.2", + "debug": "^4.4.1", + "structured-source": "^4.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/formatter": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/formatter/-/formatter-10.2.2.tgz", + "integrity": "sha512-10f/eKV+8YdGKNQmoDUD1QnYL7TzhI2kzyx95vsJKbEa8akzLAR5ZrWIZ3LbcMmBLzxlSQMMccRmi05yDQ5YDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/resolver": "^10.2.2", + "@secretlint/types": "^10.2.2", + "@textlint/linter-formatter": "^15.2.0", + "@textlint/module-interop": "^15.2.0", + "@textlint/types": "^15.2.0", + "chalk": "^5.4.1", + "debug": "^4.4.1", + "pluralize": "^8.0.0", + "strip-ansi": "^7.1.0", + "table": "^6.9.0", + "terminal-link": "^4.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/formatter/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@secretlint/node": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/node/-/node-10.2.2.tgz", + "integrity": "sha512-eZGJQgcg/3WRBwX1bRnss7RmHHK/YlP/l7zOQsrjexYt6l+JJa5YhUmHbuGXS94yW0++3YkEJp0kQGYhiw1DMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/config-loader": "^10.2.2", + "@secretlint/core": "^10.2.2", + "@secretlint/formatter": "^10.2.2", + "@secretlint/profiler": "^10.2.2", + "@secretlint/source-creator": "^10.2.2", + "@secretlint/types": "^10.2.2", + "debug": "^4.4.1", + "p-map": "^7.0.3" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/profiler": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/profiler/-/profiler-10.2.2.tgz", + "integrity": "sha512-qm9rWfkh/o8OvzMIfY8a5bCmgIniSpltbVlUVl983zDG1bUuQNd1/5lUEeWx5o/WJ99bXxS7yNI4/KIXfHexig==", + "dev": true, + "license": "MIT" + }, + "node_modules/@secretlint/resolver": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/resolver/-/resolver-10.2.2.tgz", + "integrity": "sha512-3md0cp12e+Ae5V+crPQYGd6aaO7ahw95s28OlULGyclyyUtf861UoRGS2prnUrKh7MZb23kdDOyGCYb9br5e4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@secretlint/secretlint-formatter-sarif": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-formatter-sarif/-/secretlint-formatter-sarif-10.2.2.tgz", + "integrity": "sha512-ojiF9TGRKJJw308DnYBucHxkpNovDNu1XvPh7IfUp0A12gzTtxuWDqdpuVezL7/IP8Ua7mp5/VkDMN9OLp1doQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-sarif-builder": "^3.2.0" + } + }, + "node_modules/@secretlint/secretlint-rule-no-dotenv": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-no-dotenv/-/secretlint-rule-no-dotenv-10.2.2.tgz", + "integrity": "sha512-KJRbIShA9DVc5Va3yArtJ6QDzGjg3PRa1uYp9As4RsyKtKSSZjI64jVca57FZ8gbuk4em0/0Jq+uy6485wxIdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/types": "^10.2.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/secretlint-rule-preset-recommend": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-preset-recommend/-/secretlint-rule-preset-recommend-10.2.2.tgz", + "integrity": "sha512-K3jPqjva8bQndDKJqctnGfwuAxU2n9XNCPtbXVI5JvC7FnQiNg/yWlQPbMUlBXtBoBGFYp08A94m6fvtc9v+zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/source-creator": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/source-creator/-/source-creator-10.2.2.tgz", + "integrity": "sha512-h6I87xJfwfUTgQ7irWq7UTdq/Bm1RuQ/fYhA3dtTIAop5BwSFmZyrchph4WcoEvbN460BWKmk4RYSvPElIIvxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/types": "^10.2.2", + "istextorbinary": "^9.5.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/types": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/types/-/types-10.2.2.tgz", + "integrity": "sha512-Nqc90v4lWCXyakD6xNyNACBJNJ0tNCwj2WNk/7ivyacYHxiITVgmLUFXTBOeCdy79iz6HtN9Y31uw/jbLrdOAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@textlint/ast-node-types": { + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.7.1.tgz", + "integrity": "sha512-Wii5UgUKFEh9Uv6wbq1zr4/Kf+dtjiUuzPrrXzKp8H+ifkvKNzi23V4Nz+6wVyHQn5T28AFuc8VH8OtzvGYecA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/linter-formatter": { + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/linter-formatter/-/linter-formatter-15.7.1.tgz", + "integrity": "sha512-TdwZ/debWYFD05K3CcoHtwvnCrza29wZxD+BjDTk/V5N7iRqkK1dTTHSD4A8AIgROLiDkHJmIKQbasbmsg8AvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azu/format-text": "^1.0.2", + "@azu/style-format": "^1.0.1", + "@textlint/module-interop": "15.7.1", + "@textlint/resolver": "15.7.1", + "@textlint/types": "15.7.1", + "chalk": "^4.1.2", + "debug": "^4.4.3", + "js-yaml": "^4.1.1", + "lodash": "^4.18.1", + "pluralize": "^2.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "table": "^6.9.0", + "text-table": "^0.2.0" + } + }, + "node_modules/@textlint/linter-formatter/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@textlint/linter-formatter/node_modules/pluralize": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-2.0.0.tgz", + "integrity": "sha512-TqNZzQCD4S42De9IfnnBvILN7HAW7riLqsCyp8lgjXeysyPlX5HhqKAcJHHHb9XskE4/a+7VGC9zzx8Ls0jOAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/linter-formatter/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@textlint/module-interop": { + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/module-interop/-/module-interop-15.7.1.tgz", + "integrity": "sha512-Jg+sQW2L/cRJypk59wtcMUVVpt8vmit5ZMT3gUnFwevP3A6Qp1HfOtUy9ObT4hBX3lOSGT/ekcCDxR1pL7uH1g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/resolver": { + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/resolver/-/resolver-15.7.1.tgz", + "integrity": "sha512-8XnO0pgF6mXnm41VvWmBbEIdGPhiCUt31uLZkOis1ECeg/1SoUcIT6Mx/F0e1rukq8l0UlOSeY9a31CsvRMK0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/types": { + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/types/-/types-15.7.1.tgz", + "integrity": "sha512-Vye/GmFNBTgVzZFtIFJTmLB+s2A7oIADxNG6r9UhfPuY+Czv0z5G3xeyFZZudPlfxURsKUyPIU5XsjOFqVp33A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@textlint/ast-node-types": "15.7.1" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.41", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", + "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/sarif": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@types/sarif/-/sarif-2.1.7.tgz", + "integrity": "sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/vscode": { + "version": "1.120.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.120.0.tgz", + "integrity": "sha512-feaT4Rst+FkTch5zz/ZbNCxoIvo55YU80Be2kiL7OJcod4+CUYf2lUBPdIJzozNnSEMq1VRTGrWEcCGFB3fBmA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.4.tgz", + "integrity": "sha512-PegsU+XfyJJNjd4+u/k6f9yTyp0lEXXiPopUNobZcIAUJFGICFLN+sP0Rb3JehVmiij1Ph0dFGYqODoRo/2+6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.59.4", + "@typescript-eslint/type-utils": "8.59.4", + "@typescript-eslint/utils": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.59.4", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.4.tgz", + "integrity": "sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.4.tgz", + "integrity": "sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.59.4", + "@typescript-eslint/types": "^8.59.4", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.4.tgz", + "integrity": "sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.4.tgz", + "integrity": "sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.4.tgz", + "integrity": "sha512-uonTuPAAKr9XaBGqJ3LjYTh72zy5DyGesljO9gtmk/eFW0W1fRHjnwVYKB35Lm8d5Q5CluEW3gPHjTvZTmgrfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4", + "@typescript-eslint/utils": "8.59.4", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.4.tgz", + "integrity": "sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.4.tgz", + "integrity": "sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.59.4", + "@typescript-eslint/tsconfig-utils": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.4.tgz", + "integrity": "sha512-cYXeNAUsG4lJo5dbc1FcKm+JwIWrj1/UpTORsC6tGMjEZ81DYcvIr9/ueikhMa/Y/gDQYGp+YX9/xQrXje5BJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.4.tgz", + "integrity": "sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.4", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.5.tgz", + "integrity": "sha512-yURCknZhvywvQItHMMmFSo+fq5arCUIyz/CVk7jD89MSai7dkaX8ufjCWp3NttLojoTVbcE72ri+be/TnEbMHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@vscode/test-electron": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.5.2.tgz", + "integrity": "sha512-8ukpxv4wYe0iWMRQU18jhzJOHkeGKbnw7xWRX3Zw1WJA4cEKbHcmmLPdPrPtL6rhDcrlCZN+xKRpv09n4gRHYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "jszip": "^3.10.1", + "ora": "^8.1.0", + "semver": "^7.6.2" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@vscode/vsce": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.9.1.tgz", + "integrity": "sha512-MPn5p+DoudI+3GfJSpAZZraE1lgLv0LcwbH3+xy7RgEhty3UIkmUMUA+5jPTDaxXae00AnX5u77FxGM8FhfKKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/identity": "^4.1.0", + "@secretlint/node": "^10.1.2", + "@secretlint/secretlint-formatter-sarif": "^10.1.2", + "@secretlint/secretlint-rule-no-dotenv": "^10.1.2", + "@secretlint/secretlint-rule-preset-recommend": "^10.1.2", + "@vscode/vsce-sign": "^2.0.0", + "azure-devops-node-api": "^12.5.0", + "chalk": "^4.1.2", + "cheerio": "^1.0.0-rc.9", + "cockatiel": "^3.1.2", + "commander": "^12.1.0", + "form-data": "^4.0.0", + "glob": "^11.0.0", + "hosted-git-info": "^4.0.2", + "jsonc-parser": "^3.2.0", + "leven": "^3.1.0", + "markdown-it": "^14.1.0", + "mime": "^1.3.4", + "minimatch": "^3.0.3", + "parse-semver": "^1.1.1", + "read": "^1.0.7", + "secretlint": "^10.1.2", + "semver": "^7.5.2", + "tmp": "^0.2.3", + "typed-rest-client": "^1.8.4", + "url-join": "^4.0.1", + "xml2js": "^0.5.0", + "yauzl": "^3.2.1", + "yazl": "^2.2.2" + }, + "bin": { + "vsce": "vsce" + }, + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "keytar": "^7.7.0" + } + }, + "node_modules/@vscode/vsce-sign": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign/-/vsce-sign-2.0.9.tgz", + "integrity": "sha512-8IvaRvtFyzUnGGl3f5+1Cnor3LqaUWvhaUjAYO8Y39OUYlOf3cRd+dowuQYLpZcP3uwSG+mURwjEBOSq4SOJ0g==", + "dev": true, + "hasInstallScript": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optionalDependencies": { + "@vscode/vsce-sign-alpine-arm64": "2.0.6", + "@vscode/vsce-sign-alpine-x64": "2.0.6", + "@vscode/vsce-sign-darwin-arm64": "2.0.6", + "@vscode/vsce-sign-darwin-x64": "2.0.6", + "@vscode/vsce-sign-linux-arm": "2.0.6", + "@vscode/vsce-sign-linux-arm64": "2.0.6", + "@vscode/vsce-sign-linux-x64": "2.0.6", + "@vscode/vsce-sign-win32-arm64": "2.0.6", + "@vscode/vsce-sign-win32-x64": "2.0.6" + } + }, + "node_modules/@vscode/vsce-sign-alpine-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-arm64/-/vsce-sign-alpine-arm64-2.0.6.tgz", + "integrity": "sha512-wKkJBsvKF+f0GfsUuGT0tSW0kZL87QggEiqNqK6/8hvqsXvpx8OsTEc3mnE1kejkh5r+qUyQ7PtF8jZYN0mo8Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-alpine-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.6.tgz", + "integrity": "sha512-YoAGlmdK39vKi9jA18i4ufBbd95OqGJxRvF3n6ZbCyziwy3O+JgOpIUPxv5tjeO6gQfx29qBivQ8ZZTUF2Ba0w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.6.tgz", + "integrity": "sha512-5HMHaJRIQuozm/XQIiJiA0W9uhdblwwl2ZNDSSAeXGO9YhB9MH5C4KIHOmvyjUnKy4UCuiP43VKpIxW1VWP4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-x64/-/vsce-sign-darwin-x64-2.0.6.tgz", + "integrity": "sha512-25GsUbTAiNfHSuRItoQafXOIpxlYj+IXb4/qarrXu7kmbH94jlm5sdWSCKrrREs8+GsXF1b+l3OB7VJy5jsykw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm/-/vsce-sign-linux-arm-2.0.6.tgz", + "integrity": "sha512-UndEc2Xlq4HsuMPnwu7420uqceXjs4yb5W8E2/UkaHBB9OWCwMd3/bRe/1eLe3D8kPpxzcaeTyXiK3RdzS/1CA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.6.tgz", + "integrity": "sha512-cfb1qK7lygtMa4NUl2582nP7aliLYuDEVpAbXJMkDq1qE+olIw/es+C8j1LJwvcRq1I2yWGtSn3EkDp9Dq5FdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.6.tgz", + "integrity": "sha512-/olerl1A4sOqdP+hjvJ1sbQjKN07Y3DVnxO4gnbn/ahtQvFrdhUi0G1VsZXDNjfqmXw57DmPi5ASnj/8PGZhAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-win32-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.6.tgz", + "integrity": "sha512-ivM/MiGIY0PJNZBoGtlRBM/xDpwbdlCWomUWuLmIxbi1Cxe/1nooYrEQoaHD8ojVRgzdQEUzMsRbyF5cJJgYOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vscode/vsce-sign-win32-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.6.tgz", + "integrity": "sha512-mgth9Kvze+u8CruYMmhHw6Zgy3GRX2S+Ed5oSokDEK5vPEwGGKnmuXua9tmFhomeAnhgJnL4DCna3TiNuGrBTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "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/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/azure-devops-node-api": { + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", + "integrity": "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==", + "dev": true, + "license": "MIT", + "dependencies": { + "tunnel": "0.0.6", + "typed-rest-client": "^1.8.4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/binaryextensions": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-6.11.0.tgz", + "integrity": "sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "editions": "^6.21.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/boundary": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/boundary/-/boundary-2.0.0.tgz", + "integrity": "sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true, + "license": "ISC" + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/c8": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/c8/-/c8-10.1.3.tgz", + "integrity": "sha512-LvcyrOAaOnrrlMpW22n690PUvxiq4Uf9WMhQwNJ9vgagkL/ph1+D4uvjvDA5XCbykrc0sx+ay6pVi9YZ1GnhyA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.1", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^3.1.1", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.1.6", + "test-exclude": "^7.0.1", + "v8-to-istanbul": "^9.0.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "c8": "bin/c8.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "monocart-coverage-reports": "^2" + }, + "peerDependenciesMeta": { + "monocart-coverage-reports": { + "optional": true + } + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/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/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cockatiel": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.2.1.tgz", + "integrity": "sha512-gfrHV6ZPkquExvMh9IOkKsBzNDk6sDuZ6DdBGUBkvFnTCqCxzpuq48RySgP0AnaqQkw2zynOFj9yly6T1Q2G5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/editions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/editions/-/editions-6.22.0.tgz", + "integrity": "sha512-UgGlf8IW75je7HZjNDpJdCv4cGJWIi6yumFdZ0R7A8/CIhQiWUjyGLCxdHpd8bmyD1gnkfUNK0oeOXqUS2cpfQ==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "version-range": "^4.15.0" + }, + "engines": { + "ecmascript": ">= es5", + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "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", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "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": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/fs-extra": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", + "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.3", + "path-type": "^6.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istextorbinary": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-9.5.0.tgz", + "integrity": "sha512-5mbUj3SiZXCuRf9fT3ibzbSSEWiy63gFfksmGfdOzujPjW3k+z8WvIBxcJHBoQNlaZaiyB25deviif2+osLmLw==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "binaryextensions": "^6.11.0", + "editions": "^6.21.0", + "textextensions": "^6.11.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "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/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "dev": true, + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dev": true, + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keytar": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", + "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "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": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdown-it": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", + "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/mocha": { + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/mocha/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/mocha/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/mocha/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true, + "license": "ISC" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-sarif-builder": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/node-sarif-builder/-/node-sarif-builder-3.4.0.tgz", + "integrity": "sha512-tGnJW6OKRii9u/b2WiUViTJS+h7Apxx17qsMUjsUeNDiMMX5ZFf8F8Fcz7PAQ6omvOxHZtvDTmOYKJQwmfpjeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sarif": "^2.1.7", + "fs-extra": "^11.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/normalize-package-data": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data/node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "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/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.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", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/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/ora/node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/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/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": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-semver": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", + "integrity": "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.1.0" + } + }, + "node_modules/parse-semver/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz", + "integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/path-type": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", + "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pino": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", + "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "optional": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc-config-loader": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/rc-config-loader/-/rc-config-loader-4.1.4.tgz", + "integrity": "sha512-3GiwEzklkbXTDp52UR5nT8iXgYAx1V9ZG/kDZT7p60u2GCv2XTwQq4NzinMoMpNtXhmt3WkhYXcj6HH8HdwCEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "js-yaml": "^4.1.1", + "json5": "^2.2.3", + "require-from-string": "^2.0.2" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "mute-stream": "~0.0.4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/read-pkg": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", + "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.3", + "normalize-package-data": "^6.0.0", + "parse-json": "^8.0.0", + "type-fest": "^4.6.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/secretlint": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/secretlint/-/secretlint-10.2.2.tgz", + "integrity": "sha512-xVpkeHV/aoWe4vP4TansF622nBEImzCY73y/0042DuJ29iKIaqgoJ8fGxre3rVSHHbxar4FdJobmTnLp9AU0eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/config-creator": "^10.2.2", + "@secretlint/formatter": "^10.2.2", + "@secretlint/node": "^10.2.2", + "@secretlint/profiler": "^10.2.2", + "debug": "^4.4.1", + "globby": "^14.1.0", + "read-pkg": "^9.0.1" + }, + "bin": { + "secretlint": "bin/secretlint.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true, + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/structured-source": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/structured-source/-/structured-source-4.0.0.tgz", + "integrity": "sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boundary": "^2.0.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", + "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" + } + }, + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/table/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/terminal-link": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-4.0.0.tgz", + "integrity": "sha512-lk+vH+MccxNqgVqSnkMVKx4VLJfnLjDBGzH16JVZjKE2DoxP57s6/vt6JmXV5I3jBcfGrxNrYtC+mPtU7WJztA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "supports-hyperlinks": "^3.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/test-exclude/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/test-exclude/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude/node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/test-exclude/node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/test-exclude/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/test-exclude/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/textextensions": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-6.11.0.tgz", + "integrity": "sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "editions": "^6.21.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-rest-client": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", + "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "qs": "^6.9.1", + "tunnel": "0.0.6", + "underscore": "^1.12.1" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.4.tgz", + "integrity": "sha512-Rw6+44QNFaXtgHSjPy+Kw8hrJniMYzR85E9yLmOLcfZ91/rz+JXQbDTCmc6ccxMPY6K6PgAq26f0JCBfR7LIPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.59.4", + "@typescript-eslint/parser": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4", + "@typescript-eslint/utils": "8.59.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", + "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "dev": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/version-range": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/version-range/-/version-range-4.15.0.tgz", + "integrity": "sha512-Ck0EJbAGxHwprkzFO966t4/5QkRuzh+/I1RxhLgUKKwEn+Cd8NwM60mE3AqBZg5gYODoXW0EFsQvbZjRlvdqbg==", + "dev": true, + "license": "Artistic-2.0", + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/workerpool": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yauzl": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.3.1.tgz", + "integrity": "sha512-RNPCUkiE/ZgO4w8i9U5yDQVHaFDdnzaFANElRvpJteCspvmv2VqrRb9lvS6odVD+jqI/zDsxAHJVsafpcheVQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yazl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", + "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..35fbf66 --- /dev/null +++ b/package.json @@ -0,0 +1,241 @@ +{ + "name": "diffy", + "displayName": "Diffy", + "description": "Pick two things and diff them — context-menu git diffing in VSCode.", + "version": "0.1.0", + "publisher": "nimblesite", + "license": "MIT", + "icon": "icon.png", + "repository": { + "type": "git", + "url": "https://github.com/MelbourneDeveloper/Diffy.git" + }, + "main": "./out/extension.js", + "engines": { + "vscode": "^1.85.0", + "node": ">=20" + }, + "categories": [ + "SCM Providers" + ], + "activationEvents": [ + "onStartupFinished" + ], + "extensionDependencies": [ + "vscode.git" + ], + "enabledApiProposals": [ + "contribSourceControlHistoryItemMenu" + ], + "contributes": { + "commands": [ + { + "command": "diffy.compareWith", + "title": "Diffy: Compare with…" + }, + { + "command": "diffy.compareWithWorkingCopy", + "title": "Diffy: Compare with Working Copy" + }, + { + "command": "diffy.compareWithPrevious", + "title": "Diffy: Compare with Previous" + }, + { + "command": "diffy.compareWithBranch", + "title": "Diffy: Compare with Branch…" + }, + { + "command": "diffy.compareWithTag", + "title": "Diffy: Compare with Tag…" + }, + { + "command": "diffy.compareTwoCommits", + "title": "Diffy: Compare Two Commits" + }, + { + "command": "diffy.compareFileWithCommit", + "title": "Diffy: Compare with Commit…" + }, + { + "command": "diffy.compareFileWithBranch", + "title": "Diffy: Compare with Branch…" + }, + { + "command": "diffy.compareFileWithTag", + "title": "Diffy: Compare with Tag…" + }, + { + "command": "diffy.reopenLast", + "title": "Diffy: Reopen Last Comparison" + }, + { + "command": "diffy.showLogs", + "title": "Diffy: Show Logs" + } + ], + "menus": { + "scm/historyItem/context": [ + { + "command": "diffy.compareWith", + "when": "scmProvider == git", + "group": "diffy@1" + }, + { + "command": "diffy.compareWithWorkingCopy", + "when": "scmProvider == git", + "group": "diffy@2" + }, + { + "command": "diffy.compareWithPrevious", + "when": "scmProvider == git", + "group": "diffy@3" + }, + { + "command": "diffy.compareWithBranch", + "when": "scmProvider == git", + "group": "diffy@4" + }, + { + "command": "diffy.compareWithTag", + "when": "scmProvider == git", + "group": "diffy@5" + } + ], + "scm/resourceState/context": [ + { + "command": "diffy.compareFileWithCommit", + "when": "scmProvider == git", + "group": "diffy@1" + }, + { + "command": "diffy.compareFileWithBranch", + "when": "scmProvider == git", + "group": "diffy@2" + }, + { + "command": "diffy.compareFileWithTag", + "when": "scmProvider == git", + "group": "diffy@3" + } + ], + "editor/title/context": [ + { + "command": "diffy.compareFileWithCommit", + "when": "resourceScheme == file", + "group": "diffy@1" + }, + { + "command": "diffy.compareFileWithBranch", + "when": "resourceScheme == file", + "group": "diffy@2" + }, + { + "command": "diffy.compareFileWithTag", + "when": "resourceScheme == file", + "group": "diffy@3" + } + ], + "explorer/context": [ + { + "command": "diffy.compareFileWithCommit", + "when": "resourceScheme == file && !explorerResourceIsFolder", + "group": "diffy@1" + }, + { + "command": "diffy.compareFileWithBranch", + "when": "resourceScheme == file && !explorerResourceIsFolder", + "group": "diffy@2" + }, + { + "command": "diffy.compareFileWithTag", + "when": "resourceScheme == file && !explorerResourceIsFolder", + "group": "diffy@3" + } + ], + "commandPalette": [ + { + "command": "diffy.compareWith", + "when": "false" + }, + { + "command": "diffy.compareWithWorkingCopy", + "when": "false" + }, + { + "command": "diffy.compareWithPrevious", + "when": "false" + }, + { + "command": "diffy.compareWithBranch", + "when": "false" + }, + { + "command": "diffy.compareWithTag", + "when": "false" + }, + { + "command": "diffy.showLogs", + "when": "false" + } + ] + } + }, + "scripts": { + "build": "tsc -p . && node ./scripts/sync-menus.mjs", + "watch": "tsc -p . -w", + "typecheck": "tsc -p . --noEmit", + "lint": "eslint src", + "fmt": "prettier --write \"src/**/*.ts\" \"*.json\" \"*.md\"", + "fmt:check": "prettier --check \"src/**/*.ts\" \"*.json\" \"*.md\"", + "sync:menus": "tsc -p . && node ./scripts/sync-menus.mjs", + "sync:menus:check": "tsc -p . && node ./scripts/sync-menus.mjs --check", + "pretest": "npm run build", + "test:unit": "mocha --reporter spec --timeout 10000 \"out/test/unit/**/*.test.js\"", + "test:e2e": "node ./out/test/runTests.js", + "test:coverage": "node ./scripts/run-coverage.mjs", + "shipwright:validate": "node ./scripts/validate-shipwright-manifest.mjs", + "shipwright:stamp": "node ./scripts/stamp-release-version.mjs", + "shipwright:verify": "node ./scripts/verify-versions.mjs" + }, + "dependencies": { + "pino": "^9.5.0" + }, + "devDependencies": { + "@eslint/js": "^9.13.0", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "@types/mocha": "^10.0.9", + "@types/node": "^20.16.0", + "@types/vscode": "^1.85.0", + "@vscode/test-electron": "^2.4.1", + "@vscode/vsce": "^3.2.0", + "c8": "^10.1.2", + "eslint": "^9.13.0", + "glob": "^11.0.0", + "mocha": "^10.8.2", + "prettier": "^3.3.3", + "typescript": "^5.6.3", + "typescript-eslint": "^8.12.2" + }, + "c8": { + "tempDirectory": "coverage/tmp", + "reportsDirectory": "coverage/report", + "include": [ + "out/**/*.js" + ], + "exclude": [ + "out/test/**", + "out/scripts/**", + "out/git/types.js", + "**/*.d.ts" + ], + "all": false, + "reporter": [ + "text", + "text-summary", + "json-summary", + "lcov" + ] + } +} diff --git a/schemas/shipwright.schema.json b/schemas/shipwright.schema.json new file mode 100644 index 0000000..c82c75b --- /dev/null +++ b/schemas/shipwright.schema.json @@ -0,0 +1,224 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://nimblesite.dev/schemas/shipwright/v1.json", + "title": "Nimblesite Shipwright product manifest", + "description": "Authoritative per-product manifest declaring components, bundling, version contracts, and host policies. Lives at repo root of every product as `shipwright.json`.", + "type": "object", + "required": ["manifestVersion", "product", "components"], + "additionalProperties": false, + "properties": { + "manifestVersion": { + "type": "integer", + "const": 1, + "description": "Schema version. Increment on breaking changes." + }, + "product": { + "type": "object", + "required": ["id", "version"], + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9-]{1,63}$", + "description": "Stable product identifier. kebab-case." + }, + "displayName": { "type": "string" }, + "version": { + "type": "string", + "description": "The expected product version this manifest targets. Stamped from tag at release time.", + "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$" + }, + "repository": { "type": "string", "format": "uri" }, + "homepage": { "type": "string", "format": "uri" } + } + }, + "components": { + "type": "array", + "minItems": 1, + "description": "Every deployable unit of the product: CLIs, LSPs, MCPs, sidecars, IDE extensions, assets.", + "items": { "$ref": "#/$defs/component" } + }, + "hosts": { + "type": "object", + "description": "Per-host policy bundle. A host that is absent is unsupported by the product.", + "additionalProperties": false, + "properties": { + "vscode": { "$ref": "#/$defs/hostPolicy" }, + "jetbrains": { "$ref": "#/$defs/hostPolicy" }, + "zed": { "$ref": "#/$defs/hostPolicy" }, + "cli": { "$ref": "#/$defs/hostPolicy" }, + "pkgmgr": { "$ref": "#/$defs/hostPolicy" } + } + } + }, + "$defs": { + "component": { + "type": "object", + "required": ["id", "kind"], + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9-]{1,63}$", + "description": "Component id, unique within the product." + }, + "kind": { + "type": "string", + "enum": ["cli", "lsp", "mcp", "sidecar", "dap", "tool", "extension-vscode", "extension-jetbrains", "extension-zed", "asset"] + }, + "language": { + "type": "string", + "enum": ["rust", "dotnet", "dart", "typescript", "kotlin", "javascript"] + }, + "binaryName": { + "type": "string", + "description": "argv[0] of the binary or npm bin / dotnet tool command." + }, + "expectedVersion": { + "type": "string", + "description": "semver string or ${PRODUCT_VERSION}. When set, host must verify this value against binary --version output." + }, + "platforms": { + "type": "array", + "items": { + "type": "string", + "enum": ["darwin-arm64", "darwin-x64", "linux-x64", "linux-arm64", "win32-x64", "win32-arm64", "all"] + }, + "description": "Platforms this component ships for. `all` means platform-agnostic (Node, dotnet tool, jar)." + }, + "bundled": { + "type": "object", + "description": "If set, this component is bundled inside an IDE extension artifact.", + "required": ["bundlePath"], + "additionalProperties": false, + "properties": { + "bundlePath": { + "type": "string", + "description": "Path template relative to extension root, e.g. `bin/${platform}/${binaryName}${exe}`." + }, + "perPlatformArtifact": { + "type": "boolean", + "default": true, + "description": "True = one artifact per platform (VSIX --target). False = single fat artifact." + } + } + }, + "sources": { + "type": "array", + "description": "Ordered discovery chain the host must follow. Earlier = higher priority.", + "items": { + "type": "string", + "enum": ["user-setting", "env", "path", "bundled", "pkgmgr", "dotnet-tool", "npm-global", "cargo-bin", "github-release", "lsp-initialize"] + }, + "uniqueItems": true + }, + "userSetting": { + "type": "string", + "description": "IDE settings key (e.g. `deslop.binaryPath`)." + }, + "env": { + "type": "object", + "additionalProperties": false, + "properties": { + "pathVar": { "type": "string", "description": "e.g. DESLOP_BINARY_PATH" }, + "dirVar": { "type": "string", "description": "e.g. DESLOP_BINARY_DIR" } + } + }, + "pkgmgr": { + "type": "object", + "additionalProperties": false, + "properties": { + "brew": { "type": "string" }, + "scoop": { "type": "string" }, + "apt": { "type": "string" }, + "winget": { "type": "string" } + } + }, + "dotnetTool": { + "type": "object", + "required": ["package"], + "additionalProperties": false, + "properties": { + "package": { "type": "string" }, + "command": { "type": "string" } + } + }, + "npm": { + "type": "object", + "additionalProperties": false, + "properties": { + "package": { "type": "string" }, + "bin": { "type": "string" } + } + }, + "githubRelease": { + "type": "object", + "additionalProperties": false, + "properties": { + "repo": { "type": "string", "pattern": "^[^/]+/[^/]+$" }, + "assetPattern": { "type": "string", "description": "e.g. `basilisk-${version}-${platform}.tar.gz`" }, + "checksum": { "type": "boolean", "default": true }, + "cosign": { "type": "boolean", "default": false } + } + }, + "verifyStartup": { + "type": "boolean", + "default": true, + "description": "Host must call --version (or initialize) before allowing the component to serve." + }, + "versionCheckStrategy": { + "type": "string", + "enum": ["version-flag", "version-flag-json", "lsp-initialize"], + "default": "version-flag" + }, + "required": { + "type": "boolean", + "default": true, + "description": "If true, failure to resolve + verify blocks activation." + }, + "asset": { + "type": "object", + "description": "Only for kind=asset. Non-executable payload.", + "additionalProperties": false, + "properties": { + "source": { "type": "string" }, + "target": { "type": "string" }, + "bundle": { "type": "boolean" }, + "contentHash": { "type": "boolean" }, + "downloadOnFirstUse": { "type": "boolean" } + } + } + }, + "allOf": [ + { + "if": { "properties": { "kind": { "const": "asset" } }, "required": ["kind"] }, + "then": { "required": ["asset"] } + }, + { + "if": { "properties": { "kind": { "enum": ["cli", "lsp", "mcp", "sidecar", "dap", "tool"] } }, "required": ["kind"] }, + "then": { "required": ["binaryName", "expectedVersion", "sources"] } + } + ] + }, + "hostPolicy": { + "type": "object", + "additionalProperties": false, + "properties": { + "artifact": { + "type": "string", + "enum": ["vsix-per-platform", "vsix-fat", "intellij-jar", "zed-wasm", "archive", "brew-formula", "scoop-manifest", "nuget", "pub"] + }, + "activationVerifies": { + "type": "array", + "description": "Component ids whose version the host must verify at activation.", + "items": { "type": "string" } + }, + "onMismatch": { + "type": "string", + "enum": ["error", "warn", "prompt-reinstall", "prompt-pkgmgr"], + "default": "error" + } + } + } + } +} diff --git a/scripts/check-coverage-threshold.mjs b/scripts/check-coverage-threshold.mjs new file mode 100644 index 0000000..3029ad8 --- /dev/null +++ b/scripts/check-coverage-threshold.mjs @@ -0,0 +1,26 @@ +#!/usr/bin/env node +// Reads the global line-coverage threshold from coverage-thresholds.json +// and delegates to `c8 check-coverage`. Written in Node so the recipe +// works under bash, cmd.exe, and PowerShell on Windows runners. + +import { readFileSync } from 'node:fs'; +import { spawnSync } from 'node:child_process'; +import { resolve } from 'node:path'; + +const repoRoot = resolve(import.meta.dirname, '..'); +const thresholdsPath = resolve(repoRoot, 'coverage-thresholds.json'); +const thresholds = JSON.parse(readFileSync(thresholdsPath, 'utf8')); +const threshold = thresholds.default_threshold; + +if (typeof threshold !== 'number') { + process.stderr.write(`coverage-thresholds.json: default_threshold must be a number, got ${typeof threshold}\n`); + process.exit(1); +} + +const r = spawnSync('npx', ['c8', 'check-coverage', '--lines', String(threshold)], { + cwd: repoRoot, + stdio: 'inherit', + shell: process.platform === 'win32', +}); + +process.exit(r.status ?? 1); diff --git a/scripts/run-coverage.mjs b/scripts/run-coverage.mjs new file mode 100644 index 0000000..2747ab1 --- /dev/null +++ b/scripts/run-coverage.mjs @@ -0,0 +1,75 @@ +#!/usr/bin/env node +// Orchestrates unit + E2E test runs, accumulating raw V8 coverage profiles +// into a single temp dir, then asks c8 to render the merged report. +// +// Why a separate script: the unit run is a node-mocha process, and the E2E +// run spawns Electron (extension host). Both write coverage to NODE_V8_COVERAGE +// when it's set in their env. We point both at the same dir, then `c8 report` +// merges the lot. + +import { spawnSync } from 'node:child_process'; +import { rmSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +const repoRoot = resolve(import.meta.dirname, '..'); +const tempDir = resolve(repoRoot, 'coverage', 'tmp'); + +// On Windows the mocha (Node) and electron-host (Electron) processes write V8 +// coverage records with slightly different URL normalizations — backslashes vs +// forward slashes, mixed drive-letter case. c8 keys files by URL, so the same +// physical file ends up reported twice with halved coverage. Rewrite every +// `url` field to a canonical lowercase forward-slash form before c8 merges. +const canonicalUrl = (url) => { + if (typeof url !== 'string' || !url.startsWith('file://')) { + return url; + } + return url.replace(/\\/g, '/').replace(/^file:\/\/\/?[A-Za-z]:/, (m) => m.toLowerCase()); +}; + +const normalizeCoverageDir = (dir) => { + for (const name of readdirSync(dir)) { + if (!name.endsWith('.json')) continue; + const path = resolve(dir, name); + const raw = readFileSync(path, 'utf8'); + const data = JSON.parse(raw); + if (!Array.isArray(data.result)) continue; + let changed = false; + for (const entry of data.result) { + const next = canonicalUrl(entry.url); + if (next !== entry.url) { + entry.url = next; + changed = true; + } + } + if (changed) { + writeFileSync(path, JSON.stringify(data)); + } + } +}; + +const run = (cmd, args, env) => { + process.stdout.write(`\n==> ${cmd} ${args.join(' ')}\n`); + const r = spawnSync(cmd, args, { + cwd: repoRoot, + stdio: 'inherit', + env: { ...process.env, ...env }, + shell: process.platform === 'win32', + }); + if (r.status !== 0) { + process.stderr.write(`\n!! ${cmd} ${args.join(' ')} exited ${r.status ?? '?'}\n`); + process.exit(r.status ?? 1); + } +}; + +rmSync(tempDir, { recursive: true, force: true }); +mkdirSync(tempDir, { recursive: true }); + +run('npm', ['run', 'build'], {}); + +const covEnv = { NODE_V8_COVERAGE: tempDir }; +run('npm', ['run', 'test:unit'], covEnv); +run('npm', ['run', 'test:e2e'], covEnv); + +normalizeCoverageDir(tempDir); + +run('npx', ['c8', 'report'], {}); diff --git a/scripts/stamp-release-version.mjs b/scripts/stamp-release-version.mjs new file mode 100644 index 0000000..3a1b48a --- /dev/null +++ b/scripts/stamp-release-version.mjs @@ -0,0 +1,164 @@ +#!/usr/bin/env node +// Shipwright SWR-VERSION-BUILD-STAMPING for Diffy. +// +// Stamps the release version into every deployed carrier in the runner working +// tree, then writes build-info.json. Source-controlled values are not committed +// by this script — the release workflow runs it on the tagged checkout and +// builds from the stamped tree. +// +// Carriers stamped: +// - package.json .version +// - package-lock.json .version + .packages[""].version +// - shipwright.json .product.version +// .components[].expectedVersion (literal values only) +// - build-info.json (created) { manifestVersion, version, buildTime } +// +// Usage: +// node scripts/stamp-release-version.mjs <version> # apply +// node scripts/stamp-release-version.mjs <version> --dry # report only +// +// Accepts a bare semver (1.2.3) or a v-prefixed tag (v1.2.3). Rejects anything +// else so a bad CI input fails the workflow before publish. + +import { readFileSync, writeFileSync, existsSync } from 'node:fs'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); + +const SEMVER = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/; +const PRODUCT_VERSION_TEMPLATE = '${PRODUCT_VERSION}'; + +const parseArgs = (argv) => { + const positional = []; + let dryRun = false; + for (const arg of argv) { + if (arg === '--dry' || arg === '--dry-run') { + dryRun = true; + } else if (arg.startsWith('-')) { + throw new Error(`unknown argument: ${arg}`); + } else { + positional.push(arg); + } + } + if (positional.length !== 1) { + throw new Error('expected exactly one version argument'); + } + return { rawTag: positional[0], dryRun }; +}; + +const normalizeTag = (tag) => { + const candidate = tag.startsWith('v') ? tag.slice(1) : tag; + if (!SEMVER.test(candidate)) { + throw new Error(`invalid tag '${tag}'; expected semver like v1.2.3 or 1.2.3`); + } + return candidate; +}; + +const readJson = (path) => JSON.parse(readFileSync(path, 'utf8')); + +const writeJson = (path, value, dryRun) => { + const body = `${JSON.stringify(value, null, 2)}\n`; + if (!dryRun) writeFileSync(path, body); +}; + +const stampPackageJson = ({ path, version, dryRun, report }) => { + if (!existsSync(path)) return; + const pkg = readJson(path); + const before = pkg.version; + pkg.version = version; + writeJson(path, pkg, dryRun); + report.push({ path, field: 'version', before, after: version }); +}; + +const stampPackageLock = ({ path, version, dryRun, report }) => { + if (!existsSync(path)) return; + const lock = readJson(path); + const before = lock.version; + lock.version = version; + const rootPkg = lock.packages?.['']; + if (rootPkg) rootPkg.version = version; + writeJson(path, lock, dryRun); + report.push({ path, field: 'version', before, after: version }); +}; + +const stampShipwright = ({ path, version, dryRun, report }) => { + if (!existsSync(path)) return; + const manifest = readJson(path); + const before = manifest.product?.version; + if (!manifest.product) { + throw new Error(`${path} missing product object`); + } + manifest.product.version = version; + for (const component of manifest.components ?? []) { + if ( + typeof component.expectedVersion === 'string' && + component.expectedVersion !== PRODUCT_VERSION_TEMPLATE + ) { + const previous = component.expectedVersion; + component.expectedVersion = version; + report.push({ + path, + field: `components.${component.id}.expectedVersion`, + before: previous, + after: version, + }); + } + } + writeJson(path, manifest, dryRun); + report.push({ path, field: 'product.version', before, after: version }); +}; + +const writeBuildInfo = ({ path, version, dryRun, report }) => { + const info = { + manifestVersion: 1, + version, + buildTime: new Date().toISOString().replace(/\.\d{3}Z$/, 'Z'), + }; + writeJson(path, info, dryRun); + report.push({ path, field: 'created', before: '-', after: version }); +}; + +const main = () => { + const { rawTag, dryRun } = parseArgs(process.argv.slice(2)); + const version = normalizeTag(rawTag); + const report = []; + + stampPackageJson({ + path: resolve(repoRoot, 'package.json'), + version, + dryRun, + report, + }); + stampPackageLock({ + path: resolve(repoRoot, 'package-lock.json'), + version, + dryRun, + report, + }); + stampShipwright({ + path: resolve(repoRoot, 'shipwright.json'), + version, + dryRun, + report, + }); + writeBuildInfo({ + path: resolve(repoRoot, 'build-info.json'), + version, + dryRun, + report, + }); + + const mode = dryRun ? 'dry-run' : 'applied'; + process.stdout.write(`stamp-release-version (${mode}) → ${version}\n`); + for (const row of report) { + process.stdout.write(` ${row.path} :: ${row.field} : ${row.before} → ${row.after}\n`); + } +}; + +try { + main(); +} catch (error) { + process.stderr.write(`stamp-release-version: ${error.message}\n`); + process.exit(1); +} diff --git a/scripts/sync-menus.mjs b/scripts/sync-menus.mjs new file mode 100644 index 0000000..b94a30b --- /dev/null +++ b/scripts/sync-menus.mjs @@ -0,0 +1,75 @@ +#!/usr/bin/env node +import { readFile, writeFile } from 'node:fs/promises'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const here = dirname(fileURLToPath(import.meta.url)); +const repoRoot = resolve(here, '..'); +const packageJsonPath = resolve(repoRoot, 'package.json'); +const menusModulePath = resolve(repoRoot, 'out/menus.js'); + +const mode = process.argv[2] === '--check' ? 'check' : 'write'; + +const main = async () => { + const mod = await import(pathToFileUrl(menusModulePath)); + const manifest = mod.buildMenuManifest(); + const titles = mod.COMMAND_TITLES; + const pkg = JSON.parse(await readFile(packageJsonPath, 'utf8')); + + const desiredCommands = Object.entries(titles).map(([command, title]) => ({ + command, + title, + })); + + const desiredMenus = {}; + for (const [menuId, entries] of Object.entries(manifest.menus)) { + desiredMenus[menuId] = entries.map(({ command, when, group }) => ({ + command, + when, + group, + })); + } + desiredMenus.commandPalette = manifest.commandPalette.map(({ command, when }) => ({ + command, + when, + })); + + const desiredContributes = { + ...pkg.contributes, + commands: desiredCommands, + menus: desiredMenus, + }; + + const before = JSON.stringify({ + commands: pkg.contributes?.commands, + menus: pkg.contributes?.menus, + }); + const after = JSON.stringify({ + commands: desiredContributes.commands, + menus: desiredContributes.menus, + }); + + if (before === after) { + process.stdout.write('sync-menus: package.json already in sync\n'); + return; + } + + if (mode === 'check') { + process.stderr.write( + 'sync-menus: package.json is out of sync with src/menus.ts.\n' + + 'Run `npm run sync:menus` to regenerate.\n', + ); + process.exit(1); + } + + const next = { ...pkg, contributes: desiredContributes }; + await writeFile(packageJsonPath, JSON.stringify(next, null, 2) + '\n'); + process.stdout.write('sync-menus: wrote package.json\n'); +}; + +const pathToFileUrl = (p) => `file://${p}`; + +main().catch((e) => { + process.stderr.write(`sync-menus failed: ${e?.stack ?? String(e)}\n`); + process.exit(2); +}); diff --git a/scripts/validate-shipwright-manifest.mjs b/scripts/validate-shipwright-manifest.mjs new file mode 100644 index 0000000..1af0e5a --- /dev/null +++ b/scripts/validate-shipwright-manifest.mjs @@ -0,0 +1,44 @@ +#!/usr/bin/env node +// Shipwright SWR-IDE-* / SWR-VERSION-* manifest gate. +// +// Validates shipwright.json against the canonical schema bundled at +// schemas/shipwright.schema.json. Fails closed: any AJV error blocks CI. +// +// Usage: +// node scripts/validate-shipwright-manifest.mjs # validates ./shipwright.json +// node scripts/validate-shipwright-manifest.mjs path.json # validates one manifest + +import { readFileSync } from 'node:fs'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import Ajv2020 from 'ajv/dist/2020.js'; +import addFormats from 'ajv-formats'; + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const schemaPath = resolve(repoRoot, 'schemas', 'shipwright.schema.json'); + +const readJson = (path) => JSON.parse(readFileSync(path, 'utf8')); + +const main = () => { + const target = process.argv[2] ?? resolve(repoRoot, 'shipwright.json'); + const ajv = new Ajv2020({ allErrors: true, strict: false }); + addFormats(ajv); + const validate = ajv.compile(readJson(schemaPath)); + const manifest = readJson(target); + if (validate(manifest)) { + process.stdout.write(`${target}: valid\n`); + return; + } + process.stderr.write(`${target}: invalid\n`); + for (const error of validate.errors ?? []) { + process.stderr.write(` ${error.instancePath || '/'} ${error.message ?? ''}\n`); + } + process.exit(1); +}; + +try { + main(); +} catch (error) { + process.stderr.write(`validate-shipwright-manifest: ${error.message}\n`); + process.exit(2); +} diff --git a/scripts/verify-versions.mjs b/scripts/verify-versions.mjs new file mode 100644 index 0000000..815f4dc --- /dev/null +++ b/scripts/verify-versions.mjs @@ -0,0 +1,134 @@ +#!/usr/bin/env node +// Shipwright SWR-REL-VERSION-VERIFY for Diffy. +// +// After scripts/stamp-release-version.mjs runs, assert that every deployed +// version carrier in the runner working tree agrees with the expected version. +// Exits non-zero on the first mismatch so publish jobs cannot run with a +// half-stamped tree. +// +// Usage: +// node scripts/verify-versions.mjs <version> # bare semver or v-prefixed +// +// Checks: +// - package.json .version +// - package-lock.json .version and .packages[""].version +// - shipwright.json .product.version +// every literal .components[].expectedVersion +// - build-info.json .version +// +// ${PRODUCT_VERSION} placeholders are accepted in expectedVersion fields. + +import { readFileSync, existsSync } from 'node:fs'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const SEMVER = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/; +const PRODUCT_VERSION_TEMPLATE = '${PRODUCT_VERSION}'; + +const normalizeTag = (tag) => { + const candidate = tag.startsWith('v') ? tag.slice(1) : tag; + if (!SEMVER.test(candidate)) { + throw new Error(`invalid tag '${tag}'; expected semver like v1.2.3 or 1.2.3`); + } + return candidate; +}; + +const readJson = (path) => JSON.parse(readFileSync(path, 'utf8')); + +const checks = []; + +const expect = ({ label, actual, expected }) => { + checks.push({ label, actual, expected, ok: actual === expected }); +}; + +const verifyPackageJson = (version) => { + const path = resolve(repoRoot, 'package.json'); + if (!existsSync(path)) return; + const pkg = readJson(path); + expect({ label: 'package.json :: version', actual: pkg.version, expected: version }); +}; + +const verifyPackageLock = (version) => { + const path = resolve(repoRoot, 'package-lock.json'); + if (!existsSync(path)) return; + const lock = readJson(path); + expect({ label: 'package-lock.json :: version', actual: lock.version, expected: version }); + const rootPkg = lock.packages?.['']; + if (rootPkg) { + expect({ + label: 'package-lock.json :: packages[""].version', + actual: rootPkg.version, + expected: version, + }); + } +}; + +const verifyShipwright = (version) => { + const path = resolve(repoRoot, 'shipwright.json'); + if (!existsSync(path)) return; + const manifest = readJson(path); + expect({ + label: 'shipwright.json :: product.version', + actual: manifest.product?.version, + expected: version, + }); + for (const component of manifest.components ?? []) { + if (typeof component.expectedVersion !== 'string') continue; + if (component.expectedVersion === PRODUCT_VERSION_TEMPLATE) continue; + expect({ + label: `shipwright.json :: components.${component.id}.expectedVersion`, + actual: component.expectedVersion, + expected: version, + }); + } +}; + +const verifyBuildInfo = (version) => { + const path = resolve(repoRoot, 'build-info.json'); + if (!existsSync(path)) { + checks.push({ + label: 'build-info.json', + actual: 'missing', + expected: 'present', + ok: false, + }); + return; + } + const info = readJson(path); + expect({ + label: 'build-info.json :: version', + actual: info.version, + expected: version, + }); +}; + +const main = () => { + const [tag] = process.argv.slice(2); + if (!tag) throw new Error('expected version argument'); + const version = normalizeTag(tag); + + verifyPackageJson(version); + verifyPackageLock(version); + verifyShipwright(version); + verifyBuildInfo(version); + + let failed = 0; + for (const c of checks) { + const tag = c.ok ? 'OK ' : 'FAIL'; + process.stdout.write(`${tag} ${c.label} = ${c.actual}\n`); + if (!c.ok) failed += 1; + } + if (failed > 0) { + process.stderr.write(`\nverify-versions: ${failed} mismatch(es) against ${version}\n`); + process.exit(1); + } + process.stdout.write(`\nAll ${checks.length} carriers match ${version}.\n`); +}; + +try { + main(); +} catch (error) { + process.stderr.write(`verify-versions: ${error.message}\n`); + process.exit(2); +} diff --git a/shipwright.json b/shipwright.json new file mode 100644 index 0000000..f570463 --- /dev/null +++ b/shipwright.json @@ -0,0 +1,25 @@ +{ + "manifestVersion": 1, + "product": { + "id": "diffy", + "displayName": "Diffy", + "version": "0.1.0", + "repository": "https://github.com/MelbourneDeveloper/Diffy", + "homepage": "https://github.com/MelbourneDeveloper/Diffy" + }, + "components": [ + { + "id": "diffy-vscode", + "kind": "extension-vscode", + "language": "typescript", + "expectedVersion": "${PRODUCT_VERSION}", + "platforms": ["darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64", "win32-arm64", "win32-x64"] + } + ], + "hosts": { + "vscode": { + "artifact": "vsix-per-platform", + "onMismatch": "error" + } + } +} diff --git a/src/commands/compareFileWithCommit.ts b/src/commands/compareFileWithCommit.ts new file mode 100644 index 0000000..736bb54 --- /dev/null +++ b/src/commands/compareFileWithCommit.ts @@ -0,0 +1,5 @@ +import type { CommandDeps } from "./shared"; +import { FILE_REV_SOURCES, makeCompareFileWithRev } from "./compareFileWithRev"; + +export const makeCompareFileWithCommit = (deps: CommandDeps) => + makeCompareFileWithRev({ deps, source: FILE_REV_SOURCES.commits }); diff --git a/src/commands/compareFileWithRev.ts b/src/commands/compareFileWithRev.ts new file mode 100644 index 0000000..78e759b --- /dev/null +++ b/src/commands/compareFileWithRev.ts @@ -0,0 +1,89 @@ +import * as vscode from "vscode"; +import { REV_KINDS, TITLE_PREFIX } from "../constants"; +import type { GitRepo } from "../git/GitRepo"; +import type { Sha } from "../git/types"; +import type { Result } from "../result"; +import { err, ok } from "../result"; +import { CANCELLED, type Cancelled } from "../ui/cancelled"; +import { pickCommit } from "../ui/CommitPicker"; +import { findRepoForUri } from "../vscodeGitApi"; +import { type CommandDeps, buildRepo, openDiff } from "./shared"; +import { pickRefAsSha, reportGitError, sideAFromSha } from "./flow"; + +export const FILE_REV_SOURCES = { + commits: "commits", + branch: "branch", + tag: "tag", + other: "other", +} as const; + +export type FileRevSource = (typeof FILE_REV_SOURCES)[keyof typeof FILE_REV_SOURCES]; + +const NO_EDITOR = `${TITLE_PREFIX} open a file first.`; +const NOT_IN_REPO = `${TITLE_PREFIX} file is not in a git repository.`; +const LOG_OP = "log"; + +const pickShaForFile = async ({ + repo, + source, + output, +}: { + repo: GitRepo; + source: FileRevSource; + output: vscode.OutputChannel; +}): Promise<Result<Sha, Cancelled>> => { + if (source === FILE_REV_SOURCES.commits) { + const log = await repo.log({}); + if (!log.ok) { + reportGitError({ output, op: LOG_OP, e: log.error }); + return err(CANCELLED); + } + const picked = await pickCommit({ commits: log.value }); + if (!picked.ok) { + return err(CANCELLED); + } + return ok(picked.value.sha); + } + return await pickRefAsSha({ repo, output, filter: source }); +}; + +const resolveTargetUri = (uri?: vscode.Uri): vscode.Uri | undefined => + uri ?? vscode.window.activeTextEditor?.document.uri; + +const handler = async ({ + deps, + uri, + source, +}: { + deps: CommandDeps; + uri: vscode.Uri | undefined; + source: FileRevSource; +}): Promise<void> => { + const target = resolveTargetUri(uri); + if (target === undefined) { + void vscode.window.showWarningMessage(NO_EDITOR); + return; + } + const vsRepo = findRepoForUri(deps.gitApi, target); + if (vsRepo === undefined) { + void vscode.window.showWarningMessage(NOT_IN_REPO); + return; + } + const repo = buildRepo(deps.runner, vsRepo); + const sha = await pickShaForFile({ repo, source, output: deps.output }); + if (!sha.ok) { + return; + } + await openDiff({ + revA: sideAFromSha(sha.value), + revB: { kind: REV_KINDS.workingCopy }, + repoRoot: vsRepo.rootUri.fsPath, + relPath: vscode.workspace.asRelativePath(target, false), + }); +}; + +export const makeCompareFileWithRev = + ({ deps, source }: { deps: CommandDeps; source: FileRevSource }) => + async (uri?: vscode.Uri): Promise<void> => { + await handler({ deps, uri, source }); + }; diff --git a/src/commands/compareTwoCommits.ts b/src/commands/compareTwoCommits.ts new file mode 100644 index 0000000..25bf43f --- /dev/null +++ b/src/commands/compareTwoCommits.ts @@ -0,0 +1,30 @@ +import type { MementoStore } from "../state"; +import type { CommandDeps } from "./shared"; +import { drillIntoFiles, pickRepoAndCommit, pickSideBAndResolve } from "./flow"; + +export const makeCompareTwoCommits = + (deps: CommandDeps & { readonly state: MementoStore }) => async (): Promise<void> => { + const start = await pickRepoAndCommit({ + runner: deps.runner, + gitApi: deps.gitApi, + output: deps.output, + }); + if (start === undefined) { + return; + } + const revB = await pickSideBAndResolve({ + repo: start.repo, + output: deps.output, + }); + if (!revB.ok) { + return; + } + await drillIntoFiles({ + repo: start.repo, + repoRoot: start.vsRepoRoot, + revA: start.revA, + revB: revB.value, + state: deps.state, + output: deps.output, + }); + }; diff --git a/src/commands/compareWith.ts b/src/commands/compareWith.ts new file mode 100644 index 0000000..bc52649 --- /dev/null +++ b/src/commands/compareWith.ts @@ -0,0 +1,39 @@ +import * as vscode from "vscode"; +import { TITLE_PREFIX } from "../constants"; +import type { MementoStore } from "../state"; +import { extractHistoryItemSha } from "./historyItem"; +import { type CommandDeps, buildRepo, pickRepoFrom } from "./shared"; +import { drillIntoFiles, pickSideBAndResolve, sideAFromSha } from "./flow"; + +const NOT_FROM_HISTORY = `${TITLE_PREFIX} this command must be invoked from the SCM history view.`; + +const handler = async (deps: CommandDeps & { readonly state: MementoStore }, arg: unknown): Promise<void> => { + const sha = extractHistoryItemSha(arg); + if (sha === undefined) { + void vscode.window.showWarningMessage(NOT_FROM_HISTORY); + return; + } + const vs = await pickRepoFrom(deps.gitApi); + if (!vs.ok) { + return; + } + const repo = buildRepo(deps.runner, vs.value); + const revB = await pickSideBAndResolve({ repo, output: deps.output }); + if (!revB.ok) { + return; + } + await drillIntoFiles({ + repo, + repoRoot: vs.value.rootUri.fsPath, + revA: sideAFromSha(sha), + revB: revB.value, + state: deps.state, + output: deps.output, + }); +}; + +export const makeCompareWith = + (deps: CommandDeps & { readonly state: MementoStore }) => + async (arg: unknown): Promise<void> => { + await handler(deps, arg); + }; diff --git a/src/commands/compareWithPrevious.ts b/src/commands/compareWithPrevious.ts new file mode 100644 index 0000000..4b5eb65 --- /dev/null +++ b/src/commands/compareWithPrevious.ts @@ -0,0 +1,45 @@ +import * as vscode from "vscode"; +import { REV_KINDS, TITLE_PREFIX } from "../constants"; +import type { MementoStore } from "../state"; +import { extractHistoryItemSha } from "./historyItem"; +import { type CommandDeps, buildRepo, pickRepoFrom } from "./shared"; +import { drillIntoFiles, reportGitError, sideAFromSha } from "./flow"; + +const REV_PARSE_PARENT_OP = "rev-parse parent"; +const HISTORY_VIEW_WARNING = `${TITLE_PREFIX} this command must be invoked from the SCM history view.`; + +const handler = async (deps: CommandDeps & { readonly state: MementoStore }, arg: unknown): Promise<void> => { + const sha = extractHistoryItemSha(arg); + if (sha === undefined) { + void vscode.window.showWarningMessage(HISTORY_VIEW_WARNING); + return; + } + const vs = await pickRepoFrom(deps.gitApi); + if (!vs.ok) { + return; + } + const repo = buildRepo(deps.runner, vs.value); + const parent = await repo.revParse(`${sha}^1`); + if (!parent.ok) { + reportGitError({ + output: deps.output, + op: REV_PARSE_PARENT_OP, + e: parent.error, + }); + return; + } + await drillIntoFiles({ + repo, + repoRoot: vs.value.rootUri.fsPath, + revA: sideAFromSha(sha), + revB: { kind: REV_KINDS.commit, sha: parent.value }, + state: deps.state, + output: deps.output, + }); +}; + +export const makeCompareWithPrevious = + (deps: CommandDeps & { readonly state: MementoStore }) => + async (arg: unknown): Promise<void> => { + await handler(deps, arg); + }; diff --git a/src/commands/compareWithRef.ts b/src/commands/compareWithRef.ts new file mode 100644 index 0000000..4ed15c2 --- /dev/null +++ b/src/commands/compareWithRef.ts @@ -0,0 +1,48 @@ +import * as vscode from "vscode"; +import { REV_KINDS, TITLE_PREFIX } from "../constants"; +import type { RefType } from "../git/types"; +import type { MementoStore } from "../state"; +import { extractHistoryItemSha } from "./historyItem"; +import { type CommandDeps, buildRepo, pickRepoFrom } from "./shared"; +import { drillIntoFiles, pickRefAsSha, sideAFromSha } from "./flow"; + +const NOT_FROM_HISTORY = `${TITLE_PREFIX} this command must be invoked from the SCM history view.`; + +const handler = async ({ + deps, + arg, + filter, +}: { + deps: CommandDeps & { readonly state: MementoStore }; + arg: unknown; + filter: RefType; +}): Promise<void> => { + const sha = extractHistoryItemSha(arg); + if (sha === undefined) { + void vscode.window.showWarningMessage(NOT_FROM_HISTORY); + return; + } + const vs = await pickRepoFrom(deps.gitApi); + if (!vs.ok) { + return; + } + const repo = buildRepo(deps.runner, vs.value); + const target = await pickRefAsSha({ repo, output: deps.output, filter }); + if (!target.ok) { + return; + } + await drillIntoFiles({ + repo, + repoRoot: vs.value.rootUri.fsPath, + revA: sideAFromSha(sha), + revB: { kind: REV_KINDS.commit, sha: target.value }, + state: deps.state, + output: deps.output, + }); +}; + +export const makeCompareWithRef = + ({ deps, filter }: { deps: CommandDeps & { readonly state: MementoStore }; filter: RefType }) => + async (arg: unknown): Promise<void> => { + await handler({ deps, arg, filter }); + }; diff --git a/src/commands/compareWithWorkingCopy.ts b/src/commands/compareWithWorkingCopy.ts new file mode 100644 index 0000000..b55c9b9 --- /dev/null +++ b/src/commands/compareWithWorkingCopy.ts @@ -0,0 +1,35 @@ +import * as vscode from "vscode"; +import { REV_KINDS, TITLE_PREFIX } from "../constants"; +import type { MementoStore } from "../state"; +import { extractHistoryItemSha } from "./historyItem"; +import { type CommandDeps, buildRepo, pickRepoFrom } from "./shared"; +import { drillIntoFiles, sideAFromSha } from "./flow"; + +const NOT_FROM_HISTORY = `${TITLE_PREFIX} this command must be invoked from the SCM history view.`; + +const handler = async (deps: CommandDeps & { readonly state: MementoStore }, arg: unknown): Promise<void> => { + const sha = extractHistoryItemSha(arg); + if (sha === undefined) { + void vscode.window.showWarningMessage(NOT_FROM_HISTORY); + return; + } + const vs = await pickRepoFrom(deps.gitApi); + if (!vs.ok) { + return; + } + const repo = buildRepo(deps.runner, vs.value); + await drillIntoFiles({ + repo, + repoRoot: vs.value.rootUri.fsPath, + revA: sideAFromSha(sha), + revB: { kind: REV_KINDS.workingCopy }, + state: deps.state, + output: deps.output, + }); +}; + +export const makeCompareWithWorkingCopy = + (deps: CommandDeps & { readonly state: MementoStore }) => + async (arg: unknown): Promise<void> => { + await handler(deps, arg); + }; diff --git a/src/commands/flow.ts b/src/commands/flow.ts new file mode 100644 index 0000000..bd25a61 --- /dev/null +++ b/src/commands/flow.ts @@ -0,0 +1,257 @@ +import * as vscode from "vscode"; +import { LOG_EVENTS, REF_TYPES, REV_KINDS, SIDE_B_KINDS, TITLE_PREFIX, UI_TEXT } from "../constants"; +import type { GitRepo } from "../git/GitRepo"; +import type { GitRunner } from "../git/GitRunner"; +import type { GitApi } from "../vscodeGitApi"; +import type { GitError, CommitRev, RefType, RevSpec, Sha } from "../git/types"; +import { logger } from "../logger"; +import { type Result, err, ok } from "../result"; +import { CANCELLED, type Cancelled } from "../ui/cancelled"; +import { pickCommit } from "../ui/CommitPicker"; +import { pickRef } from "../ui/RefPicker"; +import { pickSideBChoice, type SideBChoice } from "../ui/SideBPicker"; +import { mergeChangedFilesWithStats, pickFiles } from "../ui/FilePicker"; +import type { MementoStore } from "../state"; +import { buildRepo, openDiff, pickRepoFrom } from "./shared"; + +const GIT_OPS = { + listRefs: "list refs", + revParse: "rev-parse", + log: "log", + diffNameStatus: "diff --name-status", + diffNumstat: "diff --numstat", + currentBranch: "current branch", +} as const; + +export const reportGitError = ({ output, op, e }: { output: vscode.OutputChannel; op: string; e: GitError }): void => { + logger.error({ op, kind: e.kind }, LOG_EVENTS.gitError); + output.appendLine(`${TITLE_PREFIX} ${op} failed ${UI_TEXT.pathDash} ${e.message}`); + if (e.stderr !== undefined && e.stderr !== "") { + output.appendLine(e.stderr); + } + void vscode.window.showErrorMessage(`${TITLE_PREFIX} ${op} failed (see Output → Diffy).`); +}; + +export const resolveSideB = async ({ + choice, + repo, + output, +}: { + choice: SideBChoice; + repo: GitRepo; + output: vscode.OutputChannel; +}): Promise<Result<RevSpec, Cancelled>> => { + if (choice.kind === SIDE_B_KINDS.workingCopy) { + return ok({ kind: REV_KINDS.workingCopy }); + } + if (choice.kind === SIDE_B_KINDS.index) { + return ok({ kind: REV_KINDS.index }); + } + if (choice.kind === SIDE_B_KINDS.pickRef) { + return await resolveRefAsRev({ repo, output }); + } + return await resolveCommitAsRev({ repo, output }); +}; + +const placeholderForRefFilter = (filter?: RefType): string => { + if (filter === REF_TYPES.branch) { + return UI_TEXT.pickBranchPlaceholder; + } + if (filter === REF_TYPES.tag) { + return UI_TEXT.pickTagPlaceholder; + } + return UI_TEXT.pickRefPlaceholder; +}; + +const resolveExcludeBranchName = async ({ + repo, + output, +}: { + repo: GitRepo; + output: vscode.OutputChannel; +}): Promise<string | undefined> => { + const r = await repo.currentBranch(); + if (!r.ok) { + // Best-effort: log and continue with no exclusion. A failed lookup must not + // block ref picking — the worst case is the user sees their own branch. + reportGitError({ output, op: GIT_OPS.currentBranch, e: r.error }); + return undefined; + } + return r.value; +}; + +export const pickRefAsSha = async ({ + repo, + output, + filter, +}: { + repo: GitRepo; + output: vscode.OutputChannel; + filter?: RefType; +}): Promise<Result<Sha, Cancelled>> => { + const refs = await repo.refs(); + if (!refs.ok) { + reportGitError({ output, op: GIT_OPS.listRefs, e: refs.error }); + return err(CANCELLED); + } + const excludeBranchName = await resolveExcludeBranchName({ repo, output }); + const picked = await pickRef({ + refs: refs.value, + placeholder: placeholderForRefFilter(filter), + filter, + excludeBranchName, + }); + if (!picked.ok) { + return err(CANCELLED); + } + const sha = await repo.revParse(picked.value.name); + if (!sha.ok) { + reportGitError({ output, op: GIT_OPS.revParse, e: sha.error }); + return err(CANCELLED); + } + return ok(sha.value); +}; + +const resolveRefAsRev = async ({ + repo, + output, +}: { + repo: GitRepo; + output: vscode.OutputChannel; +}): Promise<Result<RevSpec, Cancelled>> => { + const sha = await pickRefAsSha({ repo, output }); + if (!sha.ok) { + return err(CANCELLED); + } + return ok({ kind: REV_KINDS.commit, sha: sha.value }); +}; + +const resolveCommitAsRev = async ({ + repo, + output, +}: { + repo: GitRepo; + output: vscode.OutputChannel; +}): Promise<Result<RevSpec, Cancelled>> => { + const log = await repo.log({}); + if (!log.ok) { + reportGitError({ output, op: GIT_OPS.log, e: log.error }); + return err(CANCELLED); + } + const picked = await pickCommit({ commits: log.value }); + if (!picked.ok) { + return err(CANCELLED); + } + return ok({ kind: REV_KINDS.commit, sha: picked.value.sha }); +}; + +export const pickSideBAndResolve = async ({ + repo, + output, +}: { + repo: GitRepo; + output: vscode.OutputChannel; +}): Promise<Result<RevSpec, Cancelled>> => { + const choice = await pickSideBChoice(); + if (!choice.ok) { + return err(CANCELLED); + } + return await resolveSideB({ choice: choice.value, repo, output }); +}; + +export const drillIntoFiles = async ({ + repo, + repoRoot, + revA, + revB, + state, + output, +}: { + repo: GitRepo; + repoRoot: string; + revA: CommitRev; + revB: RevSpec; + state: MementoStore; + output: vscode.OutputChannel; +}): Promise<void> => { + const entries = await collectChangedFiles({ repo, revA, revB, output }); + if (entries === undefined) { + return; + } + if (entries.length === 0) { + void vscode.window.showInformationMessage(UI_TEXT.noChanges); + return; + } + await state.setLastComparison({ revA, revB, repoRoot }); + await pickFiles({ + entries, + onPick: async (entry) => { + await openDiff({ revA, revB, repoRoot, relPath: entry.file.path }); + }, + }); +}; + +const collectChangedFiles = async ({ + repo, + revA, + revB, + output, +}: { + repo: GitRepo; + revA: CommitRev; + revB: RevSpec; + output: vscode.OutputChannel; +}) => { + const ns = await repo.nameStatus({ from: revA, to: revB }); + if (!ns.ok) { + reportGitError({ output, op: GIT_OPS.diffNameStatus, e: ns.error }); + return undefined; + } + const num = await repo.numstat({ from: revA, to: revB }); + if (!num.ok) { + reportGitError({ output, op: GIT_OPS.diffNumstat, e: num.error }); + return undefined; + } + return mergeChangedFilesWithStats(ns.value, num.value); +}; + +export const sideAFromSha = (sha: Sha): CommitRev => ({ + kind: REV_KINDS.commit, + sha, +}); + +export interface StartingPoint { + readonly vsRepoRoot: string; + readonly repo: GitRepo; + readonly revA: CommitRev; +} + +export const pickRepoAndCommit = async ({ + runner, + gitApi, + output, +}: { + runner: GitRunner; + gitApi: GitApi; + output: vscode.OutputChannel; +}): Promise<StartingPoint | undefined> => { + const vs = await pickRepoFrom(gitApi); + if (!vs.ok) { + return undefined; + } + const repo = buildRepo(runner, vs.value); + const log = await repo.log({}); + if (!log.ok) { + reportGitError({ output, op: GIT_OPS.log, e: log.error }); + return undefined; + } + const commit = await pickCommit({ commits: log.value }); + if (!commit.ok) { + return undefined; + } + return { + vsRepoRoot: vs.value.rootUri.fsPath, + repo, + revA: sideAFromSha(commit.value.sha), + }; +}; diff --git a/src/commands/historyItem.ts b/src/commands/historyItem.ts new file mode 100644 index 0000000..1d27302 --- /dev/null +++ b/src/commands/historyItem.ts @@ -0,0 +1,29 @@ +const isNonEmptyString = (v: unknown): v is string => typeof v === "string" && v !== ""; + +const stringIdOf = (obj: object): string | undefined => { + if (!("id" in obj)) { + return undefined; + } + return isNonEmptyString(obj.id) ? obj.id : undefined; +}; + +export const extractHistoryItemSha = (arg: unknown): string | undefined => { + if (isNonEmptyString(arg)) { + return arg; + } + if (typeof arg !== "object" || arg === null) { + return undefined; + } + const direct = stringIdOf(arg); + if (direct !== undefined) { + return direct; + } + if (!("historyItem" in arg)) { + return undefined; + } + const inner = arg.historyItem; + if (typeof inner !== "object" || inner === null) { + return undefined; + } + return stringIdOf(inner); +}; diff --git a/src/commands/reopenLast.ts b/src/commands/reopenLast.ts new file mode 100644 index 0000000..8ea43ee --- /dev/null +++ b/src/commands/reopenLast.ts @@ -0,0 +1,35 @@ +import * as vscode from "vscode"; +import { TITLE_PREFIX } from "../constants"; +import { findRepoForUri } from "../vscodeGitApi"; +import type { MementoStore } from "../state"; +import { type CommandDeps, buildRepo } from "./shared"; +import { drillIntoFiles } from "./flow"; + +const NO_PREVIOUS = `${TITLE_PREFIX} no previous comparison to reopen.`; +const REPO_GONE = `${TITLE_PREFIX} previous repository is no longer open.`; + +const handler = async (deps: CommandDeps & { readonly state: MementoStore }): Promise<void> => { + const last = deps.state.getLastComparison(); + if (last === undefined) { + void vscode.window.showInformationMessage(NO_PREVIOUS); + return; + } + const vsRepo = findRepoForUri(deps.gitApi, vscode.Uri.file(last.repoRoot)); + if (vsRepo === undefined) { + void vscode.window.showWarningMessage(REPO_GONE); + return; + } + const repo = buildRepo(deps.runner, vsRepo); + await drillIntoFiles({ + repo, + repoRoot: vsRepo.rootUri.fsPath, + revA: last.revA, + revB: last.revB, + state: deps.state, + output: deps.output, + }); +}; + +export const makeReopenLast = (deps: CommandDeps & { readonly state: MementoStore }) => async (): Promise<void> => { + await handler(deps); +}; diff --git a/src/commands/shared.ts b/src/commands/shared.ts new file mode 100644 index 0000000..df6694c --- /dev/null +++ b/src/commands/shared.ts @@ -0,0 +1,102 @@ +import * as path from "node:path"; +import * as vscode from "vscode"; +import { BUILT_IN_COMMANDS, LOG_EVENTS, REV_KINDS, SHORT_SHA_LEN, UI_TEXT } from "../constants"; +import type { GitRepo } from "../git/GitRepo"; +import type { GitRunner } from "../git/GitRunner"; +import { createGitRepo } from "../git/GitRepo"; +import { type GitApi, type GitVsRepository, findRepoForUri } from "../vscodeGitApi"; +import type { CommitRev, DiffyAddressableRev, RevSpec, Sha } from "../git/types"; +import { logger } from "../logger"; +import { type Result, err, ok } from "../result"; +import { CANCELLED, type Cancelled } from "../ui/cancelled"; +import { buildDiffyUri } from "../ui/uri"; + +export interface CommandDeps { + readonly runner: GitRunner; + readonly gitApi: GitApi; + readonly output: vscode.OutputChannel; +} + +export const shortSha = (sha: Sha): string => sha.slice(0, SHORT_SHA_LEN); + +const labelForRev = (rev: RevSpec): string => { + if (rev.kind === REV_KINDS.commit) { + return shortSha(rev.sha); + } + if (rev.kind === REV_KINDS.workingCopy) { + return UI_TEXT.workingCopy; + } + return UI_TEXT.indexLabel; +}; + +export const formatDiffTitle = ({ + revA, + revB, + basename, +}: { + revA: CommitRev; + revB: RevSpec; + basename: string; +}): string => `${labelForRev(revA)} ${UI_TEXT.pathArrow} ${labelForRev(revB)} ${UI_TEXT.pathDash} ${basename}`; + +export const uriForRev = ({ + rev, + repoRoot, + relPath, +}: { + rev: RevSpec; + repoRoot: string; + relPath: string; +}): vscode.Uri => { + if (rev.kind === REV_KINDS.workingCopy) { + return vscode.Uri.file(path.join(repoRoot, relPath)); + } + const addressable: DiffyAddressableRev = + rev.kind === REV_KINDS.commit ? { kind: REV_KINDS.commit, sha: rev.sha } : { kind: REV_KINDS.index }; + return vscode.Uri.parse(buildDiffyUri(addressable, relPath)); +}; + +export const openDiff = async ({ + revA, + revB, + repoRoot, + relPath, +}: { + revA: CommitRev; + revB: RevSpec; + repoRoot: string; + relPath: string; +}): Promise<void> => { + const left = uriForRev({ rev: revA, repoRoot, relPath }); + const right = uriForRev({ rev: revB, repoRoot, relPath }); + const title = formatDiffTitle({ + revA, + revB, + basename: path.basename(relPath), + }); + logger.info({ shaA: shortSha(revA.sha), revBKind: revB.kind }, LOG_EVENTS.diffOpen); + await vscode.commands.executeCommand(BUILT_IN_COMMANDS.diff, left, right, title); +}; + +export const repoForUri = (api: GitApi, uri: vscode.Uri): GitVsRepository | undefined => findRepoForUri(api, uri); + +export const pickRepoFrom = async (api: GitApi): Promise<Result<GitVsRepository, Cancelled>> => { + if (api.repositories.length === 0) { + return err(CANCELLED); + } + const first = api.repositories[0]; + if (api.repositories.length === 1 && first !== undefined) { + return ok(first); + } + const items = api.repositories.map((r) => ({ + label: r.rootUri.fsPath, + repo: r, + })); + const picked = await vscode.window.showQuickPick(items, { + placeHolder: UI_TEXT.pickRepoPlaceholder, + }); + return picked === undefined ? err(CANCELLED) : ok(picked.repo); +}; + +export const buildRepo = (runner: GitRunner, vsRepo: GitVsRepository): GitRepo => + createGitRepo({ runner, cwd: vsRepo.rootUri.fsPath }); diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..3a662ca --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,166 @@ +export const SCHEME = "diffy"; + +export const OUTPUT_CHANNEL_NAME = "Diffy"; + +export const COMMAND_IDS = { + compareWith: "diffy.compareWith", + compareWithWorkingCopy: "diffy.compareWithWorkingCopy", + compareWithPrevious: "diffy.compareWithPrevious", + compareWithBranch: "diffy.compareWithBranch", + compareWithTag: "diffy.compareWithTag", + compareTwoCommits: "diffy.compareTwoCommits", + compareFileWithCommit: "diffy.compareFileWithCommit", + compareFileWithBranch: "diffy.compareFileWithBranch", + compareFileWithTag: "diffy.compareFileWithTag", + reopenLast: "diffy.reopenLast", + showLogs: "diffy.showLogs", +} as const; + +export const BUILT_IN_COMMANDS = { + diff: "vscode.diff", + setContext: "setContext", +} as const; + +export const CONTEXT_KEYS = { + gitAvailable: "diffy.gitAvailable", +} as const; + +export const MEMENTO_KEYS = { + lastComparison: "diffy.lastComparison", +} as const; + +export const URI_AUTHORITIES = { + commit: "commit", + index: "index", +} as const; + +export const DEFAULT_LOG_LIMIT = 200; + +export const SHORT_SHA_LEN = 7; + +export const GIT_BINARY = "git"; + +export const NUL = "\x00"; +export const TAB = "\t"; +export const LF = "\n"; + +export const GIT_LOG_FORMAT = "%H%x00%h%x00%an%x00%at%x00%s"; + +export const REFS_FORMAT = "%(refname)%00%(refname:short)%00%(objectname)"; + +export const REF_PREFIX_HEADS = "refs/heads/"; +export const REF_PREFIX_TAGS = "refs/tags/"; + +export const REV_KINDS = { + commit: "commit", + workingCopy: "workingCopy", + index: "index", +} as const; + +export const REF_TYPES = { + branch: "branch", + tag: "tag", + other: "other", +} as const; + +export const SIDE_B_KINDS = { + workingCopy: REV_KINDS.workingCopy, + index: REV_KINDS.index, + pickRef: "pickRef", + pickCommit: "pickCommit", +} as const; + +export const URI_PARSE_ERROR_KINDS = { + invalidScheme: "invalidScheme", + invalidAuthority: "invalidAuthority", + missingSha: "missingSha", + emptyPath: "emptyPath", + badEncoding: "badEncoding", + malformed: "malformed", +} as const; + +export const GIT_ERROR_KINDS = { + spawnFailed: "spawnFailed", + nonZeroExit: "nonZeroExit", + parseError: "parseError", + notARepo: "notARepo", + notFound: "notFound", +} as const; + +export const CHANGED_FILE_STATUSES = { + added: "A", + modified: "M", + deleted: "D", + renamed: "R", + copied: "C", +} as const; + +export const MENU_IDS = { + scmHistoryItem: "scm/historyItem/context", + scmResourceState: "scm/resourceState/context", + editorTitleContext: "editor/title/context", + explorerContext: "explorer/context", + commandPalette: "commandPalette", +} as const; + +export const MENU_WHEN = { + scmGit: "scmProvider == git", + resourceFile: "resourceScheme == file", + resourceFileNotFolder: "resourceScheme == file && !explorerResourceIsFolder", + never: "false", +} as const; + +export const MENU_GROUP_PREFIX = "diffy"; + +export const TITLE_PREFIX = "Diffy:"; + +export const UI_TEXT = { + workingCopy: "Working Copy", + workingCopyDescription: "On-disk files in this repository", + indexLabel: "Index", + indexDescription: "The git staging area", + pickCommitLabel: "Pick a commit…", + pickCommitDescription: "Choose from recent log entries", + pickRefLabel: "Pick a branch or tag…", + pickRefDescription: "Choose from refs in this repository", + pickCommitPlaceholder: "Pick a commit", + pickRefPlaceholder: "Pick a branch or tag", + pickBranchPlaceholder: "Pick a branch", + pickTagPlaceholder: "Pick a tag", + pickRepoPlaceholder: "Pick a git repository", + compareAgainstPlaceholder: "Compare against…", + branchLabel: "Branch", + tagLabel: "Tag", + refLabel: "Ref", + binaryStat: "binary", + justNow: "just now", + noChanges: "Diffy: no changes between selected sides.", + pathArrow: "↔", + pathDash: "—", + bulletDot: "•", +} as const; + +export const LOG_EVENTS = { + extensionActivated: "extension.activated", + extensionDeactivated: "extension.deactivated", + gitRunStart: "git.run.start", + gitRunEnd: "git.run.end", + gitRunSpawnFailed: "git.run.spawnFailed", + gitError: "git.error", + diffOpen: "diff.open", + providerParseFailed: "provider.parseFailed", + providerRepoUnresolved: "provider.repoUnresolved", + providerShowFailed: "provider.showFailed", +} as const; + +export const VSCODE_GIT_EXTENSION_ID = "vscode.git"; + +export const LOG_LEVELS = { + trace: "trace", + debug: "debug", + info: "info", + warn: "warn", + error: "error", +} as const; + +export type LogLevel = (typeof LOG_LEVELS)[keyof typeof LOG_LEVELS]; diff --git a/src/extension.ts b/src/extension.ts new file mode 100644 index 0000000..26dc02a --- /dev/null +++ b/src/extension.ts @@ -0,0 +1,120 @@ +import * as vscode from "vscode"; +import { + BUILT_IN_COMMANDS, + COMMAND_IDS, + CONTEXT_KEYS, + LOG_EVENTS, + LOG_LEVELS, + OUTPUT_CHANNEL_NAME, + REF_TYPES, + TITLE_PREFIX, + UI_TEXT, +} from "./constants"; +import { createGitRunner } from "./git/GitRunner"; +import { type GitApi, getGitApi } from "./vscodeGitApi"; +import { addLogStream, logger } from "./logger"; +import { makeCompareFileWithCommit } from "./commands/compareFileWithCommit"; +import { FILE_REV_SOURCES, makeCompareFileWithRev } from "./commands/compareFileWithRev"; +import { makeCompareTwoCommits } from "./commands/compareTwoCommits"; +import { makeCompareWith } from "./commands/compareWith"; +import { makeCompareWithPrevious } from "./commands/compareWithPrevious"; +import { makeCompareWithRef } from "./commands/compareWithRef"; +import { makeCompareWithWorkingCopy } from "./commands/compareWithWorkingCopy"; +import { makeReopenLast } from "./commands/reopenLast"; +import { registerDiffyContentProvider } from "./providers/DiffyContentProvider"; +import type { CommandDeps } from "./commands/shared"; +import { buildRepo } from "./commands/shared"; +import { createMementoStore } from "./state"; +import type { GitRunner } from "./git/GitRunner"; + +const GIT_VERSION_ARG = "--version"; +const GIT_MISSING_MESSAGE = `${TITLE_PREFIX} git binary not found on PATH ${UI_TEXT.pathDash} commands disabled.`; +const GIT_API_MISSING_MESSAGE = `${TITLE_PREFIX} built-in git extension API unavailable.`; +const TRAILING_NEWLINE = /\n$/; + +const channelStream = (channel: vscode.OutputChannel): NodeJS.WritableStream => { + const stream = { + write(chunk: string | Uint8Array): boolean { + const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"); + channel.appendLine(text.replace(TRAILING_NEWLINE, "")); + return true; + }, + }; + return stream as NodeJS.WritableStream; +}; + +const probeGit = async (runner: GitRunner): Promise<boolean> => { + const r = await runner.run({ args: [GIT_VERSION_ARG], cwd: process.cwd() }); + return r.ok; +}; + +const setGitAvailable = async (available: boolean): Promise<void> => { + await vscode.commands.executeCommand(BUILT_IN_COMMANDS.setContext, CONTEXT_KEYS.gitAvailable, available); +}; + +const makeRepoResolver = (api: GitApi, runner: GitRunner) => () => { + const first = api.repositories[0]; + if (first === undefined) { + return undefined; + } + return buildRepo(runner, first); +}; + +const registerAll = ( + context: vscode.ExtensionContext, + deps: CommandDeps & { readonly state: ReturnType<typeof createMementoStore> } +): void => { + context.subscriptions.push( + vscode.commands.registerCommand(COMMAND_IDS.compareWith, makeCompareWith(deps)), + vscode.commands.registerCommand(COMMAND_IDS.compareWithWorkingCopy, makeCompareWithWorkingCopy(deps)), + vscode.commands.registerCommand(COMMAND_IDS.compareWithPrevious, makeCompareWithPrevious(deps)), + vscode.commands.registerCommand( + COMMAND_IDS.compareWithBranch, + makeCompareWithRef({ deps, filter: REF_TYPES.branch }) + ), + vscode.commands.registerCommand(COMMAND_IDS.compareWithTag, makeCompareWithRef({ deps, filter: REF_TYPES.tag })), + vscode.commands.registerCommand(COMMAND_IDS.compareTwoCommits, makeCompareTwoCommits(deps)), + vscode.commands.registerCommand(COMMAND_IDS.compareFileWithCommit, makeCompareFileWithCommit(deps)), + vscode.commands.registerCommand( + COMMAND_IDS.compareFileWithBranch, + makeCompareFileWithRev({ deps, source: FILE_REV_SOURCES.branch }) + ), + vscode.commands.registerCommand( + COMMAND_IDS.compareFileWithTag, + makeCompareFileWithRev({ deps, source: FILE_REV_SOURCES.tag }) + ), + vscode.commands.registerCommand(COMMAND_IDS.reopenLast, makeReopenLast(deps)), + vscode.commands.registerCommand(COMMAND_IDS.showLogs, () => { + deps.output.show(true); + }) + ); +}; + +export const activate = async (context: vscode.ExtensionContext): Promise<void> => { + const output = vscode.window.createOutputChannel(OUTPUT_CHANNEL_NAME); + context.subscriptions.push(output); + addLogStream({ stream: channelStream(output), level: LOG_LEVELS.info }); + + const runner = createGitRunner({ logger }); + const gitOk = await probeGit(runner); + await setGitAvailable(gitOk); + if (!gitOk) { + output.appendLine(GIT_MISSING_MESSAGE); + } + + const api = await getGitApi(); + if (api === undefined) { + output.appendLine(GIT_API_MISSING_MESSAGE); + return; + } + + const state = createMementoStore(context.globalState); + const deps = { runner, gitApi: api, output, state } as const; + registerDiffyContentProvider(context, makeRepoResolver(api, runner)); + registerAll(context, deps); + logger.info({ repos: api.repositories.length }, LOG_EVENTS.extensionActivated); +}; + +export const deactivate = (): void => { + logger.info({}, LOG_EVENTS.extensionDeactivated); +}; diff --git a/src/git/GitRepo.ts b/src/git/GitRepo.ts new file mode 100644 index 0000000..0993a50 --- /dev/null +++ b/src/git/GitRepo.ts @@ -0,0 +1,102 @@ +import { DEFAULT_LOG_LIMIT, GIT_LOG_FORMAT, REFS_FORMAT } from "../constants"; +import { type Result, ok, err, andThen, map } from "../result"; +import type { GitRunner } from "./GitRunner"; +import { parseLog, parseNameStatus, parseNumstat, parseRefs } from "./parsers"; +import type { + ChangedFile, + Commit, + CommitRev, + DiffStat, + DiffyAddressableRev, + GitError, + Ref, + RevSpec, + Sha, +} from "./types"; + +export interface DiffSides { + readonly from: CommitRev; + readonly to: RevSpec; +} + +export interface ShowArgs { + readonly rev: DiffyAddressableRev; + readonly path: string; +} + +export interface LogArgs { + readonly limit?: number; + readonly ref?: string; +} + +export interface GitRepo { + log: (args?: LogArgs) => Promise<Result<readonly Commit[], GitError>>; + nameStatus: (args: DiffSides) => Promise<Result<readonly ChangedFile[], GitError>>; + numstat: (args: DiffSides) => Promise<Result<readonly DiffStat[], GitError>>; + show: (args: ShowArgs) => Promise<Result<string, GitError>>; + refs: () => Promise<Result<readonly Ref[], GitError>>; + revParse: (name: string) => Promise<Result<Sha, GitError>>; + currentBranch: () => Promise<Result<string | undefined, GitError>>; +} + +const buildLogArgs = (params: { limit: number; ref?: string }): readonly string[] => { + const base = ["log", `--max-count=${params.limit.toString()}`, `--format=${GIT_LOG_FORMAT}`, "-z"]; + return params.ref === undefined ? base : [...base, params.ref]; +}; + +const buildDiffArgs = ({ from, to }: DiffSides, subcommand: "name-status" | "numstat"): readonly string[] => { + const head = ["diff", `--${subcommand}`, "-z", "--find-renames", "--find-copies"]; + if (to.kind === "commit") { + return [...head, from.sha, to.sha]; + } + if (to.kind === "workingCopy") { + return [...head, from.sha]; + } + return [...head, from.sha, "--cached"]; +}; + +const showSpec = ({ rev, path }: ShowArgs): string => (rev.kind === "commit" ? `${rev.sha}:${path}` : `:${path}`); + +const trimSha = (stdout: string): Result<Sha, GitError> => { + const sha = stdout.trim(); + if (sha.length === 0) { + return err({ kind: "parseError", message: "revParse: empty output" }); + } + return ok(sha); +}; + +export const createGitRepo = ({ runner, cwd }: { runner: GitRunner; cwd: string }): GitRepo => ({ + log: async (args = {}) => { + const limit = args.limit ?? DEFAULT_LOG_LIMIT; + const logArgs = args.ref === undefined ? buildLogArgs({ limit }) : buildLogArgs({ limit, ref: args.ref }); + const r = await runner.run({ args: logArgs, cwd }); + return andThen(r, parseLog); + }, + nameStatus: async (args) => { + const r = await runner.run({ args: buildDiffArgs(args, "name-status"), cwd }); + return andThen(r, parseNameStatus); + }, + numstat: async (args) => { + const r = await runner.run({ args: buildDiffArgs(args, "numstat"), cwd }); + return andThen(r, parseNumstat); + }, + show: async (args) => await runner.run({ args: ["show", showSpec(args)], cwd }), + refs: async () => { + const r = await runner.run({ + args: ["for-each-ref", `--format=${REFS_FORMAT}`], + cwd, + }); + return andThen(r, parseRefs); + }, + revParse: async (name) => { + const r = await runner.run({ args: ["rev-parse", "--verify", name], cwd }); + return andThen(r, trimSha); + }, + currentBranch: async () => { + const r = await runner.run({ args: ["branch", "--show-current"], cwd }); + return map(r, (stdout) => { + const name = stdout.trim(); + return name.length === 0 ? undefined : name; + }); + }, +}); diff --git a/src/git/GitRunner.ts b/src/git/GitRunner.ts new file mode 100644 index 0000000..873c6d9 --- /dev/null +++ b/src/git/GitRunner.ts @@ -0,0 +1,83 @@ +import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; +import { GIT_BINARY, GIT_ERROR_KINDS, LOG_EVENTS } from "../constants"; +import { type Result, ok, err } from "../result"; +import { logger as defaultLogger, type Logger } from "../logger"; +import type { GitError } from "./types"; + +const UNKNOWN_EXIT_LABEL = "?"; +const UNKNOWN_EXIT_CODE = -1; + +export interface GitRunArgs { + readonly args: readonly string[]; + readonly cwd: string; +} + +export interface GitRunner { + run: (args: GitRunArgs) => Promise<Result<string, GitError>>; +} + +type Resolver = (r: Result<string, GitError>) => void; + +interface WireArgs { + readonly child: ChildProcessWithoutNullStreams; + readonly argCount: number; + readonly subcommand: string; + readonly logger: Logger; + readonly resolve: Resolver; +} + +const finishRun = (params: { + code: number | null; + subcommand: string; + stdout: string; + stderr: string; +}): Result<string, GitError> => { + if (params.code === 0) { + return ok(params.stdout); + } + const codeLabel = params.code === null ? UNKNOWN_EXIT_LABEL : params.code.toString(); + return err({ + kind: GIT_ERROR_KINDS.nonZeroExit, + message: `git ${params.subcommand} exited ${codeLabel}`, + stderr: params.stderr, + exitCode: params.code ?? UNKNOWN_EXIT_CODE, + }); +}; + +const wireSubprocess = ({ child, argCount, subcommand, logger, resolve }: WireArgs): void => { + let stdout = ""; + let stderr = ""; + child.stdout.on("data", (chunk: Buffer) => { + stdout += chunk.toString("utf8"); + }); + child.stderr.on("data", (chunk: Buffer) => { + stderr += chunk.toString("utf8"); + }); + child.on("error", (e: Error) => { + logger.warn({ argCount }, LOG_EVENTS.gitRunSpawnFailed); + resolve(err({ kind: GIT_ERROR_KINDS.spawnFailed, message: e.message })); + }); + child.on("close", (code: number | null) => { + logger.debug({ exitCode: code, stdoutLen: stdout.length }, LOG_EVENTS.gitRunEnd); + resolve(finishRun({ code, subcommand, stdout, stderr })); + }); +}; + +const runGit = async ({ args, cwd, logger }: GitRunArgs & { logger: Logger }): Promise<Result<string, GitError>> => { + logger.debug({ argCount: args.length }, LOG_EVENTS.gitRunStart); + return await new Promise<Result<string, GitError>>((resolve) => { + const child = spawn(GIT_BINARY, [...args], { cwd }); + wireSubprocess({ + child, + argCount: args.length, + subcommand: args[0] ?? "", + logger, + resolve, + }); + }); +}; + +export const createGitRunner = (deps: { logger?: Logger } = {}): GitRunner => { + const logger = deps.logger ?? defaultLogger; + return { run: async (a) => await runGit({ ...a, logger }) }; +}; diff --git a/src/git/parsers.ts b/src/git/parsers.ts new file mode 100644 index 0000000..3ad9609 --- /dev/null +++ b/src/git/parsers.ts @@ -0,0 +1,288 @@ +import { + CHANGED_FILE_STATUSES, + GIT_ERROR_KINDS, + LF, + NUL, + REF_PREFIX_HEADS, + REF_PREFIX_TAGS, + REF_TYPES, + TAB, +} from "../constants"; +import { type Result, ok, err } from "../result"; +import type { ChangedFile, ChangedFileStatus, Commit, DiffStat, GitError, Ref, RefType } from "./types"; + +const LOG_FIELDS_PER_RECORD = 5; +const REF_FIELDS_PER_RECORD = 3; +const NUMSTAT_TAB_FIELDS = 3; +const CHAR_CODE_DIGIT_LO = 48; +const CHAR_CODE_DIGIT_HI = 57; + +const errParse = (message: string): Result<never, GitError> => err({ kind: GIT_ERROR_KINDS.parseError, message }); + +const stripTrailingEmpty = (arr: readonly string[]): readonly string[] => { + if (arr.length === 0) { + return arr; + } + const last = arr[arr.length - 1]; + return last === "" ? arr.slice(0, -1) : arr; +}; + +const isAllDigits = (s: string): boolean => { + if (s.length === 0) { + return false; + } + for (let i = 0; i < s.length; i++) { + const c = s.charCodeAt(i); + if (c < CHAR_CODE_DIGIT_LO || c > CHAR_CODE_DIGIT_HI) { + return false; + } + } + return true; +}; + +const refTypeFromName = (fullName: string): RefType => { + if (fullName.startsWith(REF_PREFIX_HEADS)) { + return REF_TYPES.branch; + } + if (fullName.startsWith(REF_PREFIX_TAGS)) { + return REF_TYPES.tag; + } + return REF_TYPES.other; +}; + +const parseLogRecord = (fields: readonly string[]): Result<Commit, GitError> => { + const [sha, shortSha, author, atStr, subject] = fields; + if ( + sha === undefined || + shortSha === undefined || + author === undefined || + atStr === undefined || + subject === undefined + ) { + return errParse("parseLog: missing field"); + } + if (!isAllDigits(atStr)) { + return errParse("parseLog: invalid timestamp"); + } + const authorTime = Number.parseInt(atStr, 10); + return ok({ sha, shortSha, author, authorTime, subject }); +}; + +export const parseLog = (stdout: string): Result<readonly Commit[], GitError> => { + if (stdout.length === 0) { + return ok([]); + } + const tokens = stripTrailingEmpty(stdout.split(NUL)); + if (tokens.length % LOG_FIELDS_PER_RECORD !== 0) { + return errParse("parseLog: field count not multiple of 5"); + } + const commits: Commit[] = []; + for (let i = 0; i < tokens.length; i += LOG_FIELDS_PER_RECORD) { + const slice = tokens.slice(i, i + LOG_FIELDS_PER_RECORD); + const r = parseLogRecord(slice); + if (!r.ok) { + return r; + } + commits.push(r.value); + } + return ok(commits); +}; + +interface ParsedStatus { + readonly status: ChangedFileStatus; + readonly similarity?: number; + readonly isRename: boolean; +} + +const parseStatusToken = (raw: string): Result<ParsedStatus, GitError> => { + if (raw.length === 0) { + return errParse("parseNameStatus: empty status token"); + } + const first = raw.charAt(0); + if ( + first === CHANGED_FILE_STATUSES.added || + first === CHANGED_FILE_STATUSES.modified || + first === CHANGED_FILE_STATUSES.deleted + ) { + if (raw.length !== 1) { + return errParse("parseNameStatus: extra chars on status"); + } + return ok({ status: first, isRename: false }); + } + if (first === CHANGED_FILE_STATUSES.renamed || first === CHANGED_FILE_STATUSES.copied) { + const digits = raw.slice(1); + if (!isAllDigits(digits)) { + return errParse("parseNameStatus: bad similarity"); + } + return ok({ status: first, similarity: Number.parseInt(digits, 10), isRename: true }); + } + return errParse(`parseNameStatus: unknown status '${first}'`); +}; + +const buildSimple = ( + parsed: ParsedStatus, + tokens: readonly string[], + i: number +): Result<{ file: ChangedFile; next: number }, GitError> => { + const path = tokens[i + 1]; + if (path === undefined) { + return errParse("parseNameStatus: missing path"); + } + return ok({ file: { status: parsed.status, path }, next: i + 2 }); +}; + +const buildRenameOrCopy = ( + parsed: ParsedStatus, + tokens: readonly string[], + i: number +): Result<{ file: ChangedFile; next: number }, GitError> => { + const oldPath = tokens[i + 1]; + const newPath = tokens[i + 2]; + if (oldPath === undefined || newPath === undefined) { + return errParse("parseNameStatus: rename missing paths"); + } + const similarity = parsed.similarity; + if (similarity === undefined) { + return errParse("parseNameStatus: rename missing similarity"); + } + return ok({ + file: { status: parsed.status, path: newPath, oldPath, similarity }, + next: i + 3, + }); +}; + +const readOneNameStatus = ( + tokens: readonly string[], + i: number +): Result<{ file: ChangedFile; next: number }, GitError> => { + const head = tokens[i]; + if (head === undefined) { + return errParse("parseNameStatus: missing status"); + } + const parsed = parseStatusToken(head); + if (!parsed.ok) { + return parsed; + } + return parsed.value.isRename ? buildRenameOrCopy(parsed.value, tokens, i) : buildSimple(parsed.value, tokens, i); +}; + +export const parseNameStatus = (stdout: string): Result<readonly ChangedFile[], GitError> => { + if (stdout.length === 0) { + return ok([]); + } + const tokens = stripTrailingEmpty(stdout.split(NUL)); + const out: ChangedFile[] = []; + let i = 0; + while (i < tokens.length) { + const r = readOneNameStatus(tokens, i); + if (!r.ok) { + return r; + } + out.push(r.value.file); + i = r.value.next; + } + return ok(out); +}; + +interface NumstatHead { + readonly added: number; + readonly deleted: number; + readonly binary: boolean; + readonly pathField: string; +} + +const splitNumstatHead = (head: string): Result<NumstatHead, GitError> => { + const parts = head.split(TAB); + if (parts.length !== NUMSTAT_TAB_FIELDS) { + return errParse("parseNumstat: expected 3 tab-fields"); + } + const [a, d, p] = parts; + if (a === undefined || d === undefined || p === undefined) { + return errParse("parseNumstat: missing tab fields"); + } + if (a === "-" && d === "-") { + return ok({ added: 0, deleted: 0, binary: true, pathField: p }); + } + if (!isAllDigits(a) || !isAllDigits(d)) { + return errParse("parseNumstat: non-numeric counts"); + } + return ok({ + added: Number.parseInt(a, 10), + deleted: Number.parseInt(d, 10), + binary: false, + pathField: p, + }); +}; + +const readOneNumstat = (tokens: readonly string[], i: number): Result<{ stat: DiffStat; next: number }, GitError> => { + const head = tokens[i]; + if (head === undefined) { + return errParse("parseNumstat: missing record"); + } + const fields = splitNumstatHead(head); + if (!fields.ok) { + return fields; + } + const { added, deleted, binary, pathField } = fields.value; + if (pathField !== "") { + return ok({ stat: { path: pathField, added, deleted, binary }, next: i + 1 }); + } + const oldPath = tokens[i + 1]; + const newPath = tokens[i + 2]; + if (oldPath === undefined || newPath === undefined) { + return errParse("parseNumstat: rename missing paths"); + } + return ok({ + stat: { path: newPath, oldPath, added, deleted, binary }, + next: i + 3, + }); +}; + +export const parseNumstat = (stdout: string): Result<readonly DiffStat[], GitError> => { + if (stdout.length === 0) { + return ok([]); + } + const tokens = stripTrailingEmpty(stdout.split(NUL)); + const out: DiffStat[] = []; + let i = 0; + while (i < tokens.length) { + const r = readOneNumstat(tokens, i); + if (!r.ok) { + return r; + } + out.push(r.value.stat); + i = r.value.next; + } + return ok(out); +}; + +const parseRefRecord = (fields: readonly string[]): Result<Ref, GitError> => { + const [fullName, shortName, sha] = fields; + if (fullName === undefined || shortName === undefined || sha === undefined) { + return errParse("parseRefs: missing field"); + } + if (fullName === "" || shortName === "" || sha === "") { + return errParse("parseRefs: empty field"); + } + return ok({ name: shortName, fullName, sha, type: refTypeFromName(fullName) }); +}; + +export const parseRefs = (stdout: string): Result<readonly Ref[], GitError> => { + if (stdout.length === 0) { + return ok([]); + } + const lines = stdout.split(LF).filter((l) => l.length > 0); + const refs: Ref[] = []; + for (const line of lines) { + const fields = line.split(NUL); + if (fields.length !== REF_FIELDS_PER_RECORD) { + return errParse("parseRefs: expected 3 NUL-separated fields per line"); + } + const r = parseRefRecord(fields); + if (!r.ok) { + return r; + } + refs.push(r.value); + } + return ok(refs); +}; diff --git a/src/git/repoMatch.ts b/src/git/repoMatch.ts new file mode 100644 index 0000000..11f4e04 --- /dev/null +++ b/src/git/repoMatch.ts @@ -0,0 +1,36 @@ +interface UriLike { + readonly fsPath: string; +} + +interface RepoLike<U extends UriLike> { + readonly rootUri: U; +} + +interface ApiLike<U extends UriLike, R extends RepoLike<U>> { + readonly repositories: readonly R[]; + getRepository?: (uri: U) => R | null; +} + +export const matchRepoByFsPath = <R extends RepoLike<UriLike>>( + repositories: readonly R[], + targetFsPath: string +): R | undefined => { + let best: R | undefined; + for (const r of repositories) { + const root = r.rootUri.fsPath; + if (!targetFsPath.startsWith(root)) { + continue; + } + if (best === undefined || root.length > best.rootUri.fsPath.length) { + best = r; + } + } + return best; +}; + +export const findRepoForUri = <U extends UriLike, R extends RepoLike<U>>(api: ApiLike<U, R>, uri: U): R | undefined => { + if (api.getRepository !== undefined) { + return api.getRepository(uri) ?? undefined; + } + return matchRepoByFsPath(api.repositories, uri.fsPath); +}; diff --git a/src/git/types.ts b/src/git/types.ts new file mode 100644 index 0000000..a30e957 --- /dev/null +++ b/src/git/types.ts @@ -0,0 +1,61 @@ +import type { CHANGED_FILE_STATUSES, GIT_ERROR_KINDS, REF_TYPES, REV_KINDS } from "../constants"; + +export type Sha = string; + +export type ChangedFileStatus = (typeof CHANGED_FILE_STATUSES)[keyof typeof CHANGED_FILE_STATUSES]; + +export interface ChangedFile { + readonly status: ChangedFileStatus; + readonly path: string; + readonly oldPath?: string; + readonly similarity?: number; +} + +export interface Commit { + readonly sha: Sha; + readonly shortSha: string; + readonly author: string; + readonly authorTime: number; + readonly subject: string; +} + +export interface DiffStat { + readonly path: string; + readonly oldPath?: string; + readonly added: number; + readonly deleted: number; + readonly binary: boolean; +} + +export interface CommitRev { + readonly kind: typeof REV_KINDS.commit; + readonly sha: Sha; +} +export interface WorkingCopyRev { + readonly kind: typeof REV_KINDS.workingCopy; +} +export interface IndexRev { + readonly kind: typeof REV_KINDS.index; +} + +export type RevSpec = CommitRev | WorkingCopyRev | IndexRev; + +export type DiffyAddressableRev = CommitRev | IndexRev; + +export type RefType = (typeof REF_TYPES)[keyof typeof REF_TYPES]; + +export interface Ref { + readonly name: string; + readonly fullName: string; + readonly sha: Sha; + readonly type: RefType; +} + +export type GitErrorKind = (typeof GIT_ERROR_KINDS)[keyof typeof GIT_ERROR_KINDS]; + +export interface GitError { + readonly kind: GitErrorKind; + readonly message: string; + readonly stderr?: string; + readonly exitCode?: number; +} diff --git a/src/logger.ts b/src/logger.ts new file mode 100644 index 0000000..50e1e9b --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,56 @@ +import pino, { type Logger as PinoLogger, type Level, multistream } from "pino"; +import { LOG_LEVELS } from "./constants"; + +const ENV_LOG_LEVEL_VAR = "DIFFY_LOG_LEVEL"; + +export interface Logger { + trace: (fields: object, msg?: string) => void; + debug: (fields: object, msg?: string) => void; + info: (fields: object, msg?: string) => void; + warn: (fields: object, msg?: string) => void; + error: (fields: object, msg?: string) => void; +} + +export interface LogStreamEntry { + readonly stream: NodeJS.WritableStream; + readonly level?: Level; +} + +const streams: LogStreamEntry[] = [{ stream: process.stdout }]; + +const envLevel = process.env[ENV_LOG_LEVEL_VAR]; + +const buildPino = (): PinoLogger => + pino( + { + level: envLevel ?? LOG_LEVELS.info, + base: null, + timestamp: pino.stdTimeFunctions.isoTime, + }, + multistream(streams.map((s) => ({ stream: s.stream, level: s.level ?? LOG_LEVELS.trace }))) + ); + +let underlying: PinoLogger = buildPino(); + +export const addLogStream = (entry: LogStreamEntry): void => { + streams.push(entry); + underlying = buildPino(); +}; + +export const logger: Logger = { + trace: (fields, msg) => { + underlying.trace(fields, msg); + }, + debug: (fields, msg) => { + underlying.debug(fields, msg); + }, + info: (fields, msg) => { + underlying.info(fields, msg); + }, + warn: (fields, msg) => { + underlying.warn(fields, msg); + }, + error: (fields, msg) => { + underlying.error(fields, msg); + }, +}; diff --git a/src/menus.ts b/src/menus.ts new file mode 100644 index 0000000..25bc807 --- /dev/null +++ b/src/menus.ts @@ -0,0 +1,72 @@ +import { COMMAND_IDS, MENU_GROUP_PREFIX, MENU_IDS, MENU_WHEN, TITLE_PREFIX, UI_TEXT } from "./constants"; + +export interface MenuEntry { + readonly command: string; + readonly when: string; + readonly group: string; +} + +export interface MenuManifest { + readonly menus: Record<string, readonly MenuEntry[]>; + readonly commandPalette: readonly { + readonly command: string; + readonly when: string; + }[]; +} + +const ENTRY_INDEX_OFFSET = 1; + +const commitLevelCommands = [ + COMMAND_IDS.compareWith, + COMMAND_IDS.compareWithWorkingCopy, + COMMAND_IDS.compareWithPrevious, + COMMAND_IDS.compareWithBranch, + COMMAND_IDS.compareWithTag, +] as const; + +const fileLevelCommands = [ + COMMAND_IDS.compareFileWithCommit, + COMMAND_IDS.compareFileWithBranch, + COMMAND_IDS.compareFileWithTag, +] as const; + +const groupedEntries = (commands: readonly string[], when: string, groupPrefix: string): readonly MenuEntry[] => + commands.map((command, i) => ({ + command, + when, + group: `${groupPrefix}@${(i + ENTRY_INDEX_OFFSET).toString()}`, + })); + +const hideFromPalette = (commands: readonly string[]) => + commands.map((command) => ({ command, when: MENU_WHEN.never })); + +export const buildMenuManifest = (): MenuManifest => ({ + menus: { + [MENU_IDS.scmHistoryItem]: groupedEntries(commitLevelCommands, MENU_WHEN.scmGit, MENU_GROUP_PREFIX), + [MENU_IDS.scmResourceState]: groupedEntries(fileLevelCommands, MENU_WHEN.scmGit, MENU_GROUP_PREFIX), + [MENU_IDS.editorTitleContext]: groupedEntries(fileLevelCommands, MENU_WHEN.resourceFile, MENU_GROUP_PREFIX), + [MENU_IDS.explorerContext]: groupedEntries(fileLevelCommands, MENU_WHEN.resourceFileNotFolder, MENU_GROUP_PREFIX), + }, + commandPalette: hideFromPalette([ + COMMAND_IDS.compareWith, + COMMAND_IDS.compareWithWorkingCopy, + COMMAND_IDS.compareWithPrevious, + COMMAND_IDS.compareWithBranch, + COMMAND_IDS.compareWithTag, + COMMAND_IDS.showLogs, + ]), +}); + +export const COMMAND_TITLES: Record<string, string> = { + [COMMAND_IDS.compareWith]: `${TITLE_PREFIX} Compare with…`, + [COMMAND_IDS.compareWithWorkingCopy]: `${TITLE_PREFIX} Compare with ${UI_TEXT.workingCopy}`, + [COMMAND_IDS.compareWithPrevious]: `${TITLE_PREFIX} Compare with Previous`, + [COMMAND_IDS.compareWithBranch]: `${TITLE_PREFIX} Compare with ${UI_TEXT.branchLabel}…`, + [COMMAND_IDS.compareWithTag]: `${TITLE_PREFIX} Compare with ${UI_TEXT.tagLabel}…`, + [COMMAND_IDS.compareTwoCommits]: `${TITLE_PREFIX} Compare Two Commits`, + [COMMAND_IDS.compareFileWithCommit]: `${TITLE_PREFIX} Compare with Commit…`, + [COMMAND_IDS.compareFileWithBranch]: `${TITLE_PREFIX} Compare with ${UI_TEXT.branchLabel}…`, + [COMMAND_IDS.compareFileWithTag]: `${TITLE_PREFIX} Compare with ${UI_TEXT.tagLabel}…`, + [COMMAND_IDS.reopenLast]: `${TITLE_PREFIX} Reopen Last Comparison`, + [COMMAND_IDS.showLogs]: `${TITLE_PREFIX} Show Logs`, +}; diff --git a/src/providers/DiffyContentProvider.ts b/src/providers/DiffyContentProvider.ts new file mode 100644 index 0000000..9b77844 --- /dev/null +++ b/src/providers/DiffyContentProvider.ts @@ -0,0 +1,45 @@ +import * as vscode from "vscode"; +import { LOG_EVENTS, SCHEME } from "../constants"; +import type { GitRepo } from "../git/GitRepo"; +import { logger } from "../logger"; +import { parseDiffyUri } from "../ui/uri"; + +export class DiffyContentProvider implements vscode.TextDocumentContentProvider { + private readonly emitter = new vscode.EventEmitter<vscode.Uri>(); + + readonly onDidChange: vscode.Event<vscode.Uri> = this.emitter.event; + + constructor(private readonly resolveRepo: (uri: vscode.Uri) => GitRepo | undefined) {} + + async provideTextDocumentContent(uri: vscode.Uri): Promise<string> { + const parsed = parseDiffyUri(uri.toString()); + if (!parsed.ok) { + logger.warn({ kind: parsed.error.kind }, LOG_EVENTS.providerParseFailed); + return ""; + } + const repo = this.resolveRepo(uri); + if (repo === undefined) { + logger.warn({}, LOG_EVENTS.providerRepoUnresolved); + return ""; + } + const r = await repo.show({ rev: parsed.value.rev, path: parsed.value.path }); + if (!r.ok) { + logger.debug({ kind: r.error.kind }, LOG_EVENTS.providerShowFailed); + return ""; + } + return r.value; + } + + dispose(): void { + this.emitter.dispose(); + } +} + +export const registerDiffyContentProvider = ( + context: vscode.ExtensionContext, + resolver: (uri: vscode.Uri) => GitRepo | undefined +): DiffyContentProvider => { + const provider = new DiffyContentProvider(resolver); + context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider(SCHEME, provider), provider); + return provider; +}; diff --git a/src/result.ts b/src/result.ts new file mode 100644 index 0000000..ec7c3b4 --- /dev/null +++ b/src/result.ts @@ -0,0 +1,35 @@ +export interface Ok<T> { + readonly ok: true; + readonly value: T; +} +export interface Err<E> { + readonly ok: false; + readonly error: E; +} +export type Result<T, E> = Ok<T> | Err<E>; + +export const ok = <T>(value: T): Ok<T> => ({ ok: true, value }); + +export const err = <E>(error: E): Err<E> => ({ ok: false, error }); + +export const isOk = <T, E>(r: Result<T, E>): r is Ok<T> => r.ok; + +export const isErr = <T, E>(r: Result<T, E>): r is Err<E> => !r.ok; + +export const map = <T, U, E>(r: Result<T, E>, f: (t: T) => U): Result<U, E> => (r.ok ? ok(f(r.value)) : r); + +export const andThen = <T, U, E>(r: Result<T, E>, f: (t: T) => Result<U, E>): Result<U, E> => (r.ok ? f(r.value) : r); + +export const unwrapOr = <T, E>(r: Result<T, E>, fallback: T): T => (r.ok ? r.value : fallback); + +export function expectOk<T, E>(r: Result<T, E>): asserts r is Ok<T> { + if (!r.ok) { + throw new Error(`expected Ok, got Err: ${JSON.stringify(r.error)}`); + } +} + +export function expectErr<T, E>(r: Result<T, E>): asserts r is Err<E> { + if (r.ok) { + throw new Error(`expected Err, got Ok: ${JSON.stringify(r.value)}`); + } +} diff --git a/src/state.ts b/src/state.ts new file mode 100644 index 0000000..3100f45 --- /dev/null +++ b/src/state.ts @@ -0,0 +1,45 @@ +import type * as vscode from "vscode"; +import { MEMENTO_KEYS } from "./constants"; +import type { REV_KINDS } from "./constants"; +import type { RevSpec, Sha } from "./git/types"; + +export interface LastComparison { + readonly revA: { readonly kind: typeof REV_KINDS.commit; readonly sha: Sha }; + readonly revB: RevSpec; + readonly repoRoot: string; +} + +export interface MementoStore { + getLastComparison: () => LastComparison | undefined; + setLastComparison: (value: LastComparison) => Promise<void>; + clearLastComparison: () => Promise<void>; +} + +export const isLastComparison = (raw: unknown): raw is LastComparison => { + if (typeof raw !== "object" || raw === null) { + return false; + } + if (!("repoRoot" in raw) || typeof raw.repoRoot !== "string") { + return false; + } + if (!("revA" in raw) || typeof raw.revA !== "object" || raw.revA === null) { + return false; + } + if (!("revB" in raw) || typeof raw.revB !== "object" || raw.revB === null) { + return false; + } + return true; +}; + +export const createMementoStore = (memento: vscode.Memento): MementoStore => ({ + getLastComparison: () => { + const raw = memento.get<unknown>(MEMENTO_KEYS.lastComparison); + return isLastComparison(raw) ? raw : undefined; + }, + setLastComparison: async (value) => { + await memento.update(MEMENTO_KEYS.lastComparison, value); + }, + clearLastComparison: async () => { + await memento.update(MEMENTO_KEYS.lastComparison, undefined); + }, +}); diff --git a/src/test/runTests.ts b/src/test/runTests.ts new file mode 100644 index 0000000..9ba7969 --- /dev/null +++ b/src/test/runTests.ts @@ -0,0 +1,50 @@ +import * as path from "node:path"; +import { spawnSync } from "node:child_process"; +import { existsSync } from "node:fs"; +import { runTests } from "@vscode/test-electron"; + +const repoRoot = path.resolve(__dirname, "..", ".."); +const extensionDevelopmentPath = repoRoot; +const extensionTestsPath = path.resolve(__dirname, "suite", "index"); +const workspacePath = path.resolve(repoRoot, "test-fixtures", "repo-seed", "workspace"); +const seedScript = path.resolve(repoRoot, "test-fixtures", "repo-seed", "seed.sh"); + +const ensureSeedRepo = (): void => { + if (existsSync(path.join(workspacePath, ".git"))) { + return; + } + const r = spawnSync("bash", [seedScript], { stdio: "inherit" }); + if (r.status !== 0) { + const exitLabel = r.status === null ? "?" : r.status.toString(); + throw new Error(`seed.sh failed with exit ${exitLabel}`); + } +}; + +const main = async (): Promise<void> => { + ensureSeedRepo(); + // Claude Code's host extension sets ELECTRON_RUN_AS_NODE=1 in our shell. + // That env var, if inherited by the spawned Electron in @vscode/test-electron, + // makes Electron behave as Node and refuse to launch VS Code. Drop it. + if ("ELECTRON_RUN_AS_NODE" in process.env) { + delete process.env["ELECTRON_RUN_AS_NODE"]; + } + const coverageDir = process.env["NODE_V8_COVERAGE"]; + const extensionTestsEnv: Record<string, string> = { + DIFFY_E2E: "1", + }; + if (coverageDir !== undefined && coverageDir !== "") { + extensionTestsEnv["NODE_V8_COVERAGE"] = coverageDir; + } + await runTests({ + extensionDevelopmentPath, + extensionTestsPath, + extensionTestsEnv, + launchArgs: [workspacePath, "--disable-telemetry", "--enable-proposed-api", "nimblesite.diffy"], + }); +}; + +main().catch((e: unknown) => { + const message = e instanceof Error ? e.message : String(e); + process.stderr.write(`E2E tests failed: ${message}\n`); + process.exit(1); +}); diff --git a/src/test/suite/activation.test.ts b/src/test/suite/activation.test.ts new file mode 100644 index 0000000..99f9e1a --- /dev/null +++ b/src/test/suite/activation.test.ts @@ -0,0 +1,45 @@ +import { strict as assert } from "node:assert"; +import * as vscode from "vscode"; +import { COMMAND_IDS, OUTPUT_CHANNEL_NAME } from "../../constants"; +import { tick } from "./helpers"; + +const EXTENSION_ID = "nimblesite.diffy"; +const TICK_MS = 20; + +const ALL_COMMAND_IDS: readonly string[] = Object.values(COMMAND_IDS); + +describe("activation", () => { + it("extension is present, activates, and every command id is registered", async () => { + const ext = vscode.extensions.getExtension(EXTENSION_ID); + assert.ok(ext, `extension ${EXTENSION_ID} should be present`); + if (!ext.isActive) { + await ext.activate(); + } + assert.equal(ext.isActive, true, "extension should be active after activate()"); + await tick(TICK_MS); + const registered = await vscode.commands.getCommands(true); + for (const id of ALL_COMMAND_IDS) { + assert.ok( + registered.includes(id), + `command ${id} should be registered (was: ${registered.filter((c) => c.startsWith("diffy.")).join(", ")})` + ); + } + }); + + it("exposes a workspace folder containing the seeded git repo", () => { + const folders = vscode.workspace.workspaceFolders; + assert.ok(folders, "workspace folders should be set"); + assert.equal(folders.length, 1); + const folder = folders[0]; + assert.ok(folder); + assert.match(folder.uri.fsPath.replace(/\\/g, "/"), /repo-seed\/workspace$/); + }); + + it("shows logs command opens the Diffy OutputChannel without errors", async () => { + await vscode.commands.executeCommand(COMMAND_IDS.showLogs); + // The channel's "visible" state isn't observable from the test host, but + // the command must have a registered handler and complete without throwing. + assert.equal(typeof OUTPUT_CHANNEL_NAME, "string"); + assert.ok(OUTPUT_CHANNEL_NAME.length > 0); + }); +}); diff --git a/src/test/suite/commands.test.ts b/src/test/suite/commands.test.ts new file mode 100644 index 0000000..7114b13 --- /dev/null +++ b/src/test/suite/commands.test.ts @@ -0,0 +1,490 @@ +import { strict as assert } from "node:assert"; +import * as vscode from "vscode"; +import { COMMAND_IDS } from "../../constants"; +import { + accept, + allDiffTabs, + closeAllEditors, + dismissQuickPick, + moveNext, + openFileInEditor, + readSeedShas, + tabInputUris, + tick, + waitForDiffTab, + waitForRepoReady, + workspaceRoot, +} from "./helpers"; + +const EXTENSION_ID = "nimblesite.diffy"; + +const ensureActivated = async (): Promise<void> => { + const ext = vscode.extensions.getExtension(EXTENSION_ID); + if (ext !== undefined && !ext.isActive) { + await ext.activate(); + } + await waitForRepoReady(); + await tick(20); +}; + +const labelStrings = (tab: vscode.Tab): string => tab.label; + +const moveAndAccept = async (steps: number): Promise<void> => { + for (let i = 0; i < steps; i++) { + await moveNext(); + } + await accept(); +}; + +describe("Diffy commands — end-to-end through real QuickPick UI", () => { + before(async () => { + await ensureActivated(); + }); + + beforeEach(async () => { + await closeAllEditors(); + await dismissQuickPick(); + await tick(5); + }); + + afterEach(async () => { + await dismissQuickPick(); + await closeAllEditors(); + await tick(5); + }); + + it("reopenLast with no prior comparison shows an info toast and opens no diff", async () => { + // This test must run BEFORE any compareXxx test that sets the memento. + const before = allDiffTabs().length; + await vscode.commands.executeCommand(COMMAND_IDS.reopenLast); + await tick(40); + const after = allDiffTabs().length; + assert.equal(after, before, "no diff should open when memento is empty"); + }); + + it("compareFileWithCommit with no uri and no active editor → warning, no diff", async () => { + await closeAllEditors(); + await tick(20); + const before = allDiffTabs().length; + await vscode.commands.executeCommand(COMMAND_IDS.compareFileWithCommit); + await tick(40); + assert.equal(allDiffTabs().length, before); + }); + + it("compareFileWithCommit with a file outside any repo → warning, no diff", async () => { + const outside = vscode.Uri.file("/tmp/diffy-not-in-any-repo.txt"); + const before = allDiffTabs().length; + await vscode.commands.executeCommand(COMMAND_IDS.compareFileWithCommit, outside); + await tick(40); + assert.equal(allDiffTabs().length, before); + }); + + it("compareWithWorkingCopy without a historyItem → warning, no diff", async () => { + const before = allDiffTabs().length; + await vscode.commands.executeCommand(COMMAND_IDS.compareWithWorkingCopy); + await tick(40); + assert.equal(allDiffTabs().length, before); + }); + + it("compareWithPrevious without a historyItem → warning, no diff", async () => { + const before = allDiffTabs().length; + await vscode.commands.executeCommand(COMMAND_IDS.compareWithPrevious); + await tick(40); + assert.equal(allDiffTabs().length, before); + }); + + it("compareWithPrevious({id:initial-commit}) → revParse fails → reports error, no diff", async () => { + const shas = readSeedShas(); + const before = allDiffTabs().length; + // commit 1 has no parent, so revParse(`${commit1}^1`) fails. + await vscode.commands.executeCommand(COMMAND_IDS.compareWithPrevious, { + id: shas.first, + }); + await tick(80); + assert.equal(allDiffTabs().length, before); + }); + + it("compareTwoCommits: top commit (sha3) vs Working Copy → diff tab opens for a.txt", async () => { + const shas = readSeedShas(); + const flow = vscode.commands.executeCommand(COMMAND_IDS.compareTwoCommits); + + // CommitPicker: top item = most recent (commit 3) + await accept(); + // SideBPicker: top item = Working Copy + await accept(); + // FilePicker: top file — accept + await accept(); + + const diffTab = await waitForDiffTab(); + const uris = tabInputUris(diffTab); + assert.equal(uris.left.scheme, "diffy"); + assert.equal(uris.right.scheme, "file"); + assert.match(uris.left.toString(), new RegExp(`diffy://commit/${shas.third}/`)); + assert.match(uris.right.fsPath, /a\.txt$/); + assert.match(labelStrings(diffTab), /↔ Working Copy — a\.txt$/); + assert.match(labelStrings(diffTab), new RegExp(`^${shas.third.slice(0, 7)}`)); + + await dismissQuickPick(); + await flow; + }); + + it("compareTwoCommits: pick oldest commit (sha1) vs Working Copy → diff for top changed file", async () => { + const shas = readSeedShas(); + const flow = vscode.commands.executeCommand(COMMAND_IDS.compareTwoCommits); + + // CommitPicker: navigate down 2 (commit 3 → 2 → 1) + await moveAndAccept(2); + // SideBPicker: top = Working Copy + await accept(); + // FilePicker: top + await accept(); + + const diffTab = await waitForDiffTab(); + const uris = tabInputUris(diffTab); + assert.equal(uris.left.scheme, "diffy"); + assert.match(uris.left.toString(), new RegExp(`diffy://commit/${shas.first}/`)); + assert.match(labelStrings(diffTab), new RegExp(`^${shas.first.slice(0, 7)} ↔ Working Copy — `)); + + await dismissQuickPick(); + await flow; + }); + + it("compareWith(historyItem={id:sha1}) → SideB=Working Copy → diff opens", async () => { + const shas = readSeedShas(); + const flow = vscode.commands.executeCommand(COMMAND_IDS.compareWith, { + id: shas.first, + }); + + // SideBPicker: top = Working Copy + await accept(); + // FilePicker: top + await accept(); + + const diffTab = await waitForDiffTab(); + const uris = tabInputUris(diffTab); + assert.equal(uris.left.scheme, "diffy"); + assert.match(uris.left.toString(), new RegExp(`diffy://commit/${shas.first}/`)); + assert.equal(uris.right.scheme, "file"); + assert.match(labelStrings(diffTab), new RegExp(`^${shas.first.slice(0, 7)} ↔ Working Copy — `)); + + await dismissQuickPick(); + await flow; + }); + + it("compareWith(historyItem) → SideB=Index → diff has diffy://index/ on the right", async () => { + const shas = readSeedShas(); + const flow = vscode.commands.executeCommand(COMMAND_IDS.compareWith, { + historyItem: { id: shas.first }, + }); + + // SideBPicker: Working Copy, Index, Pick a commit…, Pick a branch or tag… + // Index is item 2 → moveNext × 1, accept + await moveAndAccept(1); + // FilePicker + await accept(); + + const diffTab = await waitForDiffTab(); + const uris = tabInputUris(diffTab); + assert.equal(uris.left.scheme, "diffy"); + assert.equal(uris.right.scheme, "diffy"); + assert.match(uris.left.toString(), new RegExp(`diffy://commit/${shas.first}/`)); + assert.match(uris.right.toString(), /^diffy:\/\/index\//); + assert.match(labelStrings(diffTab), new RegExp(`^${shas.first.slice(0, 7)} ↔ Index — `)); + + await dismissQuickPick(); + await flow; + }); + + it("compareWith → SideB=pickRef → user picks the v0.1.0 tag → diff against that ref", async () => { + const shas = readSeedShas(); + const flow = vscode.commands.executeCommand(COMMAND_IDS.compareWith, { + id: shas.third, + }); + + // SideBPicker: Working Copy, Index, Pick a commit…, Pick a branch or tag… + // pickRef is item 4 → moveNext × 3 + await moveAndAccept(3); + + // RefPicker (current branch `main` excluded): refs/heads/feature first, + // refs/tags/v0.1.0 second. Pick v0.1.0 → moveNext × 1. + await moveAndAccept(1); + + // FilePicker + await accept(); + const diffTab = await waitForDiffTab(); + const uris = tabInputUris(diffTab); + assert.equal(uris.left.scheme, "diffy"); + assert.equal(uris.right.scheme, "diffy"); + assert.match(uris.left.toString(), new RegExp(`diffy://commit/${shas.third}/`)); + assert.match(uris.right.toString(), new RegExp(`diffy://commit/${shas.second}/`)); + assert.match(labelStrings(diffTab), new RegExp(`^${shas.third.slice(0, 7)} ↔ ${shas.second.slice(0, 7)} — `)); + + await dismissQuickPick(); + await flow; + }); + + it("compareWith → SideB=pickCommit → user picks commit 1 → diff against commit 1", async () => { + const shas = readSeedShas(); + const flow = vscode.commands.executeCommand(COMMAND_IDS.compareWith, { + id: shas.third, + }); + + // SideBPicker: pickCommit is item 3 → moveNext × 2, accept + await moveAndAccept(2); + + // Inner CommitPicker: order = commit3, commit2, commit1 (newest first) + // Pick commit1 → moveNext × 2, accept + await moveAndAccept(2); + + // FilePicker + await accept(); + const diffTab = await waitForDiffTab(); + const uris = tabInputUris(diffTab); + assert.match(uris.left.toString(), new RegExp(`diffy://commit/${shas.third}/`)); + assert.match(uris.right.toString(), new RegExp(`diffy://commit/${shas.first}/`)); + + await dismissQuickPick(); + await flow; + }); + + it("compareWithWorkingCopy(historyItem={id:sha2}) → no SideB picker, straight to FilePicker", async () => { + const shas = readSeedShas(); + const flow = vscode.commands.executeCommand(COMMAND_IDS.compareWithWorkingCopy, { + id: shas.second, + }); + + // No SideBPicker; FilePicker opens directly + await accept(); + + const diffTab = await waitForDiffTab(); + const uris = tabInputUris(diffTab); + assert.equal(uris.left.scheme, "diffy"); + assert.equal(uris.right.scheme, "file"); + assert.match(uris.left.toString(), new RegExp(`diffy://commit/${shas.second}/`)); + assert.match(labelStrings(diffTab), new RegExp(`^${shas.second.slice(0, 7)} ↔ Working Copy — `)); + + await dismissQuickPick(); + await flow; + }); + + it("compareWithPrevious(historyItem={id:sha3}) → diffs against parent (commit 2)", async () => { + const shas = readSeedShas(); + const flow = vscode.commands.executeCommand(COMMAND_IDS.compareWithPrevious, { + id: shas.third, + }); + + // FilePicker + await accept(); + const diffTab = await waitForDiffTab(); + const uris = tabInputUris(diffTab); + assert.equal(uris.left.scheme, "diffy"); + assert.equal(uris.right.scheme, "diffy"); + assert.match(uris.left.toString(), new RegExp(`diffy://commit/${shas.third}/`)); + assert.match(uris.right.toString(), new RegExp(`diffy://commit/${shas.second}/`)); + assert.match(labelStrings(diffTab), new RegExp(`^${shas.third.slice(0, 7)} ↔ ${shas.second.slice(0, 7)} — `)); + + await dismissQuickPick(); + await flow; + }); + + it("compareFileWithCommit (with explicit uri arg) → CommitPicker top → diff opens for commit 3", async () => { + const aTxt = vscode.Uri.file(`${workspaceRoot()}/a.txt`); + const shas = readSeedShas(); + const flow = vscode.commands.executeCommand(COMMAND_IDS.compareFileWithCommit, aTxt); + + // CommitPicker top = commit 3 + await accept(); + + const diffTab = await waitForDiffTab(); + const uris = tabInputUris(diffTab); + assert.equal(uris.left.scheme, "diffy"); + assert.match(uris.left.toString(), new RegExp(`diffy://commit/${shas.third}/a\\.txt$`)); + assert.equal(uris.right.scheme, "file"); + assert.match(uris.right.fsPath, /a\.txt$/); + assert.match(labelStrings(diffTab), new RegExp(`^${shas.third.slice(0, 7)} ↔ Working Copy — a\\.txt$`)); + + await flow; + }); + + it("compareFileWithCommit (no uri arg, falls back to active editor) → moveNext to commit 1 → diff", async () => { + const shas = readSeedShas(); + await openFileInEditor("dir/c.txt"); + await tick(20); + const flow = vscode.commands.executeCommand(COMMAND_IDS.compareFileWithCommit); + + // moveNext × 2 to commit 1, accept + await moveAndAccept(2); + + const diffTab = await waitForDiffTab(); + const uris = tabInputUris(diffTab); + assert.match(uris.left.toString(), new RegExp(`diffy://commit/${shas.first}/dir/c\\.txt$`)); + assert.match(uris.right.fsPath.replace(/\\/g, "/"), /dir\/c\.txt$/); + + await flow; + }); + + it("reopenLast: runs compareWithWorkingCopy first, then re-invokes reopenLast and gets the same FilePicker", async () => { + const shas = readSeedShas(); + // Setup: produce a known last-comparison entry by running compareWithWorkingCopy. + const setup = vscode.commands.executeCommand(COMMAND_IDS.compareWithWorkingCopy, { + id: shas.first, + }); + await accept(); // FilePicker + const firstDiff = await waitForDiffTab(); + const firstUris = tabInputUris(firstDiff); + assert.match(firstUris.left.toString(), new RegExp(`diffy://commit/${shas.first}/`)); + await dismissQuickPick(); + await setup; + await closeAllEditors(); + + // Now re-invoke reopenLast — state remembered the comparison. + const reflow = vscode.commands.executeCommand(COMMAND_IDS.reopenLast); + await accept(); // FilePicker again + const reDiff = await waitForDiffTab(); + const reUris = tabInputUris(reDiff); + assert.match(reUris.left.toString(), new RegExp(`diffy://commit/${shas.first}/`)); + assert.equal(reUris.right.scheme, "file"); + + await dismissQuickPick(); + await reflow; + }); + + it("compareWith without a historyItem id shows a warning and opens no diff", async () => { + const before = allDiffTabs().length; + await vscode.commands.executeCommand(COMMAND_IDS.compareWith, undefined); + await tick(40); + const after = allDiffTabs().length; + assert.equal(after, before, "no new diff tab should appear"); + }); + + it("compareWithPrevious(historyItem) with the wrapped { historyItem: {...} } shape still works", async () => { + const shas = readSeedShas(); + const flow = vscode.commands.executeCommand(COMMAND_IDS.compareWithPrevious, { + historyItem: { id: shas.second }, + }); + await accept(); // FilePicker + const diffTab = await waitForDiffTab(); + const uris = tabInputUris(diffTab); + assert.match(uris.left.toString(), new RegExp(`diffy://commit/${shas.second}/`)); + assert.match(uris.right.toString(), new RegExp(`diffy://commit/${shas.first}/`)); + await dismissQuickPick(); + await flow; + }); + + it("compareWithBranch(historyItem={id:sha1}) → branch-filtered RefPicker hides current branch `main` → diff vs feature(=sha2)", async () => { + const shas = readSeedShas(); + const flow = vscode.commands.executeCommand(COMMAND_IDS.compareWithBranch, { + id: shas.first, + }); + + // RefPicker with branch filter: current branch `main` is hidden, leaving + // only `feature` (at sha2). Accept top. + await accept(); + // FilePicker top + await accept(); + + const diffTab = await waitForDiffTab(); + const uris = tabInputUris(diffTab); + assert.equal(uris.left.scheme, "diffy"); + assert.equal(uris.right.scheme, "diffy"); + assert.match(uris.left.toString(), new RegExp(`diffy://commit/${shas.first}/`)); + assert.match(uris.right.toString(), new RegExp(`diffy://commit/${shas.second}/`)); + assert.match(labelStrings(diffTab), new RegExp(`^${shas.first.slice(0, 7)} ↔ ${shas.second.slice(0, 7)} — `)); + + await dismissQuickPick(); + await flow; + }); + + it("compareWithBranch without a historyItem → warning, no diff", async () => { + const before = allDiffTabs().length; + await vscode.commands.executeCommand(COMMAND_IDS.compareWithBranch); + await tick(40); + assert.equal(allDiffTabs().length, before); + }); + + it("compareWithTag(historyItem={id:sha1}) → tag-filtered RefPicker → diff vs v0.1.0(=sha2)", async () => { + const shas = readSeedShas(); + const flow = vscode.commands.executeCommand(COMMAND_IDS.compareWithTag, { + historyItem: { id: shas.first }, + }); + + // RefPicker with tag filter shows only "v0.1.0" → accept top + await accept(); + // FilePicker top + await accept(); + + const diffTab = await waitForDiffTab(); + const uris = tabInputUris(diffTab); + assert.equal(uris.left.scheme, "diffy"); + assert.equal(uris.right.scheme, "diffy"); + assert.match(uris.left.toString(), new RegExp(`diffy://commit/${shas.first}/`)); + assert.match(uris.right.toString(), new RegExp(`diffy://commit/${shas.second}/`)); + assert.match(labelStrings(diffTab), new RegExp(`^${shas.first.slice(0, 7)} ↔ ${shas.second.slice(0, 7)} — `)); + + await dismissQuickPick(); + await flow; + }); + + it("compareWithTag without a historyItem → warning, no diff", async () => { + const before = allDiffTabs().length; + await vscode.commands.executeCommand(COMMAND_IDS.compareWithTag); + await tick(40); + assert.equal(allDiffTabs().length, before); + }); + + it("compareFileWithBranch(uri:a.txt) → branch RefPicker hides current branch → diff opens for feature(=sha2) vs working copy", async () => { + const shas = readSeedShas(); + const aTxt = vscode.Uri.file(`${workspaceRoot()}/a.txt`); + const flow = vscode.commands.executeCommand(COMMAND_IDS.compareFileWithBranch, aTxt); + + // RefPicker with branch filter hides the current branch `main`, leaving + // only `feature` (at sha2). Accept. + await accept(); + + const diffTab = await waitForDiffTab(); + const uris = tabInputUris(diffTab); + assert.equal(uris.left.scheme, "diffy"); + assert.match(uris.left.toString(), new RegExp(`diffy://commit/${shas.second}/a\\.txt$`)); + assert.equal(uris.right.scheme, "file"); + assert.match(uris.right.fsPath, /a\.txt$/); + assert.match(labelStrings(diffTab), new RegExp(`^${shas.second.slice(0, 7)} ↔ Working Copy — a\\.txt$`)); + + await flow; + }); + + it("compareFileWithTag(uri:a.txt) → RefPicker shows only tags → diff opens for v0.1.0(=sha2) vs working copy", async () => { + const shas = readSeedShas(); + const aTxt = vscode.Uri.file(`${workspaceRoot()}/a.txt`); + const flow = vscode.commands.executeCommand(COMMAND_IDS.compareFileWithTag, aTxt); + + // RefPicker with tag filter shows only "v0.1.0" → accept + await accept(); + + const diffTab = await waitForDiffTab(); + const uris = tabInputUris(diffTab); + assert.equal(uris.left.scheme, "diffy"); + assert.match(uris.left.toString(), new RegExp(`diffy://commit/${shas.second}/a\\.txt$`)); + assert.equal(uris.right.scheme, "file"); + assert.match(uris.right.fsPath, /a\.txt$/); + assert.match(labelStrings(diffTab), new RegExp(`^${shas.second.slice(0, 7)} ↔ Working Copy — a\\.txt$`)); + + await flow; + }); + + it("compareFileWithBranch with no uri and no active editor → warning, no diff", async () => { + await closeAllEditors(); + await tick(20); + const before = allDiffTabs().length; + await vscode.commands.executeCommand(COMMAND_IDS.compareFileWithBranch); + await tick(40); + assert.equal(allDiffTabs().length, before); + }); + + it("compareFileWithTag with a file outside any repo → warning, no diff", async () => { + const outside = vscode.Uri.file("/tmp/diffy-not-in-any-repo.txt"); + const before = allDiffTabs().length; + await vscode.commands.executeCommand(COMMAND_IDS.compareFileWithTag, outside); + await tick(40); + assert.equal(allDiffTabs().length, before); + }); +}); diff --git a/src/test/suite/contentProvider.test.ts b/src/test/suite/contentProvider.test.ts new file mode 100644 index 0000000..b4db715 --- /dev/null +++ b/src/test/suite/contentProvider.test.ts @@ -0,0 +1,67 @@ +import { strict as assert } from "node:assert"; +import * as vscode from "vscode"; +import { readSeedShas, waitForRepoReady } from "./helpers"; + +const EXTENSION_ID = "nimblesite.diffy"; + +const readDiffy = async (uriString: string): Promise<string> => { + const doc = await vscode.workspace.openTextDocument(vscode.Uri.parse(uriString)); + return doc.getText(); +}; + +describe("DiffyContentProvider (real diffy:// URIs against the seeded repo)", () => { + before(async () => { + const ext = vscode.extensions.getExtension(EXTENSION_ID); + if (ext !== undefined && !ext.isActive) { + await ext.activate(); + } + await waitForRepoReady(); + }); + + it("resolves a.txt at every committed version with the expected content", async () => { + const shas = readSeedShas(); + + const a1 = await readDiffy(`diffy://commit/${shas.first}/a.txt`); + assert.match(a1, /a\.txt v1/); + assert.match(a1, /second line v1/); + assert.doesNotMatch(a1, /third line/); + + const a2 = await readDiffy(`diffy://commit/${shas.second}/a.txt`); + assert.match(a2, /a\.txt v2 edited/); + assert.match(a2, /third line added/); + assert.doesNotMatch(a2, /working copy uncommitted/); + + const a3 = await readDiffy(`diffy://commit/${shas.third}/a.txt`); + assert.equal(a3, a2, "a.txt is unchanged from commit 2 to commit 3"); + }); + + it("resolves dir/c.txt at v1 and v2 with the expected text", async () => { + const shas = readSeedShas(); + const c1 = await readDiffy(`diffy://commit/${shas.first}/dir/c.txt`); + assert.match(c1, /c\.txt v1/); + const c2 = await readDiffy(`diffy://commit/${shas.second}/dir/c.txt`); + assert.match(c2, /c\.txt v2 edited/); + assert.notEqual(c1, c2); + }); + + it("returns the renamed file b2.txt at the third commit but not before", async () => { + const shas = readSeedShas(); + const b2 = await readDiffy(`diffy://commit/${shas.third}/b2.txt`); + assert.match(b2, /renamed and extended/); + const missing = await readDiffy(`diffy://commit/${shas.first}/b2.txt`); + assert.equal(missing, "", "b2.txt does not exist at commit 1 and should resolve to empty"); + }); + + it("returns empty string for a deleted path (d.txt at commit 2)", async () => { + const shas = readSeedShas(); + const deletedAtV2 = await readDiffy(`diffy://commit/${shas.second}/d.txt`); + assert.equal(deletedAtV2, ""); + const aliveAtV1 = await readDiffy(`diffy://commit/${shas.first}/d.txt`); + assert.match(aliveAtV1, /d\.txt v1/); + }); + + it("returns empty string for a malformed diffy URI", async () => { + const bad = await readDiffy("diffy://nonsense/whatever"); + assert.equal(bad, ""); + }); +}); diff --git a/src/test/suite/helpers.ts b/src/test/suite/helpers.ts new file mode 100644 index 0000000..0501a33 --- /dev/null +++ b/src/test/suite/helpers.ts @@ -0,0 +1,161 @@ +import { spawnSync } from "node:child_process"; +import * as path from "node:path"; +import * as vscode from "vscode"; + +export const workspaceRoot = (): string => { + const folder = vscode.workspace.workspaceFolders?.[0]; + if (folder === undefined) { + throw new Error("No workspace folder open"); + } + return folder.uri.fsPath; +}; + +export const gitOut = (args: readonly string[]): string => { + const r = spawnSync("git", [...args], { + cwd: workspaceRoot(), + encoding: "utf8", + }); + if (r.status !== 0) { + throw new Error(`git ${args.join(" ")} failed: ${r.stderr}`); + } + return r.stdout.trim(); +}; + +export interface SeedShas { + readonly first: string; + readonly second: string; + readonly third: string; +} + +export const readSeedShas = (): SeedShas => { + const lines = gitOut(["log", "--format=%H", "--reverse"]).split("\n"); + const [first, second, third] = lines; + if (first === undefined || second === undefined || third === undefined) { + throw new Error(`Expected 3 commits, found ${lines.length.toString()}`); + } + return { first, second, third }; +}; + +export const tick = async (count = 30): Promise<void> => { + await new Promise<void>((resolve) => { + let i = 0; + const step = (): void => { + i++; + if (i >= count) { + resolve(); + return; + } + setImmediate(step); + }; + setImmediate(step); + }); +}; + +const wait = async (ms: number): Promise<void> => { + await new Promise<void>((resolve) => { + setTimeout(resolve, ms); + }); +}; + +const PICKER_RENDER_MS = 900; + +export const accept = async (): Promise<void> => { + await wait(PICKER_RENDER_MS); + await vscode.commands.executeCommand("workbench.action.acceptSelectedQuickOpenItem"); +}; + +export const typeText = async (text: string): Promise<void> => { + await wait(PICKER_RENDER_MS); + await vscode.commands.executeCommand("type", { text }); +}; + +export const moveNext = async (): Promise<void> => { + await wait(PICKER_RENDER_MS); + await vscode.commands.executeCommand("workbench.action.quickOpenSelectNext"); +}; + +export const dismissQuickPick = async (): Promise<void> => { + await wait(50); + await vscode.commands.executeCommand("workbench.action.closeQuickOpen"); +}; + +export const allDiffTabs = (): vscode.Tab[] => + vscode.window.tabGroups.all.flatMap((g) => g.tabs).filter((t) => t.input instanceof vscode.TabInputTextDiff); + +export const waitForDiffTab = async ({ + timeoutMs = 8000, +}: { + timeoutMs?: number; +} = {}): Promise<vscode.Tab> => { + return await new Promise<vscode.Tab>((resolve, reject) => { + const existing = allDiffTabs()[0]; + if (existing !== undefined) { + resolve(existing); + return; + } + const timer = setTimeout(() => { + sub.dispose(); + reject(new Error("Timed out waiting for diff tab")); + }, timeoutMs); + const sub = vscode.window.tabGroups.onDidChangeTabs(() => { + const t = allDiffTabs()[0]; + if (t !== undefined) { + clearTimeout(timer); + sub.dispose(); + resolve(t); + } + }); + }); +}; + +export const closeAllEditors = async (): Promise<void> => { + await vscode.commands.executeCommand("workbench.action.closeAllEditors"); + await tick(5); +}; + +export const openFileInEditor = async (relPath: string): Promise<vscode.TextEditor> => { + const uri = vscode.Uri.file(path.join(workspaceRoot(), relPath)); + const doc = await vscode.workspace.openTextDocument(uri); + return await vscode.window.showTextDocument(doc); +}; + +export const tabInputUris = (tab: vscode.Tab): { left: vscode.Uri; right: vscode.Uri } => { + if (!(tab.input instanceof vscode.TabInputTextDiff)) { + throw new Error(`Tab is not a TabInputTextDiff: ${tab.label}`); + } + return { left: tab.input.original, right: tab.input.modified }; +}; + +interface GitApiShape { + readonly repositories: readonly { readonly rootUri: vscode.Uri }[]; + onDidOpenRepository: (handler: () => void) => { dispose: () => void }; +} + +interface GitExtensionShape { + getAPI: (version: 1) => GitApiShape; +} + +export const waitForRepoReady = async (timeoutMs = 15000): Promise<void> => { + const ext = vscode.extensions.getExtension<GitExtensionShape>("vscode.git"); + if (ext === undefined) { + throw new Error("vscode.git extension not present"); + } + if (!ext.isActive) { + await ext.activate(); + } + const api = ext.exports.getAPI(1); + if (api.repositories.length > 0) { + return; + } + await new Promise<void>((resolve, reject) => { + const timer = setTimeout(() => { + sub.dispose(); + reject(new Error("vscode.git never opened the seeded repo")); + }, timeoutMs); + const sub = api.onDidOpenRepository(() => { + clearTimeout(timer); + sub.dispose(); + resolve(); + }); + }); +}; diff --git a/src/test/suite/index.ts b/src/test/suite/index.ts new file mode 100644 index 0000000..479d42c --- /dev/null +++ b/src/test/suite/index.ts @@ -0,0 +1,26 @@ +import * as path from "node:path"; +import Mocha from "mocha"; +import { glob } from "glob"; + +export const run = async (): Promise<void> => { + const mocha = new Mocha({ + ui: "bdd", + color: true, + timeout: 60000, + reporter: "spec", + }); + const testsRoot = __dirname; + const files = await glob("**/*.test.js", { cwd: testsRoot }); + for (const f of files) { + mocha.addFile(path.resolve(testsRoot, f)); + } + await new Promise<void>((resolve, reject) => { + mocha.run((failures) => { + if (failures > 0) { + reject(new Error(`${failures.toString()} mocha test(s) failed.`)); + } else { + resolve(); + } + }); + }); +}; diff --git a/src/test/unit/format.test.ts b/src/test/unit/format.test.ts new file mode 100644 index 0000000..b86ae15 --- /dev/null +++ b/src/test/unit/format.test.ts @@ -0,0 +1,166 @@ +import { strict as assert } from "node:assert"; +import { formatRelative } from "../../ui/format/relativeTime"; +import { formatCounts, mergeChangedFilesWithStats, statusBadge } from "../../ui/format/fileItem"; +import { refTypeLabel } from "../../ui/format/refLabel"; +import type { ChangedFile, DiffStat, Ref } from "../../git/types"; + +const HOUR = 60 * 60; +const DAY = 24 * HOUR; + +describe("formatRelative", () => { + it('returns "just now" for < 1 minute', () => { + assert.equal(formatRelative(100, 100), "just now"); + assert.equal(formatRelative(100, 130), "just now"); + assert.equal(formatRelative(100, 159), "just now"); + }); + + it('clamps negative deltas (future timestamps) to "just now"', () => { + assert.equal(formatRelative(200, 100), "just now"); + }); + + it("formats minutes when < 60 min", () => { + assert.equal(formatRelative(0, 60), "1m ago"); + assert.equal(formatRelative(0, 30 * 60), "30m ago"); + assert.equal(formatRelative(0, 59 * 60), "59m ago"); + }); + + it("formats hours when < 24 h", () => { + assert.equal(formatRelative(0, HOUR), "1h ago"); + assert.equal(formatRelative(0, 12 * HOUR), "12h ago"); + assert.equal(formatRelative(0, 23 * HOUR), "23h ago"); + }); + + it("formats days for older timestamps", () => { + assert.equal(formatRelative(0, DAY), "1d ago"); + assert.equal(formatRelative(0, 7 * DAY), "7d ago"); + assert.equal(formatRelative(0, 365 * DAY), "365d ago"); + }); +}); + +describe("statusBadge", () => { + it("returns single letter for A/M/D", () => { + assert.equal(statusBadge({ status: "A", path: "x" }), "A"); + assert.equal(statusBadge({ status: "M", path: "x" }), "M"); + assert.equal(statusBadge({ status: "D", path: "x" }), "D"); + }); + + it("appends similarity for R", () => { + const r: ChangedFile = { + status: "R", + path: "new", + oldPath: "old", + similarity: 87, + }; + assert.equal(statusBadge(r), "R87"); + }); + + it("appends similarity for C", () => { + const c: ChangedFile = { + status: "C", + path: "dst", + oldPath: "src", + similarity: 100, + }; + assert.equal(statusBadge(c), "C100"); + }); + + it("falls back to 0 when similarity is missing on R/C", () => { + const r = { status: "R", path: "new", oldPath: "old" } as ChangedFile; + assert.equal(statusBadge(r), "R0"); + const c = { status: "C", path: "dst", oldPath: "src" } as ChangedFile; + assert.equal(statusBadge(c), "C0"); + }); +}); + +describe("formatCounts", () => { + it("formats numeric +N -M for non-binary stats", () => { + const s: DiffStat = { path: "x", added: 12, deleted: 3, binary: false }; + assert.equal(formatCounts(s), "+12 -3"); + }); + + it('reports "binary" for binary stats', () => { + const s: DiffStat = { path: "x", added: 0, deleted: 0, binary: true }; + assert.equal(formatCounts(s), "binary"); + }); + + it("reports 0/0 cleanly when there are no changes", () => { + const s: DiffStat = { path: "x", added: 0, deleted: 0, binary: false }; + assert.equal(formatCounts(s), "+0 -0"); + }); +}); + +describe("mergeChangedFilesWithStats", () => { + it("joins by path", () => { + const files: ChangedFile[] = [{ status: "M", path: "a.txt" }]; + const stats: DiffStat[] = [{ path: "a.txt", added: 2, deleted: 1, binary: false }]; + const r = mergeChangedFilesWithStats(files, stats); + assert.equal(r.length, 1); + assert.deepEqual(r[0], { file: files[0], stat: stats[0] }); + }); + + it("falls back to zero stats when no matching numstat row exists", () => { + const files: ChangedFile[] = [{ status: "A", path: "new.txt" }]; + const r = mergeChangedFilesWithStats(files, []); + assert.equal(r.length, 1); + assert.deepEqual(r[0]?.stat, { + path: "new.txt", + added: 0, + deleted: 0, + binary: false, + }); + }); + + it("preserves order from the name-status list", () => { + const files: ChangedFile[] = [ + { status: "M", path: "b" }, + { status: "M", path: "a" }, + ]; + const stats: DiffStat[] = [ + { path: "a", added: 1, deleted: 1, binary: false }, + { path: "b", added: 2, deleted: 2, binary: false }, + ]; + const r = mergeChangedFilesWithStats(files, stats); + assert.equal(r[0]?.file.path, "b"); + assert.equal(r[1]?.file.path, "a"); + }); + + it("handles rename entries (new path differs from old)", () => { + const file: ChangedFile = { + status: "R", + path: "b2.txt", + oldPath: "b.txt", + similarity: 100, + }; + const stat: DiffStat = { + path: "b2.txt", + oldPath: "b.txt", + added: 1, + deleted: 0, + binary: false, + }; + const r = mergeChangedFilesWithStats([file], [stat]); + assert.deepEqual(r, [{ file, stat }]); + }); +}); + +describe("refTypeLabel", () => { + it('returns "Branch" for branches', () => { + const r: Ref = { name: "main", fullName: "refs/heads/main", sha: "a", type: "branch" }; + assert.equal(refTypeLabel(r), "Branch"); + }); + + it('returns "Tag" for tags', () => { + const r: Ref = { name: "v1", fullName: "refs/tags/v1", sha: "a", type: "tag" }; + assert.equal(refTypeLabel(r), "Tag"); + }); + + it('returns "Ref" for other ref kinds', () => { + const r: Ref = { + name: "origin/main", + fullName: "refs/remotes/origin/main", + sha: "a", + type: "other", + }; + assert.equal(refTypeLabel(r), "Ref"); + }); +}); diff --git a/src/test/unit/historyItem.test.ts b/src/test/unit/historyItem.test.ts new file mode 100644 index 0000000..770174b --- /dev/null +++ b/src/test/unit/historyItem.test.ts @@ -0,0 +1,45 @@ +import { strict as assert } from "node:assert"; +import { extractHistoryItemSha } from "../../commands/historyItem"; + +const FULL_SHA = "02920d872f11ed22afacf3966cb1da30b72dfe94"; + +describe("extractHistoryItemSha", () => { + it("returns the value when arg is a non-empty string", () => { + assert.equal(extractHistoryItemSha(FULL_SHA), FULL_SHA); + assert.equal(extractHistoryItemSha("main"), "main"); + }); + + it("returns undefined for an empty string", () => { + assert.equal(extractHistoryItemSha(""), undefined); + }); + + it("returns undefined for null / undefined / numbers / booleans", () => { + assert.equal(extractHistoryItemSha(null), undefined); + assert.equal(extractHistoryItemSha(undefined), undefined); + assert.equal(extractHistoryItemSha(42), undefined); + assert.equal(extractHistoryItemSha(true), undefined); + }); + + it("extracts id directly from a SourceControlHistoryItem-shaped object", () => { + assert.equal(extractHistoryItemSha({ id: FULL_SHA }), FULL_SHA); + assert.equal(extractHistoryItemSha({ id: "main", message: "x" }), "main"); + }); + + it("unwraps a { historyItem: { id } } shape", () => { + assert.equal(extractHistoryItemSha({ historyItem: { id: FULL_SHA } }), FULL_SHA); + }); + + it("returns undefined when wrapper.historyItem is missing or not an object", () => { + assert.equal(extractHistoryItemSha({ something: "else" }), undefined); + assert.equal(extractHistoryItemSha({ historyItem: null }), undefined); + assert.equal(extractHistoryItemSha({ historyItem: "not-an-object" }), undefined); + }); + + it("returns undefined when historyItem.id is empty / missing / wrong type", () => { + assert.equal(extractHistoryItemSha({ id: "" }), undefined); + assert.equal(extractHistoryItemSha({ id: 42 }), undefined); + assert.equal(extractHistoryItemSha({ historyItem: {} }), undefined); + assert.equal(extractHistoryItemSha({ historyItem: { id: "" } }), undefined); + assert.equal(extractHistoryItemSha({ historyItem: { id: 0 } }), undefined); + }); +}); diff --git a/src/test/unit/menus.test.ts b/src/test/unit/menus.test.ts new file mode 100644 index 0000000..6b8caf3 --- /dev/null +++ b/src/test/unit/menus.test.ts @@ -0,0 +1,162 @@ +import { strict as assert } from "node:assert"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { COMMAND_IDS, MENU_IDS, MENU_WHEN, TITLE_PREFIX } from "../../constants"; +import { COMMAND_TITLES, buildMenuManifest } from "../../menus"; + +const PROPOSED_API_HISTORY_ITEM_MENU = "contribSourceControlHistoryItemMenu"; + +const repoRoot = resolve(__dirname, "..", "..", ".."); +const readPackageJson = (): Record<string, unknown> => + JSON.parse(readFileSync(resolve(repoRoot, "package.json"), "utf8")) as Record<string, unknown>; + +const getContributes = (pkg: Record<string, unknown>): Record<string, unknown> => { + const c = pkg["contributes"]; + if (typeof c !== "object" || c === null) { + throw new Error("package.json: contributes missing"); + } + return c as Record<string, unknown>; +}; + +const getMenus = (contributes: Record<string, unknown>): Record<string, unknown> => { + const m = contributes["menus"]; + if (typeof m !== "object" || m === null) { + throw new Error("package.json: contributes.menus missing"); + } + return m as Record<string, unknown>; +}; + +const titlePrefixPattern = new RegExp(`^${TITLE_PREFIX.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")} `); + +describe("menu manifest — single source of truth", () => { + it("every COMMAND_ID has a human title in COMMAND_TITLES", () => { + for (const id of Object.values(COMMAND_IDS)) { + const title = COMMAND_TITLES[id]; + assert.ok(title !== undefined, `missing title for command ${id}`); + assert.match(title, titlePrefixPattern, `title for ${id} must start with "${TITLE_PREFIX} "`); + } + }); + + it("manifest contributes commands to the four user-discoverable menus", () => { + const manifest = buildMenuManifest(); + const menuIds = Object.keys(manifest.menus); + assert.deepEqual( + menuIds.sort(), + [ + MENU_IDS.editorTitleContext, + MENU_IDS.explorerContext, + MENU_IDS.scmHistoryItem, + MENU_IDS.scmResourceState, + ].sort(), + "menu IDs must match VSCode contribution points (note: singular scm/historyItem/context)" + ); + }); + + it("SCM history item menu contains all five commit-level Diffy commands", () => { + const entries = buildMenuManifest().menus[MENU_IDS.scmHistoryItem]; + assert.ok(entries !== undefined); + const cmds = entries.map((e) => e.command); + assert.deepEqual(cmds, [ + COMMAND_IDS.compareWith, + COMMAND_IDS.compareWithWorkingCopy, + COMMAND_IDS.compareWithPrevious, + COMMAND_IDS.compareWithBranch, + COMMAND_IDS.compareWithTag, + ]); + for (const e of entries) { + assert.equal(e.when, MENU_WHEN.scmGit); + assert.match(e.group, /^diffy@\d+$/); + } + }); + + it("editor/title/context and explorer/context and scm/resourceState/context all expose the three file-level Diffy commands", () => { + const m = buildMenuManifest().menus; + const fileLevel = [ + COMMAND_IDS.compareFileWithCommit, + COMMAND_IDS.compareFileWithBranch, + COMMAND_IDS.compareFileWithTag, + ]; + for (const menuId of [MENU_IDS.editorTitleContext, MENU_IDS.explorerContext, MENU_IDS.scmResourceState]) { + const entries = m[menuId]; + assert.ok(entries !== undefined, `missing ${menuId}`); + assert.deepEqual( + entries.map((e) => e.command), + fileLevel, + `${menuId} must list the three file-level commands in order` + ); + } + }); + + it("commandPalette block hides commit-level entries (they need a history-item arg)", () => { + const palette = buildMenuManifest().commandPalette; + const hidden = palette.map((e) => e.command); + for (const cmd of [ + COMMAND_IDS.compareWith, + COMMAND_IDS.compareWithWorkingCopy, + COMMAND_IDS.compareWithPrevious, + COMMAND_IDS.compareWithBranch, + COMMAND_IDS.compareWithTag, + COMMAND_IDS.showLogs, + ]) { + assert.ok(hidden.includes(cmd), `${cmd} should be hidden from the palette`); + } + for (const e of palette) { + assert.equal(e.when, MENU_WHEN.never); + } + }); + + it("package.json contributes.menus is byte-equal to what the manifest would write", () => { + const manifest = buildMenuManifest(); + const pkg = readPackageJson(); + const contributes = getContributes(pkg); + const pkgMenus = getMenus(contributes); + + for (const [menuId, entries] of Object.entries(manifest.menus)) { + const written = pkgMenus[menuId]; + assert.deepEqual( + written, + entries.map((e) => ({ + command: e.command, + when: e.when, + group: e.group, + })), + `package.json ${menuId} drifted from src/menus.ts — run 'npm run sync:menus'` + ); + } + + assert.deepEqual( + pkgMenus[MENU_IDS.commandPalette], + manifest.commandPalette.map((e) => ({ command: e.command, when: e.when })), + "package.json commandPalette block drifted from src/menus.ts" + ); + }); + + it("package.json contributes.commands lists every COMMAND_TITLES entry exactly", () => { + const pkg = readPackageJson(); + const contributes = getContributes(pkg); + const cmds = contributes["commands"] as readonly { + readonly command: string; + readonly title: string; + }[]; + assert.ok(Array.isArray(cmds)); + + const expected = Object.entries(COMMAND_TITLES).map(([command, title]) => ({ + command, + title, + })); + assert.deepEqual(cmds, expected, "package.json commands drifted — run npm run sync:menus"); + }); + + it("package.json declares the contribSourceControlHistoryItemMenu proposed API (required for the commit-row context menu)", () => { + const pkg = readPackageJson(); + const proposals = pkg["enabledApiProposals"]; + assert.ok( + Array.isArray(proposals), + "package.json must declare enabledApiProposals so SCM Graph commit menus actually appear" + ); + assert.ok( + (proposals as readonly string[]).includes(PROPOSED_API_HISTORY_ITEM_MENU), + `enabledApiProposals must include ${PROPOSED_API_HISTORY_ITEM_MENU}` + ); + }); +}); diff --git a/src/test/unit/parsers.test.ts b/src/test/unit/parsers.test.ts new file mode 100644 index 0000000..15f0edd --- /dev/null +++ b/src/test/unit/parsers.test.ts @@ -0,0 +1,394 @@ +import { strict as assert } from "node:assert"; +import { GIT_ERROR_KINDS, REF_TYPES } from "../../constants"; +import { parseLog, parseNameStatus, parseNumstat, parseRefs } from "../../git/parsers"; +import { expectErr, expectOk } from "../../result"; + +const NUL = "\x00"; +const TAB = "\t"; + +const logRecord = (sha: string, short: string, author: string, at: string, subject: string): string => + [sha, short, author, at, subject].join(NUL); + +describe("parseLog", () => { + it("returns empty array for empty input", () => { + const r = parseLog(""); + expectOk(r); + assert.deepEqual(r.value, []); + }); + + it("parses a single commit with trailing NUL", () => { + const stdout = logRecord("abc123def456", "abc123d", "Alice", "1700000000", "init") + NUL; + const r = parseLog(stdout); + expectOk(r); + assert.deepEqual(r.value, [ + { + sha: "abc123def456", + shortSha: "abc123d", + author: "Alice", + authorTime: 1700000000, + subject: "init", + }, + ]); + }); + + it("parses two commits separated by NUL", () => { + const a = logRecord("aaaa1111", "aaaa111", "Alice", "1700000000", "first"); + const b = logRecord("bbbb2222", "bbbb222", "Bob", "1700000100", "second commit"); + const r = parseLog(`${a}${NUL}${b}${NUL}`); + expectOk(r); + assert.deepEqual(r.value, [ + { + sha: "aaaa1111", + shortSha: "aaaa111", + author: "Alice", + authorTime: 1700000000, + subject: "first", + }, + { + sha: "bbbb2222", + shortSha: "bbbb222", + author: "Bob", + authorTime: 1700000100, + subject: "second commit", + }, + ]); + }); + + it("parses without trailing NUL when field count matches", () => { + const stdout = logRecord("abcd1234", "abcd123", "Alice", "1700000000", "init"); + const r = parseLog(stdout); + expectOk(r); + assert.equal(r.value.length, 1); + }); + + it("preserves an empty subject", () => { + const stdout = logRecord("abcd1234", "abcd123", "Alice", "1700000000", "") + NUL; + const r = parseLog(stdout); + expectOk(r); + assert.deepEqual(r.value, [ + { + sha: "abcd1234", + shortSha: "abcd123", + author: "Alice", + authorTime: 1700000000, + subject: "", + }, + ]); + }); + + it("preserves unicode and spaces in author/subject", () => { + const stdout = logRecord("abcd1234", "abcd123", "Élise Müller", "1700000000", "feat: 日本語 fix ⚡") + NUL; + const r = parseLog(stdout); + expectOk(r); + assert.deepEqual(r.value, [ + { + sha: "abcd1234", + shortSha: "abcd123", + author: "Élise Müller", + authorTime: 1700000000, + subject: "feat: 日本語 fix ⚡", + }, + ]); + }); + + it("errors when field count is not a multiple of 5", () => { + const r = parseLog(`a${NUL}b${NUL}c${NUL}d${NUL}`); + expectErr(r); + assert.equal(r.error.kind, GIT_ERROR_KINDS.parseError); + assert.match(r.error.message, /multiple of 5/); + }); + + it("errors when timestamp is not numeric", () => { + const stdout = logRecord("a", "a", "Alice", "NOT_A_NUMBER", "x") + NUL; + const r = parseLog(stdout); + expectErr(r); + assert.match(r.error.message, /timestamp/); + }); + + it("errors when timestamp is empty", () => { + const stdout = logRecord("a", "a", "Alice", "", "x") + NUL; + const r = parseLog(stdout); + expectErr(r); + assert.match(r.error.message, /timestamp/); + }); + + it("errors when timestamp has leading/trailing whitespace", () => { + const stdout = logRecord("a", "a", "Alice", " 1700000000", "x") + NUL; + const r = parseLog(stdout); + expectErr(r); + }); +}); + +describe("parseNameStatus", () => { + it("returns empty array for empty input", () => { + const r = parseNameStatus(""); + expectOk(r); + assert.deepEqual(r.value, []); + }); + + it("parses an Added file", () => { + const r = parseNameStatus(`A${NUL}new.txt${NUL}`); + expectOk(r); + assert.deepEqual(r.value, [{ status: "A", path: "new.txt" }]); + }); + + it("parses a Modified file", () => { + const r = parseNameStatus(`M${NUL}a.txt${NUL}`); + expectOk(r); + assert.deepEqual(r.value, [{ status: "M", path: "a.txt" }]); + }); + + it("parses a Deleted file", () => { + const r = parseNameStatus(`D${NUL}gone.txt${NUL}`); + expectOk(r); + assert.deepEqual(r.value, [{ status: "D", path: "gone.txt" }]); + }); + + it("parses a Rename with similarity", () => { + const r = parseNameStatus(`R100${NUL}old.txt${NUL}new.txt${NUL}`); + expectOk(r); + assert.deepEqual(r.value, [{ status: "R", path: "new.txt", oldPath: "old.txt", similarity: 100 }]); + }); + + it("parses a Copy with similarity", () => { + const r = parseNameStatus(`C75${NUL}src.txt${NUL}dst.txt${NUL}`); + expectOk(r); + assert.deepEqual(r.value, [{ status: "C", path: "dst.txt", oldPath: "src.txt", similarity: 75 }]); + }); + + it("parses a mixed batch", () => { + const stdout = + `A${NUL}added.txt${NUL}` + + `M${NUL}modified.txt${NUL}` + + `D${NUL}deleted.txt${NUL}` + + `R98${NUL}was.txt${NUL}now.txt${NUL}` + + `C50${NUL}src.txt${NUL}dst.txt${NUL}`; + const r = parseNameStatus(stdout); + expectOk(r); + assert.deepEqual(r.value, [ + { status: "A", path: "added.txt" }, + { status: "M", path: "modified.txt" }, + { status: "D", path: "deleted.txt" }, + { status: "R", path: "now.txt", oldPath: "was.txt", similarity: 98 }, + { status: "C", path: "dst.txt", oldPath: "src.txt", similarity: 50 }, + ]); + }); + + it("preserves spaces, unicode, and quotes in paths", () => { + const stdout = `M${NUL}path with spaces/日本語 "name".txt${NUL}`; + const r = parseNameStatus(stdout); + expectOk(r); + assert.deepEqual(r.value, [{ status: "M", path: 'path with spaces/日本語 "name".txt' }]); + }); + + it("errors on unknown status letter", () => { + const r = parseNameStatus(`X${NUL}a.txt${NUL}`); + expectErr(r); + assert.match(r.error.message, /unknown status/); + }); + + it("errors when A/M/D has trailing chars", () => { + const r = parseNameStatus(`A100${NUL}a.txt${NUL}`); + expectErr(r); + assert.match(r.error.message, /extra chars/); + }); + + it("errors on rename without similarity digits", () => { + const r = parseNameStatus(`R${NUL}old${NUL}new${NUL}`); + expectErr(r); + assert.match(r.error.message, /similarity/); + }); + + it("errors on rename with non-digit similarity", () => { + const r = parseNameStatus(`R1x0${NUL}old${NUL}new${NUL}`); + expectErr(r); + assert.match(r.error.message, /similarity/); + }); + + it("errors on truncated rename (missing new path)", () => { + const r = parseNameStatus(`R100${NUL}old.txt${NUL}`); + expectErr(r); + assert.match(r.error.message, /missing paths/); + }); + + it("errors when simple status is missing its path", () => { + const r = parseNameStatus(`M${NUL}`); + expectErr(r); + }); +}); + +describe("parseNumstat", () => { + it("returns empty array for empty input", () => { + const r = parseNumstat(""); + expectOk(r); + assert.deepEqual(r.value, []); + }); + + it("parses a regular numstat record", () => { + const r = parseNumstat(`12${TAB}3${TAB}hello.txt${NUL}`); + expectOk(r); + assert.deepEqual(r.value, [{ path: "hello.txt", added: 12, deleted: 3, binary: false }]); + }); + + it('parses a binary marker ("-" / "-")', () => { + const r = parseNumstat(`-${TAB}-${TAB}image.png${NUL}`); + expectOk(r); + assert.deepEqual(r.value, [{ path: "image.png", added: 0, deleted: 0, binary: true }]); + }); + + it("parses a rename record", () => { + const r = parseNumstat(`3${TAB}1${TAB}${NUL}old.txt${NUL}new.txt${NUL}`); + expectOk(r); + assert.deepEqual(r.value, [ + { + path: "new.txt", + oldPath: "old.txt", + added: 3, + deleted: 1, + binary: false, + }, + ]); + }); + + it("parses a mixed batch with regular, binary, and rename", () => { + const stdout = `2${TAB}1${TAB}a.txt${NUL}-${TAB}-${TAB}logo.png${NUL}5${TAB}0${TAB}${NUL}old.txt${NUL}new.txt${NUL}`; + const r = parseNumstat(stdout); + expectOk(r); + assert.deepEqual(r.value, [ + { path: "a.txt", added: 2, deleted: 1, binary: false }, + { path: "logo.png", added: 0, deleted: 0, binary: true }, + { + path: "new.txt", + oldPath: "old.txt", + added: 5, + deleted: 0, + binary: false, + }, + ]); + }); + + it("preserves spaces and unicode in paths", () => { + const r = parseNumstat(`1${TAB}1${TAB}dir with spaces/日本語.txt${NUL}`); + expectOk(r); + assert.deepEqual(r.value, [ + { + path: "dir with spaces/日本語.txt", + added: 1, + deleted: 1, + binary: false, + }, + ]); + }); + + it("errors when tab count is wrong", () => { + const r = parseNumstat(`1${TAB}2${NUL}`); + expectErr(r); + assert.match(r.error.message, /3 tab-fields/); + }); + + it("errors when counts are non-numeric (and not binary marker)", () => { + const r = parseNumstat(`abc${TAB}def${TAB}p${NUL}`); + expectErr(r); + assert.match(r.error.message, /non-numeric/); + }); + + it("errors on truncated rename", () => { + const r = parseNumstat(`3${TAB}1${TAB}${NUL}old.txt${NUL}`); + expectErr(r); + assert.match(r.error.message, /rename missing paths/); + }); +}); + +describe("parseRefs", () => { + it("returns empty array for empty input", () => { + const r = parseRefs(""); + expectOk(r); + assert.deepEqual(r.value, []); + }); + + it("parses a branch ref (newline-terminated record)", () => { + const r = parseRefs(`refs/heads/main${NUL}main${NUL}abc1234\n`); + expectOk(r); + assert.deepEqual(r.value, [ + { + name: "main", + fullName: "refs/heads/main", + sha: "abc1234", + type: REF_TYPES.branch, + }, + ]); + }); + + it("parses a tag ref", () => { + const r = parseRefs(`refs/tags/v1.0${NUL}v1.0${NUL}def5678\n`); + expectOk(r); + assert.deepEqual(r.value, [ + { + name: "v1.0", + fullName: "refs/tags/v1.0", + sha: "def5678", + type: REF_TYPES.tag, + }, + ]); + }); + + it("classifies a non-branch, non-tag ref as other", () => { + const r = parseRefs(`refs/remotes/origin/main${NUL}origin/main${NUL}abc1234\n`); + expectOk(r); + assert.deepEqual(r.value, [ + { + name: "origin/main", + fullName: "refs/remotes/origin/main", + sha: "abc1234", + type: REF_TYPES.other, + }, + ]); + }); + + it("parses a mixed batch of branches and tags", () => { + const stdout = + `refs/heads/main${NUL}main${NUL}aaa\n` + + `refs/heads/dev${NUL}dev${NUL}bbb\n` + + `refs/tags/v1.0${NUL}v1.0${NUL}ccc\n`; + const r = parseRefs(stdout); + expectOk(r); + assert.deepEqual(r.value, [ + { + name: "main", + fullName: "refs/heads/main", + sha: "aaa", + type: REF_TYPES.branch, + }, + { + name: "dev", + fullName: "refs/heads/dev", + sha: "bbb", + type: REF_TYPES.branch, + }, + { + name: "v1.0", + fullName: "refs/tags/v1.0", + sha: "ccc", + type: REF_TYPES.tag, + }, + ]); + }); + + it("errors when a line has fewer than 3 NUL-separated fields", () => { + const r = parseRefs(`refs/heads/main${NUL}main\n`); + expectErr(r); + assert.match(r.error.message, /3 NUL-separated/); + }); + + it("errors when a ref field is empty", () => { + const r = parseRefs(`refs/heads/main${NUL}${NUL}abc\n`); + expectErr(r); + assert.match(r.error.message, /empty field/); + }); + + it("ignores trailing blank lines", () => { + const r = parseRefs(`refs/heads/main${NUL}main${NUL}abc\n\n\n`); + expectOk(r); + assert.equal(r.value.length, 1); + }); +}); diff --git a/src/test/unit/refFilter.test.ts b/src/test/unit/refFilter.test.ts new file mode 100644 index 0000000..7391aeb --- /dev/null +++ b/src/test/unit/refFilter.test.ts @@ -0,0 +1,96 @@ +import { strict as assert } from "node:assert"; +import { REF_TYPES } from "../../constants"; +import { filterRefs } from "../../ui/format/refFilter"; +import type { Ref } from "../../git/types"; + +const branch = (name: string): Ref => ({ + name, + fullName: `refs/heads/${name}`, + sha: `sha-${name}`, + type: REF_TYPES.branch, +}); + +const tag = (name: string): Ref => ({ + name, + fullName: `refs/tags/${name}`, + sha: `sha-${name}`, + type: REF_TYPES.tag, +}); + +const refs: readonly Ref[] = [branch("main"), branch("agentpmo"), branch("feature"), tag("v0.1.0"), tag("agentpmo")]; + +describe("filterRefs", () => { + it("returns every ref untouched when no filters supplied", () => { + const r = filterRefs({ refs }); + assert.equal(r.length, refs.length); + assert.deepEqual( + r.map((x) => x.fullName), + refs.map((x) => x.fullName) + ); + }); + + it("keeps only branches when type=branch", () => { + const r = filterRefs({ refs, type: REF_TYPES.branch }); + assert.deepEqual( + r.map((x) => x.name), + ["main", "agentpmo", "feature"] + ); + }); + + it("keeps only tags when type=tag", () => { + const r = filterRefs({ refs, type: REF_TYPES.tag }); + assert.deepEqual( + r.map((x) => x.name), + ["v0.1.0", "agentpmo"] + ); + }); + + it("excludes the named branch (current HEAD) from the unfiltered list", () => { + const r = filterRefs({ refs, excludeBranchName: "agentpmo" }); + assert.equal( + r.find((x) => x.type === REF_TYPES.branch && x.name === "agentpmo"), + undefined, + "branch named agentpmo must be excluded" + ); + assert.ok( + r.find((x) => x.type === REF_TYPES.tag && x.name === "agentpmo"), + "tag named agentpmo must NOT be excluded — name collision is fine across ref types" + ); + assert.deepEqual( + r.map((x) => x.fullName), + ["refs/heads/main", "refs/heads/feature", "refs/tags/v0.1.0", "refs/tags/agentpmo"] + ); + }); + + it("excludes the named branch under a branch-type filter (the picker case the user reported)", () => { + const r = filterRefs({ + refs, + type: REF_TYPES.branch, + excludeBranchName: "agentpmo", + }); + assert.deepEqual( + r.map((x) => x.name), + ["main", "feature"] + ); + }); + + it("excludeBranchName=undefined is a no-op (detached HEAD case)", () => { + const r = filterRefs({ refs, type: REF_TYPES.branch, excludeBranchName: undefined }); + assert.deepEqual( + r.map((x) => x.name), + ["main", "agentpmo", "feature"] + ); + }); + + it("excludeBranchName never removes tags even when the tag name matches", () => { + const r = filterRefs({ + refs, + type: REF_TYPES.tag, + excludeBranchName: "agentpmo", + }); + assert.deepEqual( + r.map((x) => x.name), + ["v0.1.0", "agentpmo"] + ); + }); +}); diff --git a/src/test/unit/repoMatch.test.ts b/src/test/unit/repoMatch.test.ts new file mode 100644 index 0000000..ad6845a --- /dev/null +++ b/src/test/unit/repoMatch.test.ts @@ -0,0 +1,82 @@ +import { strict as assert } from "node:assert"; +import { findRepoForUri, matchRepoByFsPath } from "../../git/repoMatch"; + +interface UriLike { + readonly fsPath: string; +} +interface RepoLike { + readonly rootUri: UriLike; +} + +const repoAt = (fsPath: string): RepoLike => ({ rootUri: { fsPath } }); + +describe("matchRepoByFsPath", () => { + it("returns undefined for an empty repository list", () => { + const result = matchRepoByFsPath<RepoLike>([], "/anywhere"); + assert.equal(result, undefined); + }); + + it("returns the only repository when the target is inside its root", () => { + const r = repoAt("/Users/me/repo"); + assert.strictEqual(matchRepoByFsPath([r], "/Users/me/repo/file.txt"), r); + }); + + it("returns undefined when the target is outside every repository", () => { + const r = repoAt("/Users/me/repo"); + assert.equal(matchRepoByFsPath([r], "/Users/elsewhere/file.txt"), undefined); + }); + + it("prefers the longest matching root for nested repositories", () => { + const outer = repoAt("/Users/me/repo"); + const inner = repoAt("/Users/me/repo/sub/inner"); + const result = matchRepoByFsPath([outer, inner], "/Users/me/repo/sub/inner/file.txt"); + assert.strictEqual(result, inner); + }); + + it("order of the list does not affect the longest-match result", () => { + const outer = repoAt("/Users/me/repo"); + const inner = repoAt("/Users/me/repo/sub/inner"); + const result = matchRepoByFsPath([inner, outer], "/Users/me/repo/sub/inner/file.txt"); + assert.strictEqual(result, inner); + }); +}); + +describe("findRepoForUri", () => { + it("delegates to api.getRepository when available", () => { + const repo = repoAt("/Users/me/repo"); + const uri = { fsPath: "/Users/me/repo/file.txt" }; + const api = { + repositories: [] as readonly RepoLike[], + getRepository: (_u: UriLike) => repo, + }; + const result = findRepoForUri(api, uri); + assert.strictEqual(result, repo); + }); + + it("falls back to fsPath matching when getRepository is absent", () => { + const repo = repoAt("/Users/me/repo"); + const uri = { fsPath: "/Users/me/repo/file.txt" }; + const api = { repositories: [repo] }; + const result = findRepoForUri(api, uri); + assert.strictEqual(result, repo); + }); + + it("returns undefined when getRepository returns null", () => { + const uri = { fsPath: "/Users/me/repo/file.txt" }; + const api = { + repositories: [] as readonly RepoLike[], + getRepository: (_u: UriLike) => null, + }; + const result = findRepoForUri(api, uri); + assert.equal(result, undefined); + }); + + it("falls back to longest-prefix matching across multiple roots", () => { + const a = repoAt("/Users/me/repo-a"); + const b = repoAt("/Users/me/repo-b"); + const api = { repositories: [a, b] }; + assert.strictEqual(findRepoForUri(api, { fsPath: "/Users/me/repo-b/x" }), b); + assert.strictEqual(findRepoForUri(api, { fsPath: "/Users/me/repo-a/y" }), a); + assert.equal(findRepoForUri(api, { fsPath: "/Users/me/somewhere-else" }), undefined); + }); +}); diff --git a/src/test/unit/result.test.ts b/src/test/unit/result.test.ts new file mode 100644 index 0000000..f04a63b --- /dev/null +++ b/src/test/unit/result.test.ts @@ -0,0 +1,137 @@ +import { strict as assert } from "node:assert"; +import { ok, err, isOk, isErr, map, andThen, unwrapOr, expectOk, expectErr, type Result } from "../../result"; + +describe("Result", () => { + describe("ok / err constructors", () => { + it("ok wraps the value and discriminates as ok=true", () => { + const r = ok(42); + expectOk(r); + assert.equal(r.value, 42); + }); + + it("err wraps the error and discriminates as ok=false", () => { + const r = err("boom"); + expectErr(r); + assert.equal(r.error, "boom"); + }); + + it("ok preserves reference identity of the wrapped value", () => { + const obj = { x: 1 }; + const r = ok(obj); + expectOk(r); + assert.strictEqual(r.value, obj); + }); + + it("err preserves reference identity of the wrapped error", () => { + const e = new Error("x"); + const r = err(e); + expectErr(r); + assert.strictEqual(r.error, e); + }); + }); + + describe("isOk / isErr", () => { + it("isOk returns true on Ok and false on Err", () => { + assert.equal(isOk(ok(1)), true); + assert.equal(isOk(err("x")), false); + }); + + it("isErr returns true on Err and false on Ok", () => { + assert.equal(isErr(err("x")), true); + assert.equal(isErr(ok(1)), false); + }); + + it("isOk narrows the union for the compiler (value is accessible)", () => { + const r: Result<number, string> = ok(7); + if (isOk(r)) { + assert.equal(r.value + 1, 8); + } else { + assert.fail("expected ok branch"); + } + }); + + it("isErr narrows the union for the compiler (error is accessible)", () => { + const r: Result<number, string> = err("nope"); + if (isErr(r)) { + assert.equal(r.error.toUpperCase(), "NOPE"); + } else { + assert.fail("expected err branch"); + } + }); + }); + + describe("map", () => { + it("applies the function on Ok", () => { + const r = map(ok(2), (n) => n * 3); + expectOk(r); + assert.equal(r.value, 6); + }); + + it("passes Err through unchanged", () => { + const original: Result<number, string> = err("bad"); + const r = map<number, number, string>(original, (n) => n * 3); + expectErr(r); + assert.equal(r.error, "bad"); + }); + + it("supports type-changing maps", () => { + const r = map(ok(5), (n) => `n=${n.toString()}`); + expectOk(r); + assert.equal(r.value, "n=5"); + }); + }); + + describe("andThen", () => { + it("chains Ok into another Ok", () => { + const r = andThen(ok(2), (n) => ok(n + 1)); + expectOk(r); + assert.equal(r.value, 3); + }); + + it("chains Ok into an Err and propagates it", () => { + const start: Result<number, string> = ok(2); + const r = andThen(start, (_n): Result<number, string> => err("downstream")); + expectErr(r); + assert.equal(r.error, "downstream"); + }); + + it("short-circuits on Err without invoking the function", () => { + let called = false; + const start: Result<number, string> = err("upstream"); + const r = andThen(start, (n) => { + called = true; + return ok(n); + }); + assert.equal(called, false); + expectErr(r); + assert.equal(r.error, "upstream"); + }); + }); + + describe("unwrapOr", () => { + it("returns the Ok value", () => { + assert.equal(unwrapOr(ok(10), 99), 10); + }); + + it("returns the fallback on Err", () => { + const r: Result<number, string> = err("x"); + assert.equal(unwrapOr(r, 99), 99); + }); + }); + + describe("expectOk / expectErr negative paths", () => { + it("expectOk throws with the JSON-serialized error when given an Err", () => { + const r: Result<number, { kind: string }> = err({ kind: "explode" }); + assert.throws(() => { + expectOk(r); + }, /expected Ok, got Err: \{"kind":"explode"\}/); + }); + + it("expectErr throws with the JSON-serialized value when given an Ok", () => { + const r: Result<{ n: number }, string> = ok({ n: 7 }); + assert.throws(() => { + expectErr(r); + }, /expected Err, got Ok: \{"n":7\}/); + }); + }); +}); diff --git a/src/test/unit/state.test.ts b/src/test/unit/state.test.ts new file mode 100644 index 0000000..e1cd72b --- /dev/null +++ b/src/test/unit/state.test.ts @@ -0,0 +1,70 @@ +import { strict as assert } from "node:assert"; +import { isLastComparison } from "../../state"; + +const validRevA = { kind: "commit", sha: "abcdef0" }; +const validRevB = { kind: "workingCopy" }; + +describe("isLastComparison", () => { + it("accepts a valid record with commit revA + workingCopy revB", () => { + const value = { revA: validRevA, revB: validRevB, repoRoot: "/path/to/repo" }; + assert.equal(isLastComparison(value), true); + }); + + it("accepts a valid record with two commit revs", () => { + const value = { + revA: { kind: "commit", sha: "a" }, + revB: { kind: "commit", sha: "b" }, + repoRoot: "/path", + }; + assert.equal(isLastComparison(value), true); + }); + + it("rejects null / undefined / non-object", () => { + assert.equal(isLastComparison(null), false); + assert.equal(isLastComparison(undefined), false); + assert.equal(isLastComparison("string"), false); + assert.equal(isLastComparison(42), false); + assert.equal(isLastComparison(true), false); + assert.equal(isLastComparison([]), false); // arrays are objects but missing fields + }); + + it("rejects a record missing repoRoot", () => { + const value = { revA: validRevA, revB: validRevB }; + assert.equal(isLastComparison(value), false); + }); + + it("rejects a record where repoRoot is not a string", () => { + const value = { revA: validRevA, revB: validRevB, repoRoot: 42 }; + assert.equal(isLastComparison(value), false); + }); + + it("rejects a record missing revA", () => { + const value = { revB: validRevB, repoRoot: "/p" }; + assert.equal(isLastComparison(value), false); + }); + + it("rejects a record where revA is null", () => { + const value = { revA: null, revB: validRevB, repoRoot: "/p" }; + assert.equal(isLastComparison(value), false); + }); + + it("rejects a record where revA is not an object", () => { + const value = { revA: "commit", revB: validRevB, repoRoot: "/p" }; + assert.equal(isLastComparison(value), false); + }); + + it("rejects a record missing revB", () => { + const value = { revA: validRevA, repoRoot: "/p" }; + assert.equal(isLastComparison(value), false); + }); + + it("rejects a record where revB is null", () => { + const value = { revA: validRevA, revB: null, repoRoot: "/p" }; + assert.equal(isLastComparison(value), false); + }); + + it("rejects a record where revB is not an object", () => { + const value = { revA: validRevA, revB: "workingCopy", repoRoot: "/p" }; + assert.equal(isLastComparison(value), false); + }); +}); diff --git a/src/test/unit/uri.test.ts b/src/test/unit/uri.test.ts new file mode 100644 index 0000000..4511cdd --- /dev/null +++ b/src/test/unit/uri.test.ts @@ -0,0 +1,157 @@ +import { strict as assert } from "node:assert"; +import { REV_KINDS, URI_PARSE_ERROR_KINDS } from "../../constants"; +import { buildDiffyUri, parseDiffyUri } from "../../ui/uri"; +import { expectErr, expectOk } from "../../result"; +import type { DiffyAddressableRev } from "../../git/types"; + +const SHA = "abc1234def567890fedcba0987654321aaaaaaaa"; + +const commit = (sha: string): DiffyAddressableRev => ({ + kind: REV_KINDS.commit, + sha, +}); +const index = (): DiffyAddressableRev => ({ kind: REV_KINDS.index }); + +const roundTrip = (rev: DiffyAddressableRev, path: string): void => { + const uri = buildDiffyUri(rev, path); + const parsed = parseDiffyUri(uri); + expectOk(parsed); + assert.deepEqual(parsed.value.rev, rev); + assert.equal(parsed.value.path, path); +}; + +describe("buildDiffyUri", () => { + it("builds a commit URI with sha and path", () => { + const uri = buildDiffyUri(commit(SHA), "src/file.ts"); + assert.equal(uri, `diffy://commit/${SHA}/src/file.ts`); + }); + + it("builds an index URI without sha", () => { + const uri = buildDiffyUri(index(), "src/file.ts"); + assert.equal(uri, "diffy://index/src/file.ts"); + }); + + it("percent-encodes spaces in path segments", () => { + const uri = buildDiffyUri(commit(SHA), "a folder/b file.ts"); + assert.equal(uri, `diffy://commit/${SHA}/a%20folder/b%20file.ts`); + }); + + it("percent-encodes ? and # so they are not parsed as query/fragment", () => { + const uri = buildDiffyUri(commit(SHA), "weird?name#here.ts"); + assert.equal(uri, `diffy://commit/${SHA}/weird%3Fname%23here.ts`); + }); + + it("preserves forward slashes as segment separators", () => { + const uri = buildDiffyUri(commit(SHA), "a/b/c/d.ts"); + assert.equal(uri, `diffy://commit/${SHA}/a/b/c/d.ts`); + }); + + it("encodes unicode characters in path segments", () => { + const uri = buildDiffyUri(commit(SHA), "src/日本語.ts"); + const encoded = encodeURIComponent("日本語"); + assert.equal(uri, `diffy://commit/${SHA}/src/${encoded}.ts`); + }); +}); + +describe("parseDiffyUri", () => { + it("parses a commit URI and decodes the path", () => { + const r = parseDiffyUri(`diffy://commit/${SHA}/src/file.ts`); + expectOk(r); + assert.deepEqual(r.value.rev, { kind: REV_KINDS.commit, sha: SHA }); + assert.equal(r.value.path, "src/file.ts"); + }); + + it("parses an index URI and decodes the path", () => { + const r = parseDiffyUri("diffy://index/src/file.ts"); + expectOk(r); + assert.deepEqual(r.value.rev, { kind: REV_KINDS.index }); + assert.equal(r.value.path, "src/file.ts"); + }); + + it("rejects a non-diffy scheme", () => { + const r = parseDiffyUri("file:///some/path.ts"); + expectErr(r); + assert.equal(r.error.kind, URI_PARSE_ERROR_KINDS.invalidScheme); + }); + + it("rejects an unknown authority", () => { + const r = parseDiffyUri(`diffy://stash/${SHA}/x.ts`); + expectErr(r); + assert.equal(r.error.kind, URI_PARSE_ERROR_KINDS.invalidAuthority); + }); + + it("rejects a commit URI with no path after the sha", () => { + const r = parseDiffyUri(`diffy://commit/${SHA}/`); + expectErr(r); + assert.equal(r.error.kind, URI_PARSE_ERROR_KINDS.emptyPath); + }); + + it("rejects a commit URI with no sha", () => { + const r = parseDiffyUri("diffy://commit//file.ts"); + expectErr(r); + assert.equal(r.error.kind, URI_PARSE_ERROR_KINDS.missingSha); + }); + + it("rejects an index URI with no path", () => { + const r = parseDiffyUri("diffy://index/"); + expectErr(r); + assert.equal(r.error.kind, URI_PARSE_ERROR_KINDS.emptyPath); + }); + + it("rejects a malformed URI with no scheme separator", () => { + const r = parseDiffyUri("not-a-uri"); + expectErr(r); + assert.equal(r.error.kind, URI_PARSE_ERROR_KINDS.malformed); + }); + + it("rejects a URI missing the authority/path slash", () => { + const r = parseDiffyUri("diffy://commit"); + expectErr(r); + assert.equal(r.error.kind, URI_PARSE_ERROR_KINDS.malformed); + }); + + it("rejects a URI with malformed percent encoding", () => { + const r = parseDiffyUri(`diffy://commit/${SHA}/bad%ZZpath.ts`); + expectErr(r); + assert.equal(r.error.kind, URI_PARSE_ERROR_KINDS.badEncoding); + }); + + it("rejects an index URI with malformed percent encoding", () => { + const r = parseDiffyUri("diffy://index/bad%ZZpath.ts"); + expectErr(r); + assert.equal(r.error.kind, URI_PARSE_ERROR_KINDS.badEncoding); + }); +}); + +describe("buildDiffyUri ↔ parseDiffyUri round-trip", () => { + it("round-trips a simple commit URI", () => { + roundTrip(commit(SHA), "src/file.ts"); + }); + + it("round-trips a simple index URI", () => { + roundTrip(index(), "src/file.ts"); + }); + + it("round-trips paths with spaces", () => { + roundTrip(commit(SHA), "a folder/b file.ts"); + roundTrip(index(), "a folder/b file.ts"); + }); + + it("round-trips paths with unicode", () => { + roundTrip(commit(SHA), "src/日本語/файл.ts"); + roundTrip(index(), "src/日本語/файл.ts"); + }); + + it("round-trips paths containing # and ?", () => { + roundTrip(commit(SHA), "weird?name#here.ts"); + roundTrip(index(), "q=1&r=2#frag.ts"); + }); + + it("round-trips paths with quotes and brackets", () => { + roundTrip(commit(SHA), 'a "b" [c] (d).ts'); + }); + + it("round-trips deeply nested paths", () => { + roundTrip(commit(SHA), "a/b/c/d/e/f/g/h/i/j.ts"); + }); +}); diff --git a/src/ui/CommitPicker.ts b/src/ui/CommitPicker.ts new file mode 100644 index 0000000..7c28d3b --- /dev/null +++ b/src/ui/CommitPicker.ts @@ -0,0 +1,37 @@ +import type * as vscode from "vscode"; +import { UI_TEXT } from "../constants"; +import { type Result, map } from "../result"; +import type { Commit } from "../git/types"; +import { showSinglePick } from "./runQuickPick"; +import type { Cancelled } from "./cancelled"; +import { formatRelative } from "./format/relativeTime"; + +interface CommitPickItem extends vscode.QuickPickItem { + readonly commit: Commit; +} + +const toItem = (commit: Commit, now: number): CommitPickItem => ({ + label: commit.shortSha, + description: commit.subject, + detail: `${commit.author} ${UI_TEXT.bulletDot} ${formatRelative(commit.authorTime, now)}`, + commit, +}); + +export const pickCommit = async ({ + commits, + placeholder, + now, +}: { + commits: readonly Commit[]; + placeholder?: string; + now?: number; +}): Promise<Result<Commit, Cancelled>> => { + const tNow = now ?? Math.floor(Date.now() / 1000); + const r = await showSinglePick<CommitPickItem>({ + items: commits.map((c) => toItem(c, tNow)), + placeholder: placeholder ?? UI_TEXT.pickCommitPlaceholder, + matchOnDescription: true, + matchOnDetail: true, + }); + return map(r, (item) => item.commit); +}; diff --git a/src/ui/FilePicker.ts b/src/ui/FilePicker.ts new file mode 100644 index 0000000..01e62bf --- /dev/null +++ b/src/ui/FilePicker.ts @@ -0,0 +1,39 @@ +import type * as vscode from "vscode"; +import { showStayOpenPick } from "./runQuickPick"; +import { type FileEntry, formatCounts, statusBadge } from "./format/fileItem"; + +export { mergeChangedFilesWithStats } from "./format/fileItem"; +export type { FileEntry } from "./format/fileItem"; + +interface FileItem extends vscode.QuickPickItem { + readonly entry: FileEntry; +} + +const toItem = (entry: FileEntry): FileItem => ({ + label: entry.file.path, + description: formatCounts(entry.stat), + detail: statusBadge(entry.file), + entry, +}); + +export const pickFiles = async ({ + entries, + onPick, + placeholder, +}: { + entries: readonly FileEntry[]; + onPick: (entry: FileEntry) => void | Promise<void>; + placeholder?: string; +}): Promise<void> => { + await showStayOpenPick<FileItem>( + { + items: entries.map(toItem), + placeholder: placeholder ?? "Pick a file to diff (Esc to close)", + matchOnDescription: true, + matchOnDetail: true, + }, + async (item) => { + await onPick(item.entry); + } + ); +}; diff --git a/src/ui/RefPicker.ts b/src/ui/RefPicker.ts new file mode 100644 index 0000000..b7701d5 --- /dev/null +++ b/src/ui/RefPicker.ts @@ -0,0 +1,40 @@ +import type * as vscode from "vscode"; +import { SHORT_SHA_LEN, UI_TEXT } from "../constants"; +import { type Result, map } from "../result"; +import type { Ref, RefType } from "../git/types"; +import { showSinglePick } from "./runQuickPick"; +import type { Cancelled } from "./cancelled"; +import { refTypeLabel } from "./format/refLabel"; +import { filterRefs } from "./format/refFilter"; + +interface RefPickItem extends vscode.QuickPickItem { + readonly ref: Ref; +} + +const toItem = (ref: Ref): RefPickItem => ({ + label: ref.name, + description: ref.sha.slice(0, SHORT_SHA_LEN), + detail: refTypeLabel(ref), + ref, +}); + +export const pickRef = async ({ + refs, + placeholder, + filter, + excludeBranchName, +}: { + refs: readonly Ref[]; + placeholder?: string; + filter?: RefType | undefined; + excludeBranchName?: string | undefined; +}): Promise<Result<Ref, Cancelled>> => { + const items = filterRefs({ refs, type: filter, excludeBranchName }).map(toItem); + const r = await showSinglePick<RefPickItem>({ + items, + placeholder: placeholder ?? UI_TEXT.pickRefPlaceholder, + matchOnDescription: true, + matchOnDetail: true, + }); + return map(r, (item) => item.ref); +}; diff --git a/src/ui/SideBPicker.ts b/src/ui/SideBPicker.ts new file mode 100644 index 0000000..1483e87 --- /dev/null +++ b/src/ui/SideBPicker.ts @@ -0,0 +1,50 @@ +import type * as vscode from "vscode"; +import { SIDE_B_KINDS, UI_TEXT } from "../constants"; +import { type Result, map } from "../result"; +import { showSinglePick } from "./runQuickPick"; +import type { Cancelled } from "./cancelled"; + +export type SideBChoice = + | { readonly kind: typeof SIDE_B_KINDS.workingCopy } + | { readonly kind: typeof SIDE_B_KINDS.index } + | { readonly kind: typeof SIDE_B_KINDS.pickRef } + | { readonly kind: typeof SIDE_B_KINDS.pickCommit }; + +interface SideBItem extends vscode.QuickPickItem { + readonly choice: SideBChoice; +} + +const ITEMS: readonly SideBItem[] = [ + { + label: UI_TEXT.workingCopy, + description: UI_TEXT.workingCopyDescription, + choice: { kind: SIDE_B_KINDS.workingCopy }, + }, + { + label: UI_TEXT.indexLabel, + description: UI_TEXT.indexDescription, + choice: { kind: SIDE_B_KINDS.index }, + }, + { + label: UI_TEXT.pickCommitLabel, + description: UI_TEXT.pickCommitDescription, + choice: { kind: SIDE_B_KINDS.pickCommit }, + }, + { + label: UI_TEXT.pickRefLabel, + description: UI_TEXT.pickRefDescription, + choice: { kind: SIDE_B_KINDS.pickRef }, + }, +]; + +export const pickSideBChoice = async ({ + placeholder, +}: { + placeholder?: string; +} = {}): Promise<Result<SideBChoice, Cancelled>> => { + const r = await showSinglePick<SideBItem>({ + items: ITEMS, + placeholder: placeholder ?? UI_TEXT.compareAgainstPlaceholder, + }); + return map(r, (item) => item.choice); +}; diff --git a/src/ui/cancelled.ts b/src/ui/cancelled.ts new file mode 100644 index 0000000..18c270e --- /dev/null +++ b/src/ui/cancelled.ts @@ -0,0 +1,5 @@ +export interface Cancelled { + readonly cancelled: true; +} + +export const CANCELLED: Cancelled = { cancelled: true }; diff --git a/src/ui/format/fileItem.ts b/src/ui/format/fileItem.ts new file mode 100644 index 0000000..d5442b8 --- /dev/null +++ b/src/ui/format/fileItem.ts @@ -0,0 +1,43 @@ +import { CHANGED_FILE_STATUSES, UI_TEXT } from "../../constants"; +import type { ChangedFile, DiffStat } from "../../git/types"; + +export const statusBadge = (file: ChangedFile): string => { + if (file.status === CHANGED_FILE_STATUSES.renamed) { + return `${CHANGED_FILE_STATUSES.renamed}${(file.similarity ?? 0).toString()}`; + } + if (file.status === CHANGED_FILE_STATUSES.copied) { + return `${CHANGED_FILE_STATUSES.copied}${(file.similarity ?? 0).toString()}`; + } + return file.status; +}; + +export const formatCounts = (stat: DiffStat): string => { + if (stat.binary) { + return UI_TEXT.binaryStat; + } + return `+${stat.added.toString()} -${stat.deleted.toString()}`; +}; + +export interface FileEntry { + readonly file: ChangedFile; + readonly stat: DiffStat; +} + +export const mergeChangedFilesWithStats = ( + files: readonly ChangedFile[], + stats: readonly DiffStat[] +): readonly FileEntry[] => { + const byPath = new Map<string, DiffStat>(); + for (const s of stats) { + byPath.set(s.path, s); + } + return files.map((f) => ({ + file: f, + stat: byPath.get(f.path) ?? { + path: f.path, + added: 0, + deleted: 0, + binary: false, + }, + })); +}; diff --git a/src/ui/format/refFilter.ts b/src/ui/format/refFilter.ts new file mode 100644 index 0000000..1b22f07 --- /dev/null +++ b/src/ui/format/refFilter.ts @@ -0,0 +1,16 @@ +import { REF_TYPES } from "../../constants"; +import type { Ref, RefType } from "../../git/types"; + +export interface FilterRefsArgs { + readonly refs: readonly Ref[]; + readonly type?: RefType | undefined; + readonly excludeBranchName?: string | undefined; +} + +export const filterRefs = ({ refs, type, excludeBranchName }: FilterRefsArgs): readonly Ref[] => { + const byType = type === undefined ? refs : refs.filter((r) => r.type === type); + if (excludeBranchName === undefined) { + return byType; + } + return byType.filter((r) => !(r.type === REF_TYPES.branch && r.name === excludeBranchName)); +}; diff --git a/src/ui/format/refLabel.ts b/src/ui/format/refLabel.ts new file mode 100644 index 0000000..b29acbc --- /dev/null +++ b/src/ui/format/refLabel.ts @@ -0,0 +1,12 @@ +import { REF_TYPES, UI_TEXT } from "../../constants"; +import type { Ref } from "../../git/types"; + +export const refTypeLabel = (ref: Ref): string => { + if (ref.type === REF_TYPES.branch) { + return UI_TEXT.branchLabel; + } + if (ref.type === REF_TYPES.tag) { + return UI_TEXT.tagLabel; + } + return UI_TEXT.refLabel; +}; diff --git a/src/ui/format/relativeTime.ts b/src/ui/format/relativeTime.ts new file mode 100644 index 0000000..9dfd8b6 --- /dev/null +++ b/src/ui/format/relativeTime.ts @@ -0,0 +1,22 @@ +import { UI_TEXT } from "../../constants"; + +const SECONDS_PER_MINUTE = 60; +const MINUTES_PER_HOUR = 60; +const HOURS_PER_DAY = 24; + +export const formatRelative = (unixSeconds: number, now: number): string => { + const deltaSec = Math.max(0, now - unixSeconds); + const minutes = Math.floor(deltaSec / SECONDS_PER_MINUTE); + if (minutes < 1) { + return UI_TEXT.justNow; + } + if (minutes < MINUTES_PER_HOUR) { + return `${minutes.toString()}m ago`; + } + const hours = Math.floor(minutes / MINUTES_PER_HOUR); + if (hours < HOURS_PER_DAY) { + return `${hours.toString()}h ago`; + } + const days = Math.floor(hours / HOURS_PER_DAY); + return `${days.toString()}d ago`; +}; diff --git a/src/ui/runQuickPick.ts b/src/ui/runQuickPick.ts new file mode 100644 index 0000000..ef4e0a8 --- /dev/null +++ b/src/ui/runQuickPick.ts @@ -0,0 +1,65 @@ +import * as vscode from "vscode"; +import { type Result, err, ok } from "../result"; +import { CANCELLED, type Cancelled } from "./cancelled"; + +export interface QuickPickConfig<T extends vscode.QuickPickItem> { + readonly items: readonly T[]; + readonly placeholder: string; + readonly matchOnDescription?: boolean; + readonly matchOnDetail?: boolean; + readonly ignoreFocusOut?: boolean; +} + +const createConfiguredPicker = <T extends vscode.QuickPickItem>(config: QuickPickConfig<T>): vscode.QuickPick<T> => { + const qp = vscode.window.createQuickPick<T>(); + qp.placeholder = config.placeholder; + qp.matchOnDescription = config.matchOnDescription ?? false; + qp.matchOnDetail = config.matchOnDetail ?? false; + qp.ignoreFocusOut = config.ignoreFocusOut ?? false; + qp.items = config.items; + return qp; +}; + +export const showSinglePick = async <T extends vscode.QuickPickItem>( + config: QuickPickConfig<T> +): Promise<Result<T, Cancelled>> => + await new Promise<Result<T, Cancelled>>((resolve) => { + const qp = createConfiguredPicker(config); + let settled = false; + const finish = (r: Result<T, Cancelled>): void => { + if (settled) { + return; + } + settled = true; + qp.dispose(); + resolve(r); + }; + qp.onDidAccept(() => { + const choice = qp.selectedItems[0] ?? qp.activeItems[0]; + finish(choice === undefined ? err(CANCELLED) : ok(choice)); + }); + qp.onDidHide(() => { + finish(err(CANCELLED)); + }); + qp.show(); + }); + +export const showStayOpenPick = async <T extends vscode.QuickPickItem>( + config: QuickPickConfig<T>, + onPick: (item: T) => void | Promise<void> +): Promise<void> => { + await new Promise<void>((resolve) => { + const qp = createConfiguredPicker({ ...config, ignoreFocusOut: true }); + qp.onDidAccept(() => { + const choice = qp.selectedItems[0] ?? qp.activeItems[0]; + if (choice !== undefined) { + void Promise.resolve(onPick(choice)); + } + }); + qp.onDidHide(() => { + qp.dispose(); + resolve(); + }); + qp.show(); + }); +}; diff --git a/src/ui/uri.ts b/src/ui/uri.ts new file mode 100644 index 0000000..04c1454 --- /dev/null +++ b/src/ui/uri.ts @@ -0,0 +1,130 @@ +import { REV_KINDS, SCHEME, URI_AUTHORITIES, URI_PARSE_ERROR_KINDS } from "../constants"; +import { type Result, ok, err } from "../result"; +import type { DiffyAddressableRev, Sha } from "../git/types"; + +export type DiffyUriParseError = + | { + readonly kind: typeof URI_PARSE_ERROR_KINDS.invalidScheme; + readonly got: string; + } + | { + readonly kind: typeof URI_PARSE_ERROR_KINDS.invalidAuthority; + readonly got: string; + } + | { readonly kind: typeof URI_PARSE_ERROR_KINDS.missingSha } + | { readonly kind: typeof URI_PARSE_ERROR_KINDS.emptyPath } + | { + readonly kind: typeof URI_PARSE_ERROR_KINDS.badEncoding; + readonly raw: string; + } + | { + readonly kind: typeof URI_PARSE_ERROR_KINDS.malformed; + readonly reason: string; + }; + +export interface DiffyUriComponents { + readonly rev: DiffyAddressableRev; + readonly path: string; +} + +const SCHEME_SEP = "://"; +const PATH_SEP = "/"; + +const encodePath = (path: string): string => path.split(PATH_SEP).map(encodeURIComponent).join(PATH_SEP); + +const decodePath = (encoded: string): Result<string, DiffyUriParseError> => { + try { + return ok(encoded.split(PATH_SEP).map(decodeURIComponent).join(PATH_SEP)); + } catch { + return err({ kind: URI_PARSE_ERROR_KINDS.badEncoding, raw: encoded }); + } +}; + +export const buildDiffyUri = (rev: DiffyAddressableRev, path: string): string => { + const encoded = encodePath(path); + if (rev.kind === REV_KINDS.commit) { + return `${SCHEME}${SCHEME_SEP}${URI_AUTHORITIES.commit}/${rev.sha}/${encoded}`; + } + return `${SCHEME}${SCHEME_SEP}${URI_AUTHORITIES.index}/${encoded}`; +}; + +const parseCommitUri = (rest: string): Result<DiffyUriComponents, DiffyUriParseError> => { + const slash = rest.indexOf(PATH_SEP); + if (slash <= 0) { + return err({ kind: URI_PARSE_ERROR_KINDS.missingSha }); + } + const sha: Sha = rest.slice(0, slash); + const encodedPath = rest.slice(slash + 1); + if (encodedPath === "") { + return err({ kind: URI_PARSE_ERROR_KINDS.emptyPath }); + } + const decoded = decodePath(encodedPath); + if (!decoded.ok) { + return decoded; + } + return ok({ rev: { kind: REV_KINDS.commit, sha }, path: decoded.value }); +}; + +const parseIndexUri = (rest: string): Result<DiffyUriComponents, DiffyUriParseError> => { + if (rest === "") { + return err({ kind: URI_PARSE_ERROR_KINDS.emptyPath }); + } + const decoded = decodePath(rest); + if (!decoded.ok) { + return decoded; + } + return ok({ rev: { kind: REV_KINDS.index }, path: decoded.value }); +}; + +interface UriSplit { + readonly scheme: string; + readonly authority: string; + readonly rest: string; +} + +const splitUri = (uri: string): Result<UriSplit, DiffyUriParseError> => { + const sep = uri.indexOf(SCHEME_SEP); + if (sep < 0) { + return err({ + kind: URI_PARSE_ERROR_KINDS.malformed, + reason: "no scheme separator", + }); + } + const scheme = uri.slice(0, sep); + const afterScheme = uri.slice(sep + SCHEME_SEP.length); + const firstSlash = afterScheme.indexOf(PATH_SEP); + if (firstSlash < 0) { + return err({ + kind: URI_PARSE_ERROR_KINDS.malformed, + reason: "no authority/path separator", + }); + } + return ok({ + scheme, + authority: afterScheme.slice(0, firstSlash), + rest: afterScheme.slice(firstSlash + 1), + }); +}; + +export const parseDiffyUri = (uri: string): Result<DiffyUriComponents, DiffyUriParseError> => { + const split = splitUri(uri); + if (!split.ok) { + return split; + } + if (split.value.scheme !== SCHEME) { + return err({ + kind: URI_PARSE_ERROR_KINDS.invalidScheme, + got: split.value.scheme, + }); + } + if (split.value.authority === URI_AUTHORITIES.commit) { + return parseCommitUri(split.value.rest); + } + if (split.value.authority === URI_AUTHORITIES.index) { + return parseIndexUri(split.value.rest); + } + return err({ + kind: URI_PARSE_ERROR_KINDS.invalidAuthority, + got: split.value.authority, + }); +}; diff --git a/src/vscodeGitApi.ts b/src/vscodeGitApi.ts new file mode 100644 index 0000000..9054648 --- /dev/null +++ b/src/vscodeGitApi.ts @@ -0,0 +1,30 @@ +import * as vscode from "vscode"; +import { VSCODE_GIT_EXTENSION_ID } from "./constants"; + +const GIT_API_VERSION = 1; + +interface GitExtensionExports { + getAPI: (version: typeof GIT_API_VERSION) => GitApi; +} + +export interface GitVsRepository { + readonly rootUri: vscode.Uri; +} + +export interface GitApi { + readonly repositories: readonly GitVsRepository[]; + getRepository?: (uri: vscode.Uri) => GitVsRepository | null; +} + +export const getGitApi = async (): Promise<GitApi | undefined> => { + const ext = vscode.extensions.getExtension<GitExtensionExports>(VSCODE_GIT_EXTENSION_ID); + if (ext === undefined) { + return undefined; + } + if (!ext.isActive) { + await ext.activate(); + } + return ext.exports.getAPI(GIT_API_VERSION); +}; + +export { findRepoForUri } from "./git/repoMatch"; diff --git a/test-fixtures/repo-seed/seed.sh b/test-fixtures/repo-seed/seed.sh new file mode 100755 index 0000000..30582a4 --- /dev/null +++ b/test-fixtures/repo-seed/seed.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# Builds a deterministic git repo at <this-dir>/workspace for E2E tests. +# Re-run this script to rebuild; it nukes the existing workspace first. +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORKSPACE="$HERE/workspace" + +rm -rf "$WORKSPACE" +mkdir -p "$WORKSPACE" +cd "$WORKSPACE" + +export GIT_AUTHOR_NAME="Test Author" +export GIT_AUTHOR_EMAIL="test@example.com" +export GIT_COMMITTER_NAME="Test Committer" +export GIT_COMMITTER_EMAIL="committer@example.com" +export GIT_AUTHOR_DATE="2024-01-01T00:00:00Z" +export GIT_COMMITTER_DATE="2024-01-01T00:00:00Z" + +git init -q -b main + +# --- commit 1: initial layout ----------------------------------------------- +mkdir -p dir +printf 'a.txt v1\nsecond line v1\n' > a.txt +printf 'b.txt v1\n' > b.txt +printf 'c.txt v1\nsecond v1\n' > dir/c.txt +printf 'd.txt v1\n' > d.txt +git add . +GIT_AUTHOR_DATE="2024-01-01T00:00:00Z" \ +GIT_COMMITTER_DATE="2024-01-01T00:00:00Z" \ +git commit -q -m "first: add a, b, dir/c, d" + +# --- commit 2: edit a.txt, edit dir/c.txt, delete d.txt ---------------------- +printf 'a.txt v2 edited\nsecond line v1\nthird line added\n' > a.txt +printf 'c.txt v2 edited\nsecond v1\n' > dir/c.txt +git rm -q d.txt +git add a.txt dir/c.txt +GIT_AUTHOR_DATE="2024-01-02T00:00:00Z" \ +GIT_COMMITTER_DATE="2024-01-02T00:00:00Z" \ +git commit -q -m "second: edit a and c, delete d" + +# --- commit 3: rename b.txt -> b2.txt (with a small edit) ------------------- +git mv b.txt b2.txt +printf 'b.txt v1\nrenamed and extended\n' > b2.txt +git add b2.txt +GIT_AUTHOR_DATE="2024-01-03T00:00:00Z" \ +GIT_COMMITTER_DATE="2024-01-03T00:00:00Z" \ +git commit -q -m "third: rename b -> b2 and extend" + +# Tag commit 2 so RefPicker has at least one tag to pick from +git tag v0.1.0 HEAD~1 + +# Second branch at commit 2 so RefPicker has a non-current branch to pick +# (current branch is excluded — comparing main↔main makes no sense). +git branch feature HEAD~1 + +# Leave a working-copy edit on a.txt so 'compare with Working Copy' has content +printf 'a.txt v2 edited\nsecond line v1\nthird line added\nworking copy uncommitted\n' > a.txt + +echo "Seeded $WORKSPACE" +git -C "$WORKSPACE" --no-pager log --oneline diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..8ac7978 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,35 @@ +{ + "_agent_pmo": "74cf183", + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "lib": ["ES2022"], + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "out", + "rootDir": "src" + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "out", "dist", "coverage", ".vscode-test"] +}