diff --git a/docs/overview.md b/docs/overview.md index 6d44eca..2431ec7 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -1,90 +1,100 @@ -# GitHub Desktop for VS Code — Overview +# GitHub Desktop for VS Code — Overview -This document describes the structure, capabilities, and data flow for the GitHub Desktop-inspired VS Code extension contained in this workspace. +A VS Code extension that brings a GitHub Desktop-style workflow into the editor: +a Changes/History panel with a commit box, branch and sync controls, commit +actions, conflict resolution and stash management, plus multi-account GitHub +sign-in. -## Goals -- Provide a GitHub Desktop-like workflow without leaving VS Code. -- Allow simultaneous authentication with multiple GitHub accounts and easy context switching. -- Reuse existing local credentials (GitHub CLI) whenever possible to avoid pasting tokens. -- Simplify cloning, opening, and tracking repositories from the editor. -- Offer a lightweight entry point for issue creation and future GitHub interactions. +## Activation -## Extension Activation -The extension activates on startup (`onStartupFinished`), whenever one of the contributed commands runs, or when the GitHub Desktop views are opened. Activation wires up managers, registers commands, syncs workspace repositories, and initializes the panel UI. +`activate()` (`src/extension.ts`) runs on `onStartupFinished`. It: + +- creates `AccountManager` and `RepositoryManager` (state in `globalState`, + tokens in `SecretStorage`); +- registers the **Accounts** and **Repositories** tree views, and the + **History** webview view (`githubDesktop.timeline`); +- registers the sign-in / account / clone / open / issue / refresh commands; +- scans workspace folders for a `.git` dir and tracks the repositories it finds. + +## Surfaces + +| Surface | Provider | Notes | +|---|---|---| +| **History** webview | `TimelineViewProvider` | The main panel. Self-contained HTML/CSS/JS (no bundle) generated by `WebviewHtmlService`; all logic runs through `TimelineController`. | +| **Commit detail** panel | `CommitDetailViewProvider` | Opened on demand as a `WebviewPanel`; React/MUI bundle (`out/webview/commit-detail.js`). | +| **Accounts** / **Repositories** trees | `AccountsProvider` / `RepositoriesProvider` | Plain `TreeDataProvider`s. | + +## Timeline architecture (`src/webviews/timeline/`) + +The webview and the extension host talk over `postMessage`. Messages are typed +discriminated unions in `messages.ts` (`InboundMessage` / `OutboundMessage`). -```ts -activate(context: vscode.ExtensionContext) ``` -- Instantiates `AccountManager` and `RepositoryManager` using the extension's global state and secret storage. -- Boots the combined **Changes & History** webview alongside the Accounts and Repositories tree view providers. -- Registers command handlers for sign-in/out, account switching, repository cloning/opening, issue creation, and manual refresh. -- Focuses the custom activity bar container and reconciles workspace folders so repos with `.git` are listed immediately. - -## Core Components - -### AccountManager (`src/accountManager.ts`) -- Persists account metadata (`StoredAccount`) in global state. -- Stores personal access tokens (PATs) in VS Code Secret Storage under randomized keys. -- Attempts to reuse GitHub CLI (`gh auth token`) credentials before prompting for a PAT. -- Emits `onDidChangeAccounts` for tree providers or other features to react to changes. -- Provides helpers to fetch authenticated Octokit clients scoped to the active or selected account. - -### RepositoryManager (`src/repositoryManager.ts`) -- Tracks cloned or opened repositories (`TrackedRepository`) in global state, including whether they are linked to a signed-in account. -- Guarantees unique entries per local path or remote URL and exposes `updateRepository` for metadata changes. -- Emits `onDidChangeRepositories` whenever the repository list mutates. - -### TimelineViewProvider (`src/webviews/timelineView.ts`) -- Supplies the HTML/CSS/JS for the combined Changes & History panel with GitHub Desktop-style commit cards. -- Reads git status/log output via `simple-git`, formats commit cards, and renders file-level detail, inline diffs, and responds to interactions (file selection, refresh). -- Refreshes automatically when repositories change, when files are saved, or when the user triggers a manual refresh. - -### Tree Data Providers (`src/treeViews/`) -- `AccountsProvider` renders the list of signed-in accounts and indicates which one is active. -- `RepositoriesProvider` surfaces tracked repositories (local or remote) and exposes quick entry points for cloning or opening projects. - -### Commands (`contributes.commands` in `package.json`) -- `githubDesktop.signIn` / `githubDesktop.signOut` / `githubDesktop.switchAccount` -- `githubDesktop.cloneRepository` / `githubDesktop.openRepository` -- `githubDesktop.createIssue` -- `githubDesktop.refreshViews` - -Each command is registered in `src/extension.ts` and depends on the managers above to read or mutate state. - -## Authentication Flow -1. The extension tries `gh auth token`; if successful, the token is used immediately and stored securely. -2. If the CLI is unavailable, the user is prompted for a PAT (`repo`, `read:org`, `workflow`). -3. Octokit validates the token by requesting `GET /user`. -4. Account details are stored; tokens remain in Secret Storage. -5. The active account defaults to the newest signed-in account but can be changed via the switch command. - -Tokens are never written to disk outside VS Code's secure storage. If secret storage removes a token (for example, due to a user action), the manager prunes the corresponding account on the next activation. - -## Repository Detection Flow -1. On activation—and whenever workspace folders change—the extension scans each folder for a `.git` directory. -2. If a match is found, `simple-git` inspects remotes to derive the owner/name pair. -3. The repository is added to the global store (without requiring a clone) and linked to the active account if one is present. -4. Repositories without linked accounts can be associated later when issue creation or other account-dependent commands run. - -## Data Persistence -- `context.globalState` keeps serialized lists of accounts and repositories. -- `context.secrets` retains PATs mapped to generated token keys. -- Both stores survive across VS Code sessions; tree views read from them during activation. - -## UI Surface -- Activity bar container `githubDesktop` hosts the combined **Changes & History** webview and the **Accounts**/**Repositories** tree views (collapsed by default). -- Commit cards mimic GitHub Desktop styling with author/time metadata and highlight the currently selected entry. -- Selecting a commit displays a detail pane with aggregate stats and per-file changes (additions, deletions, status code). - -## Extensibility Notes -- Add new GitHub workflows by requesting Octokit clients from `AccountManager`. -- Extend repository metadata by updating `TrackedRepository` and handling state migrations in `RepositoryManager`. -- Timeline templates can be extended by posting additional messages through `TimelineViewProvider`. - -## Development Workflow -1. `npm install` -2. `npm run compile` (one-off build) or `npm run watch` -3. Launch the **Run Extension** debug configuration (see `.vscode/launch.json`). - -Tests are not yet implemented. When adding tests, prefer VS Code's extension test harness and bind them in `package.json` scripts. +webview ──InboundMessage──▶ TimelineController + │ (composition root) + ├─ MessageRouter Map, no switch + ├─ RepositoryDataService read model → RepositorySnapshot + └─ write services: + WorkingTreeService stage / commit / amend / undo / discard / diff + SyncService fetch / pull / push / publish (+ conflict detect) + BranchService checkout / create / merge + CommitActionsService reset / checkout / revert / cherry-pick / branch-from / tag / view-on-GitHub + ConflictService markResolved / continue / abort + StashService push / apply / pop / drop + PullRequestService open compare page + DiffService commit detail / per-file patch +webview ◀─OutboundMessage── (services broadcast via WebviewChannel; a mutation + triggers Refresher.refresh() → a fresh snapshot) +``` + +### Ports and adapters + +Services depend on small interfaces (`ports.ts`), never on `vscode` or +`simple-git` directly: + +- `RepositoryContext` — the active repository +- `Notifier` — info/warn/error, confirm, prompt +- `Browser` — open external URLs +- `WebviewChannel` — post an `OutboundMessage` +- `Refresher` — recompute-and-broadcast (implemented by `TimelineController`) +- `GitClientFactory` — creates `simple-git` clients; `withAuth()` injects the + active account's token through ephemeral `GIT_CONFIG_*` env vars, so it is + **never written to `.git/config`** + +VS Code implementations live in `adapters/vscode-adapters.ts`; the git factory +is `AccountGitClientFactory` (`src/core/git/git-authenticator.ts`). + +### Read model + +`RepositoryDataService.getSnapshot()` assembles one `RepositorySnapshot` +(changes, history page, branches + activity, remote status, tags, in-progress +operation, conflicted paths, `canUndo`, stashes) per refresh. +`TimelineViewProvider` debounces refreshes (400 ms) so a burst of file saves +triggers a single recompute. + +## Authentication + +`AccountManager` tries `gh auth token`, then VS Code's built-in GitHub auth, +then a manually entered PAT. Tokens live only in `SecretStorage`. Git network +operations get the token via `GitClientFactory.withAuth`. + +## Build + +- `npm run compile` — webpack bundles the extension to `dist/extension.js` + (the timeline webview HTML/JS is inlined here). +- `npm run build-webview` — Vite builds the React panels to `out/webview/` + (currently just `commit-detail.js`). +- `npm run package` — runs both, then `vsce package`. + +## Extending + +- New timeline command: add a member to `InboundMessage`, a method on the + relevant service (or a new service), and one `.on(...)` line in + `TimelineController`. The router and the rest of the services are untouched. +- New git read data: add a field to `RepositorySnapshot` and populate it in + `RepositoryDataService`, then broadcast it from `TimelineController.refresh()`. + +## Not yet implemented +Force-push UI, branch comparison, PR list / checkout, CI status on commits, +repository context menu, tests. diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..5c0f6a3 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6027 @@ +{ + "name": "vscode-github-desktop", + "version": "1.6.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "vscode-github-desktop", + "version": "1.6.0", + "dependencies": { + "@octokit/rest": "^20.0.2", + "simple-git": "^3.21.0" + }, + "devDependencies": { + "@types/node": "18.x", + "@types/vscode": "^1.84.0", + "@vscode/vsce": "^3.6.0", + "rimraf": "^5.0.5", + "ts-loader": "^9.5.4", + "ts-node": "^10.9.2", + "typescript": "^5.3.3", + "webpack": "^5.101.3", + "webpack-cli": "^6.0.1" + }, + "engines": { + "node": ">=20.0.0", + "vscode": ">=1.84.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.2.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.2.0.tgz", + "integrity": "sha512-fNAjWnA/nZ2jz31kxR/AqRaUT8ewHBw/WuBIosK0moMy1C9e5ValbDfFdIxJzVOOYaYkV/b2F1S4H/aHiqfVQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.11.0.tgz", + "integrity": "sha512-IUZydyTUkDnYdstOW9pFOOUQlBjAepK5teihDE3x6yxsPJs/hsAaaYpeGxdxrgtOiJbBKSjKW7MDk7AEhb4LRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.11.0.tgz", + "integrity": "sha512-JjQWO6akOck45PH/XBrxzsQGAiKrfFl4m5iggJ0ItMIz5omRufOXWpqCPpdjKN3vKDzlSUvFjaMb7Zwf0gvAdA==", + "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": ">=22.0.0" + } + }, + "node_modules/@azure/core-process": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@azure/core-process/-/core-process-1.0.0.tgz", + "integrity": "sha512-/shnJ+ooO8WPxDhPEeI/2oRQuubn16gZ6CvlbpWbEswZfzwI9tI/sMAHmF3x1LuQ9yZYXfLW3TjzGMLEC5blKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.25.0.tgz", + "integrity": "sha512-bMs8ekJLjX8wPV+9IPBges1SLPyuDtE9g5gLDWOpxzKcoOFQnpLGkbcT1tdw3FaAmDS1gnPmMmJ6y/T5B96kIA==", + "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": ">=22.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.4.0.tgz", + "integrity": "sha512-eGwxD0AtncrxeBM4tG8R55Pc3rdX1hNW2WibJAgYpCVA6E93mvvVH+LcssoVjOBrSKWS55yEIHsk0X8ctHmfOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.14.0.tgz", + "integrity": "sha512-9n2pWK61veAuN0V20t9lOuoV4CFMdyAZ1ygZzvBGk/pBBJRib/PjL9PLXa/aI2CcPpyHfqVsxxqLCYl6uZlfDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.13.2", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.2.tgz", + "integrity": "sha512-NXL2/pCJctLxgw8bvrwwgge743kEq8LBT+O1pmV0vyUwetzFPH9auP6jhkU/cgZCPPtWoewAe3ncaGCgPo07fA==", + "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-process": "^1.0.0", + "@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.5", + "open": "^10.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.4.0.tgz", + "integrity": "sha512-rbAE25KUfjU/s3XHUdJgceoCP5dEOpMx85J04kF+QMdta73XkuG9JGHHinch+XIoKpBdqljin+KqURpJriSzLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.21.0.tgz", + "integrity": "sha512-80OcuXDErmcEDAIH9pBtSqBsed2sPT/IWmbG3xHLoPMl5zc8TINd6SlJAbVSmN5huGa3xGAg5qR7VnpaIEK0Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.14.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.14.0.tgz", + "integrity": "sha512-A4rb55hI86Q9tBl/+jBj7TMz7iX2RFgQs/nExFzcAtoI/BFRVdaH5SL/MivrYD7qvweMpN8AgVvVMHV8UBYxew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.6.0.tgz", + "integrity": "sha512-uFY9NxrWHw8PwZx7gAX6PDn+9vdfS05+levc/kwkx77IkjfaldnQbbcQzzDIZ5Hq5Zdr6/z92oAIoRWKp6MnOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.13.0", + "jsonwebtoken": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@azure/msal-node/node_modules/@azure/msal-common": { + "version": "16.13.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.13.0.tgz", + "integrity": "sha512-rOAy0KUcyBbdwVJ+f3uPpthXatFLLZN+/KWAsTLzk1aB23Xl9DRmmXYwSvBFOZyXj4jUQQ5FKxxRkhAFW1fOow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", + "integrity": "sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.17.0" + } + }, + "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/@isaacs/cliui/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/@isaacs/cliui/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/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "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/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "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/@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1" + } + }, + "node_modules/@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", + "license": "MIT" + }, + "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/@octokit/auth-token": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.2.tgz", + "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.1.0", + "@octokit/request": "^8.4.1", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/endpoint": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", + "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "license": "MIT" + }, + "node_modules/@octokit/core/node_modules/@octokit/request": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz", + "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^9.0.6", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/request-error": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz", + "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/graphql": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz", + "integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==", + "license": "MIT", + "dependencies": { + "@octokit/request": "^8.4.1", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql/node_modules/@octokit/endpoint": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", + "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "license": "MIT" + }, + "node_modules/@octokit/graphql/node_modules/@octokit/request": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz", + "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^9.0.6", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql/node_modules/@octokit/request-error": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz", + "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "11.4.4-cjs.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.4.4-cjs.2.tgz", + "integrity": "sha512-2dK6z8fhs8lla5PaOTgqfCGBxgAv/le+EhPs27KklPhm1bKObpu6lXzwfUEQ16ajXzqNrKMujsFyo9K2eaoISw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.7.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-4.0.1.tgz", + "integrity": "sha512-GihNqNpGHorUrO7Qa9JbAl0dbLnqJVrV8OXe2Zm5/Y4wFkZQDfTreBzVmiRfJVfE4mClXdihHnbpyyO9FSX4HA==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "13.3.2-cjs.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.3.2-cjs.1.tgz", + "integrity": "sha512-VUjIjOOvF2oELQmiFpWA1aOPdawpyaCUqcEBc/UOUnj3Xp6DJGrJ1+bjUIIDzdHjnFNO6q57ODMfdEZnoBkCwQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.8.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "^5" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/rest": { + "version": "20.1.2", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-20.1.2.tgz", + "integrity": "sha512-GmYiltypkHHtihFwPRxlaorG5R9VAHuk/vbszVoRTGXnAsY60wYLkh/E2XiFmdZmqrisw+9FaazS1i5SbdWYgA==", + "license": "MIT", + "dependencies": { + "@octokit/core": "^5.0.2", + "@octokit/plugin-paginate-rest": "11.4.4-cjs.2", + "@octokit/plugin-request-log": "^4.0.0", + "@octokit/plugin-rest-endpoint-methods": "13.3.2-cjs.1" + }, + "engines": { + "node": ">= 18" + } + }, + "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/@simple-git/args-pathspec": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@simple-git/args-pathspec/-/args-pathspec-1.0.3.tgz", + "integrity": "sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==", + "license": "MIT" + }, + "node_modules/@simple-git/argv-parser": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@simple-git/argv-parser/-/argv-parser-1.1.1.tgz", + "integrity": "sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw==", + "license": "MIT", + "dependencies": { + "@simple-git/args-pathspec": "^1.0.3" + } + }, + "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.8.0", + "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.8.0.tgz", + "integrity": "sha512-5CiH9COYmovWmExQgs7763DzX6Gy9zjkjJ7JxCC95wyTcjwQn/8poNF6fv3qzRlmx8CRRde8DHr9FcgAAiPzgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/linter-formatter": { + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/@textlint/linter-formatter/-/linter-formatter-15.8.0.tgz", + "integrity": "sha512-+oU3A235NATv6Lzi4xa4kJ65PuNJlIxesaO4AvDhDWA9FWm7y4XKWaoQCW1esgaQQ6dwnUiFKArQ8TcJ86mC4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azu/format-text": "^1.0.2", + "@azu/style-format": "^1.0.1", + "@textlint/module-interop": "15.8.0", + "@textlint/resolver": "15.8.0", + "@textlint/types": "15.8.0", + "debug": "^4.4.3", + "js-yaml": "^4.3.0", + "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" + }, + "engines": { + "node": ">=20.18.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.8.0", + "resolved": "https://registry.npmjs.org/@textlint/module-interop/-/module-interop-15.8.0.tgz", + "integrity": "sha512-rt+OR1WYGoLOY8HkA/aBPrqufF6yUUEsKEAh7XohTsT3lp9IyZFT6zOIbjul9P4FAzsmSPkcrYjVx3Bz/IUfkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/resolver": { + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/@textlint/resolver/-/resolver-15.8.0.tgz", + "integrity": "sha512-E88tzfX3K8Jykk+38aJ9cy8RquD8ABVOPTO2rFEESq0wcg8x6/ypdAS8ZgR7OKiGqlRF0hkO/m5PbQwVfKM3VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/types": { + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/@textlint/types/-/types-15.8.0.tgz", + "integrity": "sha512-Anhc6y5736YIsvqae0U6k0YmB2M/QVHkEeOv2aydAn/WIkdI69dCOiDbe3/+RagS3qstFTSFWJzNRA2lUjv19w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@textlint/ast-node-types": "15.8.0" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.13.tgz", + "integrity": "sha512-gcLdvR9HO1ZJBypsOGqaP6TFEzb6vIta0KSTLt9NAQ6pXQO3cRgSVyCN6pzYqI9DlJgY71XKO0dpDhCf08b3pg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "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/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/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "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.136.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.136.0.tgz", + "integrity": "sha512-DO746VAfnHk8+Y8CsUKYMm2rjWD0mxEuYBZI+ssaqJY7yvLJPf9lcwxSSz9W60gbdvdcGyvHQxHTyByfF9mVqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.8.tgz", + "integrity": "sha512-bLMpVcWZNzq6lYOybwFwOAR1IXKcHnhUNqYeHjl1bET/qE3jFPFH+p8Wrh3rU4xwdnifPxmKNESBYnvnmc75aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@vscode/vsce": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.9.2.tgz", + "integrity": "sha512-XSxMosEEDO6vLxELAHVkwmhC0qe0ijZni2jB9Rcs8kQsW4lhTDQ/wMzmwFs/buotAWSnpmUp/dRWD2ufG3UYKA==", + "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": "^13.0.6", + "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": "^10.2.2", + "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.1.0", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign/-/vsce-sign-2.1.0.tgz", + "integrity": "sha512-9AQrqazrBgTgRSuwleLVXUrIUphY02/SFCh2TKYoLV/xifJAdblhdmEmw5gUrYSPQ3sRwNs9iyCMD14sATEE6g==", + "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/@vscode/vsce/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-3.0.1.tgz", + "integrity": "sha512-u8d0pJ5YFgneF/GuvEiDA61Tf1VDomHHYMjv/wc9XzYj7nopltpG96nXN5dJRstxZhcNpV1g+nT6CydO7pHbjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-3.0.1.tgz", + "integrity": "sha512-coEmDzc2u/ffMvuW9aCjoRzNSPDl/XLuhPdlFRpT9tZHmJ/039az33CE7uH+8s0uL1j5ZNtfdv0HkfaKRBGJsQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-3.0.1.tgz", + "integrity": "sha512-sbgw03xQaCLiT6gcY/6u3qBDn01CWw/nbaXl3gTdTFuJJ75Gffv3E3DBpgvY2fkkrdS1fpjaXNOmJlnbtKauKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.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": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "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.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "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/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "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/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": "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/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/baseline-browser-mapping": { + "version": "2.11.20", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.20.tgz", + "integrity": "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", + "license": "Apache-2.0" + }, + "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/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": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "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/browserslist": { + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "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/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "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/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/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "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/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/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone-deep/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "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/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "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==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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/default-browser": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.1.tgz", + "integrity": "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==", + "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/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "license": "ISC" + }, + "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": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "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/electron-to-chromium": { + "version": "1.5.420", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.420.tgz", + "integrity": "sha512-2yD6XreGusOfNV+dUcvipJEXc3n/n7fgr7996aszTG+YY5E4mqM4tOq/3uhP129cazL9YHbVWSpc79ePotWtPA==", + "dev": true, + "license": "ISC" + }, + "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/enhanced-resolve": { + "version": "5.24.5", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.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/envinfo": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", + "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", + "dev": true, + "license": "MIT", + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "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-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "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/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "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-uri": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", + "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fastq": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "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": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/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.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "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.4.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz", + "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "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-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": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/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/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/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.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/hosted-git-info/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/hosted-git-info/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/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": "7.0.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.8.tgz", + "integrity": "sha512-YYNsSlXBjMk92SKnkwvB5LOVSa6OznlFUGcsvrFgNJbJCd0M1XKeFVRc8ZByeCqz32FivYNHJVooLmdqrmvp/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/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", + "optional": true + }, + "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/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-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-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/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/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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": "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/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/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/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.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "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/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/jsonwebtoken/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/linkify-it": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "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.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/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/markdown-it": { + "version": "14.3.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.1.tgz", + "integrity": "sha512-4Ej49aYTDFIQ+uBkfX8GBvJGccoARxxPep+7aWTs55ozbjQJpW9M26Fe53vnGgvLeVzva/amzjQQaQu9w0vMhA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.5.0", + "linkify-it": "^5.0.2", + "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.1.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "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-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": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/minimizer-webpack-plugin": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/minimizer-webpack-plugin/-/minimizer-webpack-plugin-5.8.0.tgz", + "integrity": "sha512-2cT9+goJfBhtMz+gJqejSf09ClgmYhPhccRb/fb0ztVbixt0BkO8mRuI26FoPPuUNkRk6iEDryWAIouc5W8eJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.3", + "terser": "^5.51.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "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/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "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/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.96.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz", + "integrity": "sha512-rebQ/lz7i0EkoLzUVSrKRzA69zMkwLp95kKMWoMDkkM00Suxz0D7zEQPwRml5fQum24mj7bPvmlgLAmu2JCiYg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "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-releases": { + "version": "2.0.54", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", + "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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-package-data/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "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/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-map": { + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.7.tgz", + "integrity": "sha512-VaWRu2i4FJNRtiRWCuuQRgfQ1B7a6+gMSrO+3j0EQi/k0ULfS9kosRxGoiqwzIjZTDI02tGfk5mXXltLg6QtfQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/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-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "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.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "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/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/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.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.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "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/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/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/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/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": "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/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.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": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/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/rimraf/node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/rimraf/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/rimraf/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/rimraf/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/rimraf/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/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/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.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "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/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "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.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "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/simple-git": { + "version": "3.36.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.36.0.tgz", + "integrity": "sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q==", + "license": "MIT", + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "@simple-git/args-pathspec": "^1.0.3", + "@simple-git/argv-parser": "^1.1.0", + "debug": "^4.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/git-js?sponsor=1" + } + }, + "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/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.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/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "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": "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/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/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "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/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/terser": { + "version": "5.51.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.51.2.tgz", + "integrity": "sha512-bWnjSNscmuI+GJze6ZupnHP8G/cTcsJF+bXCeQknk2SHQsgbNJnLrqiH9jZ2W4STPVXH2mDKKRX3iwPhc9Cn/Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "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/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "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-loader": { + "version": "9.6.2", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.6.2.tgz", + "integrity": "sha512-R4iuczmtgxvtuI556s+hTZ6/7Ee03VCAk/l/M8LY1OAsUgB7YydsCxkgq9D9pKRaD7GJqUi2u8fp9zZP/ufjKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "picomatch": "^4.0.0", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "loader-utils": "*", + "typescript": "*", + "webpack": "^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "loader-utils": { + "optional": true + } + } + }, + "node_modules/ts-loader/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/ts-loader/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "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-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/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.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "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/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", + "license": "ISC" + }, + "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/update-browserslist-db": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", + "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.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", + "optional": true + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "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/watchpack": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", + "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.110.3", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.110.3.tgz", + "integrity": "sha512-GuizBzRvo9YPpyoNMf3ag7AzxbaW85qrRSqTha345KyJbAFPt3/cMzBM0h+RWg7SK/7DdzRLINP3LvQ0hvr4hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.24.4", + "es-module-lexer": "^2.1.0", + "events": "^3.2.0", + "graceful-fs": "^4.2.11", + "mime-db": "^1.54.0", + "minimizer-webpack-plugin": "^5.7.0", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "watchpack": "^2.5.2", + "webpack-sources": "^3.5.1" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-6.0.1.tgz", + "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "^0.6.1", + "@webpack-cli/configtest": "^3.0.1", + "@webpack-cli/info": "^3.0.1", + "@webpack-cli/serve": "^3.0.1", + "colorette": "^2.0.14", + "commander": "^12.1.0", + "cross-spawn": "^7.0.3", + "envinfo": "^7.14.0", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^6.0.1" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.82.0" + }, + "peerDependenciesMeta": { + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-merge": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.1.tgz", + "integrity": "sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true, + "license": "MIT" + }, + "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/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-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/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/wrap-ansi/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/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "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/yauzl": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz", + "integrity": "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "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/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/package.json b/package.json index b91a533..493733f 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "vscode-github-desktop", "displayName": "GitHub Desktop", - "description": "A comprehensive VS Code extension that brings GitHub Desktop's complete workflow experience, featuring React-based UI, advanced commit management, branch operations, and multi-account support.", - "version": "1.1.4", + "description": "A comprehensive VS Code extension that brings GitHub Desktop's complete workflow experience: commit management, branch operations, sync, conflict resolution, stashes and multi-account support.", + "version": "1.8.0", "publisher": "BetaVersion", "icon": "media/icon.png", "repository": { @@ -24,8 +24,7 @@ "commit", "branch", "repository", - "multi-account", - "react" + "multi-account" ], "activationEvents": [ "onStartupFinished" @@ -33,46 +32,6 @@ "main": "./dist/extension.js", "contributes": { "commands": [ - { - "command": "githubDesktop.commit.reset", - "title": "Reset to Commit", - "category": "GitHub Desktop" - }, - { - "command": "githubDesktop.commit.checkout", - "title": "Checkout Commit", - "category": "GitHub Desktop" - }, - { - "command": "githubDesktop.commit.revert", - "title": "Revert Commit", - "category": "GitHub Desktop" - }, - { - "command": "githubDesktop.commit.createBranch", - "title": "Create Branch from Commit", - "category": "GitHub Desktop" - }, - { - "command": "githubDesktop.commit.createTag", - "title": "Create Tag", - "category": "GitHub Desktop" - }, - { - "command": "githubDesktop.commit.cherryPick", - "title": "Cherry-pick Commit", - "category": "GitHub Desktop" - }, - { - "command": "githubDesktop.commit.copySha", - "title": "Copy SHA", - "category": "GitHub Desktop" - }, - { - "command": "githubDesktop.commit.viewOnGitHub", - "title": "View on GitHub", - "category": "GitHub Desktop" - }, { "command": "githubDesktop.signIn", "title": "GitHub Desktop: Sign In", @@ -206,44 +165,29 @@ } }, "scripts": { - "vscode:prepublish": "npm run compile && npm run build-webview", + "vscode:prepublish": "npm run compile", "compile": "webpack --mode production", - "build-webview": "vite build --config vite.webview.config.ts", "watch": "webpack --mode development --watch", - "watch-webview": "vite build --config vite.webview.config.ts --watch", - "dev-webview": "vite dev --config vite.webview.config.ts", "package": "vsce package --no-dependencies --allow-missing-repository --allow-package-secrets github", "test": "npm run compile && node ./out/test/runTest.js", "format": "prettier --write \"src/**/*.ts\"" }, "devDependencies": { "@types/node": "18.x", - "@types/react": "^19.1.13", - "@types/react-dom": "^19.1.9", "@types/vscode": "^1.84.0", - "@vitejs/plugin-react": "^5.0.3", "@vscode/vsce": "^3.6.0", "rimraf": "^5.0.5", "ts-loader": "^9.5.4", "ts-node": "^10.9.2", "typescript": "^5.3.3", - "vite": "^7.1.6", "webpack": "^5.101.3", "webpack-cli": "^6.0.1" }, "dependencies": { - "@emotion/react": "^11.14.0", - "@emotion/styled": "^11.14.1", - "@mui/icons-material": "^7.3.2", - "@mui/material": "^7.3.2", - "@octokit/auth-oauth-device": "^4.0.3", "@octokit/rest": "^20.0.2", - "@tanstack/react-query": "^5.89.0", - "axios": "^1.12.2", - "dayjs": "^1.11.18", - "react": "^19.1.0", - "react-dom": "^19.1.0", "simple-git": "^3.21.0" + }, + "volta": { + "node": "22.23.2" } } - diff --git a/src/core/git/git-authenticator.ts b/src/core/git/git-authenticator.ts new file mode 100644 index 0000000..94c4e23 --- /dev/null +++ b/src/core/git/git-authenticator.ts @@ -0,0 +1,84 @@ +import simpleGit, { SimpleGit } from "simple-git"; +import { AccountManager } from "../accounts/account-manager"; + +/** + * @description + * Abstraction over the creation of `simple-git` clients. High-level services + * depend on this port instead of importing `simpleGit` directly, which keeps + * them testable (Dependency Inversion) and confines all credential handling to + * a single place. + */ +export interface GitClientFactory { + /** + * A plain client with no credentials attached. Use for read-only or purely + * local operations (status, log, checkout, merge, reset...). + * + * @param repoPath - Absolute path to the repository working directory + */ + plain(repoPath: string): SimpleGit; + + /** + * Runs `fn` with a client whose network operations carry the active GitHub + * account's token. + * + * The token is injected as an ephemeral `-c http..extraheader=…` on each + * git invocation. It is **never** written to `.git/config`, so there is + * nothing to clean up and no shared state to race on. + * + * If no account is active or no token is available, `fn` still runs with a + * plain client (git may then fall back to a system credential helper). + * + * @param repoPath - Absolute path to the repository working directory + * @param fn - Callback receiving the (possibly authenticated) client + * @returns Whatever `fn` resolves to + * @throws Propagates any error thrown by `fn` + * + * @example + * await gitFactory.withAuth(repo.localPath, (git) => git.push(["-u", "origin", branch])); + */ + withAuth(repoPath: string, fn: (git: SimpleGit) => Promise): Promise; +} + +/** + * @description Default {@link GitClientFactory} backed by the extension's + * {@link AccountManager}. Resolves the token of the currently active account. + */ +export class AccountGitClientFactory implements GitClientFactory { + constructor(private readonly accounts: AccountManager) {} + + plain(repoPath: string): SimpleGit { + return simpleGit(repoPath); + } + + async withAuth( + repoPath: string, + fn: (git: SimpleGit) => Promise, + ): Promise { + const token = await this.resolveToken(); + if (!token) { + return fn(simpleGit(repoPath)); + } + + const header = `Authorization: Basic ${Buffer.from( + `x-access-token:${token}`, + ).toString("base64")}`; + + // `-c http..extraheader=…` on every invocation. Not written to config, + // nothing to clean up. (We do NOT call `.env()` here — passing GIT_* vars + // through trips simple-git's unsafe-env guard; GIT_TERMINAL_PROMPT is set + // once at activation instead.) + const git = simpleGit(repoPath, { + config: [`http.https://github.com/.extraheader=${header}`], + }); + + return fn(git); + } + + private async resolveToken(): Promise { + const active = this.accounts.getActiveAccount(); + if (!active) { + return undefined; + } + return (await this.accounts.getToken(active.id)) ?? undefined; + } +} diff --git a/src/extension.ts b/src/extension.ts index 2c597bb..d8cdaf5 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -8,13 +8,14 @@ import { RepositoryManager } from "./core/repositories/repository-manager"; import { AccountsProvider } from "./ui/tree-views/accounts-provider"; import { RepositoriesProvider } from "./ui/tree-views/repositories-provider"; import { TimelineViewProvider } from "./webviews/timeline/timeline-view-provider"; -import { HistoryExplorerViewProvider } from "./webviews/historyExplorer/history-explorer-view-provider"; -import { CommitDetailViewProvider } from "./webviews/commitDetail/commit-detail-view-provider"; import { TrackedRepository } from "./shared/types"; export async function activate( context: vscode.ExtensionContext, ): Promise { + // Never let a spawned git block on an interactive credential prompt. + process.env.GIT_TERMINAL_PROMPT = "0"; + const accountManager = new AccountManager( context.globalState, context.secrets, @@ -28,25 +29,14 @@ export async function activate( repositoryManager, accountManager, ); - const commitDetailProvider = new CommitDetailViewProvider( - context, - repositoryManager, - ); - const historyExplorerProvider = new HistoryExplorerViewProvider( - context, - repositoryManager, - commitDetailProvider, - ); const timelineProvider = new TimelineViewProvider( context, repositoryManager, accountManager, - commitDetailProvider, ); const refreshAllViews = () => { void timelineProvider.refresh(); - void historyExplorerProvider.refresh(); accountsProvider.refresh(); repositoriesProvider.refresh(); }; @@ -64,10 +54,6 @@ export async function activate( "githubDesktop.timeline", timelineProvider, ), - vscode.window.registerWebviewViewProvider( - HistoryExplorerViewProvider.viewType, - historyExplorerProvider, - ), vscode.commands.registerCommand("githubDesktop.signIn", async () => { const account = await accountManager.signIn(); if (account) { @@ -93,7 +79,7 @@ export async function activate( (accountId: string) => accountsProvider.switchToAccount(accountId), ), vscode.commands.registerCommand("githubDesktop.cloneRepository", () => - cloneRepository(accountManager, repositoryManager, timelineProvider, historyExplorerProvider), + cloneRepository(accountManager, repositoryManager, timelineProvider), ), vscode.commands.registerCommand( "githubDesktop.openRepository", @@ -182,7 +168,6 @@ export async function activate( }), vscode.workspace.onDidSaveTextDocument(() => { void timelineProvider.refresh(); - void historyExplorerProvider.refresh(); }), vscode.workspace.onDidChangeWorkspaceFolders(async () => { await syncWorkspaceRepositories(accountManager, repositoryManager); @@ -207,7 +192,6 @@ async function cloneRepository( accounts: AccountManager, repositories: RepositoryManager, timeline: TimelineViewProvider, - historyExplorer: HistoryExplorerViewProvider, ): Promise { const account = accounts.getActiveAccount() ?? (await accounts.signIn()); if (!account) { @@ -293,7 +277,6 @@ async function cloneRepository( remoteUrl: cleanRemote, }); await timeline.refresh(); - await historyExplorer.refresh(); vscode.window.showInformationMessage( `Cloned ${parsed.owner}/${parsed.name} successfully.`, ); @@ -499,11 +482,3 @@ async function linkUnassignedRepositories( await repositories.updateRepository(repo.id, { accountId }); } } - - - - - - - - diff --git a/src/shared/utils/github-url.ts b/src/shared/utils/github-url.ts new file mode 100644 index 0000000..27bcc5f --- /dev/null +++ b/src/shared/utils/github-url.ts @@ -0,0 +1,18 @@ +/** + * @description Normalises a git remote URL (SSH or HTTPS, with or without a + * trailing `.git`) into its browsable `https://github.com/owner/repo` form. + * + * @param remoteUrl - The value of `remote.origin.url`, or `undefined` + * @returns The web URL, or `null` when `remoteUrl` is missing + * + * @example + * toGitHubWebUrl("git@github.com:owner/repo.git"); // "https://github.com/owner/repo" + */ +export function toGitHubWebUrl(remoteUrl: string | undefined): string | null { + if (!remoteUrl) { + return null; + } + return remoteUrl + .replace(/^git@github\.com:/, "https://github.com/") + .replace(/\.git$/, ""); +} diff --git a/src/ui/tree-views/changes-provider.ts b/src/ui/tree-views/changes-provider.ts deleted file mode 100644 index ec2f446..0000000 --- a/src/ui/tree-views/changes-provider.ts +++ /dev/null @@ -1,98 +0,0 @@ -import * as path from "path"; -import * as vscode from "vscode"; -import simpleGit from "simple-git"; -import { RepositoryManager } from "../../core/repositories/repository-manager"; -import { getPrimaryRepository } from "../../shared/utils/repo-selection"; - -class ChangeItem extends vscode.TreeItem { - constructor( - label: string, - description: string | undefined, - resourceUri: vscode.Uri | undefined, - ) { - super(label, vscode.TreeItemCollapsibleState.None); - this.description = description; - this.resourceUri = resourceUri; - this.iconPath = new vscode.ThemeIcon("git-commit"); - this.contextValue = "githubDesktop.change"; - } -} - -export class ChangesProvider - implements vscode.TreeDataProvider -{ - private readonly _onDidChangeTreeData = new vscode.EventEmitter(); - readonly onDidChangeTreeData = this._onDidChangeTreeData.event; - - constructor(private readonly repositories: RepositoryManager) { - this.repositories.onDidChangeRepositories(() => this.refresh()); - } - - refresh(): void { - this._onDidChangeTreeData.fire(); - } - - getTreeItem(element: vscode.TreeItem): vscode.TreeItem { - return element; - } - - async getChildren(): Promise { - const repository = getPrimaryRepository(this.repositories); - if (!repository) { - return [ - this.createInfoItem("Open a Git repository to see local changes."), - ]; - } - - try { - const git = simpleGit(repository.localPath); - const status = await git.status(); - const items: vscode.TreeItem[] = []; - - const add = (filePath: string, state: string) => { - const absolutePath = path.join(repository.localPath, filePath); - const relPath = - path.relative(repository.localPath, absolutePath) || filePath; - const label = `${state} ${relPath}`.trim(); - const item = new ChangeItem( - label, - undefined, - vscode.Uri.file(absolutePath), - ); - items.push(item); - }; - - if (status.files.length === 0) { - return [this.createInfoItem("Working tree clean.")]; - } - - for (const file of status.files) { - const state = this.formatStatusCode(file.index, file.working_dir); - add(file.path, state); - } - - return items; - } catch (error) { - return [ - this.createInfoItem( - "Unable to read git status for the current repository.", - ), - ]; - } - } - - private formatStatusCode(index: string, workingDir: string): string { - const combined = `${index ?? ""}${workingDir ?? ""}`.trim(); - return combined.length > 0 ? combined : "ďż˝"; - } - - private createInfoItem(message: string): vscode.TreeItem { - const item = new vscode.TreeItem( - message, - vscode.TreeItemCollapsibleState.None, - ); - item.iconPath = new vscode.ThemeIcon("info"); - item.contextValue = "githubDesktop.info"; - return item; - } -} diff --git a/src/ui/tree-views/history-provider.ts b/src/ui/tree-views/history-provider.ts deleted file mode 100644 index a220753..0000000 --- a/src/ui/tree-views/history-provider.ts +++ /dev/null @@ -1,101 +0,0 @@ -import * as vscode from "vscode"; -import simpleGit from "simple-git"; -import { RepositoryManager } from "../../core/repositories/repository-manager"; -import { getPrimaryRepository } from "../../shared/utils/repo-selection"; - -const MAX_COMMITS = 50; - -class HistoryItem extends vscode.TreeItem { - constructor(label: string, description: string, tooltip: string) { - super(label, vscode.TreeItemCollapsibleState.None); - this.description = description; - this.tooltip = tooltip; - this.iconPath = new vscode.ThemeIcon("history"); - this.contextValue = "githubDesktop.historyEntry"; - } -} - -export class HistoryProvider - implements vscode.TreeDataProvider -{ - private readonly _onDidChangeTreeData = new vscode.EventEmitter(); - readonly onDidChangeTreeData = this._onDidChangeTreeData.event; - - constructor(private readonly repositories: RepositoryManager) { - this.repositories.onDidChangeRepositories(() => this.refresh()); - } - - refresh(): void { - this._onDidChangeTreeData.fire(); - } - - getTreeItem(element: vscode.TreeItem): vscode.TreeItem { - return element; - } - - async getChildren(): Promise { - const repository = getPrimaryRepository(this.repositories); - if (!repository) { - return [ - this.createInfoItem("Open a Git repository to see commit history."), - ]; - } - - try { - const git = simpleGit(repository.localPath); - const log = await git.log({ maxCount: MAX_COMMITS }); - if (log.all.length === 0) { - return [this.createInfoItem("No commits found.")]; - } - - return log.all.map((commit) => { - const when = this.formatRelativeTime(new Date(commit.date)); - const description = `${commit.author_name} ďż˝ ${when}`; - const tooltip = `${commit.hash}\n${commit.author_name} <${commit.author_email}>\n${commit.date}`; - return new HistoryItem(commit.message, description, tooltip); - }); - } catch (error) { - return [ - this.createInfoItem( - "Unable to read git history for the current repository.", - ), - ]; - } - } - - private createInfoItem(message: string): vscode.TreeItem { - const item = new vscode.TreeItem( - message, - vscode.TreeItemCollapsibleState.None, - ); - item.iconPath = new vscode.ThemeIcon("info"); - item.contextValue = "githubDesktop.info"; - return item; - } - - private formatRelativeTime(date: Date): string { - const diff = Date.now() - date.getTime(); - const seconds = Math.round(diff / 1000); - if (seconds < 60) { - return "just now"; - } - const minutes = Math.round(seconds / 60); - if (minutes < 60) { - return `${minutes} minute${minutes === 1 ? "" : "s"} ago`; - } - const hours = Math.round(minutes / 60); - if (hours < 24) { - return `${hours} hour${hours === 1 ? "" : "s"} ago`; - } - const days = Math.round(hours / 24); - if (days < 30) { - return `${days} day${days === 1 ? "" : "s"} ago`; - } - const months = Math.round(days / 30); - if (months < 12) { - return `${months} month${months === 1 ? "" : "s"} ago`; - } - const years = Math.round(months / 12); - return `${years} year${years === 1 ? "" : "s"} ago`; - } -} diff --git a/src/webviews/app/App.tsx b/src/webviews/app/App.tsx deleted file mode 100644 index c5c453b..0000000 --- a/src/webviews/app/App.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { ThemeProvider, createTheme } from '@mui/material'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { VSCodeBridge, InitialWebviewState } from './bridge'; -import { Timeline } from './components/Timeline'; - -interface AppProps { - bridge: VSCodeBridge; - initialState: InitialWebviewState; -} - -const queryClient = new QueryClient(); - -export const App: React.FC = ({ bridge, initialState }) => { - const [state, setState] = useState(initialState); - const [branchActivity, setBranchActivity] = useState>({}); - const [remoteStatus, setRemoteStatus] = useState(initialState.remoteStatus || null); - - useEffect(() => { - // Send ready message to request initial data - bridge.sendMessage('ready'); - - bridge.onMessage((message) => { - switch (message.command) { - case 'updateChanges': - setState((prev) => ({ ...prev, changes: message.changes })); - break; - case 'updateHistory': - setState((prev) => ({ - ...prev, - history: message.history, - hasMoreCommits: message.hasMoreCommits, - commitsOffset: message.offset - })); - break; - case 'updateBranches': - setState((prev) => ({ - ...prev, - branches: message.branches, - currentBranch: message.currentBranch, - })); - if (message.branchActivity) { - setBranchActivity(message.branchActivity); - } - break; - case 'updateRepository': - setState((prev) => ({ ...prev, repository: message.repository })); - break; - case 'updateRemoteStatus': - setRemoteStatus(message.remoteStatus); - break; - case 'updateAccounts': - setState((prev) => ({ - ...prev, - accounts: message.accounts, - activeAccount: message.activeAccount, - })); - break; - } - }); - }, [bridge]); - - // Create a minimal theme that respects VS Code CSS variables - const theme = createTheme({ - palette: { - mode: 'dark', // We'll use CSS variables instead of theme colors - }, - typography: { - fontFamily: 'var(--vscode-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif)', - }, - components: { - MuiCssBaseline: { - styleOverrides: { - body: { - margin: 0, - padding: 0, - backgroundColor: 'var(--vscode-sideBar-background)', - color: 'var(--vscode-foreground)', - fontFamily: 'var(--vscode-font-family)', - fontSize: 'var(--vscode-font-size, 13px)', - }, - }, - }, - }, - }); - - return ( - - - - - - ); -}; \ No newline at end of file diff --git a/src/webviews/app/bridge.ts b/src/webviews/app/bridge.ts deleted file mode 100644 index 20a181a..0000000 --- a/src/webviews/app/bridge.ts +++ /dev/null @@ -1,84 +0,0 @@ -export interface VSCodeApi { - postMessage: (message: any) => void; - getState: () => any; - setState: (state: any) => void; -} - -export interface InitialWebviewState { - changes: GitChange[]; - history: GitCommit[]; - branches: string[]; - currentBranch: string | null; - repository: Repository | null; - accounts: GitHubAccount[]; - activeAccount: GitHubAccount | null; - hasMoreCommits?: boolean; - commitsOffset?: number; - remoteStatus?: RemoteStatus; -} - -export interface GitChange { - path: string; - status: string; - staged: boolean; -} - -export interface GitCommit { - hash: string; - message: string; - author: string; - email?: string; - date: string; - relativeTime?: string; - tags?: string[]; - isPushed?: boolean; -} - -export interface RemoteStatus { - hasRemote: boolean; - isPublished: boolean; - ahead: number; - behind: number; - lastFetched: Date | null; - remoteBranch: string | null; -} - -export interface Repository { - name: string; - path: string; - remote?: string; -} - -export interface GitHubAccount { - username: string; - email: string; - avatar?: string; - token?: string; -} - -export interface VSCodeBridge { - sendMessage: (command: string, data?: any) => void; - onMessage: (handler: (message: any) => void) => void; - getState: () => any; - setState: (state: any) => void; -} - -export function createVSCodeBridge(vscodeApi: VSCodeApi): VSCodeBridge { - const messageHandlers: ((message: any) => void)[] = []; - - window.addEventListener("message", (event) => { - const message = event.data; - messageHandlers.forEach((handler) => handler(message)); - }); - - return { - sendMessage: (command: string, data?: any) => { - vscodeApi.postMessage({ command, ...data }); - }, - onMessage: (handler: (message: any) => void) => { - messageHandlers.push(handler); - }, - getState: () => vscodeApi.getState(), - setState: (state: any) => vscodeApi.setState(state), - }; -} diff --git a/src/webviews/app/components/CommitDetailPanel/CommitDetailPanel.tsx b/src/webviews/app/components/CommitDetailPanel/CommitDetailPanel.tsx deleted file mode 100644 index dbc8f43..0000000 --- a/src/webviews/app/components/CommitDetailPanel/CommitDetailPanel.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from "react"; -import { Box } from "@mui/material"; -import { CommitDetailPanelProps } from "./types"; -import { CommitHeader } from "./components/CommitHeader"; -import { FileList } from "./components/FileList"; - -export const CommitDetailPanel: React.FC = ({ - commit, - onClose, - bridge, -}) => { - if (!commit) return null; - - const handleFileSelect = async (filePath: string) => { - bridge.sendMessage("getFileDiff", { - hash: commit.hash, - filePath, - }); - }; - - return ( - - - - - ); -}; diff --git a/src/webviews/app/components/CommitDetailPanel/components/CommitHeader.tsx b/src/webviews/app/components/CommitDetailPanel/components/CommitHeader.tsx deleted file mode 100644 index ca97f38..0000000 --- a/src/webviews/app/components/CommitDetailPanel/components/CommitHeader.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import React from "react"; -import { - Box, - Typography, - IconButton, - Chip, - Avatar, -} from "@mui/material"; -import { - Close as CloseIcon, - Add as AddIcon, - Remove as RemoveIcon, -} from "@mui/icons-material"; -import { CommitHeaderProps } from "../types"; -import { formatCommitDate } from "../utils/date.utils"; -import { generateAvatarUrl } from "../utils/avatar.utils"; - -export const CommitHeader: React.FC = ({ - commit, - onClose, -}) => { - return ( - - - - {commit.message} - - - - - - - - - {(commit.author || 'U').charAt(0).toUpperCase()} - - - - {commit.author} - - - {formatCommitDate(commit.date)} - - - - - - - {commit.hash.substring(0, 7)} - - } - label={`+${commit.totalAdditions}`} - size="small" - sx={{ - height: 20, - fontSize: "10px", - bgcolor: "rgba(16, 185, 129, 0.1)", - color: "#10b981", - "& .MuiChip-icon": { color: "#10b981" }, - }} - /> - } - label={`-${commit.totalDeletions}`} - size="small" - sx={{ - height: 20, - fontSize: "10px", - bgcolor: "rgba(239, 68, 68, 0.1)", - color: "#ef4444", - "& .MuiChip-icon": { color: "#ef4444" }, - }} - /> - - {commit.files.length} files - - - - ); -}; \ No newline at end of file diff --git a/src/webviews/app/components/CommitDetailPanel/components/FileList.tsx b/src/webviews/app/components/CommitDetailPanel/components/FileList.tsx deleted file mode 100644 index 4d6f6fd..0000000 --- a/src/webviews/app/components/CommitDetailPanel/components/FileList.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import React from "react"; -import { Box, Typography, List } from "@mui/material"; -import { FileListProps } from "../types"; -import { FileListItem } from "./FileListItem"; - -export const FileList: React.FC = ({ files, onFileSelect }) => { - return ( - - - Changed Files ({files.length}) - - - - {files.map((file, index) => ( - - ))} - - - {files.length === 0 && ( - - No files changed - - )} - - ); -}; \ No newline at end of file diff --git a/src/webviews/app/components/CommitDetailPanel/components/FileListItem.tsx b/src/webviews/app/components/CommitDetailPanel/components/FileListItem.tsx deleted file mode 100644 index c53cbd4..0000000 --- a/src/webviews/app/components/CommitDetailPanel/components/FileListItem.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import React from "react"; -import { - Box, - Typography, - ListItem, - ListItemText, - ListItemIcon, - Chip, -} from "@mui/material"; -import { - Add as AddIcon, - Remove as RemoveIcon, -} from "@mui/icons-material"; -import { FileListItemProps } from "../types"; -import { getStatusColor, getFileIcon } from "../utils/status.utils"; - -export const FileListItem: React.FC = ({ - file, - onFileSelect, -}) => { - return ( - onFileSelect(file.path)} - > - - {getFileIcon(file)} - - - - {file.path} - - - - ) : (file.status || '').charAt(0) === "D" ? ( - - ) : undefined - } - sx={{ - height: 18, - fontSize: "10px", - fontWeight: 500, - bgcolor: getStatusColor(file.status), - color: "white", - "& .MuiChip-icon": { - color: "white", - fontSize: 10, - ml: 0.5, - }, - }} - /> - {file.additions > 0 && ( - - +{file.additions} - - )} - {file.deletions > 0 && ( - - -{file.deletions} - - )} - - - } - /> - - ); -}; \ No newline at end of file diff --git a/src/webviews/app/components/CommitDetailPanel/index.ts b/src/webviews/app/components/CommitDetailPanel/index.ts deleted file mode 100644 index f7112d6..0000000 --- a/src/webviews/app/components/CommitDetailPanel/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { CommitDetailPanel } from "./CommitDetailPanel"; -export { CommitHeader } from "./components/CommitHeader"; -export { FileList } from "./components/FileList"; -export { FileListItem } from "./components/FileListItem"; -export * from "./types"; -export * from "./utils/status.utils"; -export * from "./utils/date.utils"; -export * from "./utils/avatar.utils"; diff --git a/src/webviews/app/components/CommitDetailPanel/types.ts b/src/webviews/app/components/CommitDetailPanel/types.ts deleted file mode 100644 index 9f7eb6d..0000000 --- a/src/webviews/app/components/CommitDetailPanel/types.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { VSCodeBridge } from "../../bridge"; - -export interface CommitFile { - path: string; - status: string; - additions: number; - deletions: number; - iconClass?: string; - iconUri?: string; -} - -export interface CommitDetail { - hash: string; - message: string; - author: string; - email: string; - date: string; - avatarUrl?: string; - files: CommitFile[]; - totalAdditions: number; - totalDeletions: number; -} - -export interface CommitDetailPanelProps { - commit: CommitDetail | null; - onClose: () => void; - bridge: VSCodeBridge; -} - -export interface CommitHeaderProps { - commit: CommitDetail; - onClose: () => void; -} - -export interface FileListProps { - files: CommitFile[]; - onFileSelect: (filePath: string) => void; -} - -export interface FileListItemProps { - file: CommitFile; - onFileSelect: (filePath: string) => void; -} diff --git a/src/webviews/app/components/CommitDetailPanel/utils/avatar.utils.ts b/src/webviews/app/components/CommitDetailPanel/utils/avatar.utils.ts deleted file mode 100644 index de2fc55..0000000 --- a/src/webviews/app/components/CommitDetailPanel/utils/avatar.utils.ts +++ /dev/null @@ -1,38 +0,0 @@ -export const generateAvatarUrl = (email: string, name: string): string => { - const safeEmail = email || "user@example.com"; - const safeName = name || "Unknown"; - - // Try to extract GitHub username from email or name - let username = ""; - - // If email looks like a GitHub email (contains github in domain) - if (safeEmail.includes("@") && !safeEmail.includes("noreply.github.com")) { - // Use email prefix as potential username - username = safeEmail.split("@")[0]; - } else if (safeEmail.includes("users.noreply.github.com")) { - // Extract username from GitHub noreply email format - const match = safeEmail.match(/(\d+\+)?(.+)@users\.noreply\.github\.com/); - if (match) { - username = match[2]; - } - } - - // If we couldn't get username from email, try to use name - if (!username) { - // Clean the name to make it username-like (remove spaces, special chars) - username = safeName - .toLowerCase() - .replace(/\s+/g, "") - .replace(/[^a-z0-9-]/g, ""); - } - - // Fallback to a default if still no username - if (!username) { - username = "octocat"; - } - - const avatarUrl = `https://github.com/${username}.png?size=32`; - console.log("Generated GitHub avatar URL:", avatarUrl); - - return avatarUrl; -}; diff --git a/src/webviews/app/components/CommitDetailPanel/utils/date.utils.ts b/src/webviews/app/components/CommitDetailPanel/utils/date.utils.ts deleted file mode 100644 index 988ee8f..0000000 --- a/src/webviews/app/components/CommitDetailPanel/utils/date.utils.ts +++ /dev/null @@ -1,36 +0,0 @@ -import dayjs from "dayjs"; -import relativeTime from "dayjs/plugin/relativeTime"; - -export const formatCommitDate = (dateString: string): string => { - // Initialize dayjs plugins - dayjs.extend(relativeTime); - - // Handle undefined or null dateString - const safeDateString = dateString || ""; - - // If it's already a relative format (contains "ago"), return as is - if (safeDateString.includes("ago") || safeDateString.includes("just now")) { - return safeDateString; - } - - // Parse the date using dayjs - const date = dayjs(safeDateString); - - // Check if it's a valid date - if (!date.isValid()) { - return safeDateString || "Invalid date"; // Return original if parsing fails - } - - const now = dayjs(); - const diffInDays = now.diff(date, "day"); - - // For dates within the last 7 days, show relative format - if (diffInDays <= 7) { - if (diffInDays === 0) return "today"; - if (diffInDays === 1) return "yesterday"; - return `${diffInDays} days ago`; - } - - // For older dates, show formatted date with time - return date.format("D MMMM, YYYY [at] HH:mm"); -}; diff --git a/src/webviews/app/components/CommitDetailPanel/utils/status.utils.tsx b/src/webviews/app/components/CommitDetailPanel/utils/status.utils.tsx deleted file mode 100644 index 4268634..0000000 --- a/src/webviews/app/components/CommitDetailPanel/utils/status.utils.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React from "react"; -import { - InsertDriveFile as FileIcon, - Add as AddIcon, - Remove as RemoveIcon, -} from "@mui/icons-material"; - -export const getStatusColor = (status: string): string => { - const safeStatus = status || ''; - switch (safeStatus.charAt(0)) { - case "M": - return "#f59e0b"; // Modified - amber - case "A": - return "#10b981"; // Added - green - case "D": - return "#ef4444"; // Deleted - red - case "R": - return "#8b5cf6"; // Renamed - purple - default: - return "#6b7280"; - } -}; - -export const getStatusIcon = (status: string): React.ReactElement => { - const safeStatus = status || ''; - switch (safeStatus.charAt(0)) { - case "A": - return ; - case "D": - return ; - default: - return ; - } -}; - -export const getFileIcon = (file: { - path: string; - iconClass?: string; - iconUri?: string; -}): React.ReactElement => { - // If we have a custom icon URI from VS Code, use it - if (file.iconUri) { - return ( - - ); - } - - // If we have a CSS class from VS Code theme, use it - if (file.iconClass) { - return ( - - ); - } - - // Fallback to generic file icon - return ( - - ); -}; \ No newline at end of file diff --git a/src/webviews/app/components/Timeline/BranchDropdown.tsx b/src/webviews/app/components/Timeline/BranchDropdown.tsx deleted file mode 100644 index 9c66035..0000000 --- a/src/webviews/app/components/Timeline/BranchDropdown.tsx +++ /dev/null @@ -1,741 +0,0 @@ -import React, { useState, useRef, useEffect } from "react"; -import { - Box, - Typography, - TextField, - List, - ListItem, - ListItemText, - ListItemIcon, - Button, - Divider, - Popover, - IconButton, - InputAdornment, - Dialog, - DialogTitle, - DialogContent, - DialogActions, -} from "@mui/material"; -import { - AccountTree as BranchIcon, - KeyboardArrowDown as ArrowDownIcon, - Search as SearchIcon, - MergeType as MergeIcon, - GitHub as PRIcon, - Close as CloseIcon, - Add as AddIcon, -} from "@mui/icons-material"; -import { VSCodeBridge } from "../../bridge"; -import { NewBranchDialog } from "./components/NewBranchDialog"; - -interface BranchDropdownProps { - currentBranch: string | null; - branches: string[]; - branchActivity: Record; - bridge: VSCodeBridge; -} - -interface BranchGroups { - default: string[]; - recent: string[]; - other: string[]; -} - -export const BranchDropdown: React.FC = ({ - currentBranch, - branches, - branchActivity, - bridge, -}) => { - const [anchorEl, setAnchorEl] = useState(null); - const [searchTerm, setSearchTerm] = useState(""); - const [mergeDialogOpen, setMergeDialogOpen] = useState(false); - const [mergeSearchTerm, setMergeSearchTerm] = useState(""); - const [selectedMergeBranch, setSelectedMergeBranch] = useState( - null - ); - const [newBranchDialogOpen, setNewBranchDialogOpen] = useState(false); - const [newBranchName, setNewBranchName] = useState(""); - const buttonRef = useRef(null); - - const open = Boolean(anchorEl); - - const handleClick = (event: React.MouseEvent) => { - setAnchorEl(event.currentTarget); - setSearchTerm(""); - }; - - const handleClose = () => { - setAnchorEl(null); - }; - - const handleBranchSelect = (branch: string) => { - if (branch !== currentBranch) { - bridge.sendMessage("checkoutBranch", { branch }); - } - handleClose(); - }; - - const handleMergeDialogOpen = () => { - setMergeDialogOpen(true); - setMergeSearchTerm(""); - setSelectedMergeBranch(null); - handleClose(); - }; - - const handleMergeDialogClose = () => { - setMergeDialogOpen(false); - setSelectedMergeBranch(null); - setMergeSearchTerm(""); - }; - - const handleMerge = () => { - if (selectedMergeBranch && currentBranch) { - bridge.sendMessage("mergeBranch", { - fromBranch: selectedMergeBranch, - toBranch: currentBranch, - }); - handleMergeDialogClose(); - } - }; - - const handleCreatePR = () => { - if (currentBranch) { - bridge.sendMessage("createPullRequest", { branch: currentBranch }); - } - handleClose(); - }; - - const handleNewBranchDialogOpen = () => { - setNewBranchDialogOpen(true); - setNewBranchName(""); - }; - - const handleNewBranchDialogClose = () => { - setNewBranchDialogOpen(false); - setNewBranchName(""); - }; - - const handleCreateNewBranch = () => { - if (newBranchName.trim()) { - bridge.sendMessage("createBranch", { - branchName: newBranchName.trim(), - fromBranch: currentBranch - }); - handleNewBranchDialogClose(); - handleClose(); - } - }; - - // Group branches - const groupBranches = (): BranchGroups => { - // Handle undefined or null branches array - const safeBranches = branches || []; - const safeBranchActivity = branchActivity || {}; - - const defaultBranches = safeBranches.filter((branch) => - ["main", "master", "develop", "dev"].includes(branch.toLowerCase()) - ); - - const nonDefaultBranches = safeBranches.filter( - (branch) => !defaultBranches.includes(branch) && branch !== currentBranch - ); - - // Sort by activity date for recent branches - const sortedByActivity = nonDefaultBranches.sort((a, b) => { - const aActivity = safeBranchActivity[a] || ""; - const bActivity = safeBranchActivity[b] || ""; - return aActivity.localeCompare(bActivity); - }); - - const recentBranches = sortedByActivity.slice(0, 3); - const otherBranches = sortedByActivity.slice(3); - - return { - default: defaultBranches, - recent: recentBranches, - other: otherBranches, - }; - }; - - const filteredBranches = () => { - if (!searchTerm) return groupBranches(); - - const filtered = (branches || []).filter((branch) => - (branch || '').toLowerCase().includes((searchTerm || '').toLowerCase()) - ); - - return { - default: [], - recent: [], - other: filtered, - }; - }; - - const filteredMergeBranches = () => { - return (branches || []).filter( - (branch) => - branch !== currentBranch && - (branch || '').toLowerCase().includes((mergeSearchTerm || '').toLowerCase()) - ); - }; - - const branchGroups = filteredBranches(); - - const renderBranchGroup = ( - title: string, - branches: string[], - showDivider: boolean = true - ) => { - if (branches.length === 0) return null; - - return ( - - {showDivider && ( - - )} - - {title} - - {branches.map((branch) => ( - handleBranchSelect(branch)} - > - - - - - - {branch} - - {branchActivity[branch] && ( - - {branchActivity[branch]} - - )} - - } - /> - - ))} - - ); - }; - - return ( - <> - - - - - Current branch - - - {currentBranch || 'No branch'} - - - - - - - {/* Search */} - - setSearchTerm(e.target.value)} - size="small" - InputProps={{ - startAdornment: ( - - - - ), - endAdornment: searchTerm && ( - - setSearchTerm("")} - sx={{ p: 0.25 }} - > - - - - ), - }} - sx={{ - "& .MuiOutlinedInput-root": { - bgcolor: "var(--vscode-input-background)", - color: "var(--vscode-input-foreground)", - fontSize: "13px", - "& fieldset": { - borderColor: "var(--vscode-input-border)", - }, - "&:hover fieldset": { - borderColor: "var(--vscode-input-border)", - }, - "&.Mui-focused fieldset": { - borderColor: "var(--vscode-focusBorder)", - }, - }, - "& .MuiInputBase-input::placeholder": { - color: "var(--vscode-input-placeholderForeground)", - opacity: 1, - }, - }} - /> - - - - - - {/* Content */} - - {currentBranch && ( - - - - - - {currentBranch} (current) - - } - /> - - )} - - {!searchTerm && ( - <> - {renderBranchGroup("Default", branchGroups.default)} - {renderBranchGroup("Recent", branchGroups.recent)} - {renderBranchGroup("Other", branchGroups.other)} - - )} - - {searchTerm && - renderBranchGroup("Search Results", branchGroups.other, false)} - - - {/* Actions */} - - - - {currentBranch && - currentBranch !== "main" && - currentBranch !== "master" && ( - - )} - - - - {/* Merge Dialog */} - - - Merge into {currentBranch} - - - setMergeSearchTerm(e.target.value)} - size="small" - sx={{ - mb: 2, - "& .MuiOutlinedInput-root": { - bgcolor: "var(--vscode-input-background)", - color: "var(--vscode-input-foreground)", - fontSize: "13px", - "& fieldset": { - borderColor: "var(--vscode-input-border)", - }, - "&:hover fieldset": { - borderColor: "var(--vscode-input-border)", - }, - "&.Mui-focused fieldset": { - borderColor: "var(--vscode-focusBorder)", - }, - }, - "& .MuiInputBase-input::placeholder": { - color: "var(--vscode-input-placeholderForeground)", - opacity: 1, - }, - }} - /> - - - {filteredMergeBranches().map((branch) => ( - setSelectedMergeBranch(branch)} - > - - - - - - {branch} - - {branchActivity[branch] && ( - - {branchActivity[branch]} - - )} - - } - /> - - ))} - - - - - - - - - - {/* New Branch Dialog */} - - - ); -}; diff --git a/src/webviews/app/components/Timeline/Timeline.tsx b/src/webviews/app/components/Timeline/Timeline.tsx deleted file mode 100644 index 107befd..0000000 --- a/src/webviews/app/components/Timeline/Timeline.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import React from 'react'; -import { Box } from '@mui/material'; -import { TimelineProps } from './types/timeline.types'; -import { useTimelineState } from './hooks/useTimelineState'; -import { useTimelineActions } from './hooks/useTimelineActions'; -import { TimelineHeader } from './components/TimelineHeader'; -import { TimelineTabs } from './components/TimelineTabs'; -import { ChangesView } from './components/ChangesView'; -import { HistoryView } from './components/HistoryView'; -import { ContextMenuAdjusted } from './components/ContextMenuAdjusted'; -import { NewBranchDialog } from './components/NewBranchDialog'; -import { UncommittedChangesDialog } from './components/UncommittedChangesDialog'; - -export const Timeline: React.FC = ({ - changes, - history, - branches, - branchActivity, - currentBranch, - repository, - bridge, - hasMoreCommits = false, - commitsOffset = 0, - remoteStatus, -}) => { - const { state, actions } = useTimelineState(bridge, history, commitsOffset); - - const timelineActions = useTimelineActions({ - bridge, - changes, - selectedFiles: state.selectedFiles, - commitMessage: state.commitMessage, - newBranchName: state.newBranchName, - allCommits: state.allCommits, - hasMoreCommits, - isLoadingMore: state.isLoadingMore, - setSelectedFiles: actions.setSelectedFiles, - setCommitMessage: actions.setCommitMessage, - setContextMenu: actions.setContextMenu, - setNewBranchDialog: actions.setNewBranchDialog, - setNewBranchName: actions.setNewBranchName, - setUncommittedChangesDialog: actions.setUncommittedChangesDialog, - setIsLoadingMore: actions.setIsLoadingMore, - setSelectedCommitHash: actions.setSelectedCommitHash, - }); - - return ( - - {/* Header */} - - - {/* Tabs */} - {/* */} - - {/* Content */} - - {state.activeTab === 'changes' ? ( - - ) : ( - - )} - - - {/* Context Menu */} - {state.contextMenu && ( - - )} - - {/* New Branch Dialog */} - actions.setNewBranchDialog(false)} - onBranchNameChange={actions.setNewBranchName} - onConfirm={timelineActions.handleConfirmCreateBranch} - /> - - {/* Uncommitted Changes Dialog */} - actions.setUncommittedChangesDialog(false)} - onBranchNameChange={actions.setNewBranchName} - onLeaveChanges={() => timelineActions.handleCreateBranchWithChanges(false)} - onBringChanges={() => timelineActions.handleCreateBranchWithChanges(true)} - /> - - ); -}; \ No newline at end of file diff --git a/src/webviews/app/components/Timeline/components/ChangesView.tsx b/src/webviews/app/components/Timeline/components/ChangesView.tsx deleted file mode 100644 index 565a346..0000000 --- a/src/webviews/app/components/Timeline/components/ChangesView.tsx +++ /dev/null @@ -1,201 +0,0 @@ -import React from 'react'; -import { Box, Typography, TextField, Button, Divider } from '@mui/material'; -import { - Add as AddIcon, - Remove as RemoveIcon, - Check as CheckIcon, - Sync as SyncIcon, - MoreVert as MoreIcon -} from '@mui/icons-material'; -import { GitChange } from '../../../bridge'; -import { FileChangesList } from './FileChangesList'; - -interface ChangesViewProps { - changes: GitChange[]; - selectedFiles: Set; - commitMessage: string; - currentBranch: string | null; - onFileToggle: (filePath: string) => void; - onStageFiles: () => void; - onUnstageFiles: () => void; - onCommitMessageChange: (message: string) => void; - onCommit: () => void; - onPush: () => void; - onPull: () => void; -} - -export const ChangesView: React.FC = ({ - changes, - selectedFiles, - commitMessage, - currentBranch, - onFileToggle, - onStageFiles, - onUnstageFiles, - onCommitMessageChange, - onCommit, - onPush, - onPull, -}) => { - const stagedChanges = (changes || []).filter(c => c.staged); - const unstagedChanges = (changes || []).filter(c => !c.staged); - - return ( - - {/* Commit Message Section */} - - onCommitMessageChange(e.target.value)} - variant="outlined" - size="small" - minRows={2} - maxRows={4} - sx={{ - '& .MuiOutlinedInput-root': { - fontSize: '13px', - bgcolor: 'var(--vscode-input-background)', - color: 'var(--vscode-input-foreground)', - '& fieldset': { - borderColor: 'var(--vscode-input-border)', - }, - '&:hover fieldset': { - borderColor: 'var(--vscode-input-border)', - }, - '&.Mui-focused fieldset': { - borderColor: 'var(--vscode-focusBorder)', - } - }, - '& .MuiInputBase-input::placeholder': { - color: 'var(--vscode-input-placeholderForeground)', - opacity: 1, - } - }} - /> - - {/* Commit Actions */} - - - - - - - - - - {/* Changes List */} - - {stagedChanges.length > 0 && ( - } - /> - )} - - {unstagedChanges.length > 0 && ( - } - /> - )} - - {changes.length === 0 && ( - - - - No changes - - - The working tree is clean. There are no changes to commit. - - - )} - - - ); -}; \ No newline at end of file diff --git a/src/webviews/app/components/Timeline/components/CommitListItem.tsx b/src/webviews/app/components/Timeline/components/CommitListItem.tsx deleted file mode 100644 index eda0c75..0000000 --- a/src/webviews/app/components/Timeline/components/CommitListItem.tsx +++ /dev/null @@ -1,180 +0,0 @@ -import React from "react"; -import { - ListItem, - ListItemText, - Typography, - Box, - Avatar, - Chip, -} from "@mui/material"; -import { - LocalOffer as TagIcon, - ArrowUpward as UpArrowIcon, -} from "@mui/icons-material"; -import { GitCommit } from "../../../bridge"; -import { formatCommitDate } from "../utils/timeline.utils"; -import { generateAvatarUrl } from ".."; - -interface CommitListItemProps { - commit: GitCommit; - isSelected: boolean; - onSelect: (commit: GitCommit) => void; - onContextMenu: (event: React.MouseEvent, commit: GitCommit) => void; -} - -export const CommitListItem: React.FC = ({ - commit, - isSelected, - onSelect, - onContextMenu, -}) => { - return ( - onSelect(commit)} - onContextMenu={(e) => onContextMenu(e, commit)} - > - - {commit.message} - - } - secondary={ - - { - // Fallback if avatar fails to load - console.log("Avatar failed to load for:", commit.author); - (e.target as HTMLImageElement).src = ""; - }} - > - {(commit.author || "U").charAt(0).toUpperCase()} - - - {commit.author} - - - • - - - {commit.relativeTime || formatCommitDate(commit.date)} - - - {commit.tags && commit.tags.length > 0 && ( - - {commit.tags.map((tag) => ( - } - sx={{ - height: "18px", - fontSize: "11px", - bgcolor: "var(--vscode-badge-background)", - color: "var(--vscode-badge-foreground)", - borderRadius: "10px", - "& .MuiChip-icon": { - fontSize: "12px", - marginLeft: "4px", - color: "var(--vscode-badge-foreground)", - }, - "& .MuiChip-label": { - px: 0.8, - py: 0, - }, - }} - /> - ))} - - )} - - {commit.isPushed === false && ( - - - - )} - - {/* - {(commit.hash || "").substring(0, 7)} - */} - - } - /> - - ); -}; diff --git a/src/webviews/app/components/Timeline/components/CommitSection.tsx b/src/webviews/app/components/Timeline/components/CommitSection.tsx deleted file mode 100644 index 33dabdb..0000000 --- a/src/webviews/app/components/Timeline/components/CommitSection.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import React from 'react'; -import { Box, Button, TextField, IconButton } from '@mui/material'; -import { CloudUpload as PushIcon, CloudDownload as PullIcon } from '@mui/icons-material'; - -interface CommitSectionProps { - commitMessage: string; - currentBranch: string | null; - onCommitMessageChange: (message: string) => void; - onCommit: () => void; - onPush: () => void; - onPull: () => void; -} - -export const CommitSection: React.FC = ({ - commitMessage, - currentBranch, - onCommitMessageChange, - onCommit, - onPush, - onPull, -}) => { - return ( - - onCommitMessageChange(e.target.value)} - variant="outlined" - size="small" - sx={{ - mb: 2, - '& .MuiOutlinedInput-root': { - bgcolor: 'var(--vscode-input-background)', - color: 'var(--vscode-input-foreground)', - fontSize: '13px', - '& fieldset': { - borderColor: 'var(--vscode-input-border)' - }, - '&:hover fieldset': { - borderColor: 'var(--vscode-inputOption-hoverBackground)' - }, - '&.Mui-focused fieldset': { - borderColor: 'var(--vscode-focusBorder)' - } - }, - '& .MuiInputBase-input::placeholder': { - color: 'var(--vscode-input-placeholderForeground)', - opacity: 1 - } - }} - /> - - - - - - - - - - - ); -}; \ No newline at end of file diff --git a/src/webviews/app/components/Timeline/components/ContextMenu.tsx b/src/webviews/app/components/Timeline/components/ContextMenu.tsx deleted file mode 100644 index eaf55d8..0000000 --- a/src/webviews/app/components/Timeline/components/ContextMenu.tsx +++ /dev/null @@ -1,191 +0,0 @@ -import React from 'react'; -import { - Box, - Paper, - MenuList, - MenuItem, - ListItemIcon, - Divider, -} from '@mui/material'; -import { - RestartAlt as ResetIcon, - CheckCircle as CheckoutIcon, - Reorder as ReorderIcon, - Undo as RevertIcon, - AccountTree as BranchIcon, - LocalOffer as TagIcon, - CallMerge as CherryPickIcon, - ContentCopy as CopyIcon, - OpenInNew as ViewIcon, -} from '@mui/icons-material'; -import { ContextMenuState } from '../types/timeline.types'; -import { GitCommit } from '../../../bridge'; - -interface ContextMenuProps { - contextMenu: ContextMenuState; - onClose: () => void; - onAction: (action: string, commit: GitCommit) => void; -} - -export const ContextMenu: React.FC = ({ - contextMenu, - onClose, - onAction, -}) => { - const menuItemStyles = { - fontSize: '13px', - color: 'var(--vscode-menu-foreground, #cccccc)', - px: 2, - py: 0.5, - minHeight: 'auto', - '&:hover': { - bgcolor: 'var(--vscode-menu-selectionBackground, #094771)', - color: 'var(--vscode-menu-selectionForeground, #ffffff)' - }, - }; - - return ( - <> - {/* Backdrop */} - - - {/* Menu */} - - - onAction('reset', contextMenu.commit!)} - sx={menuItemStyles} - > - - - - Reset to commit... - - - onAction('checkout', contextMenu.commit!)} - sx={menuItemStyles} - > - - - - Checkout commit - - - onAction('reorder', contextMenu.commit!)} - sx={menuItemStyles} - > - - - - Reorder commit - - - onAction('revert', contextMenu.commit!)} - sx={menuItemStyles} - > - - - - Revert changes in commit - - - - - onAction('createBranch', contextMenu.commit!)} - sx={menuItemStyles} - > - - - - Create branch from commit - - - onAction('createTag', contextMenu.commit!)} - sx={menuItemStyles} - > - - - - Create tag... - - - onAction('cherryPick', contextMenu.commit!)} - sx={menuItemStyles} - > - - - - Cherry-pick commit... - - - - - onAction('copySha', contextMenu.commit!)} - sx={menuItemStyles} - > - - - - Copy SHA - - - onAction('copyTag', contextMenu.commit!)} - sx={menuItemStyles} - > - - - - Copy tag - - - onAction('viewOnGitHub', contextMenu.commit!)} - sx={menuItemStyles} - > - - - - View on GitHub - - - - - ); -}; \ No newline at end of file diff --git a/src/webviews/app/components/Timeline/components/ContextMenuAdjusted.tsx b/src/webviews/app/components/Timeline/components/ContextMenuAdjusted.tsx deleted file mode 100644 index bfd1f86..0000000 --- a/src/webviews/app/components/Timeline/components/ContextMenuAdjusted.tsx +++ /dev/null @@ -1,225 +0,0 @@ -import React, { useEffect, useRef, useState } from 'react'; -import { - Box, - Paper, - MenuList, - MenuItem, - ListItemIcon, - Divider, -} from '@mui/material'; -import { - RestartAlt as ResetIcon, - CheckCircle as CheckoutIcon, - Reorder as ReorderIcon, - Undo as RevertIcon, - AccountTree as BranchIcon, - LocalOffer as TagIcon, - CallMerge as CherryPickIcon, - ContentCopy as CopyIcon, - OpenInNew as ViewIcon, -} from '@mui/icons-material'; -import { ContextMenuState } from '../types/timeline.types'; -import { GitCommit } from '../../../bridge'; - -interface ContextMenuProps { - contextMenu: ContextMenuState; - onClose: () => void; - onAction: (action: string, commit: GitCommit) => void; -} - -export const ContextMenuAdjusted: React.FC = ({ - contextMenu, - onClose, - onAction, -}) => { - const menuRef = useRef(null); - const [position, setPosition] = useState({ top: 0, left: 0 }); - - useEffect(() => { - if (menuRef.current) { - const menuRect = menuRef.current.getBoundingClientRect(); - const viewportWidth = window.innerWidth; - const viewportHeight = window.innerHeight; - - let top = contextMenu.mouseY; - let left = contextMenu.mouseX; - - // Adjust horizontal position if menu would overflow right edge - if (left + menuRect.width > viewportWidth) { - left = viewportWidth - menuRect.width - 10; - } - - // Adjust vertical position if menu would overflow bottom edge - if (top + menuRect.height > viewportHeight) { - top = viewportHeight - menuRect.height - 10; - } - - // Ensure menu doesn't go off left or top edges - if (left < 10) left = 10; - if (top < 10) top = 10; - - setPosition({ top, left }); - } - }, [contextMenu.mouseX, contextMenu.mouseY]); - - const menuItemStyles = { - fontSize: '13px', - color: 'var(--vscode-menu-foreground, #cccccc)', - px: 2, - py: 0.5, - minHeight: 'auto', - '&:hover': { - bgcolor: 'var(--vscode-menu-selectionBackground, #094771)', - color: 'var(--vscode-menu-selectionForeground, #ffffff)' - }, - }; - - return ( - <> - {/* Backdrop */} - - - {/* Menu */} - - - onAction('reset', contextMenu.commit!)} - sx={menuItemStyles} - > - - - - Reset to commit... - - - onAction('checkout', contextMenu.commit!)} - sx={menuItemStyles} - > - - - - Checkout commit - - - onAction('reorder', contextMenu.commit!)} - sx={menuItemStyles} - > - - - - Reorder commit - - - onAction('revert', contextMenu.commit!)} - sx={menuItemStyles} - > - - - - Revert changes in commit - - - - - onAction('createBranch', contextMenu.commit!)} - sx={menuItemStyles} - > - - - - Create branch from commit - - - onAction('createTag', contextMenu.commit!)} - sx={menuItemStyles} - > - - - - Create tag... - - - onAction('cherryPick', contextMenu.commit!)} - sx={menuItemStyles} - > - - - - Cherry-pick commit... - - - - - onAction('copySha', contextMenu.commit!)} - sx={menuItemStyles} - > - - - - Copy SHA - - - onAction('copyTag', contextMenu.commit!)} - sx={menuItemStyles} - > - - - - Copy tag - - - onAction('viewOnGitHub', contextMenu.commit!)} - sx={menuItemStyles} - > - - - - View on GitHub - - - - - ); -}; \ No newline at end of file diff --git a/src/webviews/app/components/Timeline/components/FileChangesList.tsx b/src/webviews/app/components/Timeline/components/FileChangesList.tsx deleted file mode 100644 index b86ceb3..0000000 --- a/src/webviews/app/components/Timeline/components/FileChangesList.tsx +++ /dev/null @@ -1,213 +0,0 @@ -import React, { useState } from 'react'; -import { - Box, - Typography, - IconButton, - List, - ListItem, - ListItemText, - ListItemIcon, - Collapse, -} from '@mui/material'; -import { - Add as AddIcon, - Remove as RemoveIcon, - KeyboardArrowDown as ArrowDownIcon, - KeyboardArrowRight as ArrowRightIcon, - InsertDriveFile as FileIcon, -} from '@mui/icons-material'; -import { GitChange } from '../../../bridge'; - -interface FileChangesListProps { - title: string; - changes: GitChange[]; - selectedFiles: Set; - onFileToggle: (filePath: string) => void; - onActionClick?: () => void; - actionLabel?: string; - actionIcon?: React.ReactNode; -} - -const getStatusIndicator = (status: string) => { - switch (status.toUpperCase()) { - case 'M': - case 'MODIFIED': - return { label: 'M', color: '#569cd6' }; // Blue - case 'A': - case 'ADDED': - return { label: 'A', color: '#4fc1ff' }; // Light blue - case 'D': - case 'DELETED': - return { label: 'D', color: '#f85149' }; // Red - case 'R': - case 'RENAMED': - return { label: 'R', color: '#a5a5a5' }; // Gray - case 'U': - case 'UNTRACKED': - return { label: 'U', color: '#73c991' }; // Green - default: - return { label: '?', color: '#a5a5a5' }; - } -}; - -export const FileChangesList: React.FC = ({ - title, - changes, - selectedFiles, - onFileToggle, - onActionClick, - actionLabel, - actionIcon, -}) => { - const [collapsed, setCollapsed] = useState(false); - - if (changes.length === 0) return null; - - const getFileName = (path: string) => { - return path.split('/').pop() || path; - }; - - const getFileDirectory = (path: string) => { - const parts = path.split('/'); - return parts.length > 1 ? parts.slice(0, -1).join('/') : ''; - }; - - return ( - - {/* Section Header */} - setCollapsed(!collapsed)} - > - - {collapsed ? : } - - - - {title} - - - - {changes.length} - - - {onActionClick && actionLabel && ( - { - e.stopPropagation(); - onActionClick(); - }} - size="small" - sx={{ - p: 0.25, - color: 'var(--vscode-sideBarSectionHeader-foreground)', - '&:hover': { - bgcolor: 'var(--vscode-toolbar-hoverBackground)' - } - }} - > - {actionIcon} - - )} - - - {/* File List */} - - - {changes.map((change) => { - const status = getStatusIndicator(change.status); - const fileName = getFileName(change.path); - const directory = getFileDirectory(change.path); - - return ( - onFileToggle(change.path)} - > - - - - - - - {fileName} - - {directory && ( - - {directory} - - )} - - } - sx={{ my: 0 }} - /> - - - {status.label} - - - ); - })} - - - - ); -}; \ No newline at end of file diff --git a/src/webviews/app/components/Timeline/components/HistoryView.tsx b/src/webviews/app/components/Timeline/components/HistoryView.tsx deleted file mode 100644 index ba102d1..0000000 --- a/src/webviews/app/components/Timeline/components/HistoryView.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import React from 'react'; -import { Box, Typography, List } from '@mui/material'; -import { GitCommit } from '../../../bridge'; -import { CommitListItem } from './CommitListItem'; -import { LoadMoreSection } from './LoadMoreSection'; - -interface HistoryViewProps { - allCommits: GitCommit[]; - selectedCommitHash: string | null; - hasMoreCommits: boolean; - isLoadingMore: boolean; - onCommitSelect: (commit: GitCommit) => void; - onCommitContextMenu: (event: React.MouseEvent, commit: GitCommit) => void; - onLoadMoreCommits: () => void; -} - -export const HistoryView: React.FC = ({ - allCommits, - selectedCommitHash, - hasMoreCommits, - isLoadingMore, - onCommitSelect, - onCommitContextMenu, - onLoadMoreCommits, -}) => { - return ( - { - const { scrollTop, scrollHeight, clientHeight } = e.currentTarget; - // Load more when scrolled to bottom - if (scrollHeight - scrollTop <= clientHeight + 100 && hasMoreCommits && !isLoadingMore) { - onLoadMoreCommits(); - } - }} - > - - {allCommits.map((commit) => ( - - ))} - - - - - {allCommits.length === 0 && !isLoadingMore && ( - - - No commits yet - - - Create your first commit to see history - - - )} - - ); -}; \ No newline at end of file diff --git a/src/webviews/app/components/Timeline/components/LoadMoreSection.tsx b/src/webviews/app/components/Timeline/components/LoadMoreSection.tsx deleted file mode 100644 index ece8156..0000000 --- a/src/webviews/app/components/Timeline/components/LoadMoreSection.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react'; -import { Box, Typography, Button } from '@mui/material'; - -interface LoadMoreSectionProps { - hasMoreCommits: boolean; - isLoading: boolean; - onLoadMore: () => void; -} - -export const LoadMoreSection: React.FC = ({ - hasMoreCommits, - isLoading, - onLoadMore, -}) => { - if (!hasMoreCommits) return null; - - return ( - - {isLoading ? ( - - Loading more commits... - - ) : ( - - )} - - ); -}; \ No newline at end of file diff --git a/src/webviews/app/components/Timeline/components/NewBranchDialog.tsx b/src/webviews/app/components/Timeline/components/NewBranchDialog.tsx deleted file mode 100644 index 95b7e2c..0000000 --- a/src/webviews/app/components/Timeline/components/NewBranchDialog.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import React from 'react'; -import { - Dialog, - DialogTitle, - DialogContent, - DialogActions, - DialogContentText, - TextField, - Button, -} from '@mui/material'; - -interface NewBranchDialogProps { - open: boolean; - branchName: string; - onClose: () => void; - onBranchNameChange: (name: string) => void; - onConfirm: () => void; -} - -export const NewBranchDialog: React.FC = ({ - open, - branchName, - onClose, - onBranchNameChange, - onConfirm, -}) => { - const handleKeyPress = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - onConfirm(); - } - }; - - return ( - - Create New Branch - - - Enter a name for the new branch: - - onBranchNameChange(e.target.value)} - placeholder="feature/new-feature" - onKeyPress={handleKeyPress} - /> - - - - - - - ); -}; \ No newline at end of file diff --git a/src/webviews/app/components/Timeline/components/TimelineHeader.tsx b/src/webviews/app/components/Timeline/components/TimelineHeader.tsx deleted file mode 100644 index ed6cc52..0000000 --- a/src/webviews/app/components/Timeline/components/TimelineHeader.tsx +++ /dev/null @@ -1,395 +0,0 @@ -import React, { useState, useEffect } from "react"; -import { - Box, - Typography, - Popover, - MenuList, - MenuItem, - ListItemIcon, - ListItemText, -} from "@mui/material"; -import { - CallSplit as BranchIcon, - KeyboardArrowDown as DropdownIcon, - ArrowDownward as PullIcon, - CloudUpload as PublishIcon, - CloudDownload as FetchIcon, - ArrowUpward as PushIcon, - FlashOn as ForceIcon, -} from "@mui/icons-material"; -import { BranchDropdown } from "../BranchDropdown"; -import { VSCodeBridge, RemoteStatus } from "../../../bridge"; -import { useGitOperations } from "../../../hooks/useGitOperations"; - -interface TimelineHeaderProps { - currentBranch: string | null; - branches: string[]; - branchActivity: Record; - remoteStatus?: RemoteStatus | null; - bridge: VSCodeBridge; - onCreateNewBranch: () => void; - onRefresh: () => void; -} - -export const TimelineHeader: React.FC = ({ - currentBranch, - branches, - branchActivity, - remoteStatus, - bridge, - onCreateNewBranch, - onRefresh, -}) => { - const [syncAnchorEl, setSyncAnchorEl] = useState(null); - const [lastFetchedText, setLastFetchedText] = useState(""); - const gitOps = useGitOperations(bridge); - - const syncOpen = Boolean(syncAnchorEl); - - useEffect(() => { - if (!remoteStatus) return; - - const updateLastFetchedText = () => { - if (remoteStatus.lastFetched) { - const fetchedDate = new Date(remoteStatus.lastFetched); - const diff = Date.now() - fetchedDate.getTime(); - const seconds = Math.round(diff / 1000); - - if (seconds < 60) { - setLastFetchedText("just now"); - } else { - const minutes = Math.round(seconds / 60); - if (minutes < 60) { - setLastFetchedText( - `${minutes} minute${minutes === 1 ? "" : "s"} ago` - ); - } else { - const hours = Math.round(minutes / 60); - if (hours < 24) { - setLastFetchedText(`${hours} hour${hours === 1 ? "" : "s"} ago`); - } else { - const days = Math.round(hours / 24); - setLastFetchedText(`${days} day${days === 1 ? "" : "s"} ago`); - } - } - } - } else { - setLastFetchedText("Never"); - } - }; - - updateLastFetchedText(); - const interval = setInterval(updateLastFetchedText, 60000); - return () => clearInterval(interval); - }, [remoteStatus]); - - const handleSyncClick = (event: React.MouseEvent) => { - // Perform sync action based on remote status - if (!remoteStatus) return; - - if (!remoteStatus.isPublished) { - gitOps.publish.mutate(); - } else if (remoteStatus.behind > 0 && remoteStatus.ahead > 0) { - gitOps.pull.mutate(); - } else if (remoteStatus.behind > 0) { - gitOps.pull.mutate(); - } else if (remoteStatus.ahead > 0) { - gitOps.push.mutate(); - } else { - gitOps.fetch.mutate(); - } - }; - - const handleSyncDropdownClick = (event: React.MouseEvent) => { - event.stopPropagation(); - setSyncAnchorEl(event.currentTarget); - }; - - const handleSyncClose = () => { - setSyncAnchorEl(null); - }; - - const getSyncStatus = () => { - if (!remoteStatus) - return { title: "Sync", icon: FetchIcon, showCounts: false }; - - if (!remoteStatus.isPublished) { - return { title: "Publish branch", icon: PublishIcon, showCounts: false }; - } else if (remoteStatus.behind > 0 && remoteStatus.ahead > 0) { - return { title: "Pull origin", icon: PullIcon, showCounts: true }; - } else if (remoteStatus.behind > 0) { - return { title: "Pull origin", icon: PullIcon, showCounts: true }; - } else if (remoteStatus.ahead > 0) { - return { title: "Push origin", icon: PushIcon, showCounts: true }; - } else { - return { title: "Fetch origin", icon: FetchIcon, showCounts: false }; - } - }; - - const syncStatus = getSyncStatus(); - const isLoading = - gitOps.push.isLoading || - gitOps.pull.isLoading || - gitOps.fetch.isLoading || - gitOps.publish.isLoading; - - // Show dropdown only when there are remote changes or commits to push - const shouldShowDropdown = - remoteStatus && (remoteStatus.behind > 0 || remoteStatus.ahead > 0); - - const handleFetch = () => { - gitOps.fetch.mutate(); - handleSyncClose(); - }; - - const handleForcePush = () => { - bridge.sendMessage("forcePush"); - handleSyncClose(); - }; - - return ( - <> - - {/* Branch Section */} - - - - - {/* Sync Section */} - - - - - {syncStatus.title} - - - Last fetched {lastFetchedText} - - - - {/* Sync Counters */} - {remoteStatus && syncStatus.showCounts && ( - - {remoteStatus.behind > 0 && ( - <> - - {remoteStatus.behind} - - - - )} - {remoteStatus.ahead > 0 && ( - <> - - {remoteStatus.ahead} - - - - )} - - )} - - {shouldShowDropdown && ( - - - - )} - - - - {/* Sync Dropdown */} - - - - - - - - - - {remoteStatus && remoteStatus.ahead > 0 && ( - - - - - - - )} - - - - ); -}; diff --git a/src/webviews/app/components/Timeline/components/TimelineTabs.tsx b/src/webviews/app/components/Timeline/components/TimelineTabs.tsx deleted file mode 100644 index 153d919..0000000 --- a/src/webviews/app/components/Timeline/components/TimelineTabs.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import React from "react"; -import { Box, Button } from "@mui/material"; -import { TabType } from "../types/timeline.types"; -import { GitChange, GitCommit } from "../../../bridge"; - -interface TimelineTabsProps { - activeTab: TabType; - changes: GitChange[]; - history: GitCommit[]; - onTabChange: (tab: TabType) => void; -} - -export const TimelineTabs: React.FC = ({ - activeTab, - changes, - history, - onTabChange, -}) => { - const getTabButtonStyles = (isActive: boolean) => ({ - flex: 1, - py: 0.75, - px: 0, - color: isActive - ? "var(--vscode-foreground)" - : "var(--vscode-descriptionForeground)", - borderBottom: isActive ? "2px solid var(--vscode-focusBorder)" : "none", - borderRadius: 0, - fontSize: "13px", - fontWeight: 500, - textTransform: "none" as const, - "&:hover": { - bgcolor: "var(--vscode-list-hoverBackground)", - }, - }); - - return ( - - - - - ); -}; diff --git a/src/webviews/app/components/Timeline/components/UncommittedChangesDialog.tsx b/src/webviews/app/components/Timeline/components/UncommittedChangesDialog.tsx deleted file mode 100644 index f93c14c..0000000 --- a/src/webviews/app/components/Timeline/components/UncommittedChangesDialog.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import React from 'react'; -import { - Dialog, - DialogTitle, - DialogContent, - DialogActions, - DialogContentText, - TextField, - Button, -} from '@mui/material'; - -interface UncommittedChangesDialogProps { - open: boolean; - branchName: string; - onClose: () => void; - onBranchNameChange: (name: string) => void; - onLeaveChanges: () => void; - onBringChanges: () => void; -} - -export const UncommittedChangesDialog: React.FC = ({ - open, - branchName, - onClose, - onBranchNameChange, - onLeaveChanges, - onBringChanges, -}) => { - return ( - - Uncommitted Changes - - - You have uncommitted changes. What would you like to do with them? - - onBranchNameChange(e.target.value)} - placeholder="feature/new-feature" - label="New branch name" - sx={{ mb: 2 }} - /> - - - - - - - - ); -}; \ No newline at end of file diff --git a/src/webviews/app/components/Timeline/hooks/useTimelineActions.ts b/src/webviews/app/components/Timeline/hooks/useTimelineActions.ts deleted file mode 100644 index 801075e..0000000 --- a/src/webviews/app/components/Timeline/hooks/useTimelineActions.ts +++ /dev/null @@ -1,200 +0,0 @@ -import { VSCodeBridge, GitChange, GitCommit } from "../../../bridge"; -import { ContextMenuState } from "../types/timeline.types"; - -interface UseTimelineActionsProps { - bridge: VSCodeBridge; - changes: GitChange[]; - selectedFiles: Set; - commitMessage: string; - newBranchName: string; - allCommits: GitCommit[]; - hasMoreCommits: boolean; - isLoadingMore: boolean; - setSelectedFiles: (files: Set) => void; - setCommitMessage: (message: string) => void; - setContextMenu: (menu: ContextMenuState | null) => void; - setNewBranchDialog: (open: boolean) => void; - setNewBranchName: (name: string) => void; - setUncommittedChangesDialog: (open: boolean) => void; - setIsLoadingMore: (loading: boolean) => void; - setSelectedCommitHash: (hash: string | null) => void; -} - -export const useTimelineActions = ({ - bridge, - changes, - selectedFiles, - commitMessage, - newBranchName, - allCommits, - hasMoreCommits, - isLoadingMore, - setSelectedFiles, - setCommitMessage, - setContextMenu, - setNewBranchDialog, - setNewBranchName, - setUncommittedChangesDialog, - setIsLoadingMore, - setSelectedCommitHash, -}: UseTimelineActionsProps) => { - const handleFileToggle = (filePath: string) => { - const newSelected = new Set(selectedFiles); - if (newSelected.has(filePath)) { - newSelected.delete(filePath); - } else { - newSelected.add(filePath); - } - setSelectedFiles(newSelected); - }; - - const handleStageFiles = () => { - bridge.sendMessage("stageFiles", { files: Array.from(selectedFiles) }); - setSelectedFiles(new Set()); - }; - - const handleUnstageFiles = () => { - bridge.sendMessage("unstageFiles", { files: Array.from(selectedFiles) }); - setSelectedFiles(new Set()); - }; - - const handleCommit = () => { - if (commitMessage.trim()) { - bridge.sendMessage("commit", { message: commitMessage }); - setCommitMessage(""); - } - }; - - const handlePush = () => { - bridge.sendMessage("push"); - }; - - const handlePull = () => { - bridge.sendMessage("pull"); - }; - - const handleRefresh = () => { - bridge.sendMessage("refresh"); - }; - - const handleCreateNewBranch = () => { - const hasUncommittedChanges = changes.length > 0; - if (hasUncommittedChanges) { - setUncommittedChangesDialog(true); - } else { - setNewBranchDialog(true); - } - }; - - const handleCreateBranchWithChanges = (bringChanges: boolean) => { - setUncommittedChangesDialog(false); - - if (bringChanges) { - bridge.sendMessage("createBranchWithChanges", { - branchName: newBranchName, - bringChanges: true, - }); - } else { - bridge.sendMessage("createBranchWithChanges", { - branchName: newBranchName, - bringChanges: false, - }); - } - - setNewBranchName(""); - setNewBranchDialog(true); - }; - - const handleConfirmCreateBranch = () => { - if (newBranchName.trim()) { - bridge.sendMessage("createBranch", { branchName: newBranchName.trim() }); - setNewBranchDialog(false); - setNewBranchName(""); - } - }; - - const handleLoadMoreCommits = () => { - if (!isLoadingMore && hasMoreCommits) { - setIsLoadingMore(true); - bridge.sendMessage("loadMoreCommits", { offset: allCommits.length }); - } - }; - - const handleCommitSelect = (commit: GitCommit) => { - console.log("Selecting commit:", commit.hash); - setSelectedCommitHash(commit.hash); - bridge.sendMessage("openCommitDetail", { hash: commit.hash }); - }; - - const handleCommitContextMenu = ( - event: React.MouseEvent, - commit: GitCommit, - ) => { - event.preventDefault(); - setContextMenu({ - mouseX: event.clientX + 2, - mouseY: event.clientY - 6, - commit, - }); - }; - - const handleContextMenuClose = () => { - setContextMenu(null); - }; - - const handleContextMenuAction = (action: string, commit: GitCommit) => { - switch (action) { - case "reset": - bridge.sendMessage("resetToCommit", { hash: commit.hash }); - break; - case "checkout": - bridge.sendMessage("checkoutCommit", { hash: commit.hash }); - break; - case "reorder": - bridge.sendMessage("reorderCommit", { hash: commit.hash }); - break; - case "revert": - bridge.sendMessage("revertCommit", { hash: commit.hash }); - break; - case "createBranch": - bridge.sendMessage("createBranchFromCommit", { hash: commit.hash }); - break; - case "createTag": - bridge.sendMessage("createTagFromCommit", { hash: commit.hash }); - break; - case "cherryPick": - bridge.sendMessage("cherryPickCommit", { hash: commit.hash }); - break; - case "copySha": - navigator.clipboard.writeText(commit.hash); - break; - case "copyTag": - navigator.clipboard.writeText( - `${commit.message || "No message"} (${(commit.hash || "").substring(0, 7)})`, - ); - break; - case "viewOnGitHub": - bridge.sendMessage("viewCommitOnGitHub", { hash: commit.hash }); - break; - } - handleContextMenuClose(); - }; - - return { - handleFileToggle, - handleStageFiles, - handleUnstageFiles, - handleCommit, - handlePush, - handlePull, - handleRefresh, - handleCreateNewBranch, - handleCreateBranchWithChanges, - handleConfirmCreateBranch, - handleLoadMoreCommits, - handleCommitSelect, - handleCommitContextMenu, - handleContextMenuClose, - handleContextMenuAction, - }; -}; diff --git a/src/webviews/app/components/Timeline/hooks/useTimelineState.ts b/src/webviews/app/components/Timeline/hooks/useTimelineState.ts deleted file mode 100644 index 77e0e71..0000000 --- a/src/webviews/app/components/Timeline/hooks/useTimelineState.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { useState, useEffect } from "react"; -import { - TimelineState, - TabType, - ContextMenuState, -} from "../types/timeline.types"; -import { GitCommit, VSCodeBridge } from "../../../bridge"; - -export const useTimelineState = ( - bridge: VSCodeBridge, - history: GitCommit[], - commitsOffset: number, -) => { - const [activeTab, setActiveTab] = useState("history"); - const [commitMessage, setCommitMessage] = useState(""); - const [selectedFiles, setSelectedFiles] = useState>(new Set()); - const [contextMenu, setContextMenu] = useState(null); - const [newBranchDialog, setNewBranchDialog] = useState(false); - const [newBranchName, setNewBranchName] = useState(""); - const [uncommittedChangesDialog, setUncommittedChangesDialog] = - useState(false); - const [isLoadingMore, setIsLoadingMore] = useState(false); - const [allCommits, setAllCommits] = useState([]); - const [selectedCommitHash, setSelectedCommitHash] = useState( - null, - ); - - // Update allCommits when history changes - useEffect(() => { - if (commitsOffset === 0) { - setAllCommits(history); - } - }, [history, commitsOffset]); - - // Handle load more commits response - useEffect(() => { - const handleMessage = (message: any) => { - if (message.command === "loadMoreCommitsResponse") { - setAllCommits((prev) => [...prev, ...message.history]); - setIsLoadingMore(false); - } - }; - - bridge.onMessage(handleMessage); - return () => { - // Cleanup if needed - }; - }, [bridge]); - - return { - state: { - activeTab, - commitMessage, - selectedFiles, - contextMenu, - newBranchDialog, - newBranchName, - uncommittedChangesDialog, - isLoadingMore, - allCommits, - selectedCommitHash, - }, - actions: { - setActiveTab, - setCommitMessage, - setSelectedFiles, - setContextMenu, - setNewBranchDialog, - setNewBranchName, - setUncommittedChangesDialog, - setIsLoadingMore, - setAllCommits, - setSelectedCommitHash, - }, - }; -}; diff --git a/src/webviews/app/components/Timeline/index.ts b/src/webviews/app/components/Timeline/index.ts deleted file mode 100644 index 96aea5e..0000000 --- a/src/webviews/app/components/Timeline/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -export { Timeline } from "./Timeline"; -export type { - TimelineProps, - TabType, - ContextMenuState, - TimelineState, -} from "./types/timeline.types"; -export { formatCommitDate, getStatusColor } from "./utils/timeline.utils"; -export { useTimelineState } from "./hooks/useTimelineState"; -export { useTimelineActions } from "./hooks/useTimelineActions"; -export { generateAvatarUrl } from "../CommitDetailPanel/utils/avatar.utils"; - -// Component exports -export { TimelineHeader } from "./components/TimelineHeader"; -export { TimelineTabs } from "./components/TimelineTabs"; -export { ChangesView } from "./components/ChangesView"; -export { HistoryView } from "./components/HistoryView"; -export { ContextMenu } from "./components/ContextMenu"; -export { NewBranchDialog } from "./components/NewBranchDialog"; -export { UncommittedChangesDialog } from "./components/UncommittedChangesDialog"; -export { FileChangesList } from "./components/FileChangesList"; -export { CommitSection } from "./components/CommitSection"; -export { CommitListItem } from "./components/CommitListItem"; -export { LoadMoreSection } from "./components/LoadMoreSection"; diff --git a/src/webviews/app/components/Timeline/types/timeline.types.ts b/src/webviews/app/components/Timeline/types/timeline.types.ts deleted file mode 100644 index b504aa3..0000000 --- a/src/webviews/app/components/Timeline/types/timeline.types.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { - VSCodeBridge, - GitChange, - GitCommit, - Repository, - RemoteStatus, -} from "../../../bridge"; - -export interface TimelineProps { - changes: GitChange[]; - history: GitCommit[]; - branches: string[]; - branchActivity: Record; - currentBranch: string | null; - repository: Repository | null; - bridge: VSCodeBridge; - hasMoreCommits?: boolean; - commitsOffset?: number; - remoteStatus?: RemoteStatus | null; -} - -export interface ContextMenuState { - mouseX: number; - mouseY: number; - commit: GitCommit | null; -} - -export type TabType = "changes" | "history"; - -export interface TimelineState { - activeTab: TabType; - commitMessage: string; - selectedFiles: Set; - contextMenu: ContextMenuState | null; - newBranchDialog: boolean; - newBranchName: string; - uncommittedChangesDialog: boolean; - isLoadingMore: boolean; - allCommits: GitCommit[]; - selectedCommitHash: string | null; -} diff --git a/src/webviews/app/components/Timeline/utils/timeline.utils.ts b/src/webviews/app/components/Timeline/utils/timeline.utils.ts deleted file mode 100644 index b04996b..0000000 --- a/src/webviews/app/components/Timeline/utils/timeline.utils.ts +++ /dev/null @@ -1,54 +0,0 @@ -import dayjs from "dayjs"; -import relativeTime from "dayjs/plugin/relativeTime"; - -export const formatCommitDate = (dateString: string): string => { - // Initialize dayjs plugins - dayjs.extend(relativeTime); - - // Handle undefined or null dateString - const safeDateString = dateString || ""; - - // If it's already a relative format (contains "ago"), return as is - if (safeDateString.includes("ago") || safeDateString.includes("just now")) { - return safeDateString; - } - - // Parse the date using dayjs - const date = dayjs(safeDateString); - - // Check if it's a valid date - if (!date.isValid()) { - return safeDateString || "Invalid date"; - } - - const now = dayjs(); - const diffInDays = now.diff(date, "day"); - - // For dates within the last 7 days, show relative format - if (diffInDays <= 7) { - if (diffInDays === 0) return "today"; - if (diffInDays === 1) return "yesterday"; - return `${diffInDays} days ago`; - } - - // For older dates, show formatted date with time - return date.format("D MMMM, YYYY [at] HH:mm"); -}; - -export const getStatusColor = (status: string): string => { - const safeStatus = status || ""; - switch (safeStatus.charAt(0)) { - case "M": - return "#f59e0b"; // Modified - amber - case "A": - return "#10b981"; // Added - green - case "D": - return "#ef4444"; // Deleted - red - case "R": - return "#8b5cf6"; // Renamed - purple - case "?": - return "#6b7280"; // Untracked - gray - default: - return "#6b7280"; - } -}; diff --git a/src/webviews/app/domain/entities/branch.entity.ts b/src/webviews/app/domain/entities/branch.entity.ts deleted file mode 100644 index 56a532a..0000000 --- a/src/webviews/app/domain/entities/branch.entity.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { BranchName } from "../value-objects/branch-name.vo"; -import { CommitDate } from "../value-objects/commit-date.vo"; - -export class Branch { - constructor( - public readonly name: BranchName, - public readonly isCurrent: boolean = false, - public readonly lastActivity?: CommitDate, - public readonly isRemote: boolean = false, - ) {} - - get displayName(): string { - return this.name.displayName; - } - - get shortName(): string { - return this.name.shortName; - } - - get activityText(): string { - return this.lastActivity?.relative ?? "No activity"; - } - - get isLocal(): boolean { - return !this.isRemote; - } - - toJSON() { - return { - name: this.name.value, - isCurrent: this.isCurrent, - lastActivity: this.lastActivity?.formatted, - isRemote: this.isRemote, - }; - } -} diff --git a/src/webviews/app/domain/entities/commit.entity.ts b/src/webviews/app/domain/entities/commit.entity.ts deleted file mode 100644 index 9478e84..0000000 --- a/src/webviews/app/domain/entities/commit.entity.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { CommitId } from "../value-objects/commit-id.vo"; -import { CommitMessage } from "../value-objects/commit-message.vo"; -import { Author } from "../value-objects/author.vo"; -import { CommitDate } from "../value-objects/commit-date.vo"; -import { FileChange } from "./file-change.entity"; - -export class Commit { - constructor( - public readonly id: CommitId, - public readonly message: CommitMessage, - public readonly author: Author, - public readonly date: CommitDate, - public readonly files: FileChange[] = [], - public readonly avatarUrl?: string, - public readonly tags?: string[], - public readonly isPushed: boolean = true, - ) {} - - get shortHash(): string { - return this.id.shortHash; - } - - get fullHash(): string { - return this.id.value; - } - - get totalAdditions(): number { - return this.files.reduce((sum, file) => sum + file.additions, 0); - } - - get totalDeletions(): number { - return this.files.reduce((sum, file) => sum + file.deletions, 0); - } - - get fileCount(): number { - return this.files.length; - } - - get formattedDate(): string { - return this.date.formatted; - } - - get relativeDate(): string { - return this.date.relative; - } - - toJSON() { - return { - hash: this.id.value, - message: this.message.value, - author: this.author.name, - email: this.author.email, - date: this.date.formatted, - files: this.files.map((f) => f.toJSON()), - totalAdditions: this.totalAdditions, - totalDeletions: this.totalDeletions, - avatarUrl: this.avatarUrl, - tags: this.tags, - isPushed: this.isPushed, - }; - } -} diff --git a/src/webviews/app/domain/entities/file-change.entity.ts b/src/webviews/app/domain/entities/file-change.entity.ts deleted file mode 100644 index fa4fdc4..0000000 --- a/src/webviews/app/domain/entities/file-change.entity.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { FilePath } from "../value-objects/file-path.vo"; -import { FileStatus } from "../value-objects/file-status.vo"; - -export class FileChange { - constructor( - public readonly path: FilePath, - public readonly status: FileStatus, - public readonly additions: number = 0, - public readonly deletions: number = 0, - public readonly iconClass?: string, - public readonly iconUri?: string, - ) {} - - get fileName(): string { - return this.path.fileName; - } - - get directory(): string { - return this.path.directory; - } - - get extension(): string { - return this.path.extension; - } - - get statusColor(): string { - return this.status.color; - } - - get statusLabel(): string { - return this.status.label; - } - - get hasAdditions(): boolean { - return this.additions > 0; - } - - get hasDeletions(): boolean { - return this.deletions > 0; - } - - toJSON() { - return { - path: this.path.value, - status: this.status.value, - additions: this.additions, - deletions: this.deletions, - iconClass: this.iconClass, - iconUri: this.iconUri, - }; - } -} diff --git a/src/webviews/app/domain/value-objects/author.vo.ts b/src/webviews/app/domain/value-objects/author.vo.ts deleted file mode 100644 index 7a36dd1..0000000 --- a/src/webviews/app/domain/value-objects/author.vo.ts +++ /dev/null @@ -1,38 +0,0 @@ -export class Author { - constructor( - public readonly name: string, - public readonly email: string, - ) { - if (!name || name.trim().length === 0) { - throw new Error("Author name cannot be empty"); - } - if (!email || !this.isValidEmail(email)) { - throw new Error("Invalid email address"); - } - } - - get initials(): string { - return this.name - .split(" ") - .map((part) => (part || "").charAt(0).toUpperCase()) - .join("") - .substring(0, 2); - } - - get displayName(): string { - return this.name; - } - - private isValidEmail(email: string): boolean { - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - return emailRegex.test(email); - } - - equals(other: Author): boolean { - return this.name === other.name && this.email === other.email; - } - - toString(): string { - return `${this.name} <${this.email}>`; - } -} diff --git a/src/webviews/app/domain/value-objects/branch-name.vo.ts b/src/webviews/app/domain/value-objects/branch-name.vo.ts deleted file mode 100644 index 491c3d2..0000000 --- a/src/webviews/app/domain/value-objects/branch-name.vo.ts +++ /dev/null @@ -1,63 +0,0 @@ -export class BranchName { - private readonly _value: string; - - constructor(value: string) { - if (!value || value.trim().length === 0) { - throw new Error("Branch name cannot be empty"); - } - - if (!this.isValidBranchName(value)) { - throw new Error("Invalid branch name format"); - } - - this._value = value.trim(); - } - - get value(): string { - return this._value; - } - - get displayName(): string { - // Remove origin/ prefix for remote branches - return this._value.replace(/^origin\//, ""); - } - - get shortName(): string { - // Get just the branch name without any prefixes - const parts = this._value.split("/"); - return parts[parts.length - 1]; - } - - get isRemote(): boolean { - return ( - this._value.startsWith("origin/") || this._value.startsWith("remotes/") - ); - } - - get isFeatureBranch(): boolean { - return this._value.includes("feature/") || this._value.includes("feat/"); - } - - get isBugfixBranch(): boolean { - return this._value.includes("bugfix/") || this._value.includes("fix/"); - } - - private isValidBranchName(name: string): boolean { - // Basic git branch name validation - const invalidChars = /[\s~^:?*[\]\\]/; - return ( - !invalidChars.test(name) && - !name.startsWith(".") && - !name.endsWith(".") && - !name.includes("..") - ); - } - - equals(other: BranchName): boolean { - return this._value === other._value; - } - - toString(): string { - return this._value; - } -} diff --git a/src/webviews/app/domain/value-objects/commit-date.vo.ts b/src/webviews/app/domain/value-objects/commit-date.vo.ts deleted file mode 100644 index 306cde8..0000000 --- a/src/webviews/app/domain/value-objects/commit-date.vo.ts +++ /dev/null @@ -1,54 +0,0 @@ -import dayjs from "dayjs"; -import relativeTime from "dayjs/plugin/relativeTime"; - -export class CommitDate { - private readonly _date: dayjs.Dayjs; - - constructor(date: string | Date | dayjs.Dayjs) { - dayjs.extend(relativeTime); - this._date = dayjs(date); - - if (!this._date.isValid()) { - throw new Error("Invalid date provided"); - } - } - - get value(): Date { - return this._date.toDate(); - } - - get formatted(): string { - const now = dayjs(); - const diffInDays = now.diff(this._date, "day"); - - // For dates within the last 7 days, show relative format - if (diffInDays <= 7) { - if (diffInDays === 0) return "today"; - if (diffInDays === 1) return "yesterday"; - return `${diffInDays} days ago`; - } - - // For older dates, show formatted date with time - return this._date.format("D MMMM, YYYY [at] HH:mm"); - } - - get relative(): string { - return this._date.fromNow(); - } - - get iso(): string { - return this._date.toISOString(); - } - - isAfter(other: CommitDate): boolean { - return this._date.isAfter(other._date); - } - - isBefore(other: CommitDate): boolean { - return this._date.isBefore(other._date); - } - - equals(other: CommitDate): boolean { - return this._date.isSame(other._date); - } -} diff --git a/src/webviews/app/domain/value-objects/commit-id.vo.ts b/src/webviews/app/domain/value-objects/commit-id.vo.ts deleted file mode 100644 index cee405e..0000000 --- a/src/webviews/app/domain/value-objects/commit-id.vo.ts +++ /dev/null @@ -1,26 +0,0 @@ -export class CommitId { - private readonly _value: string; - - constructor(value: string) { - if (!value || value.length < 7) { - throw new Error("Invalid commit ID: must be at least 7 characters"); - } - this._value = value; - } - - get value(): string { - return this._value; - } - - get shortHash(): string { - return this._value.substring(0, 7); - } - - equals(other: CommitId): boolean { - return this._value === other._value; - } - - toString(): string { - return this._value; - } -} diff --git a/src/webviews/app/domain/value-objects/commit-message.vo.ts b/src/webviews/app/domain/value-objects/commit-message.vo.ts deleted file mode 100644 index b950868..0000000 --- a/src/webviews/app/domain/value-objects/commit-message.vo.ts +++ /dev/null @@ -1,38 +0,0 @@ -export class CommitMessage { - private readonly _value: string; - - constructor(value: string) { - if (!value || value.trim().length === 0) { - throw new Error("Commit message cannot be empty"); - } - this._value = value.trim(); - } - - get value(): string { - return this._value; - } - - get summary(): string { - const firstLine = this._value.split("\n")[0]; - return firstLine.length > 72 - ? firstLine.substring(0, 69) + "..." - : firstLine; - } - - get hasBody(): boolean { - return this._value.includes("\n"); - } - - get body(): string { - const lines = this._value.split("\n"); - return lines.slice(1).join("\n").trim(); - } - - equals(other: CommitMessage): boolean { - return this._value === other._value; - } - - toString(): string { - return this._value; - } -} diff --git a/src/webviews/app/domain/value-objects/file-path.vo.ts b/src/webviews/app/domain/value-objects/file-path.vo.ts deleted file mode 100644 index 8d6ff1f..0000000 --- a/src/webviews/app/domain/value-objects/file-path.vo.ts +++ /dev/null @@ -1,45 +0,0 @@ -export class FilePath { - private readonly _value: string; - - constructor(value: string) { - if (!value || value.trim().length === 0) { - throw new Error("File path cannot be empty"); - } - this._value = value.trim(); - } - - get value(): string { - return this._value; - } - - get fileName(): string { - return this._value.split("/").pop() || this._value; - } - - get directory(): string { - const parts = this._value.split("/"); - return parts.length > 1 ? parts.slice(0, -1).join("/") : ""; - } - - get extension(): string { - const fileName = this.fileName; - const lastDot = fileName.lastIndexOf("."); - return lastDot > 0 ? fileName.substring(lastDot + 1).toLowerCase() : ""; - } - - get depth(): number { - return this._value.split("/").length; - } - - isInDirectory(directory: string): boolean { - return this._value.startsWith(directory + "/"); - } - - equals(other: FilePath): boolean { - return this._value === other._value; - } - - toString(): string { - return this._value; - } -} diff --git a/src/webviews/app/domain/value-objects/file-status.vo.ts b/src/webviews/app/domain/value-objects/file-status.vo.ts deleted file mode 100644 index 5417bb1..0000000 --- a/src/webviews/app/domain/value-objects/file-status.vo.ts +++ /dev/null @@ -1,76 +0,0 @@ -export type FileStatusType = "M" | "A" | "D" | "R" | "C" | "??"; - -export class FileStatus { - private readonly _value: FileStatusType; - - constructor(value: string) { - const safeValue = value || ""; - const statusCode = safeValue.charAt(0) as FileStatusType; - if (!this.isValidStatus(statusCode)) { - throw new Error(`Invalid file status: ${value}`); - } - this._value = statusCode; - } - - get value(): FileStatusType { - return this._value; - } - - get label(): string { - switch (this._value) { - case "M": - return "Modified"; - case "A": - return "Added"; - case "D": - return "Deleted"; - case "R": - return "Renamed"; - case "C": - return "Copied"; - case "??": - return "Untracked"; - default: - return "Unknown"; - } - } - - get color(): string { - switch (this._value) { - case "M": - return "#f59e0b"; // Modified - amber - case "A": - return "#10b981"; // Added - green - case "D": - return "#ef4444"; // Deleted - red - case "R": - return "#8b5cf6"; // Renamed - purple - case "C": - return "#06b6d4"; // Copied - cyan - case "??": - return "#6b7280"; // Untracked - gray - default: - return "#6b7280"; - } - } - - get isModified(): boolean { - return this._value === "M"; - } - - get isAdded(): boolean { - return this._value === "A"; - } - - get isDeleted(): boolean { - return this._value === "D"; - } - - private isValidStatus(status: string): status is FileStatusType { - return ["M", "A", "D", "R", "C", "??"].includes(status); - } - - equals(other: FileStatus): boolean { - return this._value === other._value; - } -} diff --git a/src/webviews/app/hooks/useGitOperations.ts b/src/webviews/app/hooks/useGitOperations.ts deleted file mode 100644 index 3ae1fde..0000000 --- a/src/webviews/app/hooks/useGitOperations.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { GitService } from "../services/git.service"; -import { VSCodeBridge } from "../bridge"; - -export const useGitOperations = (bridge: VSCodeBridge) => { - const queryClient = useQueryClient(); - const gitService = new GitService(bridge); - - const pushMutation = useMutation({ - mutationFn: () => gitService.push(), - onSuccess: () => { - // Invalidate and refetch remote status - queryClient.invalidateQueries({ queryKey: ["remoteStatus"] }); - // Trigger a refresh of commits to update isPushed status - queryClient.invalidateQueries({ queryKey: ["commits"] }); - }, - onError: (error) => { - console.error("Push failed:", error); - }, - }); - - const pullMutation = useMutation({ - mutationFn: () => gitService.pull(), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["remoteStatus"] }); - queryClient.invalidateQueries({ queryKey: ["commits"] }); - queryClient.invalidateQueries({ queryKey: ["changes"] }); - }, - onError: (error) => { - console.error("Pull failed:", error); - }, - }); - - const fetchMutation = useMutation({ - mutationFn: () => gitService.fetch(), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["remoteStatus"] }); - }, - onError: (error) => { - console.error("Fetch failed:", error); - }, - }); - - const publishMutation = useMutation({ - mutationFn: () => gitService.publish(), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["remoteStatus"] }); - }, - onError: (error) => { - console.error("Publish failed:", error); - }, - }); - - return { - push: { - mutate: pushMutation.mutate, - isLoading: pushMutation.isPending, - error: pushMutation.error, - isError: pushMutation.isError, - }, - pull: { - mutate: pullMutation.mutate, - isLoading: pullMutation.isPending, - error: pullMutation.error, - isError: pullMutation.isError, - }, - fetch: { - mutate: fetchMutation.mutate, - isLoading: fetchMutation.isPending, - error: fetchMutation.error, - isError: fetchMutation.isError, - }, - publish: { - mutate: publishMutation.mutate, - isLoading: publishMutation.isPending, - error: publishMutation.error, - isError: publishMutation.isError, - }, - }; -}; diff --git a/src/webviews/app/services/git.service.ts b/src/webviews/app/services/git.service.ts deleted file mode 100644 index a5f5c4e..0000000 --- a/src/webviews/app/services/git.service.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { VSCodeBridge } from "../bridge"; - -export class GitService { - constructor(private bridge: VSCodeBridge) {} - - async push(): Promise { - this.bridge.sendMessage("push"); - // Simple delay to allow the operation to complete - await new Promise((resolve) => setTimeout(resolve, 2000)); - } - - async pull(): Promise { - this.bridge.sendMessage("pull"); - // Simple delay to allow the operation to complete - await new Promise((resolve) => setTimeout(resolve, 2000)); - } - - async fetch(): Promise { - this.bridge.sendMessage("fetch"); - // Simple delay to allow the operation to complete - await new Promise((resolve) => setTimeout(resolve, 2000)); - } - - async publish(): Promise { - this.bridge.sendMessage("publish"); - // Simple delay to allow the operation to complete - await new Promise((resolve) => setTimeout(resolve, 2000)); - } -} diff --git a/src/webviews/app/shared/infrastructure/bridge/vscode-bridge.interface.ts b/src/webviews/app/shared/infrastructure/bridge/vscode-bridge.interface.ts deleted file mode 100644 index c1b9f7a..0000000 --- a/src/webviews/app/shared/infrastructure/bridge/vscode-bridge.interface.ts +++ /dev/null @@ -1,53 +0,0 @@ -export interface VSCodeMessage { - command: string; - [key: string]: any; -} - -export interface VSCodeBridge { - sendMessage(command: string, data?: any): void; - onMessage(handler: (message: VSCodeMessage) => void): void; - offMessage(handler: (message: VSCodeMessage) => void): void; - getState(): any; - setState(state: any): void; -} - -export class VSCodeBridgeImpl implements VSCodeBridge { - private messageHandlers: Set<(message: VSCodeMessage) => void> = new Set(); - private vscodeApi: any; - - constructor() { - if (typeof window !== "undefined" && (window as any).vscodeApi) { - this.vscodeApi = (window as any).vscodeApi; - this.setupMessageListener(); - } - } - - sendMessage(command: string, data: any = {}): void { - if (this.vscodeApi) { - this.vscodeApi.postMessage({ command, ...data }); - } - } - - onMessage(handler: (message: VSCodeMessage) => void): void { - this.messageHandlers.add(handler); - } - - offMessage(handler: (message: VSCodeMessage) => void): void { - this.messageHandlers.delete(handler); - } - - getState(): any { - return this.vscodeApi?.getState(); - } - - setState(state: any): void { - this.vscodeApi?.setState(state); - } - - private setupMessageListener(): void { - window.addEventListener("message", (event) => { - const message = event.data; - this.messageHandlers.forEach((handler) => handler(message)); - }); - } -} diff --git a/src/webviews/app/shared/infrastructure/config/service-registration.ts b/src/webviews/app/shared/infrastructure/config/service-registration.ts deleted file mode 100644 index 0c9354b..0000000 --- a/src/webviews/app/shared/infrastructure/config/service-registration.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { ServiceContainer } from "../di/service-container"; -import { VSCodeBridgeImpl } from "../bridge/vscode-bridge.interface"; -import { CommitServiceImpl } from "../../../features/commits/services/commit.service"; -import { BranchServiceImpl } from "../../../features/branches/services/branch.service"; - -export const configureServices = (): ServiceContainer => { - const container = new ServiceContainer(); - - // Register bridge - container.register("VSCodeBridge", () => new VSCodeBridgeImpl()); - - // Register feature services - container.register("CommitService", () => { - const bridge = container.get("VSCodeBridge"); - return new CommitServiceImpl(bridge); - }); - - container.register("BranchService", () => { - const bridge = container.get("VSCodeBridge"); - return new BranchServiceImpl(bridge); - }); - - return container; -}; diff --git a/src/webviews/app/shared/infrastructure/di/service-container.ts b/src/webviews/app/shared/infrastructure/di/service-container.ts deleted file mode 100644 index 737a1ad..0000000 --- a/src/webviews/app/shared/infrastructure/di/service-container.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { ServiceKey, ServiceFactory } from "./service-locator"; - -export class ServiceContainer { - private services = new Map(); - private factories = new Map(); - - register(key: ServiceKey, factory: ServiceFactory): ServiceContainer { - this.factories.set(key, factory); - return this; - } - - registerInstance(key: ServiceKey, instance: T): ServiceContainer { - this.services.set(key, instance); - return this; - } - - get(key: ServiceKey): T { - // Return existing instance if available - if (this.services.has(key)) { - return this.services.get(key); - } - - // Create new instance using factory - const factory = this.factories.get(key); - if (!factory) { - throw new Error(`Service '${key}' not registered`); - } - - const instance = factory(); - this.services.set(key, instance); - return instance; - } - - has(key: ServiceKey): boolean { - return this.services.has(key) || this.factories.has(key); - } - - clear(): void { - this.services.clear(); - this.factories.clear(); - } - - clone(): ServiceContainer { - const container = new ServiceContainer(); - container.factories = new Map(this.factories); - container.services = new Map(this.services); - return container; - } -} diff --git a/src/webviews/app/shared/infrastructure/di/service-context.tsx b/src/webviews/app/shared/infrastructure/di/service-context.tsx deleted file mode 100644 index d355ea3..0000000 --- a/src/webviews/app/shared/infrastructure/di/service-context.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React, { createContext, ReactNode } from 'react'; -import { ServiceContainer } from './service-container'; - -export const ServiceContext = createContext(null); - -interface ServiceProviderProps { - container: ServiceContainer; - children: ReactNode; -} - -export const ServiceProvider: React.FC = ({ - container, - children -}) => { - return ( - - {children} - - ); -}; \ No newline at end of file diff --git a/src/webviews/app/shared/infrastructure/di/service-locator.ts b/src/webviews/app/shared/infrastructure/di/service-locator.ts deleted file mode 100644 index 5b15b46..0000000 --- a/src/webviews/app/shared/infrastructure/di/service-locator.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { useContext } from "react"; -import { ServiceContainer } from "./service-container"; -import { ServiceContext } from "./service-context"; - -export type ServiceKey = string; -export type ServiceFactory = () => T; - -export class ServiceLocator { - private static instance: ServiceLocator; - private services = new Map(); - private factories = new Map(); - - static getInstance(): ServiceLocator { - if (!ServiceLocator.instance) { - ServiceLocator.instance = new ServiceLocator(); - } - return ServiceLocator.instance; - } - - register(key: ServiceKey, factory: ServiceFactory): void { - this.factories.set(key, factory); - } - - registerInstance(key: ServiceKey, instance: T): void { - this.services.set(key, instance); - } - - get(key: ServiceKey): T { - // Return existing instance if available - if (this.services.has(key)) { - return this.services.get(key); - } - - // Create new instance using factory - const factory = this.factories.get(key); - if (!factory) { - throw new Error(`Service '${key}' not registered`); - } - - const instance = factory(); - this.services.set(key, instance); - return instance; - } - - clear(): void { - this.services.clear(); - this.factories.clear(); - } -} - -export const useService = (key: ServiceKey): T => { - const container = useContext(ServiceContext); - if (!container) { - throw new Error("useService must be used within ServiceProvider"); - } - return container.get(key); -}; diff --git a/src/webviews/app/shared/utils/date.utils.ts b/src/webviews/app/shared/utils/date.utils.ts deleted file mode 100644 index 73e3d03..0000000 --- a/src/webviews/app/shared/utils/date.utils.ts +++ /dev/null @@ -1,73 +0,0 @@ -import dayjs from "dayjs"; -import relativeTime from "dayjs/plugin/relativeTime"; - -// Initialize dayjs plugins -dayjs.extend(relativeTime); - -export class DateUtils { - static formatRelativeTime(date: string | Date | dayjs.Dayjs): string { - const parsedDate = dayjs(date); - - if (!parsedDate.isValid()) { - return "Invalid date"; - } - - const now = dayjs(); - const diffInDays = now.diff(parsedDate, "day"); - - // For dates within the last 7 days, show relative format - if (diffInDays <= 7) { - if (diffInDays === 0) return "today"; - if (diffInDays === 1) return "yesterday"; - return `${diffInDays} days ago`; - } - - // For older dates, show formatted date with time - return parsedDate.format("D MMMM, YYYY [at] HH:mm"); - } - - static formatAbsoluteTime(date: string | Date | dayjs.Dayjs): string { - const parsedDate = dayjs(date); - - if (!parsedDate.isValid()) { - return "Invalid date"; - } - - return parsedDate.format("D MMMM, YYYY [at] HH:mm"); - } - - static fromNow(date: string | Date | dayjs.Dayjs): string { - const parsedDate = dayjs(date); - - if (!parsedDate.isValid()) { - return "Invalid date"; - } - - return parsedDate.fromNow(); - } - - static isValid(date: string | Date | dayjs.Dayjs): boolean { - return dayjs(date).isValid(); - } - - static isSameDay( - date1: string | Date | dayjs.Dayjs, - date2: string | Date | dayjs.Dayjs, - ): boolean { - return dayjs(date1).isSame(dayjs(date2), "day"); - } - - static isAfter( - date1: string | Date | dayjs.Dayjs, - date2: string | Date | dayjs.Dayjs, - ): boolean { - return dayjs(date1).isAfter(dayjs(date2)); - } - - static isBefore( - date1: string | Date | dayjs.Dayjs, - date2: string | Date | dayjs.Dayjs, - ): boolean { - return dayjs(date1).isBefore(dayjs(date2)); - } -} diff --git a/src/webviews/app/shared/utils/file.utils.ts b/src/webviews/app/shared/utils/file.utils.ts deleted file mode 100644 index 8b47ea9..0000000 --- a/src/webviews/app/shared/utils/file.utils.ts +++ /dev/null @@ -1,84 +0,0 @@ -export class FileUtils { - static getFileName(path: string): string { - return path.split("/").pop() || path; - } - - static getDirectory(path: string): string { - const parts = path.split("/"); - return parts.length > 1 ? parts.slice(0, -1).join("/") : ""; - } - - static getExtension(path: string): string { - const fileName = FileUtils.getFileName(path); - const lastDot = fileName.lastIndexOf("."); - return lastDot > 0 ? fileName.substring(lastDot + 1).toLowerCase() : ""; - } - - static getFileNameWithoutExtension(path: string): string { - const fileName = FileUtils.getFileName(path); - const lastDot = fileName.lastIndexOf("."); - return lastDot > 0 ? fileName.substring(0, lastDot) : fileName; - } - - static isInDirectory(filePath: string, directory: string): boolean { - return filePath.startsWith(directory + "/"); - } - - static getDepth(path: string): number { - return path.split("/").length; - } - - static normalizePath(path: string): string { - return path.replace(/\\/g, "/").replace(/\/+/g, "/"); - } - - static isImageFile(path: string): boolean { - const extension = FileUtils.getExtension(path); - return ["png", "jpg", "jpeg", "gif", "svg", "webp", "ico"].includes( - extension, - ); - } - - static isTextFile(path: string): boolean { - const extension = FileUtils.getExtension(path); - return ["txt", "md", "json", "xml", "yml", "yaml", "csv"].includes( - extension, - ); - } - - static isCodeFile(path: string): boolean { - const extension = FileUtils.getExtension(path); - return [ - "js", - "jsx", - "ts", - "tsx", - "vue", - "py", - "java", - "c", - "cpp", - "h", - "hpp", - "cs", - "php", - "rb", - "go", - "rs", - "swift", - "kt", - "scala", - "html", - "css", - "scss", - "sass", - "less", - "sql", - "sh", - "bash", - "ps1", - "r", - "matlab", - ].includes(extension); - } -} diff --git a/src/webviews/bootstrap.tsx b/src/webviews/bootstrap.tsx deleted file mode 100644 index c783549..0000000 --- a/src/webviews/bootstrap.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from "react"; -import { createRoot } from "react-dom/client"; -import { App } from "./app/App"; -import { createVSCodeBridge, InitialWebviewState, VSCodeApi } from "./app/bridge"; - -declare global { - interface Window { - vscodeApi: VSCodeApi; - initialData?: InitialWebviewState; - } -} - -const container = document.getElementById("root"); - -if (!container) { - throw new Error("Failed to locate root element for GitHub Desktop UI"); -} - -const bridge = createVSCodeBridge(window.vscodeApi); -const fallbackState: InitialWebviewState = { - changes: [], - history: [], - branches: [], - currentBranch: null, - repository: null, - accounts: [], - activeAccount: null, -}; -const initialState: InitialWebviewState = { ...fallbackState, ...(window.initialData ?? {}) }; - -const root = createRoot(container); -root.render( - - - -); \ No newline at end of file diff --git a/src/webviews/commitDetail/App.tsx b/src/webviews/commitDetail/App.tsx deleted file mode 100644 index 1d24e8f..0000000 --- a/src/webviews/commitDetail/App.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { ThemeProvider, createTheme } from '@mui/material'; -import { VSCodeBridge } from '../app/bridge'; -import { CommitDetailPanel } from '../app/components/CommitDetailPanel/CommitDetailPanel'; - -interface CommitDetail { - hash: string; - message: string; - author: string; - email: string; - date: string; - files: Array<{ - path: string; - status: string; - additions: number; - deletions: number; - iconUri?: string; - iconClass?: string; - }>; - totalAdditions: number; - totalDeletions: number; -} - -interface AppProps { - bridge: VSCodeBridge; -} - -export const App: React.FC = ({ bridge }) => { - const [commitDetail, setCommitDetail] = useState(null); - - useEffect(() => { - bridge.onMessage((message) => { - if (message.command === 'commitDetails') { - setCommitDetail(message.commitDetail); - } - }); - }, [bridge]); - - // Create a minimal theme that respects VS Code CSS variables - const theme = createTheme({ - palette: { - mode: 'dark', // We'll use CSS variables instead of theme colors - }, - typography: { - fontFamily: 'var(--vscode-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif)', - }, - components: { - MuiCssBaseline: { - styleOverrides: { - body: { - margin: 0, - padding: 0, - backgroundColor: 'var(--vscode-editor-background)', - color: 'var(--vscode-editor-foreground)', - fontFamily: 'var(--vscode-font-family)', - fontSize: 'var(--vscode-font-size, 13px)', - }, - }, - }, - }, - }); - - return ( - - {commitDetail ? ( - { - // Close the webview panel - bridge.sendMessage('close'); - }} - bridge={bridge} - /> - ) : ( -
- Loading commit details... -
- )} -
- ); -}; diff --git a/src/webviews/commitDetail/commit-detail-view-provider.ts b/src/webviews/commitDetail/commit-detail-view-provider.ts deleted file mode 100644 index f7f4cc6..0000000 --- a/src/webviews/commitDetail/commit-detail-view-provider.ts +++ /dev/null @@ -1,140 +0,0 @@ -import * as vscode from "vscode"; -import dayjs from "dayjs"; -import relativeTime from "dayjs/plugin/relativeTime"; -import { RepositoryManager } from "../../core/repositories/repository-manager"; -import { - ICommitDetailViewProvider, - CommitDetailMessage, -} from "./interfaces/commit-detail.interface"; -import { CommitDetailService } from "./services/commit-detail.service"; -import { CommitDetailHtmlService } from "./services/commit-detail-html.service"; -import { CommitDetailMessageHandlerService } from "./services/commit-detail-message-handler.service"; - -dayjs.extend(relativeTime); - -export class CommitDetailViewProvider implements ICommitDetailViewProvider { - private panel: vscode.WebviewPanel | undefined; - private commitHash: string | undefined; - private commitDetailService: CommitDetailService; - private htmlService: CommitDetailHtmlService; - private messageHandler: CommitDetailMessageHandlerService | undefined; - - constructor( - private readonly context: vscode.ExtensionContext, - private readonly repositories: RepositoryManager, - ) { - this.commitDetailService = new CommitDetailService(context, repositories); - this.htmlService = new CommitDetailHtmlService(context); - } - - public async showCommitDetails(commitHash: string): Promise { - this.commitHash = commitHash; - - // Create or show the webview panel - if (this.panel) { - // Panel exists, just update content and bring to front - this.panel.reveal(vscode.ViewColumn.Active); - this.panel.title = `Commit: ${commitHash.substring(0, 7)}`; - // Update the message handler with the new commit hash - this.updateMessageHandler(); - } else { - await this.createPanel(); - } - - // Load commit details - await this.loadCommitDetails(); - } - - public async refresh(): Promise { - await this.loadCommitDetails(); - } - - private async createPanel(): Promise { - this.panel = vscode.window.createWebviewPanel( - "commitDetail", - `Commit: ${this.commitHash?.substring(0, 7) || "Details"}`, - vscode.ViewColumn.Active, // Open in the currently active column - { - enableScripts: true, - localResourceRoots: [ - vscode.Uri.joinPath(this.context.extensionUri, "out", "webview"), - vscode.Uri.joinPath( - this.context.extensionUri, - "out", - "webview", - "assets", - ), - vscode.Uri.joinPath(this.context.extensionUri, "out"), - ], - }, - ); - - this.setupPanel(); - } - - private setupPanel(): void { - if (!this.panel || !this.commitHash) return; - - this.panel.webview.html = this.htmlService.generateHtml(this.panel.webview); - - // Setup message handler - this.updateMessageHandler(); - - // Handle panel disposal - this.panel.onDidDispose(() => { - this.panel = undefined; - this.messageHandler = undefined; - }); - - // Handle messages from webview - this.panel.webview.onDidReceiveMessage( - async (message: CommitDetailMessage) => { - if (this.messageHandler) { - await this.messageHandler.handleMessage(message); - } - }, - ); - } - - private updateMessageHandler(): void { - if (!this.panel || !this.commitHash) return; - - // Create or update the message handler with the current commit hash - this.messageHandler = new CommitDetailMessageHandlerService( - this.commitDetailService, - this.panel, - this.commitHash, - ); - } - - private async loadCommitDetails(): Promise { - if (!this.commitHash || !this.messageHandler) return; - - await this.messageHandler.handleMessage({ command: "ready" }); - } - - // Legacy methods for backward compatibility - public async getFileDiff(hash: string, filePath: string): Promise { - try { - await this.commitDetailService.createFileDiff(hash, filePath); - } catch (error) { - vscode.window.showErrorMessage(`Failed to open file diff: ${error}`); - } - } - - public formatRelativeTime(date: Date): string { - // This method is now handled internally by the service, but kept for compatibility - const dayjsDate = dayjs(date); - const diffInDays = dayjs().diff(dayjsDate, "day"); - - if (diffInDays > 30) { - return dayjsDate.format("D MMMM, YYYY [at] h:mm A"); - } - - return dayjsDate.fromNow(); - } - - public getHtml(webview: vscode.Webview): string { - return this.htmlService.generateHtml(webview); - } -} diff --git a/src/webviews/commitDetail/index.tsx b/src/webviews/commitDetail/index.tsx deleted file mode 100644 index 5daeb72..0000000 --- a/src/webviews/commitDetail/index.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import { createVSCodeBridge } from '../app/bridge'; -import { App } from './App'; - -// Initialize the VS Code API bridge -const vscode = (window as any).vscodeApi; -const bridge = createVSCodeBridge(vscode); - -// Render the React app -const container = document.getElementById('root'); -if (container) { - const root = ReactDOM.createRoot(container); - root.render( - - - - ); -} \ No newline at end of file diff --git a/src/webviews/commitDetail/interfaces/commit-detail.interface.ts b/src/webviews/commitDetail/interfaces/commit-detail.interface.ts deleted file mode 100644 index cf79c32..0000000 --- a/src/webviews/commitDetail/interfaces/commit-detail.interface.ts +++ /dev/null @@ -1,39 +0,0 @@ -import * as vscode from "vscode"; - -export interface CommitDetailFile { - path: string; - status: string; - additions: number; - deletions: number; - iconUri?: string; -} - -export interface CommitDetail { - hash: string; - message: string; - author: string; - email: string; - date: string; - files: CommitDetailFile[]; - totalAdditions: number; - totalDeletions: number; -} - -export interface CommitDetailMessage { - command: string; - hash?: string; - filePath?: string; -} - -export interface ICommitDetailViewProvider { - showCommitDetails(commitHash: string): Promise; - refresh(): Promise; -} - -export interface ICommitDetailService { - getCommitDetail( - commitHash: string, - webview?: vscode.Webview, - ): Promise; - createFileDiff(hash: string, filePath: string): Promise; -} diff --git a/src/webviews/commitDetail/services/commit-detail-html.service.ts b/src/webviews/commitDetail/services/commit-detail-html.service.ts deleted file mode 100644 index 893432d..0000000 --- a/src/webviews/commitDetail/services/commit-detail-html.service.ts +++ /dev/null @@ -1,67 +0,0 @@ -import * as vscode from "vscode"; - -export class CommitDetailHtmlService { - constructor(private readonly context: vscode.ExtensionContext) {} - - generateHtml(webview: vscode.Webview): string { - const reactAppUri = webview.asWebviewUri( - vscode.Uri.joinPath( - this.context.extensionUri, - "out", - "webview", - "commit-detail.js", - ), - ); - - return ` - - - - - - Commit Details - - - -
- - - -`; - } -} diff --git a/src/webviews/commitDetail/services/commit-detail-message-handler.service.ts b/src/webviews/commitDetail/services/commit-detail-message-handler.service.ts deleted file mode 100644 index 666917b..0000000 --- a/src/webviews/commitDetail/services/commit-detail-message-handler.service.ts +++ /dev/null @@ -1,57 +0,0 @@ -import * as vscode from "vscode"; -import { CommitDetailMessage } from "../interfaces/commit-detail.interface"; -import { CommitDetailService } from "./commit-detail.service"; - -export class CommitDetailMessageHandlerService { - constructor( - private readonly commitDetailService: CommitDetailService, - private readonly panel: vscode.WebviewPanel, - private readonly commitHash: string, - ) {} - - async handleMessage(message: CommitDetailMessage): Promise { - try { - switch (message.command) { - case "ready": - await this.handleReady(); - break; - case "getFileDiff": - if (message.hash && message.filePath) { - await this.handleGetFileDiff(message.hash, message.filePath); - } - break; - default: - break; - } - } catch (error) { - vscode.window.showErrorMessage(`Message handling failed: ${error}`); - } - } - - private async handleReady(): Promise { - try { - const commitDetail = await this.commitDetailService.getCommitDetail( - this.commitHash, - this.panel.webview, - ); - - this.panel.webview.postMessage({ - command: "commitDetails", - commitDetail, - }); - } catch (error) { - vscode.window.showErrorMessage(`Failed to load commit details: ${error}`); - } - } - - private async handleGetFileDiff( - hash: string, - filePath: string, - ): Promise { - try { - await this.commitDetailService.createFileDiff(hash, filePath); - } catch (error) { - vscode.window.showErrorMessage(`Failed to open file diff: ${error}`); - } - } -} diff --git a/src/webviews/commitDetail/services/commit-detail.service.ts b/src/webviews/commitDetail/services/commit-detail.service.ts deleted file mode 100644 index 9f51a68..0000000 --- a/src/webviews/commitDetail/services/commit-detail.service.ts +++ /dev/null @@ -1,197 +0,0 @@ -import * as path from "path"; -import * as vscode from "vscode"; -import simpleGit from "simple-git"; -import dayjs from "dayjs"; -import relativeTime from "dayjs/plugin/relativeTime"; -import { iconThemeService } from "../../../core/services/icon-theme.service"; -import { RepositoryManager } from "../../../core/repositories/repository-manager"; -import { getPrimaryRepository } from "../../../shared/utils/repo-selection"; -import { - CommitDetail, - CommitDetailFile, - ICommitDetailService, -} from "../interfaces/commit-detail.interface"; - -dayjs.extend(relativeTime); - -export class CommitDetailService implements ICommitDetailService { - private static diffViewColumn: vscode.ViewColumn = vscode.ViewColumn.Three; - - constructor( - private readonly context: vscode.ExtensionContext, - private readonly repositories: RepositoryManager, - ) {} - - async getCommitDetail( - commitHash: string, - webview?: vscode.Webview, - ): Promise { - const repository = getPrimaryRepository(this.repositories); - if (!repository) { - throw new Error("No repository found"); - } - - try { - const git = simpleGit(repository.localPath); - - // Get commit info - const commitInfo = await git.show([ - commitHash, - "--name-status", - "--format=%H%n%an%n%ae%n%ad%n%s%n%b", - ]); - const lines = commitInfo.split("\n"); - - const fullHash = lines[0]; - const author = lines[1]; - const email = lines[2]; - const date = lines[3]; - const message = lines[4]; - - // Get file changes with statistics - const stats = await git.raw([ - "show", - commitHash, - "--numstat", - "--format=", - ]); - const statLines = stats.split("\n").filter((line) => line.trim()); - - const files: CommitDetailFile[] = []; - let totalAdditions = 0; - let totalDeletions = 0; - - // Get file status - const statusOutput = await git.raw([ - "show", - commitHash, - "--name-status", - "--format=", - ]); - const statusLines = statusOutput - .split("\n") - .filter((line) => line.trim()); - - const statusMap = new Map(); - for (const line of statusLines) { - const [status, filePath] = line.split("\t"); - if (filePath) { - statusMap.set(filePath, status); - } - } - - // Parse numstat output - for (const line of statLines) { - const [addStr, delStr, filePath] = line.split("\t"); - if (!filePath) continue; - - const additions = addStr === "-" ? 0 : parseInt(addStr, 10) || 0; - const deletions = delStr === "-" ? 0 : parseInt(delStr, 10) || 0; - const status = statusMap.get(filePath) || "M"; - - const iconInfo = webview - ? await iconThemeService.getIconForFile(webview, filePath) - : undefined; - - files.push({ - path: filePath, - status, - additions, - deletions, - iconUri: iconInfo?.iconUri, - }); - - totalAdditions += additions; - totalDeletions += deletions; - } - - return { - hash: fullHash, - message, - author, - email, - date: this.formatRelativeTime(new Date(date)), - files, - totalAdditions, - totalDeletions, - }; - } catch (error) { - throw new Error(`Failed to get commit details: ${error}`); - } - } - - async createFileDiff(hash: string, filePath: string): Promise { - const repository = getPrimaryRepository(this.repositories); - if (!repository || !hash || !filePath) { - throw new Error("Invalid parameters for file diff"); - } - - try { - const git = simpleGit(repository.localPath); - - // Create temporary files for diff comparison - const tempDir = vscode.Uri.joinPath( - this.context.globalStorageUri, - "temp", - ); - await vscode.workspace.fs.createDirectory(tempDir); - - const oldTempFile = vscode.Uri.joinPath( - tempDir, - `${hash}_old_${path.basename(filePath)}`, - ); - const newTempFile = vscode.Uri.joinPath( - tempDir, - `${hash}_new_${path.basename(filePath)}`, - ); - - // Get file content before and after commit - let oldContent = ""; - try { - oldContent = await git.show([`${hash}~1:${filePath}`]); - } catch { - // File might be new, so old content is empty - } - - const newContent = await git.show([`${hash}:${filePath}`]); - - // Write temporary files - await vscode.workspace.fs.writeFile( - oldTempFile, - Buffer.from(oldContent, "utf8"), - ); - await vscode.workspace.fs.writeFile( - newTempFile, - Buffer.from(newContent, "utf8"), - ); - - // Open diff in a specific dedicated column - const title = `${path.basename(filePath)} (${hash.substring(0, 7)})`; - - // Use the native diff command - it will automatically reuse the column - await vscode.commands.executeCommand( - "vscode.diff", - oldTempFile, - newTempFile, - title, - ); - - // Show success message - vscode.window.showInformationMessage(`Opened diff for ${filePath}`); - } catch (error) { - throw new Error(`Failed to open file diff: ${error}`); - } - } - - private formatRelativeTime(date: Date): string { - const dayjsDate = dayjs(date); - const diffInDays = dayjs().diff(dayjsDate, "day"); - - if (diffInDays > 30) { - // For dates older than 30 days, show formatted date with time - return dayjsDate.format("D MMMM, YYYY [at] h:mm A"); - } - - return dayjsDate.fromNow(); - } -} diff --git a/src/webviews/historyExplorer/history-explorer-view-provider.ts b/src/webviews/historyExplorer/history-explorer-view-provider.ts deleted file mode 100644 index 55cdd6a..0000000 --- a/src/webviews/historyExplorer/history-explorer-view-provider.ts +++ /dev/null @@ -1,315 +0,0 @@ -import * as path from "path"; -import * as vscode from "vscode"; -import simpleGit from "simple-git"; -import dayjs from "dayjs"; -import relativeTime from "dayjs/plugin/relativeTime"; -import { RepositoryManager } from "../../core/repositories/repository-manager"; -import { getPrimaryRepository } from "../../shared/utils/repo-selection"; -import { CommitDetailViewProvider } from "../commitDetail/commit-detail-view-provider"; -import { - HistoryExplorerCommit, - HistoryExplorerInitialState, - HistoryExplorerFilters, - HistoryExplorerResponse, -} from "./types/history-explorer.types"; - -dayjs.extend(relativeTime); - -type WebviewMessage = { - command: string; - [key: string]: any; -}; - -export class HistoryExplorerViewProvider - implements vscode.WebviewViewProvider -{ - public static readonly viewType = "githubDesktop.historyExplorer"; - - private view: vscode.WebviewView | undefined; - private currentFilters: HistoryExplorerFilters = {}; - - constructor( - private readonly context: vscode.ExtensionContext, - private readonly repositories: RepositoryManager, - private readonly commitDetailProvider: CommitDetailViewProvider, - ) {} - - resolveWebviewView(webviewView: vscode.WebviewView): void | Thenable { - this.view = webviewView; - webviewView.webview.options = { - enableScripts: true, - localResourceRoots: [ - vscode.Uri.joinPath(this.context.extensionUri, "out", "webview"), - vscode.Uri.joinPath(this.context.extensionUri, "out"), - ], - }; - - webviewView.webview.html = this.getHtmlContent(webviewView.webview); - - webviewView.webview.onDidReceiveMessage(async (message: WebviewMessage) => { - await this.handleMessage(message); - }); - - return this.refresh(); - } - - async refresh(): Promise { - if (!this.view) return; - const payload = await this.getHistoryData(this.currentFilters, 0); - this.postMessage({ type: "historyData", payload }); - } - - private postMessage(message: any): void { - this.view?.webview.postMessage(message); - } - - private async handleMessage(message: WebviewMessage): Promise { - switch (message.command) { - case "ready": { - await this.refresh(); - break; - } - case "applyFilters": { - const filters: HistoryExplorerFilters = message.filters || {}; - this.currentFilters = { ...filters }; - const payload = await this.getHistoryData(this.currentFilters, 0); - this.postMessage({ type: "historyData", payload }); - break; - } - case "loadMore": { - const offset = typeof message.offset === "number" ? message.offset : 0; - const payload = await this.getHistoryData(this.currentFilters, offset); - this.postMessage({ type: "historyAppend", payload }); - break; - } - case "openCommit": { - if (typeof message.hash === "string" && message.hash.length > 0) { - await this.commitDetailProvider.showCommitDetails(message.hash); - } - break; - } - default: - break; - } - } - - private getHtmlContent(webview: vscode.Webview): string { - const scriptUri = webview.asWebviewUri( - vscode.Uri.joinPath( - this.context.extensionUri, - "out", - "webview", - "history-explorer.js", - ), - ); - - const codiconsUri = vscode.Uri.joinPath( - this.context.extensionUri, - "node_modules", - "@vscode/codicons", - "dist", - "codicon.css", - ); - - const stylesUri = webview.asWebviewUri(codiconsUri); - - const initialState: HistoryExplorerInitialState = { - graphLines: [], - commits: [], - filters: {}, - branches: [], - authors: [], - hasMore: false, - offset: 0, - repository: null, - }; - - const csp = `default-src 'none'; img-src ${webview.cspSource} https: data:; style-src ${webview.cspSource} 'unsafe-inline'; font-src ${webview.cspSource} https:; script-src ${webview.cspSource};`; - - return ` - - - - - - Git History Explorer - - - - -
- - - -`; - } - - private async getHistoryData( - filters: HistoryExplorerFilters, - offset: number, - ): Promise { - const repository = getPrimaryRepository(this.repositories); - if (!repository) { - return { - graphLines: [], - commits: [], - filters: { ...filters, offset }, - branches: [], - authors: [], - hasMore: false, - nextOffset: offset, - repository: null, - }; - } - - const resolvedFilters: HistoryExplorerFilters = { - pageSize: filters.pageSize ?? 50, - branches: filters.branches, - author: filters.author, - searchText: filters.searchText, - allBranches: filters.allBranches ?? false, - filePath: filters.filePath, - }; - - const git = simpleGit(repository.localPath); - const pageSize = resolvedFilters.pageSize ?? 50; - - const baseArgs = [ - "log", - `--max-count=${pageSize}`, - `--skip=${offset}`, - "--date=iso", - "--pretty=format:%H%x09%an%x09%ae%x09%ad%x09%s", - ]; - - const graphArgs = [ - "log", - "--graph", - "--color=never", - "--date=iso", - `--max-count=${pageSize}`, - `--skip=${offset}`, - "--pretty=format:%h%x09%s", - ]; - - if (resolvedFilters.searchText) { - baseArgs.push(`--grep=${resolvedFilters.searchText}`); - baseArgs.push("--regexp-ignore-case"); - graphArgs.push(`--grep=${resolvedFilters.searchText}`); - graphArgs.push("--regexp-ignore-case"); - } - - if (resolvedFilters.author) { - baseArgs.push(`--author=${resolvedFilters.author}`); - graphArgs.push(`--author=${resolvedFilters.author}`); - } - - if (resolvedFilters.allBranches) { - baseArgs.push("--all"); - graphArgs.push("--all"); - } else if (resolvedFilters.branches && resolvedFilters.branches.length > 0) { - baseArgs.push(...resolvedFilters.branches); - graphArgs.push(...resolvedFilters.branches); - } - - if (resolvedFilters.filePath) { - baseArgs.push("--", resolvedFilters.filePath); - graphArgs.push("--", resolvedFilters.filePath); - } - - const [logOutput, graphOutput, branchesData, authorsData] = await Promise.all([ - git.raw(baseArgs), - git.raw(graphArgs), - git.branch(["-a"]), - git.raw(["log", "--no-merges", "--format=%an%x09%ae"]), - ]); - - const commits: HistoryExplorerCommit[] = logOutput - .split("\n") - .filter((line) => line.trim().length > 0) - .map((line) => { - const [hash, authorName, authorEmail, committedAt, message] = line.split("\t"); - const shortHash = hash?.slice(0, 7) ?? ""; - const dateIso = committedAt ?? ""; - const relative = dateIso ? dayjs(dateIso).fromNow() : ""; - return { - hash: hash ?? "", - shortHash, - message: message ?? "", - author: authorName ?? "", - authorEmail: authorEmail ?? "", - date: dateIso, - relativeTime: relative, - }; - }); - - const graphLines = graphOutput - .split("\n") - .filter((line) => line.trim().length > 0) - .map((line) => line.replace(/\t/g, " ")); - - const authors = Array.from( - new Map( - authorsData - .split("\n") - .filter((line) => line.trim().length > 0) - .map((line) => { - const [name, email] = line.split("\t"); - return [ - `${name ?? ""}|${email ?? ""}`, - { - name: name ?? "", - email: email ?? "", - }, - ]; - }), - ).values(), - ).sort((a, b) => a.name.localeCompare(b.name)); - - const branches = branchesData.all ?? []; - - const hasMore = commits.length === pageSize; - const nextOffset = offset + commits.length; - - return { - graphLines, - commits, - filters: { ...resolvedFilters, offset }, - branches, - authors, - hasMore, - nextOffset, - repository: { - name: path.basename(repository.localPath), - path: repository.localPath, - remote: repository.remoteUrl, - }, - }; - } -} - diff --git a/src/webviews/historyExplorer/types/history-explorer.types.ts b/src/webviews/historyExplorer/types/history-explorer.types.ts deleted file mode 100644 index 3286891..0000000 --- a/src/webviews/historyExplorer/types/history-explorer.types.ts +++ /dev/null @@ -1,54 +0,0 @@ -export interface HistoryExplorerCommit { - hash: string; - shortHash: string; - message: string; - author: string; - authorEmail: string; - date: string; - relativeTime: string; -} - -export interface HistoryExplorerFilters { - branches?: string[]; - author?: string; - searchText?: string; - allBranches?: boolean; - filePath?: string; - pageSize?: number; - offset?: number; -} - -export interface HistoryExplorerAuthor { - name: string; - email: string; -} - -export interface HistoryExplorerInitialState { - graphLines: string[]; - commits: HistoryExplorerCommit[]; - filters: HistoryExplorerFilters; - branches: string[]; - authors: HistoryExplorerAuthor[]; - hasMore: boolean; - offset: number; - repository: { - name: string; - path: string; - remote?: string; - } | null; -} - -export interface HistoryExplorerResponse { - graphLines: string[]; - commits: HistoryExplorerCommit[]; - filters: HistoryExplorerFilters; - branches: string[]; - authors: HistoryExplorerAuthor[]; - hasMore: boolean; - nextOffset: number; - repository: { - name: string; - path: string; - remote?: string; - } | null; -} diff --git a/src/webviews/historyExplorerApp/HistoryExplorerApp.tsx b/src/webviews/historyExplorerApp/HistoryExplorerApp.tsx deleted file mode 100644 index 1a72061..0000000 --- a/src/webviews/historyExplorerApp/HistoryExplorerApp.tsx +++ /dev/null @@ -1,451 +0,0 @@ -import React, { useEffect, useMemo, useState } from "react"; -import { - ThemeProvider, - createTheme, - CssBaseline, - Box, - Stack, - Typography, - FormControl, - InputLabel, - Select, - MenuItem, - TextField, - Button, - FormControlLabel, - Checkbox, - Divider, - Paper, -} from "@mui/material"; -import { createVSCodeBridge, VSCodeBridge } from "../app/bridge"; -import { - HistoryExplorerAuthor, - HistoryExplorerCommit, - HistoryExplorerFilters, - HistoryExplorerInitialState, -} from "@webview/historyExplorer/types/history-explorer.types"; - -const theme = createTheme({ - palette: { - mode: "dark", - }, - typography: { - fontFamily: - 'var(--vscode-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif)', - }, -}); - -type HistoryExplorerMessage = { - type: string; - payload?: any; - message?: string; -}; - -interface HistoryExplorerAppProps { - bridge: VSCodeBridge; - initialState: HistoryExplorerInitialState; -} - -export const HistoryExplorerApp: React.FC = ({ - bridge, - initialState, -}) => { - const [graphLines, setGraphLines] = useState(initialState.graphLines); - const [commits, setCommits] = useState(initialState.commits); - const [branches, setBranches] = useState(initialState.branches); - const [authors, setAuthors] = useState(initialState.authors); - const [filters, setFilters] = useState(initialState.filters); - const [formFilters, setFormFilters] = useState(initialState.filters); - const [hasMore, setHasMore] = useState(initialState.hasMore); - const [offset, setOffset] = useState(initialState.offset ?? 0); - const [repository, setRepository] = useState(initialState.repository); - const [error, setError] = useState(null); - const [loading, setLoading] = useState(true); - const [loadingMore, setLoadingMore] = useState(false); - - useEffect(() => { - const handler = (event: HistoryExplorerMessage) => { - switch (event.type) { - case "historyData": { - const payload = event.payload; - setGraphLines(payload.graphLines ?? []); - setCommits(payload.commits ?? []); - setBranches(payload.branches ?? []); - setAuthors(payload.authors ?? []); - setFilters(payload.filters ?? {}); - setFormFilters(payload.filters ?? {}); - setHasMore(Boolean(payload.hasMore)); - setOffset(payload.nextOffset ?? 0); - setRepository(payload.repository ?? null); - setError(null); - setLoading(false); - setLoadingMore(false); - break; - } - case "historyAppend": { - const payload = event.payload; - setGraphLines((prev) => [...prev, ...(payload.graphLines ?? [])]); - setCommits((prev) => [...prev, ...(payload.commits ?? [])]); - setHasMore(Boolean(payload.hasMore)); - setOffset(payload.nextOffset ?? 0); - setError(null); - setLoading(false); - setLoadingMore(false); - break; - } - case "historyError": { - setError(event.message ?? "Unable to load history"); - setLoading(false); - setLoadingMore(false); - break; - } - default: - break; - } - }; - - bridge.onMessage(handler); - bridge.sendMessage("ready"); - }, [bridge]); - - const branchOptions = useMemo(() => { - return branches - .map((branch) => branch.trim()) - .filter((branch) => branch.length > 0) - .sort((a, b) => a.localeCompare(b)); - }, [branches]); - - const handleFiltersChange = ( - key: K, - value: HistoryExplorerFilters[K], - ) => { - setFormFilters((prev) => ({ - ...prev, - [key]: value, - })); - }; - - const handleApplyFilters = () => { - setLoading(true); - bridge.sendMessage("applyFilters", { filters: formFilters }); - }; - - const handleResetFilters = () => { - const reset: HistoryExplorerFilters = { - branches: undefined, - author: undefined, - searchText: undefined, - allBranches: false, - filePath: undefined, - }; - setFormFilters(reset); - setLoading(true); - bridge.sendMessage("applyFilters", { filters: reset }); - }; - - const handleLoadMore = () => { - if (!hasMore || loadingMore) { - return; - } - setLoadingMore(true); - bridge.sendMessage("loadMore", { offset }); - }; - - const handleOpenCommit = (hash: string) => { - bridge.sendMessage("openCommit", { hash }); - }; - - return ( - - - - - - Git History Explorer - - {repository && ( - - {repository.name} — {repository.path} - - )} - - - - - - - Branch - - - - - handleFiltersChange("allBranches", event.target.checked) - } - /> - } - label="All branches" - /> - - - Author - - - - - handleFiltersChange( - "searchText", - event.target.value ? event.target.value : undefined, - ) - } - sx={{ flexGrow: 1, minWidth: 200 }} - /> - - - handleFiltersChange( - "filePath", - event.target.value ? event.target.value : undefined, - ) - } - sx={{ flexGrow: 1, minWidth: 200 }} - /> - - - - - - - - - {error && ( - - {error} - - )} - - - - - Commit Graph - - - {graphLines.length > 0 ? graphLines.join("\n") : loading ? "Loading…" : "No history available"} - - - - - - Commits - - - {loading && commits.length === 0 ? ( - - Loading history… - - ) : commits.length === 0 ? ( - - No commits match the selected filters. - - ) : ( - commits.map((commit) => ( - - - - {commit.shortHash} - - - {commit.message || "(no message)"} - - - - {commit.author} — {commit.relativeTime || commit.date} - - - - - - )) - )} - - - - - Showing {commits.length} commit{commits.length === 1 ? "" : "s"} - - - - - - - - - ); -}; - -export function createHistoryExplorerApp( - bridge: VSCodeBridge, - initialState: HistoryExplorerInitialState, -): React.ReactElement { - return ; -} diff --git a/src/webviews/historyExplorerApp/index.tsx b/src/webviews/historyExplorerApp/index.tsx deleted file mode 100644 index b8d1093..0000000 --- a/src/webviews/historyExplorerApp/index.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from "react"; -import { createRoot } from "react-dom/client"; -import { createVSCodeBridge } from "../app/bridge"; -import { HistoryExplorerApp } from "./HistoryExplorerApp"; -import { HistoryExplorerInitialState } from "@webview/historyExplorer/types/history-explorer.types"; - -declare global { - interface Window { - vscodeApi: any; - historyExplorerInitialState?: HistoryExplorerInitialState; - } -} - -const container = document.getElementById("root"); - -if (!container) { - throw new Error("History explorer root element was not found"); -} - -const bridge = createVSCodeBridge(window.vscodeApi); -const initialState = - window.historyExplorerInitialState ?? { - graphLines: [], - commits: [], - filters: {}, - branches: [], - authors: [], - hasMore: false, - offset: 0, - repository: null, - }; - -const root = createRoot(container); -root.render( - - - , -); diff --git a/src/webviews/timeline/adapters/vscode-adapters.ts b/src/webviews/timeline/adapters/vscode-adapters.ts new file mode 100644 index 0000000..bd0f844 --- /dev/null +++ b/src/webviews/timeline/adapters/vscode-adapters.ts @@ -0,0 +1,233 @@ +import * as path from "path"; +import { promises as fs } from "fs"; +import * as vscode from "vscode"; +import simpleGit from "simple-git"; +import { RepositoryManager } from "../../../core/repositories/repository-manager"; +import { AccountManager } from "../../../core/accounts/account-manager"; +import { getPrimaryRepository } from "../../../shared/utils/repo-selection"; +import { TrackedRepository } from "../../../shared/types"; +import { toGitHubWebUrl } from "../../../shared/utils/github-url"; +import { OutboundMessage } from "../messages"; +import { + Browser, + GitHubApi, + Notifier, + PullRequestSummary, + RepositoryContext, + WebviewChannel, +} from "../ports"; + +/** + * @description {@link RepositoryContext} backed by the extension's + * {@link RepositoryManager} and the VS Code workspace. Holds an in-memory + * "active repository" override so the user can switch repos from the picker + * without changing the VS Code workspace folder. + */ +export class WorkspaceRepositoryContext implements RepositoryContext { + private activePath: string | undefined; + + constructor(private readonly repositories: RepositoryManager) {} + + getPrimary(): TrackedRepository | undefined { + if (this.activePath) { + const pinned = this.repositories.findByPath(this.activePath); + if (pinned) { + return pinned; + } + } + return getPrimaryRepository(this.repositories); + } + + list(): TrackedRepository[] { + return this.repositories.getRepositories(); + } + + async setActive(localPath: string): Promise { + this.activePath = localPath; + const folders = vscode.workspace.workspaceFolders ?? []; + const alreadyOpen = folders.some( + (f) => path.resolve(f.uri.fsPath) === path.resolve(localPath), + ); + if (alreadyOpen) { + return false; + } + await vscode.commands.executeCommand( + "vscode.openFolder", + vscode.Uri.file(localPath), + { forceNewWindow: false }, + ); + return true; + } + + async addLocal(): Promise { + const picked = await vscode.window.showOpenDialog({ + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + openLabel: "Add repository", + }); + const folder = picked?.[0]?.fsPath; + if (!folder) { + return; + } + if (!(await exists(path.join(folder, ".git")))) { + void vscode.window.showErrorMessage( + "That folder is not a git repository.", + ); + return; + } + let owner = "local"; + let name = path.basename(folder); + let remoteUrl: string | undefined; + try { + const remotes = await simpleGit(folder).getRemotes(true); + const origin = remotes.find((r) => r.name === "origin") ?? remotes[0]; + remoteUrl = origin?.refs.fetch ?? origin?.refs.push; + const m = remoteUrl?.match(/[/:]([^/]+)\/([^/]+?)(?:\.git)?$/); + if (m) { + owner = m[1]; + name = m[2]; + } + } catch { + // keep folder-name defaults + } + await this.repositories.addRepository({ + localPath: folder, + owner, + name, + remoteUrl, + }); + await this.setActive(folder); + } + + async clone(): Promise { + await vscode.commands.executeCommand("githubDesktop.cloneRepository"); + } +} + +async function exists(p: string): Promise { + try { + await fs.access(p); + return true; + } catch { + return false; + } +} + +/** + * @description {@link Notifier} backed by `vscode.window`. + */ +export class VsCodeNotifier implements Notifier { + info(message: string): void { + void vscode.window.showInformationMessage(message); + } + + warn(message: string): void { + void vscode.window.showWarningMessage(message); + } + + error(message: string): void { + void vscode.window.showErrorMessage(message); + } + + async confirm(message: string, confirmLabel: string): Promise { + const choice = await vscode.window.showWarningMessage( + message, + { modal: true }, + confirmLabel, + ); + return choice === confirmLabel; + } + + async prompt(options: { + prompt: string; + placeHolder?: string; + validateInput?: (value: string) => string | null; + }): Promise { + const value = await vscode.window.showInputBox({ + prompt: options.prompt, + placeHolder: options.placeHolder, + ignoreFocusOut: true, + validateInput: options.validateInput, + }); + return value?.trim() || undefined; + } +} + +/** + * @description {@link Browser} backed by `vscode.env.openExternal`. + */ +export class VsCodeBrowser implements Browser { + open(url: string): void { + void vscode.env.openExternal(vscode.Uri.parse(url)); + } +} + +/** + * @description {@link WebviewChannel} backed by a concrete `vscode.Webview`. + */ +export class VsCodeWebviewChannel implements WebviewChannel { + constructor(private readonly webview: vscode.Webview) {} + + post(message: OutboundMessage): void { + void this.webview.postMessage(message); + } +} + +/** + * @description Resolves a repository's `owner/name` GitHub slug, preferring the + * remote URL (authoritative) over the tracked fields (which fall back to the + * folder name for repos added without a GitHub remote). + */ +function gitHubSlug( + repo: TrackedRepository, +): { owner: string; name: string } | null { + const web = toGitHubWebUrl(repo.remoteUrl); + const m = web?.match(/github\.com\/([^/]+)\/([^/]+?)$/); + if (m) { + return { owner: m[1], name: m[2] }; + } + if (repo.owner && repo.owner !== "local" && repo.name) { + return { owner: repo.owner, name: repo.name }; + } + return null; +} + +/** + * @description {@link GitHubApi} backed by {@link AccountManager}'s + * authenticated Octokit. + */ +export class AccountGitHubApi implements GitHubApi { + constructor(private readonly accounts: AccountManager) {} + + async listPullRequests( + repo: TrackedRepository, + ): Promise { + const slug = gitHubSlug(repo); + if (!slug) { + throw new Error("This repository has no GitHub remote."); + } + const octokit = await this.accounts.getOctokit(repo.accountId); + if (!octokit) { + throw new Error("Sign in to a GitHub account to see pull requests."); + } + const { data } = await octokit.rest.pulls.list({ + owner: slug.owner, + repo: slug.name, + state: "open", + sort: "updated", + direction: "desc", + per_page: 50, + }); + return data.map((pr) => ({ + number: pr.number, + title: pr.title, + author: pr.user?.login ?? "", + headRef: pr.head.ref, + baseRef: pr.base.ref, + isFork: pr.head.repo?.fork ?? pr.head.repo?.full_name !== `${slug.owner}/${slug.name}`, + isDraft: pr.draft ?? false, + updatedAt: pr.updated_at, + })); + } +} diff --git a/src/webviews/timeline/interfaces/repository-snapshot.ts b/src/webviews/timeline/interfaces/repository-snapshot.ts new file mode 100644 index 0000000..14d2b13 --- /dev/null +++ b/src/webviews/timeline/interfaces/repository-snapshot.ts @@ -0,0 +1,61 @@ +import { ChangeEntry, CommitEntry } from "./timeline-view-provider.interface"; + +/** + * @description Tracking state of the current branch against its upstream. + */ +export interface RemoteStatus { + hasRemote: boolean; + isPublished: boolean; + ahead: number; + behind: number; + lastFetched: Date | null; + remoteBranch: string | null; +} + +/** + * @description Minimal repository identity shown in the timeline toolbar. + */ +export interface RepositorySummary { + name: string; + path: string; + remote?: string; +} + +/** + * @description A multi-step git operation left mid-flight by a conflict. + */ +export type InProgressOperation = "merge" | "cherry-pick" | "revert" | "rebase"; + +/** + * @description One entry of `git stash list`. + */ +export interface StashEntry { + index: number; + message: string; + branch: string | null; +} + +/** + * @description Everything the timeline needs for one render pass, assembled by + * the repository read model in a single call. + */ +export interface RepositorySnapshot { + changes: ChangeEntry[]; + commits: CommitEntry[]; + hasMoreCommits: boolean; + branches: string[]; + currentBranch: string | null; + branchActivity: Record; + repository: RepositorySummary; + remoteStatus: RemoteStatus; + tags: Record; + /** Non-null when a merge/cherry-pick/revert/rebase is stopped in conflict. */ + operation: InProgressOperation | null; + conflicted: string[]; + /** True when HEAD has a parent and no operation is in progress (undo is safe). */ + canUndo: boolean; + stashes: StashEntry[]; +} + +/** Shared page size for commit history queries. */ +export const COMMITS_PAGE_SIZE = 50; diff --git a/src/webviews/timeline/interfaces/timeline-view-provider.interface.ts b/src/webviews/timeline/interfaces/timeline-view-provider.interface.ts index 85af762..f2150ea 100644 --- a/src/webviews/timeline/interfaces/timeline-view-provider.interface.ts +++ b/src/webviews/timeline/interfaces/timeline-view-provider.interface.ts @@ -1,11 +1,17 @@ import * as vscode from "vscode"; +/** + * @description One entry in the Changes tab: a path and its git status code. + */ export interface ChangeEntry { path: string; status: string; staged?: boolean; } +/** + * @description One row of the History tab. + */ export interface CommitEntry { hash: string; shortHash: string; @@ -15,9 +21,13 @@ export interface CommitEntry { relativeTime: string; committedAt: string; tags?: string[]; + /** `false` when the commit is known to be ahead of the upstream. */ isPushed?: boolean; } +/** + * @description Aggregate detail of a single commit shown in the diff pane. + */ export interface CommitDetail { summary: CommitEntry & { additions: number; @@ -32,25 +42,9 @@ export interface CommitDetail { }>; } -export interface FileIconInfo { - iconClass?: string; - iconUri?: string; -} - -export interface CommitDetailFile { - path: string; - status: string; - additions: number; - deletions: number; - iconClass?: string; - iconUri?: string; -} - -export interface WebviewMessage { - command: string; - [key: string]: any; -} - +/** + * @description Contract the extension host relies on to refresh the timeline. + */ export interface ITimelineViewProvider extends vscode.WebviewViewProvider { refresh(): Promise; } diff --git a/src/webviews/timeline/message-router.ts b/src/webviews/timeline/message-router.ts new file mode 100644 index 0000000..51e2391 --- /dev/null +++ b/src/webviews/timeline/message-router.ts @@ -0,0 +1,54 @@ +import { InboundCommand, InboundMessage } from "./messages"; +import { Notifier } from "./ports"; + +/** + * @description Handler for one inbound command. The message is narrowed to the + * matching union member by {@link MessageRouter.on}. + */ +export type CommandHandler = ( + message: Extract, +) => Promise | void; + +/** + * @description Maps inbound commands to handlers. Adding a command means + * registering a handler — the router itself never changes (Open/Closed). + */ +export class MessageRouter { + private readonly handlers = new Map< + InboundCommand, + CommandHandler + >(); + + constructor(private readonly notifier: Notifier) {} + + /** + * Registers `handler` for `command`, replacing any previous registration. + * + * @returns `this`, for chaining. + */ + on(command: C, handler: CommandHandler): this { + this.handlers.set( + command, + handler as unknown as CommandHandler, + ); + return this; + } + + /** + * Dispatches `message` to its handler. Unknown commands are ignored; handler + * errors are surfaced to the user rather than swallowed silently. + */ + async dispatch(message: InboundMessage): Promise { + const handler = this.handlers.get(message.command); + if (!handler) { + return; + } + try { + await handler(message); + } catch (error) { + this.notifier.error( + error instanceof Error ? error.message : String(error), + ); + } + } +} diff --git a/src/webviews/timeline/messages.ts b/src/webviews/timeline/messages.ts new file mode 100644 index 0000000..8f0a7e8 --- /dev/null +++ b/src/webviews/timeline/messages.ts @@ -0,0 +1,144 @@ +import { + ChangeEntry, + CommitEntry, + CommitDetail, +} from "./interfaces/timeline-view-provider.interface"; +import { + InProgressOperation, + RemoteStatus, + RepositorySummary, + StashEntry, +} from "./interfaces/repository-snapshot"; +import { PullRequestSummary } from "./ports"; + +/* ------------------------------------------------------------------ * + * Inbound: webview -> extension + * ------------------------------------------------------------------ */ + +/** + * @description Every message the timeline webview can send, as a discriminated + * union keyed on `command`. Handlers narrow on `command` and get fully typed + * payloads — no `any`, no manual casts. + */ +export type InboundMessage = + | { command: "ready" } + | { command: "refresh" } + | { command: "selectRepository"; path: string } + | { command: "addLocalRepository" } + | { command: "cloneRepository" } + | { command: "stageFiles"; files: string[] } + | { command: "unstageFiles"; files: string[] } + | { command: "commit"; message: string } + | { command: "commitFiles"; message: string; files: string[] } + | { command: "amendCommit"; message: string; files: string[] } + | { command: "undoLastCommit" } + | { command: "discardFiles"; files: string[] } + | { command: "markResolved"; files: string[] } + | { command: "abortOperation" } + | { command: "continueOperation" } + | { command: "getStashes" } + | { command: "stashPush"; message: string } + | { command: "stashApply"; index: number; drop: boolean } + | { command: "stashDrop"; index: number } + | { command: "getWorkingDiff"; filePath: string } + | { command: "fetch" } + | { command: "pull" } + | { command: "push" } + | { command: "forcePush" } + | { command: "publish" } + | { command: "checkoutBranch"; branch: string } + | { command: "createBranch"; branchName: string } + | { + command: "createBranchWithChanges"; + branchName: string; + bringChanges: boolean; + } + | { command: "mergeBranch"; fromBranch: string; toBranch: string } + | { command: "compareBranch"; branch: string } + | { command: "createPullRequest"; branch: string } + | { command: "getPullRequests" } + | { command: "checkoutPullRequest"; number: number } + | { command: "openRepoOnGitHub" } + | { command: "loadMoreCommits"; offset: number } + | { command: "getCommitDetails"; hash: string } + | { command: "getFileDiff"; hash: string; filePath: string } + | { command: "selectCommit"; hash: string } + | { command: "selectFile"; hash: string; path: string } + | { command: "resetToCommit"; hash: string } + | { command: "checkoutCommit"; hash: string } + | { command: "revertCommit"; hash: string } + | { command: "cherryPickCommit"; hash: string } + | { command: "createBranchFromCommit"; hash: string } + | { command: "createTagFromCommit"; hash: string } + | { command: "viewCommitOnGitHub"; hash: string }; + +export type InboundCommand = InboundMessage["command"]; + +/* ------------------------------------------------------------------ * + * Outbound: extension -> webview + * ------------------------------------------------------------------ */ + +export type OutboundMessage = + | { command: "updateRepository"; repository: RepositorySummary | null } + | { + command: "updateRepositoryList"; + repositories: Array<{ name: string; path: string }>; + activePath: string | null; + } + | { command: "updateChanges"; changes: ChangeEntry[] } + | { + command: "updateHistory"; + history: CommitEntry[]; + hasMoreCommits: boolean; + offset: number; + } + | { + command: "updateBranches"; + branches: string[]; + currentBranch: string | null; + branchActivity: Record; + } + | { + command: "updateAccounts"; + activeAccount: { + login: string; + name?: string; + avatarUrl?: string; + } | null; + } + | { + command: "updateRemoteStatus"; + remoteStatus: RemoteStatus; + tags: Record; + } + | { + command: "loadMoreCommitsResponse"; + history: CommitEntry[]; + hasMoreCommits: boolean; + offset: number; + } + | { + command: "branchComparison"; + branch: string; + ahead: CommitEntry[]; + behind: CommitEntry[]; + } + | { command: "commitSucceeded" } + | { command: "mergeConflict"; operation: string; files: string[] } + | { + command: "updateOperation"; + operation: InProgressOperation | null; + conflicted: string[]; + canUndo: boolean; + lastCommitSummary: string | null; + } + | { command: "updateStashes"; stashes: StashEntry[] } + | { + command: "updatePullRequests"; + pullRequests: PullRequestSummary[]; + error?: string; + } + | { command: "workingDiff"; payload: { path: string; diff: string } } + | { command: "fileDiff"; payload: { path: string; diff: string } } + | { command: "commitDetail"; payload: CommitDetail } + | { command: "error"; message: string }; diff --git a/src/webviews/timeline/ports.ts b/src/webviews/timeline/ports.ts new file mode 100644 index 0000000..d4a6750 --- /dev/null +++ b/src/webviews/timeline/ports.ts @@ -0,0 +1,112 @@ +import { TrackedRepository } from "../../shared/types"; +import { OutboundMessage } from "./messages"; + +/** + * @description Resolves which repository the timeline currently operates on. + * Abstracts {@link getPrimaryRepository} so services never touch the + * repository manager or the VS Code workspace API directly. + */ +export interface RepositoryContext { + /** The active repository, or `undefined` when there is none. */ + getPrimary(): TrackedRepository | undefined; + /** Every tracked repository, for the repository picker. */ + list(): TrackedRepository[]; + /** + * Makes `localPath` the active repository. If it is not already a workspace + * folder, opens that folder in VS Code (which reloads the window); otherwise + * just switches the in-session override. + * + * @returns `true` when the window is about to reload (caller should not + * bother refreshing). + */ + setActive(localPath: string): Promise; + /** Prompts for a local folder and tracks it as a repository. */ + addLocal(): Promise; + /** Runs the "clone repository" flow. */ + clone(): Promise; +} + +/** + * @description Thin abstraction over the user-facing notification surface + * (`vscode.window.*`). Keeps services free of the `vscode` namespace and + * trivially mockable in tests. + */ +export interface Notifier { + info(message: string): void; + warn(message: string): void; + error(message: string): void; + + /** + * Asks the user to confirm a destructive action. + * + * @param message - The question to display (modal) + * @param confirmLabel - Label of the confirming button + * @returns `true` only when the user picked the confirming button + */ + confirm(message: string, confirmLabel: string): Promise; + + /** + * Prompts the user for a single line of text. + * + * @returns The trimmed input, or `undefined` when cancelled + */ + prompt(options: { + prompt: string; + placeHolder?: string; + validateInput?: (value: string) => string | null; + }): Promise; +} + +/** + * @description Outbound half of the webview message channel. Services push + * {@link OutboundMessage}s; they never receive through this port. + */ +export interface WebviewChannel { + post(message: OutboundMessage): void; +} + +/** + * @description Opens URLs in the user's external browser. + */ +export interface Browser { + open(url: string): void; +} + +/** + * @description One open pull request, as shown in the branch dropdown's + * "Pull requests" tab. + */ +export interface PullRequestSummary { + number: number; + title: string; + author: string; + headRef: string; + baseRef: string; + isFork: boolean; + isDraft: boolean; + updatedAt: string; +} + +/** + * @description Read-only GitHub REST access scoped to the active repository. + * Backed by the account manager's authenticated Octokit; kept behind a port so + * services stay free of `@octokit/*` and are mockable in tests. + */ +export interface GitHubApi { + /** + * Lists open pull requests for `repo`, most-recently-updated first. + * + * @throws when no GitHub account is signed in or the repo has no + * `owner`/`name` resolvable to a GitHub slug. + */ + listPullRequests(repo: TrackedRepository): Promise; +} + +/** + * @description Triggers a full recompute-and-broadcast of the timeline state. + * Mutating services call this after a successful operation instead of pushing + * partial updates themselves. + */ +export interface Refresher { + refresh(): Promise; +} diff --git a/src/webviews/timeline/services/branch.service.ts b/src/webviews/timeline/services/branch.service.ts new file mode 100644 index 0000000..a429d99 --- /dev/null +++ b/src/webviews/timeline/services/branch.service.ts @@ -0,0 +1,151 @@ +import { GitClientFactory } from "../../../core/git/git-authenticator"; +import { + Notifier, + RepositoryContext, + Refresher, + WebviewChannel, +} from "../ports"; +import { logRange } from "./commit-log"; +import { RepositoryScopedService } from "./repository-scoped.service"; + +/** + * @description Branch lifecycle for the branch dropdown: switching, creating + * (optionally carrying or stashing uncommitted work), and merging. + */ +export class BranchService extends RepositoryScopedService { + constructor( + repos: RepositoryContext, + notifier: Notifier, + private readonly git: GitClientFactory, + private readonly channel: WebviewChannel, + private readonly refresher: Refresher, + ) { + super(repos, notifier); + } + + /** + * Compares `branch` against the current HEAD and pushes the result to the + * webview: commits HEAD has that `branch` lacks (`ahead`) and vice versa + * (`behind`). + */ + compare(branch: string): Promise { + return this.withRepo(async (repo) => { + if (!branch) { + return; + } + const git = this.git.plain(repo.localPath); + const [ahead, behind] = await Promise.all([ + logRange(git, `${branch}..HEAD`), + logRange(git, `HEAD..${branch}`), + ]); + this.channel.post({ + command: "branchComparison", + branch, + ahead, + behind, + }); + }); + } + + checkout(branch: string): Promise { + return this.withRepo(async (repo) => { + if (!branch) { + return; + } + // Never check out a `remotes//` ref directly: that detaches + // HEAD. Fall back to the short name so git sets up local tracking. + const remoteMatch = branch.match(/^remotes\/[^/]+\/(.+)$/); + const target = remoteMatch ? remoteMatch[1] : branch; + await this.git.plain(repo.localPath).checkout(target); + this.notifier.info(`Switched to ${target}`); + await this.refresher.refresh(); + }); + } + + create(branchName: string): Promise { + return this.withRepo(async (repo) => { + if (!branchName) { + return; + } + await this.git.plain(repo.localPath).checkoutLocalBranch(branchName); + this.notifier.info(`Created and switched to '${branchName}'`); + await this.refresher.refresh(); + }); + } + + createWithChanges( + branchName: string, + bringChanges: boolean, + ): Promise { + return this.withRepo(async (repo) => { + if (!branchName) { + return; + } + const git = this.git.plain(repo.localPath); + if (!bringChanges) { + await git.stash(["push", "-m", `Stash before creating ${branchName}`]); + } + await git.checkoutLocalBranch(branchName); + this.notifier.info( + bringChanges + ? `Created '${branchName}' with your changes` + : `Created '${branchName}' and stashed your changes`, + ); + await this.refresher.refresh(); + }); + } + + merge(fromBranch: string, toBranch: string): Promise { + return this.withRepo(async (repo) => { + if (!fromBranch || !toBranch) { + return; + } + const defaultBranch = repo.defaultBranch || "main"; + const isDefault = (b: string) => + b === defaultBranch || (!repo.defaultBranch && b === "master"); + + if (isDefault(fromBranch)) { + // Merging the default branch INTO something is usually fine (keeping + // a feature branch up to date), but it's also the exact mistake that + // happens when you meant to merge the other way (current -> default) + // and picked the wrong side. Make sure that's intentional. + const proceed = await this.notifier.confirm( + `"${fromBranch}" is the default branch. Merging it into "${toBranch}" ` + + `will bring in its entire history. If you meant to update ` + + `"${fromBranch}" instead, cancel and check out "${fromBranch}" first.`, + "Merge anyway", + ); + if (!proceed) { + return; + } + } else if (!isDefault(toBranch)) { + this.notifier.warn( + `Merging into "${toBranch}", which is not the default branch.`, + ); + } + + const git = this.git.plain(repo.localPath); + await git.checkout(toBranch); + try { + await git.merge([fromBranch]); + } catch (error) { + const status = await git.status(); + if (status.conflicted.length > 0) { + this.channel.post({ + command: "mergeConflict", + operation: `merge ${fromBranch}`, + files: status.conflicted, + }); + this.notifier.warn( + `Merge stopped: ${status.conflicted.length} file(s) in conflict`, + ); + await this.refresher.refresh(); + return; + } + throw error; + } + this.notifier.info(`Merged ${fromBranch} into ${toBranch}`); + await this.refresher.refresh(); + }); + } +} diff --git a/src/webviews/timeline/services/commit-actions.service.ts b/src/webviews/timeline/services/commit-actions.service.ts new file mode 100644 index 0000000..02f3e6c --- /dev/null +++ b/src/webviews/timeline/services/commit-actions.service.ts @@ -0,0 +1,146 @@ +import { GitClientFactory } from "../../../core/git/git-authenticator"; +import { toGitHubWebUrl } from "../../../shared/utils/github-url"; +import { Browser, Notifier, RepositoryContext, Refresher } from "../ports"; +import { RepositoryScopedService } from "./repository-scoped.service"; + +const shortHash = (hash: string): string => hash.substring(0, 7); + +/** + * @description Actions available from a commit's context menu in the History + * tab: reset, checkout, revert, branch-from, tag-from, cherry-pick and + * "view on GitHub". + */ +export class CommitActionsService extends RepositoryScopedService { + constructor( + repos: RepositoryContext, + notifier: Notifier, + private readonly git: GitClientFactory, + private readonly browser: Browser, + private readonly refresher: Refresher, + ) { + super(repos, notifier); + } + + reset(hash: string): Promise { + return this.withRepo(async (repo) => { + const confirmed = await this.notifier.confirm( + `Reset to ${shortHash(hash)}? All changes after this commit are discarded.`, + "Reset", + ); + if (!confirmed) { + return; + } + await this.git.plain(repo.localPath).reset(["--hard", hash]); + this.notifier.info(`Reset to ${shortHash(hash)}`); + await this.refresher.refresh(); + }); + } + + checkout(hash: string): Promise { + return this.withRepo(async (repo) => { + await this.git.plain(repo.localPath).checkout(hash); + this.notifier.info(`Checked out ${shortHash(hash)} (detached HEAD)`); + await this.refresher.refresh(); + }); + } + + revert(hash: string): Promise { + return this.withRepo(async (repo) => { + const git = this.git.plain(repo.localPath); + try { + await git.revert(hash, ["--no-edit"]); + } catch (error) { + if ( + await this.reportConflict(repo.localPath, `revert ${shortHash(hash)}`) + ) { + return; + } + throw error; + } + this.notifier.info(`Reverted ${shortHash(hash)}`); + await this.refresher.refresh(); + }); + } + + cherryPick(hash: string): Promise { + return this.withRepo(async (repo) => { + const git = this.git.plain(repo.localPath); + try { + await git.raw(["cherry-pick", hash]); + } catch (error) { + if ( + await this.reportConflict( + repo.localPath, + `cherry-pick ${shortHash(hash)}`, + ) + ) { + return; + } + throw error; + } + this.notifier.info(`Cherry-picked ${shortHash(hash)}`); + await this.refresher.refresh(); + }); + } + + branchFrom(hash: string): Promise { + return this.withRepo(async (repo) => { + const name = await this.notifier.prompt({ + prompt: "New branch name", + placeHolder: "feature/my-branch", + }); + if (!name) { + return; + } + await this.git.plain(repo.localPath).checkoutBranch(name, hash); + this.notifier.info(`Created '${name}' from ${shortHash(hash)}`); + await this.refresher.refresh(); + }); + } + + tagFrom(hash: string): Promise { + return this.withRepo(async (repo) => { + const name = await this.notifier.prompt({ + prompt: "Tag name", + placeHolder: "v1.0.0", + validateInput: (v) => + /^[A-Za-z0-9._/-]+$/.test(v) ? null : "Invalid tag name", + }); + if (!name) { + return; + } + await this.git.withAuth(repo.localPath, async (g) => { + await g.raw(["tag", "-a", name, hash, "-m", `Release ${name}`]); + await g.raw(["push", "origin", `refs/tags/${name}`]); + }); + this.notifier.info(`Created and pushed tag '${name}'`); + await this.refresher.refresh(); + }); + } + + viewOnGitHub(hash: string): Promise { + return this.withRepo(async (repo) => { + const base = toGitHubWebUrl(repo.remoteUrl); + if (!base) { + this.notifier.warn("No GitHub remote configured"); + return; + } + this.browser.open(`${base}/commit/${hash}`); + }); + } + + private async reportConflict( + repoPath: string, + operation: string, + ): Promise { + const status = await this.git.plain(repoPath).status(); + if (status.conflicted.length === 0) { + return false; + } + this.notifier.warn( + `${operation} stopped: ${status.conflicted.length} file(s) in conflict`, + ); + await this.refresher.refresh(); + return true; + } +} diff --git a/src/webviews/timeline/services/commit-log.ts b/src/webviews/timeline/services/commit-log.ts new file mode 100644 index 0000000..041106d --- /dev/null +++ b/src/webviews/timeline/services/commit-log.ts @@ -0,0 +1,81 @@ +import { SimpleGit } from "simple-git"; +import { CommitEntry } from "../interfaces/timeline-view-provider.interface"; + +/** + * @description Formats a git date as a coarse "N minutes ago" string. + * + * @param input - Any date string git may emit (ISO, RFC 2822, epoch...) + * @returns A relative phrase, or "unknown" when unparseable + */ +export function formatRelativeTime(input: string): string { + const date = new Date(input); + if (Number.isNaN(date.getTime())) { + return "unknown"; + } + const seconds = Math.round((Date.now() - date.getTime()) / 1000); + const units: Array<[number, string]> = [ + [60, "second"], + [60, "minute"], + [24, "hour"], + [30, "day"], + [12, "month"], + [Number.POSITIVE_INFINITY, "year"], + ]; + let value = Math.max(seconds, 0); + for (const [size, name] of units) { + if (value < size) { + const rounded = Math.round(value); + return `${rounded} ${name}${rounded === 1 ? "" : "s"} ago`; + } + value /= size; + } + return "just now"; +} + +/** `git log` format string that {@link parseCommitLogLine} understands. */ +export const COMMIT_LOG_FORMAT = "%H%x09%an%x09%ae%x09%ad%x09%s"; + +/** + * @description Parses one tab-separated line produced with + * {@link COMMIT_LOG_FORMAT} into a {@link CommitEntry}. + */ +export function parseCommitLogLine(line: string): CommitEntry { + const [hash = "", author = "", email = "", date = "", ...rest] = + line.split("\t"); + return { + hash, + shortHash: hash.slice(0, 7), + message: rest.join("\t"), + authorName: author, + authorEmail: email, + relativeTime: formatRelativeTime(date), + committedAt: date, + }; +} + +/** + * @description Runs `git log ` and returns the commits, newest first. + * + * @param git - A client bound to the repository + * @param range - Any rev range, e.g. `"main..HEAD"` + * @param limit - Max commits to return (default 200) + */ +export async function logRange( + git: SimpleGit, + range: string, + limit = 200, +): Promise { + const raw = await git + .raw([ + "log", + `--format=${COMMIT_LOG_FORMAT}`, + "--date=iso", + `--max-count=${limit}`, + range, + ]) + .catch(() => ""); + return raw + .split("\n") + .filter((l) => l.trim()) + .map(parseCommitLogLine); +} diff --git a/src/webviews/timeline/services/conflict.service.ts b/src/webviews/timeline/services/conflict.service.ts new file mode 100644 index 0000000..d3c5aba --- /dev/null +++ b/src/webviews/timeline/services/conflict.service.ts @@ -0,0 +1,90 @@ +import { GitClientFactory } from "../../../core/git/git-authenticator"; +import { InProgressOperation } from "../interfaces/repository-snapshot"; +import { Notifier, RepositoryContext, Refresher } from "../ports"; +import { detectInProgressOperation } from "./git-operation-state"; +import { RepositoryScopedService } from "./repository-scoped.service"; + +const CONTINUE: Record = { + merge: ["merge", "--continue"], + "cherry-pick": ["cherry-pick", "--continue"], + revert: ["revert", "--continue"], + rebase: ["rebase", "--continue"], +}; + +const ABORT: Record = { + merge: ["merge", "--abort"], + "cherry-pick": ["cherry-pick", "--abort"], + revert: ["revert", "--abort"], + rebase: ["rebase", "--abort"], +}; + +/** + * @description Drives a merge/cherry-pick/revert/rebase that stopped in + * conflict: mark files resolved, continue, or abort. The operation in progress + * is detected from the repository, not passed by the webview. + */ +export class ConflictService extends RepositoryScopedService { + constructor( + repos: RepositoryContext, + notifier: Notifier, + private readonly git: GitClientFactory, + private readonly refresher: Refresher, + ) { + super(repos, notifier); + } + + /** `git add`s the given paths, marking them resolved. */ + markResolved(files: string[]): Promise { + return this.withRepo(async (repo) => { + if (files.length === 0) { + return; + } + await this.git.plain(repo.localPath).add(files); + await this.refresher.refresh(); + }); + } + + /** Finishes the operation once every conflict is resolved. */ + continue(): Promise { + return this.withRepo(async (repo) => { + const git = this.git.plain(repo.localPath); + const op = await this.currentOperation(repo.localPath); + if (!op) { + this.notifier.warn("No operation in progress"); + return; + } + const status = await git.status(); + if (status.conflicted.length > 0) { + this.notifier.warn( + `${status.conflicted.length} file(s) still in conflict`, + ); + return; + } + await git + .raw([...CONTINUE[op], "--no-edit"]) + .catch(() => git.raw(CONTINUE[op])); + this.notifier.info(`${op} completed`); + await this.refresher.refresh(); + }); + } + + /** Rolls the operation back to its pre-conflict state. */ + abort(): Promise { + return this.withRepo(async (repo) => { + const op = await this.currentOperation(repo.localPath); + if (!op) { + this.notifier.warn("No operation in progress"); + return; + } + await this.git.plain(repo.localPath).raw(ABORT[op]); + this.notifier.info(`${op} aborted`); + await this.refresher.refresh(); + }); + } + + private currentOperation( + repoPath: string, + ): Promise { + return detectInProgressOperation(this.git.plain(repoPath)); + } +} diff --git a/src/webviews/timeline/services/diff.service.ts b/src/webviews/timeline/services/diff.service.ts new file mode 100644 index 0000000..1286850 --- /dev/null +++ b/src/webviews/timeline/services/diff.service.ts @@ -0,0 +1,46 @@ +import { GitClientFactory } from "../../../core/git/git-authenticator"; +import { Notifier, RepositoryContext, WebviewChannel } from "../ports"; +import { RepositoryScopedService } from "./repository-scoped.service"; +import { RepositoryDataService } from "./repository-data.service"; + +/** + * @description Read-only diff/detail lookups requested from the History tab: + * a commit's aggregate detail and the patch of one file within a commit. + */ +export class DiffService extends RepositoryScopedService { + constructor( + repos: RepositoryContext, + notifier: Notifier, + private readonly git: GitClientFactory, + private readonly data: RepositoryDataService, + private readonly channel: WebviewChannel, + ) { + super(repos, notifier); + } + + async commitDetail(hash: string): Promise { + try { + const payload = await this.data.getCommitDetail(hash); + this.channel.post({ command: "commitDetail", payload }); + } catch (error) { + this.channel.post({ + command: "error", + message: error instanceof Error ? error.message : String(error), + }); + } + } + + fileDiff(hash: string, filePath: string): Promise { + return this.withRepo(async (repo) => { + const git = this.git.plain(repo.localPath); + let diff = await git.raw(["show", hash, "--patch", "--", filePath]); + if (!diff.trim()) { + diff = await git.raw(["show", hash, "--", filePath]); + } + this.channel.post({ + command: "fileDiff", + payload: { path: filePath, diff }, + }); + }); + } +} diff --git a/src/webviews/timeline/services/file-icon.service.ts b/src/webviews/timeline/services/file-icon.service.ts deleted file mode 100644 index b194d57..0000000 --- a/src/webviews/timeline/services/file-icon.service.ts +++ /dev/null @@ -1,109 +0,0 @@ -import * as path from "path"; -import { FileIconInfo } from "../interfaces/timeline-view-provider.interface"; - -export class FileIconService { - getFileIconInfo(filePath: string): FileIconInfo { - try { - const ext = path.extname(filePath).toLowerCase(); - const baseName = path.basename(filePath).toLowerCase(); - - let iconName = "file"; - - // Special file names - if (baseName === "package.json") iconName = "package"; - else if (baseName === "tsconfig.json") iconName = "gear"; - else if (baseName === ".gitignore") iconName = "git-commit"; - else if (baseName.startsWith(".env")) iconName = "gear"; - else if (baseName === "readme.md") iconName = "book"; - else if (baseName.startsWith("dockerfile")) iconName = "file-binary"; - // File extensions - else { - switch (ext) { - case ".ts": - iconName = "symbol-class"; - break; - case ".tsx": - iconName = "symbol-class"; - break; - case ".js": - iconName = "symbol-method"; - break; - case ".jsx": - iconName = "symbol-method"; - break; - case ".json": - iconName = "json"; - break; - case ".html": - iconName = "code"; - break; - case ".css": - iconName = "symbol-color"; - break; - case ".scss": - iconName = "symbol-color"; - break; - case ".md": - iconName = "markdown"; - break; - case ".yml": - case ".yaml": - iconName = "settings-gear"; - break; - case ".xml": - iconName = "code"; - break; - case ".svg": - iconName = "file-media"; - break; - case ".png": - case ".jpg": - case ".jpeg": - case ".gif": - iconName = "file-media"; - break; - case ".pdf": - iconName = "file-pdf"; - break; - case ".zip": - iconName = "file-zip"; - break; - case ".py": - iconName = "symbol-namespace"; - break; - case ".java": - iconName = "symbol-class"; - break; - case ".go": - iconName = "symbol-method"; - break; - case ".rs": - iconName = "symbol-misc"; - break; - case ".vue": - iconName = "symbol-namespace"; - break; - case ".sql": - iconName = "database"; - break; - case ".lock": - iconName = "lock"; - break; - case ".txt": - iconName = "file-text"; - break; - case ".log": - iconName = "output"; - break; - default: - iconName = "file"; - break; - } - } - - return { iconClass: `codicon codicon-${iconName}` }; - } catch (error) { - return { iconClass: "codicon codicon-file" }; - } - } -} diff --git a/src/webviews/timeline/services/git-operation-state.ts b/src/webviews/timeline/services/git-operation-state.ts new file mode 100644 index 0000000..ec38b60 --- /dev/null +++ b/src/webviews/timeline/services/git-operation-state.ts @@ -0,0 +1,36 @@ +import * as fs from "fs"; +import * as path from "path"; +import { SimpleGit } from "simple-git"; +import { InProgressOperation } from "../interfaces/repository-snapshot"; + +/** + * @description Detects a multi-step git operation left mid-flight (typically by + * a conflict) by probing the marker files in the git directory. + * + * @param git - A client bound to the repository + * @returns The operation in progress, or `null` when the tree is idle + */ +export async function detectInProgressOperation( + git: SimpleGit, +): Promise { + const dir = ( + await git.raw(["rev-parse", "--absolute-git-dir"]).catch(() => "") + ).trim(); + if (!dir) { + return null; + } + const has = (p: string): boolean => fs.existsSync(path.join(dir, p)); + if (has("rebase-merge") || has("rebase-apply")) { + return "rebase"; + } + if (has("CHERRY_PICK_HEAD")) { + return "cherry-pick"; + } + if (has("REVERT_HEAD")) { + return "revert"; + } + if (has("MERGE_HEAD")) { + return "merge"; + } + return null; +} diff --git a/src/webviews/timeline/services/git-operations.service.ts b/src/webviews/timeline/services/git-operations.service.ts deleted file mode 100644 index 42347ff..0000000 --- a/src/webviews/timeline/services/git-operations.service.ts +++ /dev/null @@ -1,573 +0,0 @@ -import * as path from "path"; -import * as vscode from "vscode"; -import simpleGit from "simple-git"; -import { RepositoryManager } from "../../../core/repositories/repository-manager"; -import { getPrimaryRepository } from "../../../shared/utils/repo-selection"; -import { - ChangeEntry, - CommitEntry, - CommitDetail, - CommitDetailFile, -} from "../interfaces/timeline-view-provider.interface"; -const dayjs = require("dayjs"); -const relativeTime = require("dayjs/plugin/relativeTime"); - -dayjs.extend(relativeTime); - -export class GitOperationsService { - constructor(private readonly repositories: RepositoryManager) {} - - async getRepositoryData(): Promise<{ - changes: ChangeEntry[]; - commits: CommitEntry[]; - hasMoreCommits: boolean; - branches: string[]; - currentBranch: string | null; - branchActivity: Record; - repository: any; - remoteStatus: { - hasRemote: boolean; - isPublished: boolean; - ahead: number; - behind: number; - lastFetched: Date | null; - remoteBranch: string | null; - }; - tags: Record; - } | null> { - const repository = getPrimaryRepository(this.repositories); - if (!repository) { - return null; - } - - try { - const git = simpleGit(repository.localPath); - const [status, log, branch, remotes, tags] = await Promise.all([ - git.status(), - git.log({ maxCount: 50 }), - git.branch(), - git.getRemotes(true), - git.tags(), - ]); - - const changes: ChangeEntry[] = status.files.map((file) => ({ - path: file.path, - status: this.formatStatusCode(file.index, file.working_dir), - staged: file.index !== " " && file.index !== "?", - })); - - // Get tags for each commit - const tagMap = await this.getTagsForCommits(git); - - // Get remote tracking status to determine which commits are pushed - const remoteStatus = await this.getRemoteStatus( - git, - branch.current, - remotes, - ); - - const commits: CommitEntry[] = log.all.map((commit, index) => ({ - hash: commit.hash || "", - shortHash: (commit.hash || "").slice(0, 7), - message: commit.message || "", - author: commit.author_name || "Unknown", - email: commit.author_email || "", - authorName: commit.author_name || "", - authorEmail: commit.author_email || "", - relativeTime: this.formatRelativeTime(commit.date), - committedAt: commit.date ? String(commit.date) : "", - tags: tagMap[commit.hash || ""] || [], - // First 'ahead' commits are unpushed - isPushed: remoteStatus.isPublished - ? index >= remoteStatus.ahead - : false, - })); - - const hasMoreCommits = log.all.length === 50; - - // Get branch activity dates - const branchActivity: Record = {}; - for (const branchName of branch.all) { - try { - const lastCommit = await git.raw([ - "log", - "-1", - "--format=%cr", - branchName, - ]); - branchActivity[branchName] = lastCommit.trim(); - } catch (error) { - // If we can't get the date, skip it - } - } - - return { - changes, - commits, - hasMoreCommits, - branches: branch.all, - currentBranch: branch.current, - branchActivity, - repository: { - name: path.basename(repository.localPath), - path: repository.localPath, - remote: repository.remoteUrl, - }, - remoteStatus, - tags: tagMap, - }; - } catch (error) { - throw new Error(`Failed to load git data: ${error}`); - } - } - - async getCommitDetail(hash: string): Promise { - const repository = getPrimaryRepository(this.repositories); - if (!repository) { - throw new Error("No repository found"); - } - - try { - const git = simpleGit(repository.localPath); - const raw = await git.raw([ - "show", - hash, - "--numstat", - "--pretty=format:%H%n%an%n%ae%n%ad%n%s", - "--date=iso", - ]); - - const lines = raw.split("\n"); - const summaryHash = lines.shift() ?? hash; - const authorName = lines.shift() ?? ""; - const authorEmail = lines.shift() ?? ""; - const committedAt = lines.shift() ?? ""; - const message = lines.shift() ?? ""; - if (lines[0] === "") { - lines.shift(); - } - - const files: CommitDetail["files"] = []; - let additions = 0; - let deletions = 0; - - for (const line of lines) { - if (!line.trim()) continue; - const [addStr, delStr, filePath] = line.split("\t"); - if (!filePath) continue; - - const addVal = addStr === "-" ? null : Number.parseInt(addStr, 10); - const delVal = delStr === "-" ? null : Number.parseInt(delStr, 10); - - if (typeof addVal === "number" && !Number.isNaN(addVal)) { - additions += addVal; - } - if (typeof delVal === "number" && !Number.isNaN(delVal)) { - deletions += delVal; - } - - files.push({ - path: filePath, - status: "", - additions: addVal ?? null, - deletions: delVal ?? null, - }); - } - - // Get file status information - const statusRaw = await git.raw([ - "show", - hash, - "--name-status", - "--pretty=format:", - ]); - const statusMap = new Map(); - for (const line of statusRaw.split("\n")) { - if (!line.trim()) continue; - const [state, filePath] = line.split("\t"); - if (filePath) { - statusMap.set(filePath, state); - } - } - - // Update file status - for (const file of files) { - file.status = statusMap.get(file.path) ?? "M"; - } - - return { - summary: { - hash: summaryHash, - shortHash: summaryHash.slice(0, 7), - message, - authorName, - authorEmail, - committedAt, - relativeTime: this.formatRelativeTime(new Date(committedAt)), - additions, - deletions, - fileCount: files.length, - }, - files, - }; - } catch (error) { - throw new Error(`Failed to load commit details: ${error}`); - } - } - - async loadMoreCommits(offset: number): Promise<{ - commits: CommitEntry[]; - hasMoreCommits: boolean; - newOffset: number; - }> { - const repository = getPrimaryRepository(this.repositories); - if (!repository) { - throw new Error("No repository found"); - } - - try { - const git = simpleGit(repository.localPath); - const [logOutput, branch, remotes] = await Promise.all([ - git.raw([ - "log", - "--oneline", - "--format=%H%x09%an%x09%ae%x09%ad%x09%s", - "--date=iso", - "--max-count=50", - `--skip=${offset}`, - ]), - git.branch(), - git.getRemotes(true), - ]); - - // Get remote status to determine which commits are pushed - const remoteStatus = await this.getRemoteStatus( - git, - branch.current, - remotes, - ); - - const lines = logOutput - .trim() - .split("\n") - .filter((line) => line.trim()); - const commits: CommitEntry[] = lines.map((line, index) => { - const [hash, author, email, date, ...messageParts] = line.split("\t"); - const message = messageParts.join("\t"); - - return { - hash: hash || "", - shortHash: (hash || "").slice(0, 7), - message: message || "", - author: author || "Unknown", - email: email || "", - authorName: author || "", - authorEmail: email || "", - relativeTime: this.formatRelativeTime(this.parseGitDate(date)), - committedAt: date || "", - // Commits loaded after offset are likely pushed (since first 'ahead' commits from offset 0 are unpushed) - isPushed: remoteStatus.isPublished - ? offset + index >= remoteStatus.ahead - : false, - }; - }); - - const hasMoreCommits = commits.length === 50; - - return { - commits, - hasMoreCommits, - newOffset: offset + 50, - }; - } catch (error) { - throw new Error(`Failed to load more commits: ${error}`); - } - } - - private formatStatusCode(index: string, workingDir: string): string { - const combined = `${index ?? ""}${workingDir ?? ""}`.trim(); - return combined.length > 0 ? combined : "--"; - } - - private parseGitDate(dateInput: string | Date | any): Date { - // If it's already a Date object (from simple-git), return it - if (dateInput instanceof Date) { - return Number.isNaN(dateInput.getTime()) - ? new Date("1970-01-01") - : dateInput; - } - - // If it's not a string, convert to string - const dateString = String(dateInput || ""); - - if ( - !dateString || - dateString.trim() === "" || - dateString === "undefined" || - dateString === "null" - ) { - return new Date("1970-01-01"); // Return epoch as fallback - } - - // Try to parse the date string directly - const parsedDate = new Date(dateString); - if (!Number.isNaN(parsedDate.getTime())) { - return parsedDate; - } - - // If standard parsing fails, try to handle Git's ISO format - try { - // Git typically returns dates in ISO format like "2024-01-15 10:30:45 +0000" - const isoMatch = dateString.match( - /(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2})/, - ); - if (isoMatch) { - const [, datePart, timePart] = isoMatch; - return new Date(`${datePart}T${timePart}Z`); - } - - // Try Unix timestamp (if it's just numbers) - const timestamp = parseInt(dateString, 10); - if (!Number.isNaN(timestamp) && timestamp > 0) { - // If it looks like a Unix timestamp (seconds), convert to milliseconds - const date = new Date( - timestamp > 1000000000000 ? timestamp : timestamp * 1000, - ); - if (!Number.isNaN(date.getTime())) { - return date; - } - } - } catch (error) { - // Fall through to fallback - } - - // Return epoch as final fallback - return new Date("1970-01-01"); - } - - private async getTagsForCommits(git: any): Promise> { - const tagMap: Record = {}; - - try { - const tagList = await git.raw(["show-ref", "--tags"]); - const lines = tagList.split("\n").filter((line: string) => line.trim()); - - for (const line of lines) { - const [hash, ref] = line.split(" "); - if (!hash || !ref) continue; - - const tagName = ref.replace("refs/tags/", ""); - if (!tagMap[hash]) { - tagMap[hash] = []; - } - tagMap[hash].push(tagName); - } - - // Also get annotated tags pointing to commits - const annotatedTags = await git.raw([ - "for-each-ref", - "--format=%(objectname) %(refname:short) %(object)", - "refs/tags", - ]); - const annotatedLines = annotatedTags - .split("\n") - .filter((line: string) => line.trim()); - - for (const line of annotatedLines) { - const parts = line.split(" "); - if (parts.length >= 3) { - const [, tagName, commitHash] = parts; - if (commitHash && commitHash.length === 40) { - if (!tagMap[commitHash]) { - tagMap[commitHash] = []; - } - if (!tagMap[commitHash].includes(tagName)) { - tagMap[commitHash].push(tagName); - } - } - } - } - } catch (error) { - // Silently fail if tags cannot be retrieved - } - - return tagMap; - } - - private async getRemoteStatus( - git: any, - currentBranch: string | null, - remotes: any[], - ): Promise<{ - hasRemote: boolean; - isPublished: boolean; - ahead: number; - behind: number; - lastFetched: Date | null; - remoteBranch: string | null; - }> { - if (!currentBranch || remotes.length === 0) { - return { - hasRemote: false, - isPublished: false, - ahead: 0, - behind: 0, - lastFetched: null, - remoteBranch: null, - }; - } - - try { - // Check if current branch has a remote tracking branch - const trackingInfo = await git.raw(["branch", "-vv", "--no-color"]); - const lines = trackingInfo.split("\n"); - let remoteBranch: string | null = null; - let ahead = 0; - let behind = 0; - - for (const line of lines) { - if (line.trim().startsWith("*")) { - // Parse remote tracking info - const match = line.match(/\[([^\]]+)\]/); - if (match) { - const trackingPart = match[1]; - const remoteParts = trackingPart.split(":"); - remoteBranch = remoteParts[0].trim(); - - // Parse ahead/behind - const aheadMatch = trackingPart.match(/ahead (\d+)/); - const behindMatch = trackingPart.match(/behind (\d+)/); - - if (aheadMatch) { - ahead = parseInt(aheadMatch[1], 10); - } - if (behindMatch) { - behind = parseInt(behindMatch[1], 10); - } - } - break; - } - } - - // Get last fetch time from git reflog - let lastFetched: Date | null = null; - try { - // Try to get fetch time from reflog entries for origin - const fetchLog = await git.raw([ - "reflog", - "show", - "refs/remotes/origin/HEAD", - "--date=iso", - "-n", - "1", - "--format=%cd", - ]); - if (fetchLog.trim()) { - const fetchDate = new Date(fetchLog.trim()); - if (!isNaN(fetchDate.getTime())) { - lastFetched = fetchDate; - } - } - } catch { - // Fallback: Try to get from main/master branch reflog - try { - const branches = [remoteBranch, "main", "master"]; - for (const branch of branches) { - if (!branch) continue; - try { - const branchLog = await git.raw([ - "reflog", - "show", - `refs/remotes/origin/${branch.replace('origin/', '')}`, - "--date=iso", - "-n", - "1", - "--format=%cd", - ]); - if (branchLog.trim()) { - const branchDate = new Date(branchLog.trim()); - if (!isNaN(branchDate.getTime())) { - lastFetched = branchDate; - break; - } - } - } catch { - // Continue to next branch - } - } - } catch { - // Final fallback: check FETCH_HEAD file modification time - try { - const fs = require("fs"); - const fetchHeadPath = path.join(git.gitDir || ".git", "FETCH_HEAD"); - const stats = fs.statSync(fetchHeadPath); - // Only use FETCH_HEAD if it's recent (within last 30 days) - const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000); - if (stats.mtime.getTime() > thirtyDaysAgo) { - lastFetched = stats.mtime; - } - } catch { - // Ignore if FETCH_HEAD doesn't exist or is inaccessible - } - } - } - - return { - hasRemote: remotes.length > 0, - isPublished: remoteBranch !== null, - ahead, - behind, - lastFetched, - remoteBranch, - }; - } catch (error) { - return { - hasRemote: remotes.length > 0, - isPublished: false, - ahead: 0, - behind: 0, - lastFetched: null, - remoteBranch: null, - }; - } - } - - private formatRelativeTime(date: Date | string | any): string { - let actualDate: Date; - - if (date instanceof Date) { - actualDate = date; - } else { - actualDate = this.parseGitDate(date); - } - - if (Number.isNaN(actualDate.getTime())) { - return "Unknown date"; - } - - // Use simple relative time calculation instead of dayjs - const diff = Date.now() - actualDate.getTime(); - const seconds = Math.round(diff / 1000); - if (seconds < 60) { - return "just now"; - } - const minutes = Math.round(seconds / 60); - if (minutes < 60) { - return `${minutes} minute${minutes === 1 ? "" : "s"} ago`; - } - const hours = Math.round(minutes / 60); - if (hours < 24) { - return `${hours} hour${hours === 1 ? "" : "s"} ago`; - } - const days = Math.round(hours / 24); - if (days < 30) { - return `${days} day${days === 1 ? "" : "s"} ago`; - } - const months = Math.round(days / 30); - if (months < 12) { - return `${months} month${months === 1 ? "" : "s"} ago`; - } - const years = Math.round(months / 12); - return `${years} year${years === 1 ? "" : "s"} ago`; - } -} diff --git a/src/webviews/timeline/services/message-handler.service.ts b/src/webviews/timeline/services/message-handler.service.ts deleted file mode 100644 index 0792798..0000000 --- a/src/webviews/timeline/services/message-handler.service.ts +++ /dev/null @@ -1,844 +0,0 @@ -import * as path from "path"; -import * as vscode from "vscode"; -import simpleGit from "simple-git"; -import { RepositoryManager } from "../../../core/repositories/repository-manager"; -import { AccountManager } from "../../../core/accounts/account-manager"; -import { getPrimaryRepository } from "../../../shared/utils/repo-selection"; -import { CommitDetailViewProvider } from "../../commitDetail/commit-detail-view-provider"; -import { - WebviewMessage, - FileIconInfo, -} from "../interfaces/timeline-view-provider.interface"; -import { GitOperationsService } from "./git-operations.service"; - -export class MessageHandlerService { - constructor( - private readonly context: vscode.ExtensionContext, - private readonly repositories: RepositoryManager, - private readonly accounts: AccountManager, - private readonly commitDetailProvider: CommitDetailViewProvider, - private readonly gitService: GitOperationsService, - private readonly view: vscode.WebviewView, - ) {} - - async handleMessage(message: WebviewMessage): Promise { - switch (message.command) { - case "ready": - case "refresh": - await this.handleRefresh(); - break; - case "stageFiles": - await this.handleStageFiles(message.files); - break; - case "unstageFiles": - await this.handleUnstageFiles(message.files); - break; - case "commit": - await this.handleCommit(message.message); - break; - case "push": - await this.handlePush(); - break; - case "pull": - await this.handlePull(); - break; - case "checkoutBranch": - await this.handleCheckoutBranch(message.branch); - break; - case "mergeBranch": - await this.handleMergeBranch(message.fromBranch, message.toBranch); - break; - case "createPullRequest": - await this.handleCreatePullRequest(message.branch); - break; - case "getCommitDetails": - await this.handleGetCommitDetails(message.hash); - break; - case "getFileDiff": - await this.handleGetFileDiff(message.hash, message.filePath); - break; - case "openCommitDetail": - if (typeof message.hash === "string") { - await this.commitDetailProvider.showCommitDetails(message.hash); - } - break; - case "resetToCommit": - if (typeof message.hash === "string") { - await this.handleResetToCommit(message.hash); - } - break; - case "checkoutCommit": - if (typeof message.hash === "string") { - await this.handleCheckoutCommit(message.hash); - } - break; - case "revertCommit": - if (typeof message.hash === "string") { - await this.handleRevertCommit(message.hash); - } - break; - case "createBranchFromCommit": - if (typeof message.hash === "string") { - await this.handleCreateBranchFromCommit(message.hash); - } - break; - case "createTagFromCommit": - if (typeof message.hash === "string") { - await this.handleCreateTagFromCommit(message.hash); - } - break; - case "cherryPickCommit": - if (typeof message.hash === "string") { - await this.handleCherryPickCommit(message.hash); - } - break; - case "viewCommitOnGitHub": - if (typeof message.hash === "string") { - await this.handleViewCommitOnGitHub(message.hash); - } - break; - case "createBranch": - if (typeof message.branchName === "string") { - await this.handleCreateBranch(message.branchName); - } - break; - case "createBranchWithChanges": - if ( - typeof message.branchName === "string" && - typeof message.bringChanges === "boolean" - ) { - await this.handleCreateBranchWithChanges( - message.branchName, - message.bringChanges, - ); - } - break; - case "loadMoreCommits": - if (typeof message.offset === "number") { - await this.handleLoadMoreCommits(message.offset); - } - break; - case "fetch": - await this.handleFetch(); - break; - case "publish": - await this.handlePublish(); - break; - case "selectCommit": - if (typeof message.hash === "string") { - await this.handleSelectCommit(message.hash); - } - break; - case "selectFile": - if ( - typeof message.hash === "string" && - typeof message.path === "string" - ) { - await this.handleSelectFile(message.hash, message.path); - } - break; - default: - break; - } - } - - private async configureGitWithAuth( - repositoryPath: string, - ): Promise { - const activeAccount = this.accounts.getActiveAccount(); - if (!activeAccount) { - return null; - } - - const token = await this.accounts.getToken(activeAccount.id); - if (!token) { - return null; - } - - const git = simpleGit(repositoryPath); - - // Configure git with authentication for GitHub operations - try { - // Get the repository info to determine the remote URL format - const remotes = await git.getRemotes(true); - const origin = remotes.find((remote) => remote.name === "origin"); - - if (origin && origin.refs.fetch) { - let remoteUrl = origin.refs.fetch; - - // If it's a GitHub HTTPS URL, update it with the token - if (remoteUrl.includes("github.com")) { - // Convert SSH to HTTPS if necessary - if (remoteUrl.startsWith("git@github.com:")) { - remoteUrl = remoteUrl.replace( - "git@github.com:", - "https://github.com/", - ); - if (!remoteUrl.endsWith(".git")) { - remoteUrl += ".git"; - } - } - - // Add authentication to HTTPS URL - if (remoteUrl.startsWith("https://github.com/")) { - // For GitHub tokens, use the token as the password with any username (or 'x-access-token') - // GitHub accepts both formats: username:token or x-access-token:token - const urlWithAuth = remoteUrl.replace( - "https://github.com/", - `https://x-access-token:${encodeURIComponent(token)}@github.com/`, - ); - - console.log( - "Configuring auth for:", - activeAccount.login, - "on repo:", - remoteUrl, - ); - - // Temporarily update the remote URL for this operation - await git.remote(["set-url", "origin", urlWithAuth]); - - // Store original URL for cleanup - const originalUrl = remoteUrl; - - // Create a wrapper that ensures URL cleanup after operations - const gitWithAuth = Object.create(git); - - // Override push method - gitWithAuth.push = async function (...args: any[]) { - try { - const result = await git.push(...args); - // Restore original URL after successful push - await git.remote(["set-url", "origin", originalUrl]); - return result; - } catch (error) { - // Restore original URL even on error - await git.remote(["set-url", "origin", originalUrl]); - throw error; - } - }; - - // Override pull method - gitWithAuth.pull = async function (...args: any[]) { - try { - const result = await git.pull(...args); - // Restore original URL after successful pull - await git.remote(["set-url", "origin", originalUrl]); - return result; - } catch (error) { - // Restore original URL even on error - await git.remote(["set-url", "origin", originalUrl]); - throw error; - } - }; - - // Override fetch method - gitWithAuth.fetch = async function (...args: any[]) { - try { - const result = await git.fetch(...args); - // Restore original URL after successful fetch - await git.remote(["set-url", "origin", originalUrl]); - return result; - } catch (error) { - // Restore original URL even on error - await git.remote(["set-url", "origin", originalUrl]); - throw error; - } - }; - - return gitWithAuth; - } - } - } - } catch (error) { - console.error("Failed to configure git with auth:", error); - } - - // Return regular git instance if no special auth needed - return git; - } - - private async handleRefresh(): Promise { - try { - const data = await this.gitService.getRepositoryData(); - if (!data) { - this.view.webview.postMessage({ - command: "updateRepository", - repository: null, - }); - return; - } - - this.view.webview.postMessage({ - command: "updateChanges", - changes: data.changes, - }); - - this.view.webview.postMessage({ - command: "updateHistory", - history: data.commits, - hasMoreCommits: data.hasMoreCommits, - offset: 0, - }); - - this.view.webview.postMessage({ - command: "updateBranches", - branches: data.branches, - currentBranch: data.currentBranch, - branchActivity: data.branchActivity, - }); - - this.view.webview.postMessage({ - command: "updateRepository", - repository: data.repository, - }); - - this.view.webview.postMessage({ - command: "updateRemoteStatus", - remoteStatus: data.remoteStatus, - tags: data.tags, - }); - } catch (error) { - vscode.window.showErrorMessage(`Failed to refresh: ${error}`); - } - } - - private async handleStageFiles(files: string[]): Promise { - const repository = getPrimaryRepository(this.repositories); - if (!repository || !files?.length) return; - - try { - const git = simpleGit(repository.localPath); - await git.add(files); - await this.handleRefresh(); - } catch (error) { - vscode.window.showErrorMessage(`Failed to stage files: ${error}`); - } - } - - private async handleUnstageFiles(files: string[]): Promise { - const repository = getPrimaryRepository(this.repositories); - if (!repository || !files?.length) return; - - try { - const git = simpleGit(repository.localPath); - await git.reset(["HEAD", ...files]); - await this.handleRefresh(); - } catch (error) { - vscode.window.showErrorMessage(`Failed to unstage files: ${error}`); - } - } - - private async handleCommit(message: string): Promise { - const repository = getPrimaryRepository(this.repositories); - if (!repository || !message?.trim()) return; - - try { - const git = simpleGit(repository.localPath); - await git.commit(message); - await this.handleRefresh(); - vscode.window.showInformationMessage("Commit created successfully"); - } catch (error) { - vscode.window.showErrorMessage(`Failed to commit: ${error}`); - } - } - - private async handlePush(): Promise { - const repository = getPrimaryRepository(this.repositories); - if (!repository) { - vscode.window.showErrorMessage( - "No repository found in the current workspace", - ); - return; - } - - try { - // Log repository info for debugging - console.log( - "Pushing to repository:", - repository.name, - "at", - repository.localPath, - ); - - const git = await this.configureGitWithAuth(repository.localPath); - if (!git) { - vscode.window.showErrorMessage( - "No authenticated GitHub account found. Please sign in first.", - ); - return; - } - - await git.push(); - await this.handleRefresh(); // Refresh all data including remote status - vscode.window.showInformationMessage("Pushed successfully"); - } catch (error: any) { - console.error("Push failed with error:", error); - vscode.window.showErrorMessage( - `Failed to push: ${error.message || error}`, - ); - } - } - - private async handlePull(): Promise { - const repository = getPrimaryRepository(this.repositories); - if (!repository) return; - - try { - const git = await this.configureGitWithAuth(repository.localPath); - if (!git) { - vscode.window.showErrorMessage( - "No authenticated GitHub account found. Please sign in first.", - ); - return; - } - - await git.pull(); - await this.handleRefresh(); - vscode.window.showInformationMessage("Pulled successfully"); - } catch (error) { - vscode.window.showErrorMessage(`Failed to pull: ${error}`); - } - } - - private async handleCheckoutBranch(branchName: string): Promise { - const repository = getPrimaryRepository(this.repositories); - if (!repository || !branchName) return; - - try { - const git = simpleGit(repository.localPath); - await git.checkout(branchName); - await this.handleRefresh(); - vscode.window.showInformationMessage(`Checked out branch: ${branchName}`); - } catch (error) { - vscode.window.showErrorMessage(`Failed to checkout branch: ${error}`); - } - } - - private async handleMergeBranch( - fromBranch: string, - toBranch: string, - ): Promise { - const repository = getPrimaryRepository(this.repositories); - if (!repository || !fromBranch || !toBranch) return; - - try { - const git = simpleGit(repository.localPath); - await git.checkout(toBranch); - await git.merge([fromBranch]); - await this.handleRefresh(); - vscode.window.showInformationMessage( - `Successfully merged ${fromBranch} into ${toBranch}`, - ); - } catch (error) { - vscode.window.showErrorMessage(`Failed to merge branches: ${error}`); - } - } - - private async handleCreatePullRequest(branchName: string): Promise { - const repository = getPrimaryRepository(this.repositories); - if (!repository || !branchName) return; - - try { - const repoUrl = repository.remoteUrl; - if (repoUrl) { - const githubUrl = repoUrl - .replace(/^git@github\.com:/, "https://github.com/") - .replace(/\.git$/, ""); - - const prUrl = `${githubUrl}/compare/${branchName}?expand=1`; - vscode.env.openExternal(vscode.Uri.parse(prUrl)); - vscode.window.showInformationMessage( - `Opening pull request creation for ${branchName}`, - ); - } else { - vscode.window.showWarningMessage("No remote repository configured"); - } - } catch (error) { - vscode.window.showErrorMessage(`Failed to create pull request: ${error}`); - } - } - - private async handleGetCommitDetails(hash: string): Promise { - try { - const detail = await this.gitService.getCommitDetail(hash); - this.view.webview.postMessage({ - type: "commitDetail", - payload: detail, - }); - } catch (error) { - this.view.webview.postMessage({ - type: "error", - message: - error instanceof Error - ? error.message - : "Failed to load commit details.", - }); - } - } - - private async handleGetFileDiff( - hash: string, - filePath: string, - ): Promise { - const repository = getPrimaryRepository(this.repositories); - if (!repository) return; - - try { - const git = simpleGit(repository.localPath); - let diff = await git.raw(["show", hash, "--patch", "--", filePath]); - if (!diff.trim()) { - diff = await git.raw(["show", hash, "--", filePath]); - } - - this.view.webview.postMessage({ - type: "fileDiff", - payload: { - path: filePath, - diff, - }, - }); - } catch (error) { - this.view.webview.postMessage({ - type: "error", - message: - error instanceof Error ? error.message : "Failed to load file diff.", - }); - } - } - - private async handleResetToCommit(hash: string): Promise { - const repository = getPrimaryRepository(this.repositories); - if (!repository) return; - - const confirm = await vscode.window.showWarningMessage( - `Are you sure you want to reset to commit ${hash.substring(0, 7)}? This will discard all changes after this commit.`, - { modal: true }, - "Reset", - "Cancel", - ); - - if (confirm === "Reset") { - try { - const git = simpleGit(repository.localPath); - await git.reset(["--hard", hash]); - vscode.window.showInformationMessage( - `Reset to commit ${hash.substring(0, 7)}`, - ); - await this.handleRefresh(); - } catch (error) { - vscode.window.showErrorMessage(`Failed to reset: ${error}`); - } - } - } - - private async handleCheckoutCommit(hash: string): Promise { - const repository = getPrimaryRepository(this.repositories); - if (!repository) return; - - try { - const git = simpleGit(repository.localPath); - await git.checkout(hash); - vscode.window.showInformationMessage( - `Checked out commit ${hash.substring(0, 7)}`, - ); - await this.handleRefresh(); - } catch (error) { - vscode.window.showErrorMessage(`Failed to checkout commit: ${error}`); - } - } - - private async handleRevertCommit(hash: string): Promise { - const repository = getPrimaryRepository(this.repositories); - if (!repository) return; - - try { - const git = simpleGit(repository.localPath); - await git.revert(hash, ["--no-edit"]); - vscode.window.showInformationMessage( - `Reverted commit ${hash.substring(0, 7)}`, - ); - await this.handleRefresh(); - } catch (error) { - vscode.window.showErrorMessage(`Failed to revert commit: ${error}`); - } - } - - private async handleCreateBranchFromCommit(hash: string): Promise { - const branchName = await vscode.window.showInputBox({ - prompt: "Enter new branch name", - placeHolder: "feature-branch-name", - ignoreFocusOut: true, - }); - - if (!branchName) return; - - const repository = getPrimaryRepository(this.repositories); - if (!repository) return; - - try { - const git = simpleGit(repository.localPath); - await git.checkoutBranch(branchName, hash); - vscode.window.showInformationMessage( - `Created branch '${branchName}' from commit ${hash.substring(0, 7)}`, - ); - await this.handleRefresh(); - } catch (error) { - vscode.window.showErrorMessage(`Failed to create branch: ${error}`); - } - } - - private async handleCreateTagFromCommit(hash: string): Promise { - const tagName = await vscode.window.showInputBox({ - prompt: "Enter tag name (e.g., v1.0.0)", - placeHolder: "v1.0.0", - ignoreFocusOut: true, - validateInput: (value) => { - if (!value) return "Tag name is required"; - if (!/^[a-zA-Z0-9._-]+$/.test(value)) - return "Tag name contains invalid characters"; - return null; - }, - }); - - if (!tagName) return; - - const repository = getPrimaryRepository(this.repositories); - if (!repository) return; - - try { - const git = await this.configureGitWithAuth(repository.localPath); - if (!git) { - vscode.window.showErrorMessage( - "No authenticated GitHub account found. Please sign in first.", - ); - return; - } - - // Create annotated tag with default message - await git.raw(["tag", "-a", tagName, hash, "-m", `Release ${tagName}`]); - - // Push the tag to remote - await git.pushTags("origin"); - - vscode.window.showInformationMessage( - `Created and pushed tag '${tagName}' to GitHub`, - ); - } catch (error) { - vscode.window.showErrorMessage(`Failed to create tag: ${error}`); - } - } - - private async handleCherryPickCommit(hash: string): Promise { - const repository = getPrimaryRepository(this.repositories); - if (!repository) return; - - try { - const git = simpleGit(repository.localPath); - await git.raw(["cherry-pick", hash]); - vscode.window.showInformationMessage( - `Cherry-picked commit ${hash.substring(0, 7)}`, - ); - await this.handleRefresh(); - } catch (error) { - vscode.window.showErrorMessage(`Failed to cherry-pick commit: ${error}`); - } - } - - private async handleViewCommitOnGitHub(hash: string): Promise { - const repository = getPrimaryRepository(this.repositories); - if (!repository?.remoteUrl) { - vscode.window.showWarningMessage("No GitHub remote found"); - return; - } - - try { - const githubUrl = repository.remoteUrl - .replace(/^git@github\.com:/, "https://github.com/") - .replace(/\.git$/, ""); - - const commitUrl = `${githubUrl}/commit/${hash}`; - vscode.env.openExternal(vscode.Uri.parse(commitUrl)); - } catch (error) { - vscode.window.showErrorMessage(`Failed to open GitHub: ${error}`); - } - } - - private async handleCreateBranch(branchName: string): Promise { - const repository = getPrimaryRepository(this.repositories); - if (!repository || !branchName) return; - - try { - const git = simpleGit(repository.localPath); - await git.checkoutLocalBranch(branchName); - vscode.window.showInformationMessage( - `Created and switched to branch '${branchName}'`, - ); - await this.handleRefresh(); - } catch (error) { - vscode.window.showErrorMessage(`Failed to create branch: ${error}`); - } - } - - private async handleCreateBranchWithChanges( - branchName: string, - bringChanges: boolean, - ): Promise { - const repository = getPrimaryRepository(this.repositories); - if (!repository || !branchName) return; - - try { - const git = simpleGit(repository.localPath); - - if (bringChanges) { - await git.checkoutLocalBranch(branchName); - vscode.window.showInformationMessage( - `Created branch '${branchName}' with uncommitted changes`, - ); - } else { - await git.stash(["push", "-m", "Auto-stash before branch creation"]); - await git.checkoutLocalBranch(branchName); - vscode.window.showInformationMessage( - `Created branch '${branchName}' and stashed changes`, - ); - } - - await this.handleRefresh(); - } catch (error) { - vscode.window.showErrorMessage( - `Failed to create branch with changes: ${error}`, - ); - } - } - - private async handleLoadMoreCommits(offset: number): Promise { - try { - const result = await this.gitService.loadMoreCommits(offset); - this.view.webview.postMessage({ - command: "loadMoreCommitsResponse", - history: result.commits, - hasMoreCommits: result.hasMoreCommits, - offset: result.newOffset, - }); - } catch (error) { - vscode.window.showErrorMessage(`Failed to load more commits: ${error}`); - } - } - - private async handleSelectCommit(hash: string): Promise { - try { - const detail = await this.gitService.getCommitDetail(hash); - this.view.webview.postMessage({ - type: "commitDetail", - payload: detail, - }); - } catch (error) { - this.view.webview.postMessage({ - type: "error", - message: - error instanceof Error - ? error.message - : "Failed to load commit details.", - }); - } - } - - private async handleSelectFile( - hash: string, - filePath: string, - ): Promise { - const repository = getPrimaryRepository(this.repositories); - if (!repository) return; - - try { - const git = simpleGit(repository.localPath); - let diff = await git.raw(["show", hash, "--patch", "--", filePath]); - if (!diff.trim()) { - diff = await git.raw(["show", hash, "--", filePath]); - } - - this.view.webview.postMessage({ - type: "fileDiff", - payload: { - path: filePath, - diff, - }, - }); - } catch (error) { - this.view.webview.postMessage({ - type: "error", - message: - error instanceof Error ? error.message : "Failed to load file diff.", - }); - } - } - - private async handleFetch(): Promise { - const repository = getPrimaryRepository(this.repositories); - if (!repository) { - vscode.window.showErrorMessage( - "No repository found in the current workspace", - ); - return; - } - - try { - // Log repository info for debugging - console.log( - "Fetching from repository:", - repository.name, - "at", - repository.localPath, - ); - - const git = await this.configureGitWithAuth(repository.localPath); - if (!git) { - vscode.window.showErrorMessage( - "No authenticated GitHub account found. Please sign in first.", - ); - return; - } - - await git.fetch(); - vscode.window.showInformationMessage("Fetched from remote successfully"); - await this.handleRefresh(); - } catch (error: any) { - console.error("Fetch failed with error:", error); - vscode.window.showErrorMessage( - `Failed to fetch: ${error.message || error}`, - ); - } - } - - private async handlePublish(): Promise { - const repository = getPrimaryRepository(this.repositories); - if (!repository) return; - - try { - const git = simpleGit(repository.localPath); - const branch = await git.branch(); - const currentBranch = branch.current; - - if (!currentBranch) { - vscode.window.showErrorMessage("No branch selected"); - return; - } - - // Check if we have a remote - const remotes = await git.getRemotes(true); - if (remotes.length === 0) { - vscode.window.showErrorMessage("No remote repository configured"); - return; - } - - const defaultRemote = remotes[0].name; - await git.push(["-u", defaultRemote, currentBranch]); - vscode.window.showInformationMessage( - `Published branch '${currentBranch}' to ${defaultRemote}`, - ); - await this.handleRefresh(); - } catch (error) { - vscode.window.showErrorMessage(`Failed to publish branch: ${error}`); - } - } -} diff --git a/src/webviews/timeline/services/pull-request.service.ts b/src/webviews/timeline/services/pull-request.service.ts new file mode 100644 index 0000000..fb93e49 --- /dev/null +++ b/src/webviews/timeline/services/pull-request.service.ts @@ -0,0 +1,99 @@ +import { GitClientFactory } from "../../../core/git/git-authenticator"; +import { toGitHubWebUrl } from "../../../shared/utils/github-url"; +import { + Browser, + GitHubApi, + Notifier, + Refresher, + RepositoryContext, + WebviewChannel, +} from "../ports"; +import { RepositoryScopedService } from "./repository-scoped.service"; + +/** + * @description Pull request surface for the branch dropdown: lists open PRs + * through the GitHub API and checks one out locally. Creating a PR still hands + * off to the browser, matching GitHub Desktop. + */ +export class PullRequestService extends RepositoryScopedService { + constructor( + repos: RepositoryContext, + notifier: Notifier, + private readonly browser: Browser, + private readonly git: GitClientFactory, + private readonly channel: WebviewChannel, + private readonly api: GitHubApi, + private readonly refresher: Refresher, + ) { + super(repos, notifier); + } + + openCompare(branch: string): Promise { + return this.withRepo(async (repo) => { + const base = toGitHubWebUrl(repo.remoteUrl); + if (!base) { + this.notifier.warn("No GitHub remote configured"); + return; + } + this.browser.open(`${base}/compare/${branch}?expand=1`); + }); + } + + /** Opens the repository's GitHub page in the browser. */ + openRepo(): Promise { + return this.withRepo(async (repo) => { + const base = toGitHubWebUrl(repo.remoteUrl); + if (!base) { + this.notifier.warn("No GitHub remote configured"); + return; + } + this.browser.open(base); + }); + } + + /** Fetches open PRs and pushes them to the webview. */ + list(): Promise { + return this.withRepo(async (repo) => { + try { + const pullRequests = await this.api.listPullRequests(repo); + this.channel.post({ command: "updatePullRequests", pullRequests }); + } catch (error) { + this.channel.post({ + command: "updatePullRequests", + pullRequests: [], + error: error instanceof Error ? error.message : String(error), + }); + } + }); + } + + /** + * Checks out PR #`number` into a local `pr/` branch. + * + * Uses `git fetch origin pull//head:pr/`, which GitHub serves for both + * same-repo and fork PRs, so no fork remote juggling is needed. + */ + checkout(prNumber: number): Promise { + return this.withRepo(async (repo) => { + if (!Number.isInteger(prNumber) || prNumber <= 0) { + return; + } + const local = `pr/${prNumber}`; + try { + await this.git.withAuth(repo.localPath, (git) => + git.fetch(["origin", `pull/${prNumber}/head:${local}`]), + ); + } catch (error) { + this.notifier.error( + `Could not fetch PR #${prNumber}: ${ + error instanceof Error ? error.message : String(error) + }`, + ); + return; + } + await this.git.plain(repo.localPath).checkout(local); + this.notifier.info(`Checked out PR #${prNumber} as ${local}`); + await this.refresher.refresh(); + }); + } +} diff --git a/src/webviews/timeline/services/repository-data.service.ts b/src/webviews/timeline/services/repository-data.service.ts new file mode 100644 index 0000000..0e02dfc --- /dev/null +++ b/src/webviews/timeline/services/repository-data.service.ts @@ -0,0 +1,503 @@ +import * as path from "path"; +import { GitClientFactory } from "../../../core/git/git-authenticator"; +import { RepositoryContext } from "../ports"; +import { + COMMIT_LOG_FORMAT, + formatRelativeTime, + parseCommitLogLine, +} from "./commit-log"; +import { detectInProgressOperation } from "./git-operation-state"; +import { + ChangeEntry, + CommitEntry, + CommitDetail, +} from "../interfaces/timeline-view-provider.interface"; +import { + COMMITS_PAGE_SIZE, + InProgressOperation, + RemoteStatus, + RepositorySnapshot, + StashEntry, +} from "../interfaces/repository-snapshot"; + +/** + * @description Read model for the timeline. Given the active repository it + * assembles a {@link RepositorySnapshot} (changes, history, branches, remote + * status, tags) in one pass, and answers per-commit detail lookups. + * + * Purely read-only: it never mutates the repository. + */ +export class RepositoryDataService { + constructor( + private readonly repos: RepositoryContext, + private readonly git: GitClientFactory, + ) {} + + /** + * Builds a full snapshot for the current repository. + * + * @returns The snapshot, or `null` when no repository is open + */ + async getSnapshot(): Promise { + const repo = this.repos.getPrimary(); + if (!repo) { + return null; + } + + const git = this.git.plain(repo.localPath); + const [status, log, branch, remotes] = await Promise.all([ + git.status(), + git.log({ maxCount: COMMITS_PAGE_SIZE }), + git.branch(), + git.getRemotes(true), + ]); + + const tags = await this.getTagsForCommits(git); + const remoteStatus = await this.getRemoteStatus( + git, + branch.current, + remotes, + ); + + const changes: ChangeEntry[] = status.files.map((file) => ({ + path: file.path, + status: `${file.index ?? ""}${file.working_dir ?? ""}`.trim() || "--", + staged: file.index !== " " && file.index !== "?", + })); + + const commits: CommitEntry[] = log.all.map((commit, index) => + this.toCommitEntry(commit, index, remoteStatus), + ); + + const branchList = await this.listBranches(git, branch.all, branch.current); + const operation = await this.detectOperation(git); + const canUndo = operation === null && (await this.hasParentCommit(git)); + + return { + changes, + commits, + hasMoreCommits: log.all.length === COMMITS_PAGE_SIZE, + branches: branchList, + currentBranch: branch.current || null, + branchActivity: await this.getBranchActivity(git), + repository: { + name: path.basename(repo.localPath), + path: repo.localPath, + remote: repo.remoteUrl, + }, + remoteStatus, + tags, + operation, + conflicted: status.conflicted, + canUndo, + stashes: await this.getStashes(git), + }; + } + + /** + * Loads a further page of history. + * + * @param offset - Number of commits already shown + * @param remoteStatus - Reused from the last snapshot so ahead/behind need + * not be recomputed + */ + async loadMoreCommits( + offset: number, + remoteStatus: RemoteStatus, + ): Promise<{ + commits: CommitEntry[]; + hasMoreCommits: boolean; + offset: number; + }> { + const repo = this.repos.getPrimary(); + if (!repo) { + return { commits: [], hasMoreCommits: false, offset }; + } + + const raw = await this.git + .plain(repo.localPath) + .raw([ + "log", + `--format=${COMMIT_LOG_FORMAT}`, + "--date=iso", + `--max-count=${COMMITS_PAGE_SIZE}`, + `--skip=${offset}`, + ]); + + const commits = raw + .split("\n") + .filter((line) => line.trim()) + .map((line, index) => ({ + ...parseCommitLogLine(line), + isPushed: remoteStatus.isPublished + ? offset + index >= remoteStatus.ahead + : false, + })); + + return { + commits, + hasMoreCommits: commits.length === COMMITS_PAGE_SIZE, + offset: offset + COMMITS_PAGE_SIZE, + }; + } + + /** + * Aggregate detail (stats + per-file status) for a single commit. + * + * @throws When the commit cannot be read + */ + async getCommitDetail(hash: string): Promise { + const repo = this.repos.getPrimary(); + if (!repo) { + throw new Error("No repository open"); + } + const git = this.git.plain(repo.localPath); + + const raw = await git.raw([ + "show", + hash, + "--numstat", + "--pretty=format:%H%n%an%n%ae%n%ad%n%s", + "--date=iso", + ]); + const lines = raw.split("\n"); + const summaryHash = lines.shift() ?? hash; + const authorName = lines.shift() ?? ""; + const authorEmail = lines.shift() ?? ""; + const committedAt = lines.shift() ?? ""; + const message = lines.shift() ?? ""; + if (lines[0] === "") { + lines.shift(); + } + + const files: CommitDetail["files"] = []; + let additions = 0; + let deletions = 0; + for (const line of lines) { + if (!line.trim()) { + continue; + } + const [addStr, delStr, filePath] = line.split("\t"); + if (!filePath) { + continue; + } + const add = addStr === "-" ? null : Number.parseInt(addStr, 10); + const del = delStr === "-" ? null : Number.parseInt(delStr, 10); + if (typeof add === "number" && !Number.isNaN(add)) { + additions += add; + } + if (typeof del === "number" && !Number.isNaN(del)) { + deletions += del; + } + files.push({ + path: filePath, + status: "M", + additions: add, + deletions: del, + }); + } + + const statusRaw = await git.raw([ + "show", + hash, + "--name-status", + "--pretty=format:", + ]); + const statusMap = new Map(); + for (const line of statusRaw.split("\n")) { + const [state, filePath] = line.split("\t"); + if (filePath) { + statusMap.set(filePath, state); + } + } + for (const file of files) { + file.status = statusMap.get(file.path) ?? "M"; + } + + return { + summary: { + hash: summaryHash, + shortHash: summaryHash.slice(0, 7), + message, + authorName, + authorEmail, + relativeTime: formatRelativeTime(committedAt), + committedAt, + additions, + deletions, + fileCount: files.length, + }, + files, + }; + } + + /* ---------------- internals ---------------- */ + + private detectOperation( + git: ReturnType, + ): Promise { + return detectInProgressOperation(git); + } + + private async hasParentCommit( + git: ReturnType, + ): Promise { + try { + await git.raw(["rev-parse", "--verify", "-q", "HEAD^"]); + return true; + } catch { + return false; + } + } + + private async getStashes( + git: ReturnType, + ): Promise { + try { + const raw = await git.raw(["stash", "list", "--format=%gd%x09%gs"]); + return raw + .split("\n") + .filter((l) => l.trim()) + .map((line, index) => { + const [, subject = ""] = line.split("\t"); + const onBranch = subject.match(/^(?:WIP on|On) ([^:]+):/); + return { + index, + message: subject.replace(/^(?:WIP on|On) [^:]+:\s*/, "") || subject, + branch: onBranch ? onBranch[1] : null, + }; + }); + } catch { + return []; + } + } + + private toCommitEntry( + commit: { + hash?: string; + author_name?: string; + author_email?: string; + date?: string; + message?: string; + }, + index: number, + remoteStatus: RemoteStatus, + ): CommitEntry { + const hash = commit.hash ?? ""; + return { + hash, + shortHash: hash.slice(0, 7), + message: commit.message ?? "", + authorName: commit.author_name ?? "", + authorEmail: commit.author_email ?? "", + relativeTime: formatRelativeTime(commit.date ?? ""), + committedAt: commit.date ?? "", + isPushed: remoteStatus.isPublished ? index >= remoteStatus.ahead : false, + }; + } + + /** + * Local branches ordered most-recently-used first (current branch, then + * branches by last `git checkout` from the reflog, then the rest by last + * commit date), followed by remote-only branches. Never exposes + * `remotes//` refs — checking one out detaches HEAD. + */ + private async listBranches( + git: ReturnType, + all: string[], + current: string | undefined, + ): Promise { + const local = all.filter((b) => !b.startsWith("remotes/")); + const localSet = new Set(local); + const remoteOnly: string[] = []; + for (const b of all) { + const m = b.match(/^remotes\/[^/]+\/(.+)$/); + if ( + m && + m[1] !== "HEAD" && + !localSet.has(m[1]) && + !remoteOnly.includes(m[1]) + ) { + remoteOnly.push(m[1]); + } + } + + // Base order: last commit date, newest first. + let ordered = local; + try { + const raw = await git.raw([ + "for-each-ref", + "--sort=-committerdate", + "--format=%(refname:short)", + "refs/heads", + ]); + const byDate = raw + .split("\n") + .map((s) => s.trim()) + .filter(Boolean); + ordered = byDate.filter((b) => localSet.has(b)); + for (const b of local) { + if (!ordered.includes(b)) { + ordered.push(b); + } + } + } catch { + // keep git.branch() order + } + + // Bump branches to the front in reflog checkout order (true MRU). + try { + const reflog = await git.raw(["reflog", "--format=%gs", "-n", "400"]); + const mru: string[] = []; + for (const line of reflog.split("\n")) { + const m = line.match(/^checkout: moving from \S+ to (\S+)$/); + if (m && localSet.has(m[1]) && !mru.includes(m[1])) { + mru.push(m[1]); + } + } + ordered = [...mru, ...ordered.filter((b) => !mru.includes(b))]; + } catch { + // reflog unavailable — date order stands + } + + if (current && localSet.has(current)) { + ordered = [current, ...ordered.filter((b) => b !== current)]; + } + return [...ordered, ...remoteOnly]; + } + + /** One `for-each-ref` call instead of one `git log` per branch. */ + private async getBranchActivity( + git: ReturnType, + ): Promise> { + const activity: Record = {}; + try { + const raw = await git.raw([ + "for-each-ref", + "--format=%(refname:short)%09%(committerdate:relative)", + "refs/heads", + ]); + for (const line of raw.split("\n")) { + const [name, when] = line.split("\t"); + if (name && when) { + activity[name] = when; + } + } + } catch { + // Non-fatal: the dropdown just omits the "updated N ago" hint. + } + return activity; + } + + private async getTagsForCommits( + git: ReturnType, + ): Promise> { + const map: Record = {}; + const add = (hash: string, tag: string) => { + (map[hash] ??= []).includes(tag) || map[hash].push(tag); + }; + try { + const raw = await git.raw([ + "for-each-ref", + "--format=%(objectname)%09%(refname:short)%09%(*objectname)", + "refs/tags", + ]); + for (const line of raw.split("\n")) { + if (!line.trim()) { + continue; + } + const [objectName, tagName, peeled] = line.split("\t"); + const commitHash = peeled || objectName; + if (commitHash && tagName) { + add(commitHash, tagName); + } + } + } catch { + // Non-fatal. + } + return map; + } + + private async getRemoteStatus( + git: ReturnType, + currentBranch: string | undefined, + remotes: Array<{ name: string }>, + ): Promise { + const empty: RemoteStatus = { + hasRemote: remotes.length > 0, + isPublished: false, + ahead: 0, + behind: 0, + lastFetched: null, + remoteBranch: null, + }; + if (!currentBranch || remotes.length === 0) { + return { ...empty, hasRemote: false }; + } + + let remoteBranch: string | null = null; + try { + remoteBranch = + ( + await git.raw([ + "rev-parse", + "--abbrev-ref", + "--symbolic-full-name", + "@{upstream}", + ]) + ).trim() || null; + } catch { + remoteBranch = null; + } + if (!remoteBranch) { + return empty; + } + + let ahead = 0; + let behind = 0; + try { + const [b, a] = ( + await git.raw([ + "rev-list", + "--left-right", + "--count", + "@{upstream}...HEAD", + ]) + ) + .trim() + .split(/\s+/); + behind = Number.parseInt(b, 10) || 0; + ahead = Number.parseInt(a, 10) || 0; + } catch { + // leave at 0 + } + + let lastFetched: Date | null = null; + try { + const raw = await git.raw([ + "reflog", + "show", + "--date=iso", + "-n", + "1", + "--format=%cd", + `refs/remotes/${remoteBranch}`, + ]); + const date = new Date(raw.trim()); + if (!Number.isNaN(date.getTime())) { + lastFetched = date; + } + } catch { + // unknown + } + + return { + hasRemote: true, + isPublished: true, + ahead, + behind, + lastFetched, + remoteBranch, + }; + } +} diff --git a/src/webviews/timeline/services/repository-scoped.service.ts b/src/webviews/timeline/services/repository-scoped.service.ts new file mode 100644 index 0000000..3bcf7a4 --- /dev/null +++ b/src/webviews/timeline/services/repository-scoped.service.ts @@ -0,0 +1,29 @@ +import { TrackedRepository } from "../../../shared/types"; +import { Notifier, RepositoryContext } from "../ports"; + +/** + * @description Base for services that operate on the current repository. + * Centralises the "no repository open" guard so each concrete method stays a + * single responsibility. + */ +export abstract class RepositoryScopedService { + protected constructor( + protected readonly repos: RepositoryContext, + protected readonly notifier: Notifier, + ) {} + + /** + * Runs `fn` with the active repository, or notifies the user and resolves to + * `undefined` when there is none. + */ + protected async withRepo( + fn: (repo: TrackedRepository) => Promise, + ): Promise { + const repo = this.repos.getPrimary(); + if (!repo) { + this.notifier.error("No repository found in the current workspace"); + return undefined; + } + return fn(repo); + } +} diff --git a/src/webviews/timeline/services/stash.service.ts b/src/webviews/timeline/services/stash.service.ts new file mode 100644 index 0000000..051c313 --- /dev/null +++ b/src/webviews/timeline/services/stash.service.ts @@ -0,0 +1,61 @@ +import { GitClientFactory } from "../../../core/git/git-authenticator"; +import { Notifier, RepositoryContext, Refresher } from "../ports"; +import { RepositoryScopedService } from "./repository-scoped.service"; + +/** + * @description Stash management: save the working tree, re-apply an entry + * (optionally dropping it), or discard one. + */ +export class StashService extends RepositoryScopedService { + constructor( + repos: RepositoryContext, + notifier: Notifier, + private readonly git: GitClientFactory, + private readonly refresher: Refresher, + ) { + super(repos, notifier); + } + + push(message: string): Promise { + return this.withRepo(async (repo) => { + const args = ["stash", "push", "--include-untracked"]; + if (message.trim()) { + args.push("-m", message.trim()); + } + const out = await this.git.plain(repo.localPath).raw(args); + if (/No local changes/i.test(out)) { + this.notifier.info("Nothing to stash"); + return; + } + this.notifier.info("Changes stashed"); + await this.refresher.refresh(); + }); + } + + apply(index: number, drop: boolean): Promise { + return this.withRepo(async (repo) => { + await this.git + .plain(repo.localPath) + .raw(["stash", drop ? "pop" : "apply", `stash@{${index}}`]); + this.notifier.info(drop ? "Stash popped" : "Stash applied"); + await this.refresher.refresh(); + }); + } + + drop(index: number): Promise { + return this.withRepo(async (repo) => { + const ok = await this.notifier.confirm( + `Delete stash@{${index}}? This cannot be undone.`, + "Delete", + ); + if (!ok) { + return; + } + await this.git + .plain(repo.localPath) + .raw(["stash", "drop", `stash@{${index}}`]); + this.notifier.info("Stash deleted"); + await this.refresher.refresh(); + }); + } +} diff --git a/src/webviews/timeline/services/sync.service.ts b/src/webviews/timeline/services/sync.service.ts new file mode 100644 index 0000000..632c03b --- /dev/null +++ b/src/webviews/timeline/services/sync.service.ts @@ -0,0 +1,134 @@ +import { SimpleGit } from "simple-git"; +import { GitClientFactory } from "../../../core/git/git-authenticator"; +import { + Notifier, + RepositoryContext, + Refresher, + WebviewChannel, +} from "../ports"; +import { RepositoryScopedService } from "./repository-scoped.service"; + +/** + * @description Owns the four remote-sync operations behind the toolbar's sync + * cell: fetch, pull, push and publish. Authentication is delegated to + * {@link GitClientFactory.withAuth} (no token ever touches `.git/config`). + */ +export class SyncService extends RepositoryScopedService { + constructor( + repos: RepositoryContext, + notifier: Notifier, + private readonly git: GitClientFactory, + private readonly channel: WebviewChannel, + private readonly refresher: Refresher, + ) { + super(repos, notifier); + } + + fetch(): Promise { + return this.withRepo(async (repo) => { + await this.git.withAuth(repo.localPath, (g) => g.fetch()); + this.notifier.info("Fetched from remote"); + await this.refresher.refresh(); + }); + } + + pull(): Promise { + return this.withRepo(async (repo) => { + try { + await this.git.withAuth(repo.localPath, (g) => g.pull()); + } catch (error) { + if (await this.reportIfConflict(repo.localPath, "pull")) { + return; + } + throw error; + } + this.notifier.info("Pulled from remote"); + await this.refresher.refresh(); + }); + } + + push(): Promise { + return this.withRepo(async (repo) => { + const git = this.git.plain(repo.localPath); + const { current, tracking } = await git.status(); + await this.git.withAuth(repo.localPath, (g) => + tracking ? g.push() : g.push(["-u", "origin", current ?? "HEAD"]), + ); + this.notifier.info("Pushed to remote"); + await this.refresher.refresh(); + }); + } + + /** + * Overwrites the upstream branch with the local one. Uses + * `--force-with-lease` so it still fails if someone else pushed in the + * meantime. Meant for after an amend / undo / reset that rewrote history. + */ + forcePush(): Promise { + return this.withRepo(async (repo) => { + const git = this.git.plain(repo.localPath); + const { current, tracking } = await git.status(); + if (!tracking) { + this.notifier.warn("This branch has no upstream — use Publish instead"); + return; + } + const ok = await this.notifier.confirm( + `Force-push ${current}? This overwrites the remote branch with your local history.`, + "Force push", + ); + if (!ok) { + return; + } + await this.git.withAuth(repo.localPath, (g) => + g.push(["--force-with-lease"]), + ); + this.notifier.info("Force-pushed to remote"); + await this.refresher.refresh(); + }); + } + + publish(): Promise { + return this.withRepo(async (repo) => { + const git = this.git.plain(repo.localPath); + const { current } = await git.branch(); + if (!current) { + this.notifier.error("No branch to publish"); + return; + } + const remotes = await git.getRemotes(); + if (remotes.length === 0) { + this.notifier.error("No remote repository configured"); + return; + } + const remote = remotes[0].name; + await this.git.withAuth(repo.localPath, (g) => + g.push(["-u", remote, current]), + ); + this.notifier.info(`Published '${current}' to ${remote}`); + await this.refresher.refresh(); + }); + } + + /** + * If the working tree is now in a conflicted state, tells the webview and + * returns `true`. Otherwise returns `false` and the caller rethrows. + */ + private async reportIfConflict( + repoPath: string, + operation: string, + ): Promise { + const status = await this.git.plain(repoPath).status(); + if (status.conflicted.length === 0) { + return false; + } + this.channel.post({ + command: "mergeConflict", + operation, + files: status.conflicted, + }); + this.notifier.warn( + `${operation} stopped: ${status.conflicted.length} file(s) in conflict`, + ); + return true; + } +} diff --git a/src/webviews/timeline/services/webview-html.service.ts b/src/webviews/timeline/services/webview-html.service.ts index 714d20d..cb0e88b 100644 --- a/src/webviews/timeline/services/webview-html.service.ts +++ b/src/webviews/timeline/services/webview-html.service.ts @@ -3,6 +3,14 @@ import * as vscode from "vscode"; import { RepositoryManager } from "../../../core/repositories/repository-manager"; import { getPrimaryRepository } from "../../../shared/utils/repo-selection"; +/** + * Renders the timeline webview as a self-contained GitHub Desktop-style UI: + * a three-cell toolbar (repository / branch / sync), a left column with the + * Changes and History tabs plus the commit box, and a diff pane on the right. + * + * Everything is inlined (no bundle, no CDN, nonce-based CSP) so the view keeps + * working regardless of the webview module loader. + */ export class WebviewHtmlService { constructor( private readonly context: vscode.ExtensionContext, @@ -10,21 +18,9 @@ export class WebviewHtmlService { ) {} generateHtml(webview: vscode.Webview): string { - const reactAppUri = webview.asWebviewUri( - vscode.Uri.joinPath( - this.context.extensionUri, - "out", - "webview", - "main.js", - ), - ); - + const nonce = getNonce(); const repository = getPrimaryRepository(this.repositories); const initialData = { - changes: [], - history: [], - branches: [], - currentBranch: null, repository: repository ? { name: path.basename(repository.localPath), @@ -32,67 +28,1579 @@ export class WebviewHtmlService { remote: repository.remoteUrl, } : null, - accounts: [], - activeAccount: null, }; + const csp = [ + `default-src 'none'`, + `style-src 'nonce-${nonce}'`, + `script-src 'nonce-${nonce}'`, + `img-src ${webview.cspSource} https: data:`, + `font-src ${webview.cspSource}`, + ].join("; "); + return ` - - - - GitHub Desktop Timeline - - + + + +GitHub Desktop + - -
- - + `; } } + +function getNonce(): string { + let text = ""; + const possible = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + for (let i = 0; i < 32; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; +} + +const ICON = { + repo: ``, + branch: ``, + fetch: ``, + push: ``, + pull: ``, + publish: ``, + caret: ``, + check: ``, + file: ``, + people: ``, + linkExt: ``, +}; + +const STYLES = ` +* { box-sizing: border-box; margin: 0; padding: 0; } +[hidden] { display: none !important; } +:root { + color-scheme: light dark; + --gd-accent: var(--vscode-button-background, #1f6feb); + --gd-accent-fg: var(--vscode-button-foreground, #fff); + --gd-chrome: var(--vscode-sideBarSectionHeader-background, var(--vscode-sideBar-background)); + --gd-border: var(--vscode-panel-border, rgba(128,128,128,.28)); + --gd-radius: 6px; +} +html, body { height: 100%; } +body { + display: flex; flex-direction: column; + font-family: var(--vscode-font-family, -apple-system, "Segoe UI", system-ui, sans-serif); + font-size: 12px; line-height: 1.4; + color: var(--vscode-foreground); + background: var(--vscode-sideBar-background); + overflow: hidden; +} +button, input, textarea { font: inherit; color: inherit; } +::-webkit-scrollbar { width: 10px; height: 10px; } +::-webkit-scrollbar-thumb { background: var(--vscode-scrollbarSlider-background); border-radius: 5px; background-clip: padding-box; border: 2px solid transparent; } +::-webkit-scrollbar-thumb:hover { background: var(--vscode-scrollbarSlider-hoverBackground); } + +/* ---- toolbar (compact single line — the panel is short on height) ---- */ +#toolbar { + display: flex; flex: 0 0 auto; height: 30px; + background: var(--gd-chrome); + border-bottom: 1px solid var(--gd-border); +} +.cell { + display: flex; align-items: center; gap: 6px; + flex: 1 1 0; min-width: 0; padding: 0 9px; + background: transparent; border: 0; + border-right: 1px solid var(--gd-border); + cursor: pointer; text-align: left; +} +.cell:last-child { border-right: 0; } +.cell:hover { background: var(--vscode-list-hoverBackground); } +.cell:active { background: var(--vscode-list-activeSelectionBackground); } +.cell-ico { flex: 0 0 auto; display: flex; opacity: .75; } +.cell-ico svg { width: 13px; height: 13px; } +.cell-text { display: flex; min-width: 0; } +.cell-label { display: none; } +.cell-value { + flex: 1 1 auto; font-weight: 600; font-size: 11px; + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; +} +.cell-caret { flex: 0 0 auto; opacity: .45; } +.cell-caret svg { width: 9px; height: 9px; } +.cell-count { + flex: 0 0 auto; display: inline-flex; align-items: center; gap: 2px; + font-size: 10px; font-weight: 600; padding: 0 5px; border-radius: 9px; + background: var(--vscode-badge-background); color: var(--vscode-badge-foreground); +} + +/* ---- body split ---- */ +#body { flex: 1 1 auto; display: flex; min-height: 0; } +#left { + flex: 0 0 46%; max-width: 340px; min-width: 220px; + display: flex; flex-direction: column; min-height: 0; + background: var(--vscode-sideBar-background); + border-right: 1px solid var(--gd-border); +} +body.narrow #left { flex: 1 1 auto; max-width: none; border-right: 0; } +body.narrow #right { display: none; } +#right { flex: 1 1 auto; display: flex; flex-direction: column; min-width: 0; background: var(--vscode-editor-background); } + +/* ---- right: rich empty state (GitHub Desktop "No local changes") ---- */ +#rightEmpty { flex: 1 1 auto; overflow: auto; padding: 32px 28px; } +#emptyHeading { font-size: 26px; font-weight: 300; color: var(--vscode-foreground); } +#emptySub { margin-top: 8px; font-size: 12px; color: var(--vscode-descriptionForeground); max-width: 420px; line-height: 1.5; } +#emptyCards { margin-top: 22px; display: flex; flex-direction: column; gap: 12px; max-width: 520px; } +.empty-card { + border: 1px solid var(--gd-border); border-radius: 8px; padding: 14px 16px; + display: flex; align-items: center; gap: 14px; +} +.empty-card.is-primary { border-color: var(--gd-accent); background: color-mix(in srgb, var(--gd-accent) 8%, transparent); } +.empty-card-body { flex: 1 1 auto; min-width: 0; } +.empty-card-title { font-size: 12px; font-weight: 600; } +.empty-card-desc { font-size: 11px; color: var(--vscode-descriptionForeground); margin-top: 3px; line-height: 1.45; } +.empty-card-btn { + flex: 0 0 auto; padding: 6px 14px; border-radius: var(--gd-radius); cursor: pointer; + font-size: 12px; font-weight: 600; border: 1px solid var(--gd-border); + background: var(--vscode-button-secondaryBackground, transparent); color: var(--vscode-foreground); + display: inline-flex; align-items: center; gap: 6px; +} +.empty-card.is-primary .empty-card-btn { background: var(--gd-accent); color: var(--gd-accent-fg); border-color: transparent; } +.empty-card-btn:hover { filter: brightness(1.1); } + +/* ---- tabs ---- */ +#tabs { display: flex; flex: 0 0 auto; background: var(--gd-chrome); border-bottom: 1px solid var(--gd-border); } +.tab { + flex: 1 1 0; height: 28px; background: transparent; border: 0; + border-bottom: 2px solid transparent; cursor: pointer; font-size: 12px; + color: var(--vscode-descriptionForeground); font-weight: 500; + display: flex; align-items: center; justify-content: center; gap: 6px; +} +.tab:hover { color: var(--vscode-foreground); } +.tab.is-active { color: var(--vscode-foreground); border-bottom-color: var(--gd-accent); } +.badge { + font-size: 10px; font-weight: 600; min-width: 16px; padding: 1px 5px; border-radius: 9px; + background: var(--vscode-badge-background); color: var(--vscode-badge-foreground); +} +.tabpane { flex: 1 1 auto; min-height: 0; } +/* Changes: one scroll for the whole pane; the commit box sticks to the bottom + but is still reachable by scrolling when the panel is very short. */ +/* Changes: one scroll for the whole pane — file list AND the commit form + flow together, nothing overlays. */ +#pane-changes { display: block; overflow-y: auto; overflow-x: hidden; } +#pane-history { display: flex; flex-direction: column; overflow: hidden; } + +/* ---- changed files ---- */ +#filterWrap { flex: 0 0 auto; padding: 8px 10px 4px; } +#filter { + width: 100%; padding: 5px 8px; border-radius: var(--gd-radius); font-size: 12px; + background: var(--vscode-input-background); color: var(--vscode-input-foreground); + border: 1px solid var(--vscode-input-border, var(--gd-border)); +} +#filter:focus { outline: 0; border-color: var(--vscode-focusBorder); } +#allRow { + flex: 0 0 auto; display: flex; align-items: center; gap: 8px; + padding: 6px 12px; font-size: 11px; color: var(--vscode-descriptionForeground); + cursor: pointer; +} +#allRow input, .file-row input[type=checkbox] { width: 13px; height: 13px; accent-color: var(--gd-accent); cursor: pointer; } +#fileList { min-height: 0; } +.file-row { + display: flex; align-items: center; gap: 8px; height: 28px; padding: 0 12px; cursor: pointer; + white-space: nowrap; font-size: 12px; +} +.file-row:hover { background: var(--vscode-list-hoverBackground); } +.file-row.is-selected { background: var(--vscode-list-inactiveSelectionBackground); } +.file-name { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; } +.file-dir { opacity: .55; } +.status-sq { + flex: 0 0 auto; width: 16px; height: 16px; border-radius: 3px; + display: flex; align-items: center; justify-content: center; + font-size: 10px; font-weight: 700; line-height: 1; color: #fff; +} +.st-M { background: var(--vscode-gitDecoration-modifiedResourceForeground, #d0a215); } +.st-A, .st-U { background: var(--vscode-gitDecoration-untrackedResourceForeground, #2ea043); } +.st-D { background: var(--vscode-gitDecoration-deletedResourceForeground, #d73a49); } +.st-R { background: var(--vscode-gitDecoration-renamedResourceForeground, #8250df); } +.st-C { background: var(--vscode-gitDecoration-conflictingResourceForeground, #e4676b); } +.file-x { + flex: 0 0 auto; opacity: 0; width: 18px; height: 18px; border: 0; border-radius: 4px; + background: transparent; cursor: pointer; color: inherit; font-size: 14px; line-height: 1; +} +.file-row:hover .file-x { opacity: .6; } +.file-x:hover { opacity: 1; background: var(--vscode-toolbar-hoverBackground, rgba(128,128,128,.2)); } + +/* ---- commit box ---- */ +#commitBox { + flex: 0 0 auto; + padding: 7px 8px; border-top: 1px solid var(--gd-border); + display: flex; flex-direction: column; gap: 5px; + background: var(--gd-chrome); +} +.commit-summary { display: flex; align-items: center; gap: 8px; } +.avatar { + flex: 0 0 auto; width: 28px; height: 28px; border-radius: 50%; overflow: hidden; + display: flex; align-items: center; justify-content: center; + font-size: 11px; font-weight: 700; text-transform: uppercase; + background: var(--gd-accent); color: var(--gd-accent-fg); +} +.avatar img { width: 100%; height: 100%; object-fit: cover; display: block; } +#summary, #description { + width: 100%; padding: 6px 9px; border-radius: var(--gd-radius); font-size: 12px; + background: var(--vscode-input-background); color: var(--vscode-input-foreground); + border: 1px solid var(--vscode-input-border, var(--gd-border)); +} +#summary { font-weight: 600; } +#summary:focus, #description:focus { outline: 0; border-color: var(--vscode-focusBorder); } +#description { resize: vertical; min-height: 38px; max-height: 120px; } +#commitBtn { + width: 100%; padding: 8px 12px; border: 0; border-radius: var(--gd-radius); cursor: pointer; + font-size: 12px; font-weight: 600; + background: var(--gd-accent); color: var(--gd-accent-fg); +} +#commitBtn:hover:not(:disabled) { background: var(--vscode-button-hoverBackground, var(--gd-accent)); filter: brightness(1.08); } +#commitBtn:disabled { opacity: .45; cursor: default; } +#commitBtn strong { font-weight: 700; } + +/* ---- history ---- */ +#commitList { flex: 1 1 auto; overflow: auto; min-height: 0; } +.commit-row { + display: flex; flex-direction: column; gap: 3px; padding: 9px 12px; cursor: pointer; + border-bottom: 1px solid var(--gd-border); +} +.commit-row:hover { background: var(--vscode-list-hoverBackground); } +.commit-row.is-selected { background: var(--vscode-list-inactiveSelectionBackground); box-shadow: inset 2px 0 0 var(--gd-accent); } +.commit-msg { font-weight: 500; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.commit-meta { font-size: 11px; color: var(--vscode-descriptionForeground); display: flex; align-items: center; gap: 6px; } +.unpushed-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--gd-accent); flex: 0 0 auto; } + +/* ---- diff ---- */ +#diffHeader { + flex: 0 0 auto; padding: 10px 16px; font-size: 12px; font-weight: 600; + background: var(--gd-chrome); + border-bottom: 1px solid var(--gd-border); + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; +} +#diffBody { + flex: 1 1 auto; overflow: auto; min-height: 0; + background: var(--vscode-editor-background); + font-family: var(--vscode-editor-font-family, ui-monospace, "SF Mono", Menlo, monospace); + font-size: var(--vscode-editor-font-size, 12px); line-height: 1.5; +} +.dl { display: flex; white-space: pre; align-items: flex-start; } +.dg { + flex: 0 0 auto; width: 40px; padding: 0 6px; text-align: right; + color: var(--vscode-editorLineNumber-foreground); opacity: .55; + user-select: none; font-variant-numeric: tabular-nums; +} +.ds { flex: 0 0 auto; width: 16px; text-align: center; user-select: none; opacity: .6; } +.dt { flex: 1 1 auto; padding: 0 10px 0 4px; white-space: pre-wrap; word-break: break-word; } +.d-add { background: var(--vscode-diffEditor-insertedLineBackground, var(--vscode-diffEditor-insertedTextBackground, rgba(46,160,67,.12))); } +.d-del { background: var(--vscode-diffEditor-removedLineBackground, var(--vscode-diffEditor-removedTextBackground, rgba(248,81,73,.12))); } +.d-add .ds { color: var(--vscode-gitDecoration-addedResourceForeground, #2ea043); opacity: 1; } +.d-del .ds { color: var(--vscode-gitDecoration-deletedResourceForeground, #f85149); opacity: 1; } +.dw-add { background: var(--vscode-diffEditor-insertedTextBackground, rgba(46,160,67,.4)); border-radius: 2px; } +.dw-del { background: var(--vscode-diffEditor-removedTextBackground, rgba(248,81,73,.4)); border-radius: 2px; } +.dh { + color: var(--vscode-descriptionForeground); + background: var(--vscode-editor-inactiveSelectionBackground, rgba(128,128,128,.12)); + padding: 2px 10px; margin: 4px 0; font-size: 11px; +} +.dh .dt { padding: 0; white-space: pre; } +.diff-meta { color: var(--vscode-descriptionForeground); } + +/* ---- commit detail ---- */ +.cd-head { + padding: 12px 16px; border-bottom: 1px solid var(--gd-border); + background: var(--gd-chrome); font-family: var(--vscode-font-family); +} +.cd-subject { font-size: 12px; font-weight: 600; margin-bottom: 4px; } +.cd-meta { font-size: 11px; color: var(--vscode-descriptionForeground); margin-top: 2px; } +.cd-sha { font-family: var(--vscode-editor-font-family, monospace); } +.cd-add { color: var(--vscode-gitDecoration-addedResourceForeground, #2ea043); } +.cd-del { color: var(--vscode-gitDecoration-deletedResourceForeground, #f85149); } +.cd-files { font-family: var(--vscode-font-family); } +.cd-file { + display: flex; align-items: center; gap: 8px; padding: 6px 16px; cursor: pointer; + border-bottom: 1px solid var(--gd-border); font-size: 12px; +} +.cd-file:hover { background: var(--vscode-list-hoverBackground); } +.cd-file-path { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; direction: rtl; text-align: left; } +.cd-file-stat { flex: 0 0 auto; font-size: 11px; } +.cd-back { + display: flex; align-items: center; gap: 10px; padding: 8px 16px; + border-bottom: 1px solid var(--gd-border); background: var(--gd-chrome); +} +.cd-back button { + background: none; border: 0; color: var(--gd-accent); cursor: pointer; + font-size: 12px; padding: 0; +} +.cd-back-path { + font-size: 11px; color: var(--vscode-descriptionForeground); + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; +} + +/* ---- empty states ---- */ +.empty-block { + flex: 1 1 auto; display: flex; flex-direction: column; align-items: center; + justify-content: center; text-align: center; padding: 24px 20px; gap: 8px; +} +#noChanges { flex: 0 0 auto; padding: 28px 20px; } +/* The rich "No local changes" lives in the right pane; only show the left + placeholder when the right pane is hidden (narrow / sidebar). */ +body:not(.narrow) #noChanges { display: none !important; } +.empty-emoji { opacity: .3; } +.empty-emoji svg { width: 32px; height: 32px; } +.empty-title { font-size: 13px; font-weight: 400; color: var(--vscode-foreground); } +.empty-sub { font-size: 11px; color: var(--vscode-descriptionForeground); max-width: 240px; line-height: 1.5; } + +/* ---- popup menu ---- */ +.menu { + position: fixed; z-index: 50; min-width: 240px; max-height: 66vh; overflow: auto; + background: var(--vscode-menu-background, var(--vscode-dropdown-background, var(--gd-chrome))); + border: 1px solid var(--vscode-menu-border, var(--gd-border)); + border-radius: 8px; box-shadow: 0 8px 28px rgba(0,0,0,.45); padding: 5px; +} +.menu input.menu-filter { + width: 100%; margin-bottom: 4px; padding: 6px 8px; border-radius: var(--gd-radius); + background: var(--vscode-input-background); color: var(--vscode-input-foreground); + border: 1px solid var(--vscode-input-border, var(--gd-border)); +} +.menu input.menu-filter:focus { outline: 0; border-color: var(--vscode-focusBorder); } +.menu-item { + display: flex; align-items: center; gap: 8px; padding: 6px 9px; border-radius: var(--gd-radius); + cursor: pointer; white-space: nowrap; font-size: 12px; +} +.menu-item:hover { background: var(--vscode-list-hoverBackground); } +.menu-item.is-current { color: var(--vscode-descriptionForeground); } +.menu-sep { height: 1px; margin: 5px 4px; background: var(--gd-border); } +.menu-tabs { display: flex; gap: 4px; margin-bottom: 5px; } +.menu-tab { + flex: 1 1 0; padding: 5px 6px; border: 0; border-radius: var(--gd-radius); cursor: pointer; + font-size: 11px; font-weight: 600; background: transparent; + color: var(--vscode-descriptionForeground); +} +.menu-tab:hover { background: var(--vscode-list-hoverBackground); } +.menu-tab.is-active { background: var(--gd-accent); color: var(--gd-accent-fg); } +.menu-empty { padding: 7px 9px; color: var(--vscode-descriptionForeground); font-size: 12px; } +.menu-item.is-danger { color: var(--vscode-errorForeground, #f14c4c); } +.menu-btn { + flex: 1 1 0; padding: 6px 8px; border: 0; border-radius: var(--gd-radius); cursor: pointer; font-size: 11px; font-weight: 600; + background: var(--vscode-button-secondaryBackground, var(--gd-accent)); + color: var(--vscode-button-secondaryForeground, var(--gd-accent-fg)); +} +.menu-btn:hover { background: var(--vscode-button-hoverBackground); } +.load-more { + width: 100%; padding: 9px; border: 0; background: transparent; cursor: pointer; + color: var(--vscode-textLink-foreground); font-size: 12px; +} +.load-more:hover:not(:disabled) { background: var(--vscode-list-hoverBackground); } +#conflictBar { + flex: 0 0 auto; padding: 8px 12px; font-size: 11px; line-height: 1.45; + display: flex; flex-direction: column; gap: 6px; + background: var(--vscode-inputValidation-warningBackground, rgba(228,103,107,.15)); + border-bottom: 1px solid var(--vscode-inputValidation-warningBorder, #e4676b); +} +#conflictActions { display: flex; gap: 6px; } +#conflictActions button { + flex: 1 1 0; padding: 5px 8px; border: 0; border-radius: var(--gd-radius); + cursor: pointer; font-size: 11px; font-weight: 600; + background: var(--vscode-button-secondaryBackground, rgba(128,128,128,.25)); + color: var(--vscode-button-secondaryForeground, var(--vscode-foreground)); +} +#conflictContinue { background: var(--gd-accent); color: var(--gd-accent-fg); } +#conflictActions button:disabled { opacity: .45; cursor: default; } + +/* ---- commit meta (amend / co-authors) ---- */ +#commitMeta { display: flex; align-items: center; gap: 10px; } +#coAuthToggle { + flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; + width: 24px; height: 24px; border: 0; border-radius: 4px; cursor: pointer; + background: transparent; color: var(--vscode-descriptionForeground); +} +#coAuthToggle:hover, #coAuthToggle.is-on { color: var(--vscode-foreground); background: var(--vscode-toolbar-hoverBackground, rgba(128,128,128,.18)); } +#amendRow { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--vscode-descriptionForeground); cursor: pointer; } +#amendRow input { width: 13px; height: 13px; accent-color: var(--gd-accent); } +#coAuthors { + width: 100%; padding: 6px 9px; border-radius: var(--gd-radius); font-size: 12px; + background: var(--vscode-input-background); color: var(--vscode-input-foreground); + border: 1px solid var(--vscode-input-border, var(--gd-border)); +} +#coAuthors:focus { outline: 0; border-color: var(--vscode-focusBorder); } + +/* ---- last commit bar (Committed … · Undo) ---- */ +#lastCommitBar { + flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between; gap: 8px; + padding: 6px 10px; font-size: 11px; color: var(--vscode-descriptionForeground); + background: var(--gd-chrome); border-top: 1px solid var(--gd-border); +} +#lastCommitText { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +#undoBtn2 { + flex: 0 0 auto; padding: 3px 10px; border: 1px solid var(--gd-border); border-radius: var(--gd-radius); + background: transparent; color: var(--vscode-foreground); cursor: pointer; font-size: 11px; font-weight: 600; +} +#undoBtn2:hover { background: var(--vscode-list-hoverBackground); } + +/* ---- compare bar ---- */ +#compareBar { + flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between; + gap: 8px; padding: 7px 12px; font-size: 11px; + background: var(--vscode-list-inactiveSelectionBackground, var(--gd-chrome)); + border-bottom: 1px solid var(--gd-border); +} +#compareText { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +#compareExit { flex: 0 0 auto; border: 0; background: transparent; color: inherit; cursor: pointer; font-size: 13px; opacity: .7; } +#compareExit:hover { opacity: 1; } +.compare-section { + padding: 6px 12px; font-size: 10px; font-weight: 700; text-transform: uppercase; + letter-spacing: .04em; color: var(--vscode-descriptionForeground); + background: var(--gd-chrome); border-bottom: 1px solid var(--gd-border); position: sticky; top: 0; +} + +.file-row.is-conflict .file-name { color: var(--vscode-errorForeground, #f14c4c); } +.file-resolve { + flex: 0 0 auto; padding: 1px 7px; border: 1px solid var(--gd-border); border-radius: 10px; + background: transparent; color: inherit; cursor: pointer; font-size: 10px; +} +.file-resolve:hover { background: var(--vscode-toolbar-hoverBackground, rgba(128,128,128,.2)); } +`; + +const SCRIPT = String.raw` +const vscode = acquireVsCodeApi(); +const post = (command, extra) => vscode.postMessage(Object.assign({ command }, extra || {})); +const $ = (id) => document.getElementById(id); +const esc = (s) => String(s == null ? "" : s).replace(/[&<>"]/g, (c) => ( + { "&": "&", "<": "<", ">": ">", '"': """ }[c] +)); + +const state = { + repository: (window.__INITIAL__ && window.__INITIAL__.repository) || null, + repositories: [], + activeRepoPath: null, + account: null, + changes: [], + history: [], + branches: [], + currentBranch: null, + branchActivity: {}, + remote: null, + tab: "changes", + selectedFiles: new Set(), + selectedPath: null, + selectedCommit: null, + commitDetail: null, + commitFilePath: null, + filter: "", + hasMore: false, + loadingMore: false, + operation: null, + conflicted: [], + canUndo: false, + lastCommitSummary: null, + stashes: [], + amend: false, + coAuthors: "", + compare: null, + prMenuTab: "branches", + pullRequests: null, + prError: null, + prLoading: false, +}; + +/* ---------- toolbar ---------- */ +function renderToolbar() { + $("repoName").textContent = state.repository ? state.repository.name : "No repository"; + $("branchName").textContent = state.currentBranch || "—"; + // (the commit button label — including the branch — is owned by updateCommitBtn) + + const r = state.remote || {}; + const ico = $("syncIco"), label = $("syncLabel"), count = $("syncCount"), cell = $("syncCell"); + const svg = { fetch: ${JSON.stringify(ICON.fetch)}, push: ${JSON.stringify(ICON.push)}, pull: ${JSON.stringify(ICON.pull)}, publish: ${JSON.stringify(ICON.publish)} }; + // Only claim "not published" when we positively know: a remote exists and + // git reported no upstream. Anything uncertain (status not received yet) + // defaults to Fetch rather than a misleading "Publish branch". + let mode = "fetch"; + if (r.hasRemote === true && r.isPublished === false && r.ahead === 0 && r.behind === 0) mode = "publish"; + else if (r.ahead > 0 && r.behind > 0) mode = "diverged"; + else if (r.behind > 0) mode = "pull"; + else if (r.ahead > 0) mode = "push"; + + ico.innerHTML = svg[mode === "diverged" ? "push" : mode]; + count.hidden = true; + const sub = $("syncSub"); + if (mode === "publish") { label.textContent = "Publish branch"; sub.textContent = "This branch is not on GitHub yet"; } + else if (mode === "diverged") { label.textContent = "Diverged"; sub.textContent = "Local and origin differ — click for options"; count.hidden = false; count.textContent = "↑" + r.ahead + " ↓" + r.behind; } + else if (mode === "pull") { label.textContent = "Pull origin"; sub.textContent = relFetched(r.lastFetched); count.hidden = false; count.textContent = "↓ " + r.behind; } + else if (mode === "push") { label.textContent = "Push origin"; sub.textContent = relFetched(r.lastFetched); count.hidden = false; count.textContent = "↑ " + r.ahead; } + else { label.textContent = "Fetch origin"; sub.textContent = relFetched(r.lastFetched); } + cell.title = sub.textContent; + cell.dataset.mode = mode; +} +const SYNC_MENU = [ + ["fetch", "Fetch origin"], + ["pull", "Pull origin"], + ["push", "Push origin"], + ["forcePush", "Force-push origin (overwrite remote)"], +]; +function openSyncMenu(x, y) { + openMenuAt(x, y, (m) => { + for (const [cmd, label] of SYNC_MENU) { + const it = document.createElement("div"); + it.className = "menu-item" + (cmd === "forcePush" ? " is-danger" : ""); + it.textContent = label; + it.onclick = () => { closeMenu(); post(cmd); }; + m.appendChild(it); + } + }); +} +$("syncCell").oncontextmenu = (e) => { e.preventDefault(); openSyncMenu(e.clientX, e.clientY); }; +function relFetched(d) { + if (!d) return "Never fetched"; + const t = new Date(d).getTime(); + if (isNaN(t)) return "Last fetched recently"; + const mins = Math.round((Date.now() - t) / 60000); + if (mins < 1) return "Last fetched just now"; + if (mins < 60) return "Last fetched " + mins + "m ago"; + const hrs = Math.round(mins / 60); + if (hrs < 24) return "Last fetched " + hrs + "h ago"; + return "Last fetched " + Math.round(hrs / 24) + "d ago"; +} + +$("syncCell").onclick = (e) => { + const mode = $("syncCell").dataset.mode; + if (mode === "diverged") { openSyncMenu(e.clientX, e.clientY); return; } + if (mode === "publish") post("publish"); + else if (mode === "pull") post("pull"); + else if (mode === "push") post("push"); + else post("fetch"); +}; + +/* ---------- tabs ---------- */ +document.querySelectorAll(".tab").forEach((btn) => { + btn.onclick = () => { + state.tab = btn.dataset.tab; + document.querySelectorAll(".tab").forEach((b) => b.classList.toggle("is-active", b === btn)); + $("pane-changes").hidden = state.tab !== "changes"; + $("pane-history").hidden = state.tab !== "history"; + if (state.tab === "changes" && state.selectedCommit) { + state.selectedCommit = null; + state.commitDetail = null; + state.commitFilePath = null; + } + updateLayout(); + renderLastCommitBar(); + }; +}); + +/* ---------- changed files ---------- */ +function fileParts(p) { + const i = p.lastIndexOf("/"); + return i < 0 ? { dir: "", name: p } : { dir: p.slice(0, i + 1), name: p.slice(i + 1) }; +} +function statusLetter(st) { + const s = (st || "").replace(/\s/g, ""); + if (s.indexOf("D") >= 0) return "D"; + if (s.indexOf("A") >= 0 || s === "??" || s.indexOf("?") >= 0) return "A"; + if (s.indexOf("R") >= 0) return "R"; + if (s.indexOf("U") >= 0 || s.indexOf("C") >= 0) return "C"; + return "M"; +} +function visibleChanges() { + const f = state.filter.trim().toLowerCase(); + return state.changes.filter((c) => !f || c.path.toLowerCase().indexOf(f) >= 0); +} +function renderChanges() { + const list = $("fileList"); + const vis = visibleChanges(); + const has = state.changes.length > 0; + $("changesBadge").textContent = state.changes.length; + $("noChanges").hidden = has; + $("fileList").hidden = !has; + // The commit box stays visible even with a clean tree so "Amend last commit" + // is reachable (GitHub Desktop behaviour). + $("commitBox").hidden = false; + $("allRow").hidden = !has; + $("filterWrap").hidden = !has; + + const n = state.selectedFiles.size; + $("allText").textContent = n + " of " + state.changes.length + " file" + (state.changes.length === 1 ? "" : "s") + " to commit"; + $("allCheck").checked = state.changes.length > 0 && n === state.changes.length; + $("allCheck").indeterminate = n > 0 && n < state.changes.length; + + list.innerHTML = ""; + for (const c of vis) { + const parts = fileParts(c.path); + const L = statusLetter(c.status); + const isConflict = state.conflicted.indexOf(c.path) >= 0; + const row = document.createElement("div"); + row.className = "file-row" + (state.selectedPath === c.path ? " is-selected" : "") + (isConflict ? " is-conflict" : ""); + row.innerHTML = + '' + + '' + (isConflict ? "C" : L) + '' + + '' + esc(parts.dir) + '' + esc(parts.name) + '' + + (isConflict + ? '' + : ''); + const cb = row.querySelector("input"); + cb.onclick = (e) => { + e.stopPropagation(); + if (isConflict) return; + if (cb.checked) state.selectedFiles.add(c.path); else state.selectedFiles.delete(c.path); + renderChanges(); + }; + if (isConflict) { + row.querySelector(".file-resolve").onclick = (e) => { e.stopPropagation(); post("markResolved", { files: [c.path] }); }; + } else { + row.querySelector(".file-x").onclick = (e) => { e.stopPropagation(); post("discardFiles", { files: [c.path] }); }; + } + row.onclick = () => selectFile(c.path); + list.appendChild(row); + } + updateCommitBtn(); +} +function applyWidth() { + document.body.classList.toggle("narrow", window.innerWidth < 620); +} +window.addEventListener("resize", applyWidth); + +function updateLayout() { + if (state.selectedCommit) { + renderCommitDetail(); + return; + } + const hasDiff = !!state.selectedPath; + const hasChanges = state.changes.length > 0; + // Right pane: a file's diff > "pick a file" hint (when there are changes) > + // the rich "No local changes" state. + $("diffHeader").hidden = !hasDiff; + $("diffBody").hidden = !hasDiff && !hasChanges; + $("rightEmpty").hidden = hasDiff || hasChanges; + if (hasChanges && !hasDiff) { + $("diffBody").innerHTML = + '
Select a file on the left to see its changes.
'; + } + if (!hasDiff && !hasChanges) renderRightEmpty(); +} + +const CARD_ICO = { linkExt: ${JSON.stringify(ICON.linkExt)} }; +function renderRightEmpty() { + const host = $("emptyCards"); + host.innerHTML = ""; + const r = state.remote || {}; + const card = (primary, title, desc, btn, onClick, icoHtml) => { + const el = document.createElement("div"); + el.className = "empty-card" + (primary ? " is-primary" : ""); + el.innerHTML = + '
' + esc(title) + '
' + + '
' + esc(desc) + '
' + + ''; + el.querySelector("button").onclick = onClick; + host.appendChild(el); + }; + + const mode = $("syncCell").dataset.mode; + if (mode === "publish") { + card(true, "Publish your branch", "This branch is not on GitHub yet. Publish it to share and open a pull request.", "Publish branch", () => post("publish")); + } else if (mode === "push") { + card(true, "Push commits to origin", "You have local commits waiting to be pushed to GitHub.", "Push origin", () => post("push")); + } else if (mode === "pull") { + card(true, "Pull from origin", "origin has commits you don't have locally.", "Pull origin", () => post("pull")); + } else if (mode === "diverged") { + card(true, "Your branch has diverged", "Local and origin have both moved. Review the options before pushing.", "Sync options", (e) => openSyncMenu(e.clientX, e.clientY)); + } + if (state.currentBranch) { + card(false, "Open a pull request", "Start a pull request for " + state.currentBranch + " on GitHub.", "Create pull request", () => post("createPullRequest", { branch: state.currentBranch }), CARD_ICO.linkExt); + } + if (state.repository && state.repository.remote) { + card(false, "View this repository on GitHub", "Open the repository page in your browser.", "View on GitHub", () => post("openRepoOnGitHub"), CARD_ICO.linkExt); + } +} +function selectFile(p) { + state.selectedPath = p; + state.selectedCommit = null; + renderChanges(); + updateLayout(); + $("diffHeader").hidden = false; + $("diffPath").textContent = p; + $("diffBody").innerHTML = '
Loading…
'; + post("getWorkingDiff", { filePath: p }); +} +$("allCheck").onclick = () => { + if ($("allCheck").checked) state.changes.forEach((c) => state.selectedFiles.add(c.path)); + else state.selectedFiles.clear(); + renderChanges(); +}; +$("filter").oninput = (e) => { state.filter = e.target.value; renderChanges(); }; + +/* ---------- commit ---------- */ +function coAuthorTrailers() { + const raw = state.coAuthors.trim(); + if (!raw) return ""; + const parts = raw.split(/[,\n]/).map((s) => s.trim()).filter(Boolean); + const lines = parts.map((p) => { + const m = p.match(/^(.*?)\s*<([^>]+)>$/); + if (m) return "Co-Authored-By: " + m[1].trim() + " <" + m[2].trim() + ">"; + const h = p.replace(/^@/, ""); + return "Co-Authored-By: " + h + " <" + h + "@users.noreply.github.com>"; + }); + return lines.length ? "\n\n" + lines.join("\n") : ""; +} +function updateCommitBtn() { + const n = state.selectedFiles.size; + const hasMsg = $("summary").value.trim().length > 0; + const ok = hasMsg && (state.amend || n > 0); + const btn = $("commitBtn"); + btn.disabled = !ok; + if (state.amend) { + btn.innerHTML = "Amend last commit" + (n > 0 ? " (+" + n + " file" + (n === 1 ? "" : "s") + ")" : ""); + } else { + btn.innerHTML = "Commit " + (n > 0 ? n + " file" + (n === 1 ? "" : "s") + " " : "") + + "to " + esc(state.currentBranch || "branch") + ""; + } +} +$("summary").oninput = updateCommitBtn; +function doCommit() { + const summary = $("summary").value.trim(); + const n = state.selectedFiles.size; + if (!summary || (!state.amend && n === 0)) return; + const desc = $("description").value.trim(); + const message = summary + (desc ? "\n\n" + desc : "") + coAuthorTrailers(); + const files = Array.from(state.selectedFiles); + post(state.amend ? "amendCommit" : "commitFiles", { message: message, files: files }); + $("commitBtn").disabled = true; + // Inputs cleared only on the "commitSucceeded" ack. +} +$("commitBtn").onclick = doCommit; +["summary", "description", "coAuthors"].forEach((id) => { + $(id).addEventListener("keydown", (e) => { + if ((e.ctrlKey || e.metaKey) && e.key === "Enter") { e.preventDefault(); doCommit(); } + }); +}); +$("coAuthors").oninput = (e) => { state.coAuthors = e.target.value; }; +$("coAuthToggle").onclick = () => { + const el = $("coAuthors"); + el.hidden = !el.hidden; + $("coAuthToggle").classList.toggle("is-on", !el.hidden); + $("coAuthToggle").title = el.hidden ? "Add co-authors" : "Hide co-authors"; + if (!el.hidden) el.focus(); +}; +$("amendCheck").onchange = (e) => { + state.amend = e.target.checked; + if (state.amend && !$("summary").value.trim() && state.lastCommitSummary) { + $("summary").value = state.lastCommitSummary; + } + updateCommitBtn(); +}; +$("undoBtn2").onclick = () => post("undoLastCommit"); +$("conflictAbort").onclick = () => post("abortOperation"); +$("conflictContinue").onclick = () => post("continueOperation"); + +/* ---------- operation / undo bars ---------- */ +function renderOperation() { + const bar = $("conflictBar"); + if (!state.operation) { bar.hidden = true; return; } + bar.hidden = false; + const nc = state.conflicted.length; + $("conflictText").textContent = nc > 0 + ? "⚠ " + state.operation + " paused — " + nc + " file(s) in conflict. Resolve each, then Continue." + : "✔ All conflicts resolved. Click Continue to finish the " + state.operation + "."; + $("conflictContinue").disabled = nc > 0; + if (state.operation) { state.tab = "changes"; $("pane-changes").hidden = false; $("pane-history").hidden = true; + document.querySelectorAll(".tab").forEach((b) => b.classList.toggle("is-active", b.dataset.tab === "changes")); } +} +function renderLastCommitBar() { + const show = !state.operation && state.canUndo && !!state.lastCommitSummary; + $("lastCommitBar").hidden = !show; + if (show) $("lastCommitText").textContent = 'Committed "' + (state.lastCommitSummary || "") + '"'; +} + +/* ---------- history ---------- */ +function commitRow(c) { + const row = document.createElement("div"); + row.className = "commit-row" + (state.selectedCommit === c.hash ? " is-selected" : ""); + row.innerHTML = + '
' + esc((c.message || "").split("\n")[0]) + "
" + + '
' + (c.isPushed === false ? '' : "") + + esc(c.authorName || c.author || "") + " · " + esc(c.relativeTime || "") + "
"; + row.onclick = () => { + state.selectedCommit = c.hash; + state.selectedPath = null; + state.commitDetail = null; + state.commitFilePath = null; + renderHistory(); + $("diffHeader").hidden = false; + $("diffPath").textContent = (c.message || "").split("\n")[0]; + $("rightEmpty").hidden = true; + $("diffBody").hidden = false; + $("diffBody").innerHTML = '
Loading…
'; + post("getCommitDetails", { hash: c.hash }); + }; + row.oncontextmenu = (e) => { e.preventDefault(); openCommitMenu(e.clientX, e.clientY, c); }; + return row; +} +function renderHistory() { + const list = $("commitList"); + list.innerHTML = ""; + + if (state.compare) { + $("compareBar").hidden = false; + $("compareText").textContent = "Comparing " + (state.currentBranch || "HEAD") + " ⇄ " + state.compare.branch; + $("noHistory").hidden = true; + const section = (title, commits) => { + const h = document.createElement("div"); + h.className = "compare-section"; + h.textContent = title + " (" + commits.length + ")"; + list.appendChild(h); + if (!commits.length) { + const e = document.createElement("div"); e.className = "menu-empty"; e.textContent = "none"; list.appendChild(e); + } + for (const c of commits) list.appendChild(commitRow(c)); + }; + section("On " + (state.currentBranch || "HEAD") + ", not on " + state.compare.branch, state.compare.ahead); + section("On " + state.compare.branch + ", not here", state.compare.behind); + return; + } + + $("compareBar").hidden = true; + $("noHistory").hidden = state.history.length > 0; + for (const c of state.history) list.appendChild(commitRow(c)); + if (state.hasMore) { + const more = document.createElement("button"); + more.className = "load-more"; more.type = "button"; + more.textContent = state.loadingMore ? "Loading…" : "Load more commits"; + more.disabled = state.loadingMore; + more.onclick = loadMore; + list.appendChild(more); + } +} +$("compareExit").onclick = () => { state.compare = null; renderHistory(); renderLastCommitBar(); }; +function loadMore() { + if (state.compare || state.loadingMore || !state.hasMore) return; + state.loadingMore = true; + renderHistory(); + post("loadMoreCommits", { offset: state.history.length }); +} +$("commitList").addEventListener("scroll", () => { + const el = $("commitList"); + if (el.scrollTop + el.clientHeight >= el.scrollHeight - 40) loadMore(); +}); + +const COMMIT_MENU = [ + ["checkout", "Checkout this commit"], + ["createBranch", "Create branch from commit…"], + ["createTag", "Create tag…"], + ["sep"], + ["cherryPick", "Cherry-pick to current branch"], + ["revert", "Revert this commit"], + ["reset", "Reset current branch to here"], + ["sep"], + ["copySha", "Copy SHA"], + ["viewOnGitHub", "View on GitHub"], +]; +function openCommitMenu(x, y, commit) { + openMenuAt(x, y, (m) => { + for (const entry of COMMIT_MENU) { + if (entry[0] === "sep") { const s = document.createElement("div"); s.className = "menu-sep"; m.appendChild(s); continue; } + const it = document.createElement("div"); + it.className = "menu-item" + (entry[0] === "reset" ? " is-danger" : ""); + it.textContent = entry[1]; + it.onclick = () => { closeMenu(); runCommitAction(entry[0], commit); }; + m.appendChild(it); + } + }); +} +function runCommitAction(action, commit) { + const h = commit.hash; + switch (action) { + case "checkout": post("checkoutCommit", { hash: h }); break; + case "createBranch": post("createBranchFromCommit", { hash: h }); break; + case "createTag": post("createTagFromCommit", { hash: h }); break; + case "cherryPick": post("cherryPickCommit", { hash: h }); break; + case "revert": post("revertCommit", { hash: h }); break; + case "reset": post("resetToCommit", { hash: h }); break; + case "copySha": navigator.clipboard && navigator.clipboard.writeText(h); break; + case "viewOnGitHub": post("viewCommitOnGitHub", { hash: h }); break; + } +} + +/* ---------- commit detail (right pane) ---------- */ +function exitCommitDetail() { + state.selectedCommit = null; + state.commitDetail = null; + state.commitFilePath = null; + renderHistory(); + updateLayout(); +} +function renderCommitDetail() { + $("rightEmpty").hidden = true; + $("diffHeader").hidden = false; + $("diffBody").hidden = false; + const d = state.commitDetail; + const s = d && d.summary; + $("diffPath").textContent = s ? (s.message || "").split("\n")[0] : "Commit"; + if (!d) return; + if (state.commitFilePath) return; // a file diff is showing + + const body = $("diffBody"); + const stat = + '+' + (s.additions || 0) + ' ' + + '-' + (s.deletions || 0) + ' · ' + + (s.fileCount || d.files.length) + ' file' + ((s.fileCount || d.files.length) === 1 ? '' : 's'); + let html = + '
' + + '
' + esc((s.message || "").split("\n")[0]) + '
' + + '
' + esc(s.authorName || "") + ' · ' + esc(s.relativeTime || "") + + ' · ' + esc(s.shortHash || (s.hash || state.selectedCommit).slice(0, 7)) + '
' + + '
' + stat + '
' + + '
'; + for (const f of d.files) { + const n = + (f.additions != null ? '+' + f.additions + ' ' : '') + + (f.deletions != null ? '-' + f.deletions + '' : ''); + html += + '
' + + '' + esc(f.path) + '' + + '' + n + '
'; + } + html += '
'; + body.innerHTML = html; + body.scrollTop = 0; + body.querySelectorAll(".cd-file").forEach((el) => { + el.onclick = () => { + state.commitFilePath = el.dataset.path; + body.innerHTML = '
Loading…
'; + post("selectFile", { hash: state.selectedCommit, path: el.dataset.path }); + }; + }); +} +function renderCommitFileDiff(diff) { + const body = $("diffBody"); + body.innerHTML = + '
' + + '' + esc(state.commitFilePath || "") + '
' + + '
'; + $("cdBack").onclick = () => { state.commitFilePath = null; renderCommitDetail(); }; + renderDiffInto($("cdDiff"), diff); +} + +/* ---------- diff rendering ---------- */ +function renderDiff(text) { + renderDiffInto($("diffBody"), text); +} +// Token-level LCS, used to highlight the exact words that changed between a +// removed line and the added line that replaced it (like VS Code's diff editor). +function wordDiff(a, b) { + const split = (s) => s.match(/\s+|\w+|[^\s\w]/g) || []; + const at = split(a), bt = split(b); + const n = at.length, m = bt.length; + const dp = Array.from({ length: n + 1 }, () => new Int32Array(m + 1)); + for (let i = n - 1; i >= 0; i--) + for (let j = m - 1; j >= 0; j--) + dp[i][j] = at[i] === bt[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]); + const aOut = [], bOut = []; + let i = 0, j = 0; + while (i < n && j < m) { + if (at[i] === bt[j]) { aOut.push([0, at[i]]); bOut.push([0, bt[j]]); i++; j++; } + else if (dp[i + 1][j] >= dp[i][j + 1]) { aOut.push([-1, at[i]]); i++; } + else { bOut.push([1, bt[j]]); j++; } + } + while (i < n) aOut.push([-1, at[i++]]); + while (j < m) bOut.push([1, bt[j++]]); + return [aOut, bOut]; +} +function paintTokens(tokens, changedCls) { + let html = ""; + for (const [flag, tok] of tokens) { + html += flag === 0 ? esc(tok) : '' + esc(tok) + ""; + } + return html; +} + +function renderDiffInto(body, text) { + if (!text || !text.trim()) { + body.innerHTML = '
No textual changes (binary file or whitespace only).
'; + return; + } + const src = text.split("\n"); + // Parse into typed rows first so we can pair adjacent -/+ runs for word diff. + const rows = []; + let oldNo = 0, newNo = 0; + for (const raw of src) { + if (raw.indexOf("@@") === 0) { + const m = raw.match(/@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@(.*)/); + if (m) { oldNo = +m[1]; newNo = +m[2]; } + rows.push({ t: "hunk", text: m ? m[3].trim() || raw : raw }); + } else if (/^(diff |index |--- |\+\+\+ |new file|deleted file|similarity |rename |Binary )/.test(raw)) { + continue; // file headers are shown in the pane header already + } else if (raw.charAt(0) === "+") { + rows.push({ t: "add", n: newNo++, text: raw.slice(1) }); + } else if (raw.charAt(0) === "-") { + rows.push({ t: "del", o: oldNo++, text: raw.slice(1) }); + } else { + rows.push({ t: "ctx", o: oldNo++, n: newNo++, text: raw.slice(1) }); + } + } + + let out = ""; + const cell = (o, n, sign, cls, inner) => + '
' + + '' + (o == null ? "" : o) + '' + + '' + (n == null ? "" : n) + '' + + '' + sign + '' + + '' + inner + '
'; + + for (let i = 0; i < rows.length; i++) { + const r = rows[i]; + if (r.t === "hunk") { out += '
' + esc(r.text) + '
'; continue; } + if (r.t === "ctx") { out += cell(r.o + 1, r.n + 1, " ", "", esc(r.text) || " "); continue; } + if (r.t === "del") { + // collect the del run and the following add run + const dels = []; while (i < rows.length && rows[i].t === "del") dels.push(rows[i++]); + const adds = []; while (i < rows.length && rows[i].t === "add") adds.push(rows[i++]); + i--; + for (let k = 0; k < dels.length; k++) { + const d = dels[k], pair = adds[k]; + const inner = pair ? paintTokens(wordDiff(d.text, pair.text)[0], "dw-del") : (esc(d.text) || " "); + out += cell(d.o + 1, null, "-", "d-del", inner); + } + for (let k = 0; k < adds.length; k++) { + const a = adds[k], pair = dels[k]; + const inner = pair ? paintTokens(wordDiff(pair.text, a.text)[1], "dw-add") : (esc(a.text) || " "); + out += cell(null, a.n + 1, "+", "d-add", inner); + } + continue; + } + if (r.t === "add") { out += cell(null, r.n + 1, "+", "d-add", esc(r.text) || " "); continue; } + } + body.innerHTML = out || '
No changes.
'; + body.scrollTop = 0; +} + +/* ---------- dropdown menus ---------- */ +const menu = $("menu"); +let redrawBranchMenu = null; +function closeMenu() { menu.hidden = true; menu.innerHTML = ""; document.removeEventListener("mousedown", onDocDown, true); redrawBranchMenu = null; } +function onDocDown(e) { if (!menu.contains(e.target)) closeMenu(); } +function openMenu(anchor, build) { + menu.innerHTML = ""; build(menu); + menu.hidden = false; + const r = anchor.getBoundingClientRect(); + menu.style.visibility = "hidden"; + requestAnimationFrame(() => { + const mw = menu.offsetWidth, mh = menu.offsetHeight; + let x = r.left, y = r.bottom + 2; + if (x + mw > window.innerWidth - 8) x = window.innerWidth - 8 - mw; + if (y + mh > window.innerHeight - 8) y = Math.max(8, r.top - 2 - mh); + menu.style.left = Math.max(8, x) + "px"; + menu.style.top = Math.max(8, y) + "px"; + menu.style.visibility = ""; + }); + setTimeout(() => document.addEventListener("mousedown", onDocDown, true), 0); +} +function openMenuAt(x, y, build) { + menu.innerHTML = ""; build(menu); + menu.hidden = false; + menu.style.visibility = "hidden"; + requestAnimationFrame(() => { + const mw = menu.offsetWidth, mh = menu.offsetHeight; + let nx = x, ny = y; + if (nx + mw > window.innerWidth - 8) nx = window.innerWidth - 8 - mw; + if (ny + mh > window.innerHeight - 8) ny = Math.max(8, window.innerHeight - 8 - mh); + menu.style.left = Math.max(8, nx) + "px"; + menu.style.top = Math.max(8, ny) + "px"; + menu.style.visibility = ""; + }); + setTimeout(() => document.addEventListener("mousedown", onDocDown, true), 0); +} + +$("branchCell").onclick = () => { + openMenu($("branchCell"), (m) => { + const tabs = document.createElement("div"); + tabs.className = "menu-tabs"; + const mkTab = (id, label) => { + const b = document.createElement("button"); + b.type = "button"; b.textContent = label; + b.className = "menu-tab" + (state.prMenuTab === id ? " is-active" : ""); + b.onclick = () => { + state.prMenuTab = id; + if (id === "prs") { state.prLoading = true; state.prError = null; post("getPullRequests"); } + rebuild(); + }; + tabs.appendChild(b); + }; + mkTab("branches", "Branches"); + mkTab("prs", "Pull requests"); + + const rebuild = () => { closeMenu(); $("branchCell").onclick(); }; + redrawBranchMenu = () => { + if (state.prMenuTab === "prs") { closeMenu(); $("branchCell").onclick(); } + }; + + m.appendChild(tabs); + + if (state.prMenuTab === "prs") { + renderPrMenu(m); + return; + } + + const mkAction = (label, fn) => { + const it = document.createElement("div"); + it.className = "menu-item"; it.textContent = label; + it.onclick = () => { closeMenu(); fn(); }; + m.appendChild(it); + }; + mkAction("+ New branch…", startNewBranch); + if (state.currentBranch && state.branches.filter((b) => !b.startsWith("remotes/")).length > 1) { + mkAction("⑂ Merge into current branch…", startMergeBranch); + } + if (state.currentBranch) mkAction("⇡ Create pull request…", () => post("createPullRequest", { branch: state.currentBranch })); + if (state.changes.length > 0) mkAction("⇩ Stash all changes", () => post("stashPush", { message: "" })); + for (const s of state.stashes) { + const it = document.createElement("div"); + it.className = "menu-item"; + it.innerHTML = '↤ ' + esc(s.message || ("stash@{" + s.index + "}")) + '' + + ''; + it.querySelector('[data-a=pop]').onclick = (e) => { e.stopPropagation(); closeMenu(); post("stashApply", { index: s.index, drop: true }); }; + it.querySelector('[data-a=drop]').onclick = (e) => { e.stopPropagation(); closeMenu(); post("stashDrop", { index: s.index }); }; + m.appendChild(it); + } + const sep = document.createElement("div"); sep.className = "menu-sep"; m.appendChild(sep); + + const filter = document.createElement("input"); + filter.className = "menu-filter"; filter.placeholder = "Find a branch…"; + m.appendChild(filter); + const holder = document.createElement("div"); + m.appendChild(holder); + const draw = () => { + const q = filter.value.trim().toLowerCase(); + holder.innerHTML = ""; + const items = state.branches.filter((b) => !q || b.toLowerCase().indexOf(q) >= 0); + if (!items.length) { holder.innerHTML = ''; return; } + for (const b of items) { + const it = document.createElement("div"); + const isCur = b === state.currentBranch; + it.className = "menu-item" + (isCur ? " is-current" : ""); + it.innerHTML = '' + esc(b) + (isCur ? " (current)" : "") + ''; + it.onclick = () => { closeMenu(); if (!isCur) post("checkoutBranch", { branch: b }); }; + if (!isCur) { + const cmp = document.createElement("button"); + cmp.className = "file-resolve"; cmp.type = "button"; cmp.textContent = "⇄"; + cmp.title = "Compare with this branch"; + cmp.onclick = (e) => { e.stopPropagation(); closeMenu(); post("compareBranch", { branch: b }); }; + it.appendChild(cmp); + } + holder.appendChild(it); + } + }; + filter.oninput = draw; draw(); + setTimeout(() => filter.focus(), 0); + }); +}; + +function renderPrMenu(m) { + if (state.prLoading && !state.pullRequests) { + const d = document.createElement("div"); d.className = "menu-empty"; d.textContent = "Loading pull requests…"; + m.appendChild(d); return; + } + if (state.prError) { + const d = document.createElement("div"); d.className = "menu-empty"; d.textContent = state.prError; + m.appendChild(d); return; + } + const prs = state.pullRequests || []; + if (!prs.length) { + const d = document.createElement("div"); d.className = "menu-empty"; d.textContent = "No open pull requests"; + m.appendChild(d); return; + } + const filter = document.createElement("input"); + filter.className = "menu-filter"; filter.placeholder = "Find a pull request…"; + m.appendChild(filter); + const holder = document.createElement("div"); m.appendChild(holder); + const draw = () => { + const q = filter.value.trim().toLowerCase(); + holder.innerHTML = ""; + const items = prs.filter((p) => !q || + ("#" + p.number).indexOf(q) >= 0 || + p.title.toLowerCase().indexOf(q) >= 0 || + (p.author || "").toLowerCase().indexOf(q) >= 0); + if (!items.length) { holder.innerHTML = ''; return; } + for (const p of items) { + const it = document.createElement("div"); + it.className = "menu-item"; + it.innerHTML = + '' + + '#' + p.number + ' ' + esc(p.title) + + (p.isDraft ? ' (draft)' : '') + '' + + 'by ' + + esc(p.author || "?") + (p.isFork ? " · fork" : "") + " · " + esc(p.headRef) + ''; + it.onclick = () => { closeMenu(); post("checkoutPullRequest", { number: p.number }); }; + holder.appendChild(it); + } + }; + filter.oninput = draw; draw(); + setTimeout(() => filter.focus(), 0); +} + +function startNewBranch() { + openMenu($("branchCell"), (m) => { + const input = document.createElement("input"); + input.className = "menu-filter"; input.placeholder = "New branch name"; + m.appendChild(input); + const dirty = state.changes.length > 0; + const hint = document.createElement("div"); + hint.className = "menu-empty"; + hint.textContent = dirty + ? "You have uncommitted changes — choose what to do:" + : "Press Enter to create from " + (state.currentBranch || "HEAD"); + m.appendChild(hint); + const submit = (bringChanges) => { + const name = input.value.trim(); + if (!name) return; + closeMenu(); + if (dirty) post("createBranchWithChanges", { branchName: name, bringChanges: bringChanges }); + else post("createBranch", { branchName: name }); + }; + if (dirty) { + const row = document.createElement("div"); row.style.display = "flex"; row.style.gap = "4px"; row.style.padding = "4px"; + const a = document.createElement("button"); a.className = "menu-btn"; a.type = "button"; a.textContent = "Bring changes"; + a.onclick = () => submit(true); + const b = document.createElement("button"); b.className = "menu-btn"; b.type = "button"; b.textContent = "Stash them"; + b.onclick = () => submit(false); + row.appendChild(a); row.appendChild(b); m.appendChild(row); + } + input.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); submit(true); } }); + setTimeout(() => input.focus(), 0); + }); +} + +function startMergeBranch() { + openMenu($("branchCell"), (m) => { + const into = state.currentBranch || "HEAD"; + const hint = document.createElement("div"); + hint.className = "menu-empty"; + hint.textContent = "Merge a branch into " + into + ":"; + m.appendChild(hint); + + const filter = document.createElement("input"); + filter.className = "menu-filter"; filter.placeholder = "Find a branch to merge…"; + m.appendChild(filter); + const holder = document.createElement("div"); m.appendChild(holder); + const candidates = state.branches.filter((b) => b !== state.currentBranch); + const draw = () => { + const q = filter.value.trim().toLowerCase(); + holder.innerHTML = ""; + const items = candidates.filter((b) => !q || b.toLowerCase().indexOf(q) >= 0); + if (!items.length) { holder.innerHTML = ''; return; } + for (const b of items) { + const it = document.createElement("div"); + it.className = "menu-item"; + it.innerHTML = '' + esc(b) + ''; + it.onclick = () => { + closeMenu(); + post("mergeBranch", { fromBranch: b, toBranch: state.currentBranch }); + }; + holder.appendChild(it); + } + }; + filter.oninput = draw; draw(); + setTimeout(() => filter.focus(), 0); + }); +} + +$("repoCell").onclick = () => { + openMenu($("repoCell"), (m) => { + const repos = state.repositories || []; + if (repos.length > 1 || (repos.length && repos[0].path !== state.activeRepoPath)) { + const filter = document.createElement("input"); + filter.className = "menu-filter"; filter.placeholder = "Find a repository…"; + m.appendChild(filter); + const holder = document.createElement("div"); m.appendChild(holder); + const draw = () => { + const q = filter.value.trim().toLowerCase(); + holder.innerHTML = ""; + const items = repos.filter((r) => !q || r.name.toLowerCase().indexOf(q) >= 0 || r.path.toLowerCase().indexOf(q) >= 0); + if (!items.length) { holder.innerHTML = ''; return; } + for (const r of items) { + const isCur = r.path === state.activeRepoPath; + const it = document.createElement("div"); + it.className = "menu-item" + (isCur ? " is-current" : ""); + it.innerHTML = '' + esc(r.name) + (isCur ? " (current)" : "") + + '' + esc(r.path) + ''; + it.onclick = () => { closeMenu(); if (!isCur) post("selectRepository", { path: r.path }); }; + holder.appendChild(it); + } + }; + filter.oninput = draw; draw(); + setTimeout(() => filter.focus(), 0); + } else if (repos.length === 1) { + const it = document.createElement("div"); + it.className = "menu-item is-current"; + it.innerHTML = '' + esc(repos[0].path) + ''; + m.appendChild(it); + } + const sep = document.createElement("div"); sep.className = "menu-sep"; m.appendChild(sep); + const add = (label, cmd) => { + const it = document.createElement("div"); + it.className = "menu-item"; it.textContent = label; + it.onclick = () => { closeMenu(); post(cmd); }; + m.appendChild(it); + }; + add("+ Add local repository…", "addLocalRepository"); + add("⤓ Clone repository…", "cloneRepository"); + }); +}; + +/* ---------- inbound messages ---------- */ +window.addEventListener("message", (ev) => { + const msg = ev.data || {}; + switch (msg.command || msg.type) { + case "updateChanges": { + const prevPaths = new Set(state.changes.map((c) => c.path)); + state.changes = msg.changes || []; + const paths = new Set(state.changes.map((c) => c.path)); + // Auto-check newly appeared files (GitHub Desktop checks everything by default), + // keep the user's choices for files that were already listed. + state.selectedFiles = new Set( + Array.from(state.selectedFiles).filter((p) => paths.has(p)), + ); + state.changes.forEach((c) => { + if (!prevPaths.has(c.path)) state.selectedFiles.add(c.path); + }); + if (state.selectedPath && !paths.has(state.selectedPath)) { + state.selectedPath = null; + $("diffHeader").hidden = true; + } + if (!state.changes.some((c) => /[UC]/.test(c.status || ""))) $("conflictBar").hidden = true; + renderChanges(); + updateLayout(); + break; + } + case "updateHistory": + state.history = msg.history || []; + state.hasMore = !!msg.hasMoreCommits; + state.loadingMore = false; + state.compare = null; + if (state.selectedCommit && !state.history.some((c) => c.hash === state.selectedCommit)) { + state.selectedCommit = null; + state.commitDetail = null; + state.commitFilePath = null; + updateLayout(); + } + renderHistory(); + break; + case "loadMoreCommitsResponse": { + const seen = new Set(state.history.map((c) => c.hash)); + state.history = state.history.concat((msg.history || []).filter((c) => !seen.has(c.hash))); + state.hasMore = !!msg.hasMoreCommits; + state.loadingMore = false; + renderHistory(); + break; + } + case "commitSucceeded": + $("summary").value = ""; $("description").value = ""; $("coAuthors").value = ""; + state.coAuthors = ""; + state.selectedFiles.clear(); + state.amend = false; $("amendCheck").checked = false; + updateCommitBtn(); + break; + case "mergeConflict": + // Detailed state arrives via updateOperation on the following refresh; + // this just flips to the Changes tab immediately. + state.tab = "changes"; + $("pane-changes").hidden = false; $("pane-history").hidden = true; + document.querySelectorAll(".tab").forEach((b) => b.classList.toggle("is-active", b.dataset.tab === "changes")); + break; + case "updateOperation": + state.operation = msg.operation || null; + state.conflicted = msg.conflicted || []; + state.canUndo = !!msg.canUndo; + state.lastCommitSummary = msg.lastCommitSummary || null; + renderOperation(); renderChanges(); renderLastCommitBar(); + break; + case "updateStashes": + state.stashes = msg.stashes || []; + break; + case "updatePullRequests": + state.prLoading = false; + state.pullRequests = msg.pullRequests || []; + state.prError = msg.error || null; + if (redrawBranchMenu) redrawBranchMenu(); + break; + case "branchComparison": + state.compare = { branch: msg.branch, ahead: msg.ahead || [], behind: msg.behind || [] }; + state.tab = "history"; + $("pane-changes").hidden = true; $("pane-history").hidden = false; + document.querySelectorAll(".tab").forEach((b) => b.classList.toggle("is-active", b.dataset.tab === "history")); + renderHistory(); renderLastCommitBar(); + break; + case "updateBranches": + state.branches = msg.branches || []; + state.currentBranch = msg.currentBranch || null; + state.branchActivity = msg.branchActivity || {}; + renderToolbar(); updateCommitBtn(); updateLayout(); + break; + case "updateRepository": + state.repository = msg.repository || null; + renderToolbar(); setAvatar(); updateLayout(); + break; + case "updateRepositoryList": + state.repositories = msg.repositories || []; + state.activeRepoPath = msg.activePath || null; + break; + case "updateAccounts": + state.account = msg.activeAccount || null; + setAvatar(); + break; + case "updateRemoteStatus": + state.remote = msg.remoteStatus || null; + renderToolbar(); updateLayout(); + break; + case "commitDetail": + state.commitDetail = msg.payload || null; + if (state.selectedCommit) renderCommitDetail(); + break; + case "workingDiff": + if (msg.payload && msg.payload.path === state.selectedPath) renderDiff(msg.payload.diff); + break; + case "fileDiff": + if (!msg.payload) break; + if (state.selectedCommit && state.commitFilePath === msg.payload.path) { + renderCommitFileDiff(msg.payload.diff); + } else if (!state.selectedCommit) { + renderDiff(msg.payload.diff); + } + break; + case "error": + if (state.selectedCommit || state.selectedPath) { + $("diffBody").innerHTML = '
' + esc(msg.message || "Error") + "
"; + } + break; + } +}); + +/* ---------- avatar ---------- */ +function setAvatar() { + const a = state.account; + const el = $("avatar"); + if (a && a.avatarUrl) { + el.innerHTML = ''; + el.title = a.login || ""; + return; + } + const s = (a && (a.name || a.login)) || (state.repository && state.repository.name) || "?"; + el.textContent = s.slice(0, 2); + el.title = (a && a.login) || ""; +} +applyWidth(); +setAvatar(); +renderToolbar(); +renderOperation(); +renderLastCommitBar(); +updateLayout(); +post("ready"); +`; diff --git a/src/webviews/timeline/services/working-tree.service.ts b/src/webviews/timeline/services/working-tree.service.ts new file mode 100644 index 0000000..b6978fe --- /dev/null +++ b/src/webviews/timeline/services/working-tree.service.ts @@ -0,0 +1,170 @@ +import { GitClientFactory } from "../../../core/git/git-authenticator"; +import { + Notifier, + RepositoryContext, + Refresher, + WebviewChannel, +} from "../ports"; +import { RepositoryScopedService } from "./repository-scoped.service"; + +/** + * @description Working-tree operations of the Changes tab: staging, committing + * a chosen set of files (the GitHub Desktop model), discarding, and producing + * the diff of an uncommitted file. + */ +export class WorkingTreeService extends RepositoryScopedService { + constructor( + repos: RepositoryContext, + notifier: Notifier, + private readonly git: GitClientFactory, + private readonly channel: WebviewChannel, + private readonly refresher: Refresher, + ) { + super(repos, notifier); + } + + stage(files: string[]): Promise { + return this.mutate(files, (g) => g.add(files)); + } + + unstage(files: string[]): Promise { + return this.mutate(files, (g) => g.reset(["HEAD", "--", ...files])); + } + + /** Commits exactly `files` (stages them first, then commits with a pathspec). */ + commitFiles(message: string, files: string[]): Promise { + return this.withRepo(async (repo) => { + if (!message.trim() || files.length === 0) { + return; + } + const git = this.git.plain(repo.localPath); + await git.add(files); + await git.commit(message, files); + this.channel.post({ command: "commitSucceeded" }); + this.notifier.info("Commit created"); + await this.refresher.refresh(); + }); + } + + /** Commits whatever is already staged. */ + commit(message: string): Promise { + return this.withRepo(async (repo) => { + if (!message.trim()) { + return; + } + await this.git.plain(repo.localPath).commit(message); + this.channel.post({ command: "commitSucceeded" }); + this.notifier.info("Commit created"); + await this.refresher.refresh(); + }); + } + + /** + * Rewrites the last commit with `message` and, if `files` is non-empty, adds + * those paths to it. Refuses when the commit is already published. + */ + amend(message: string, files: string[]): Promise { + return this.withRepo(async (repo) => { + if (!message.trim()) { + return; + } + const git = this.git.plain(repo.localPath); + const pushed = await git + .raw(["branch", "-r", "--contains", "HEAD"]) + .catch(() => ""); + if (pushed.trim()) { + const ok = await this.notifier.confirm( + "The last commit is already on a remote. Amending rewrites history and will need a force-push. Continue?", + "Amend anyway", + ); + if (!ok) { + return; + } + } + if (files.length > 0) { + await git.add(files); + } + await git.raw(["commit", "--amend", "-m", message]); + this.channel.post({ command: "commitSucceeded" }); + this.notifier.info("Last commit amended"); + await this.refresher.refresh(); + }); + } + + /** + * Undoes the last commit, keeping its changes staged in the working tree + * (`git reset --soft HEAD~1`) — the GitHub Desktop "Undo" behaviour. + */ + undoLastCommit(): Promise { + return this.withRepo(async (repo) => { + const git = this.git.plain(repo.localPath); + const summary = ( + await git.raw(["log", "-1", "--format=%s"]).catch(() => "") + ).trim(); + const ok = await this.notifier.confirm( + summary + ? `Undo commit "${summary}"? Its changes stay in your working tree.` + : "Undo the last commit? Its changes stay in your working tree.", + "Undo", + ); + if (!ok) { + return; + } + await git.reset(["--soft", "HEAD~1"]); + this.notifier.info("Last commit undone"); + await this.refresher.refresh(); + }); + } + + discard(files: string[]): Promise { + return this.withRepo(async (repo) => { + if (files.length === 0) { + return; + } + const confirmed = await this.notifier.confirm( + files.length === 1 + ? `Discard changes to ${files[0]}?` + : `Discard changes to ${files.length} files?`, + "Discard", + ); + if (!confirmed) { + return; + } + const git = this.git.plain(repo.localPath); + await git + .raw(["checkout", "HEAD", "--", ...files]) + .catch(() => undefined); + await git.raw(["clean", "-fd", "--", ...files]).catch(() => undefined); + await this.refresher.refresh(); + }); + } + + workingDiff(filePath: string): Promise { + return this.withRepo(async (repo) => { + const git = this.git.plain(repo.localPath); + let diff = await git.raw(["diff", "HEAD", "--", filePath]); + if (!diff.trim()) { + diff = await git + .raw(["diff", "--no-index", "--", "/dev/null", filePath]) + .catch(() => ""); + } + this.channel.post({ + command: "workingDiff", + payload: { path: filePath, diff }, + }); + }); + } + + private mutate( + files: string[], + op: (git: ReturnType) => Promise, + ): Promise { + return this.withRepo(async (repo) => { + if (files.length === 0) { + return; + } + await op(this.git.plain(repo.localPath)); + await this.refresher.refresh(); + }); + } +} diff --git a/src/webviews/timeline/timeline-controller.ts b/src/webviews/timeline/timeline-controller.ts new file mode 100644 index 0000000..2b5ca61 --- /dev/null +++ b/src/webviews/timeline/timeline-controller.ts @@ -0,0 +1,244 @@ +import { AccountManager } from "../../core/accounts/account-manager"; +import { GitClientFactory } from "../../core/git/git-authenticator"; +import { InboundMessage } from "./messages"; +import { RemoteStatus } from "./interfaces/repository-snapshot"; +import { MessageRouter } from "./message-router"; +import { + Browser, + GitHubApi, + Notifier, + RepositoryContext, + Refresher, + WebviewChannel, +} from "./ports"; +import { BranchService } from "./services/branch.service"; +import { CommitActionsService } from "./services/commit-actions.service"; +import { ConflictService } from "./services/conflict.service"; +import { DiffService } from "./services/diff.service"; +import { PullRequestService } from "./services/pull-request.service"; +import { RepositoryDataService } from "./services/repository-data.service"; +import { StashService } from "./services/stash.service"; +import { SyncService } from "./services/sync.service"; +import { WorkingTreeService } from "./services/working-tree.service"; + +/** + * @description Composition root for the timeline webview's message handling. + * + * It owns nothing git-specific itself: it wires the cohesive services to the + * {@link MessageRouter}, implements {@link Refresher} (the single place that + * recomputes and broadcasts timeline state), and validates raw webview + * payloads into a typed {@link InboundMessage} before dispatch. + */ +export class TimelineController implements Refresher { + private readonly router: MessageRouter; + private readonly data: RepositoryDataService; + private lastRemoteStatus: RemoteStatus = { + hasRemote: false, + isPublished: false, + ahead: 0, + behind: 0, + lastFetched: null, + remoteBranch: null, + }; + + constructor( + private readonly deps: { + repos: RepositoryContext; + notifier: Notifier; + channel: WebviewChannel; + browser: Browser; + git: GitClientFactory; + accounts: AccountManager; + githubApi: GitHubApi; + }, + ) { + const { repos, notifier, channel, browser, git } = deps; + + this.data = new RepositoryDataService(repos, git); + + const working = new WorkingTreeService(repos, notifier, git, channel, this); + const sync = new SyncService(repos, notifier, git, channel, this); + const branch = new BranchService(repos, notifier, git, channel, this); + const commitActions = new CommitActionsService( + repos, + notifier, + git, + browser, + this, + ); + const pr = new PullRequestService( + repos, + notifier, + browser, + git, + channel, + deps.githubApi, + this, + ); + const diff = new DiffService(repos, notifier, git, this.data, channel); + const conflict = new ConflictService(repos, notifier, git, this); + const stash = new StashService(repos, notifier, git, this); + + this.router = new MessageRouter(notifier) + .on("ready", () => this.refresh()) + .on("refresh", () => this.refresh()) + .on("selectRepository", async (m) => { + const reloading = await repos.setActive(m.path); + if (!reloading) { + await this.refresh(); + } + }) + .on("addLocalRepository", async () => { + await repos.addLocal(); + await this.refresh(); + }) + .on("cloneRepository", () => repos.clone()) + .on("stageFiles", (m) => working.stage(m.files)) + .on("unstageFiles", (m) => working.unstage(m.files)) + .on("commit", (m) => working.commit(m.message)) + .on("commitFiles", (m) => working.commitFiles(m.message, m.files)) + .on("amendCommit", (m) => working.amend(m.message, m.files)) + .on("undoLastCommit", () => working.undoLastCommit()) + .on("discardFiles", (m) => working.discard(m.files)) + .on("markResolved", (m) => conflict.markResolved(m.files)) + .on("continueOperation", () => conflict.continue()) + .on("abortOperation", () => conflict.abort()) + .on("getStashes", () => this.refresh()) + .on("stashPush", (m) => stash.push(m.message)) + .on("stashApply", (m) => stash.apply(m.index, m.drop)) + .on("stashDrop", (m) => stash.drop(m.index)) + .on("getWorkingDiff", (m) => working.workingDiff(m.filePath)) + .on("fetch", () => sync.fetch()) + .on("pull", () => sync.pull()) + .on("push", () => sync.push()) + .on("forcePush", () => sync.forcePush()) + .on("publish", () => sync.publish()) + .on("checkoutBranch", (m) => branch.checkout(m.branch)) + .on("createBranch", (m) => branch.create(m.branchName)) + .on("createBranchWithChanges", (m) => + branch.createWithChanges(m.branchName, m.bringChanges), + ) + .on("mergeBranch", (m) => branch.merge(m.fromBranch, m.toBranch)) + .on("compareBranch", (m) => branch.compare(m.branch)) + .on("createPullRequest", (m) => pr.openCompare(m.branch)) + .on("getPullRequests", () => pr.list()) + .on("checkoutPullRequest", (m) => pr.checkout(m.number)) + .on("openRepoOnGitHub", () => pr.openRepo()) + .on("loadMoreCommits", (m) => this.loadMore(m.offset)) + .on("getCommitDetails", (m) => diff.commitDetail(m.hash)) + .on("selectCommit", (m) => diff.commitDetail(m.hash)) + .on("getFileDiff", (m) => diff.fileDiff(m.hash, m.filePath)) + .on("selectFile", (m) => diff.fileDiff(m.hash, m.path)) + .on("resetToCommit", (m) => commitActions.reset(m.hash)) + .on("checkoutCommit", (m) => commitActions.checkout(m.hash)) + .on("revertCommit", (m) => commitActions.revert(m.hash)) + .on("cherryPickCommit", (m) => commitActions.cherryPick(m.hash)) + .on("createBranchFromCommit", (m) => commitActions.branchFrom(m.hash)) + .on("createTagFromCommit", (m) => commitActions.tagFrom(m.hash)) + .on("viewCommitOnGitHub", (m) => commitActions.viewOnGitHub(m.hash)); + } + + /** + * Validates a raw `postMessage` payload and dispatches it. Malformed + * payloads are dropped. + */ + handle(raw: unknown): Promise { + const message = coerceInbound(raw); + if (!message) { + return Promise.resolve(); + } + return this.router.dispatch(message); + } + + /** Recomputes the whole snapshot and broadcasts it to the webview. */ + async refresh(): Promise { + const { channel, accounts } = this.deps; + const snapshot = await this.data.getSnapshot(); + + if (!snapshot) { + channel.post({ command: "updateRepository", repository: null }); + return; + } + + this.lastRemoteStatus = snapshot.remoteStatus; + + channel.post({ command: "updateChanges", changes: snapshot.changes }); + channel.post({ + command: "updateHistory", + history: snapshot.commits, + hasMoreCommits: snapshot.hasMoreCommits, + offset: 0, + }); + channel.post({ + command: "updateBranches", + branches: snapshot.branches, + currentBranch: snapshot.currentBranch, + branchActivity: snapshot.branchActivity, + }); + channel.post({ + command: "updateRepository", + repository: snapshot.repository, + }); + channel.post({ + command: "updateRepositoryList", + repositories: this.deps.repos + .list() + .map((r) => ({ name: r.name, path: r.localPath })), + activePath: snapshot.repository.path, + }); + + const active = accounts.getActiveAccount(); + channel.post({ + command: "updateAccounts", + activeAccount: active + ? { + login: active.login, + name: active.name, + avatarUrl: active.avatarUrl, + } + : null, + }); + + channel.post({ + command: "updateRemoteStatus", + remoteStatus: snapshot.remoteStatus, + tags: snapshot.tags, + }); + + channel.post({ + command: "updateOperation", + operation: snapshot.operation, + conflicted: snapshot.conflicted, + canUndo: snapshot.canUndo, + lastCommitSummary: snapshot.commits[0]?.message.split("\n")[0] ?? null, + }); + + channel.post({ command: "updateStashes", stashes: snapshot.stashes }); + } + + private async loadMore(offset: number): Promise { + const page = await this.data.loadMoreCommits(offset, this.lastRemoteStatus); + this.deps.channel.post({ + command: "loadMoreCommitsResponse", + history: page.commits, + hasMoreCommits: page.hasMoreCommits, + offset: page.offset, + }); + } +} + +/** + * @description Narrows an untrusted `postMessage` value to an + * {@link InboundMessage}. Only the `command` discriminant is checked here; each + * service still guards its own inputs. + */ +function coerceInbound(raw: unknown): InboundMessage | null { + if ( + typeof raw === "object" && + raw !== null && + typeof (raw as { command?: unknown }).command === "string" + ) { + return raw as InboundMessage; + } + return null; +} diff --git a/src/webviews/timeline/timeline-view-provider.ts b/src/webviews/timeline/timeline-view-provider.ts index 19d3266..7b10a91 100644 --- a/src/webviews/timeline/timeline-view-provider.ts +++ b/src/webviews/timeline/timeline-view-provider.ts @@ -1,99 +1,81 @@ import * as vscode from "vscode"; import { RepositoryManager } from "../../core/repositories/repository-manager"; import { AccountManager } from "../../core/accounts/account-manager"; -import { CommitDetailViewProvider } from "../commitDetail/commit-detail-view-provider"; +import { AccountGitClientFactory } from "../../core/git/git-authenticator"; +import { ITimelineViewProvider } from "./interfaces/timeline-view-provider.interface"; import { - ITimelineViewProvider, - WebviewMessage, -} from "./interfaces/timeline-view-provider.interface"; -import { GitOperationsService } from "./services/git-operations.service"; + AccountGitHubApi, + VsCodeBrowser, + VsCodeNotifier, + VsCodeWebviewChannel, + WorkspaceRepositoryContext, +} from "./adapters/vscode-adapters"; +import { TimelineController } from "./timeline-controller"; import { WebviewHtmlService } from "./services/webview-html.service"; -import { MessageHandlerService } from "./services/message-handler.service"; -import { FileIconService } from "./services/file-icon.service"; +const REFRESH_DEBOUNCE_MS = 400; + +/** + * @description Binds the timeline webview to a {@link TimelineController}. + * Owns only VS Code lifecycle concerns: HTML, message plumbing, visibility and + * a debounced refresh so a burst of file saves triggers one recompute. + */ export class TimelineViewProvider implements ITimelineViewProvider { - private view: vscode.WebviewView | undefined; - private gitService: GitOperationsService; - private htmlService: WebviewHtmlService; - private messageHandler: MessageHandlerService | undefined; - private fileIconService: FileIconService; + private controller: TimelineController | undefined; + private readonly htmlService: WebviewHtmlService; + private refreshTimer: ReturnType | undefined; constructor( private readonly context: vscode.ExtensionContext, private readonly repositories: RepositoryManager, private readonly accounts: AccountManager, - private readonly commitDetailProvider: CommitDetailViewProvider, ) { - this.gitService = new GitOperationsService(repositories); this.htmlService = new WebviewHtmlService(context, repositories); - this.fileIconService = new FileIconService(); - } - - resolveWebviewView(webviewView: vscode.WebviewView): void | Thenable { - this.view = webviewView; - this.setupWebview(webviewView); - this.setupMessageHandler(); - this.setupEventHandlers(webviewView); - return this.refreshData(); } - async refresh(): Promise { - if (this.view && this.messageHandler) { - await this.messageHandler.handleMessage({ command: "refresh" }); - } - } - - private setupWebview(webviewView: vscode.WebviewView): void { + resolveWebviewView(webviewView: vscode.WebviewView): void { webviewView.webview.options = { enableScripts: true, localResourceRoots: [ - vscode.Uri.joinPath(this.context.extensionUri, "out", "webview"), - vscode.Uri.joinPath( - this.context.extensionUri, - "out", - "webview", - "assets", - ), vscode.Uri.joinPath(this.context.extensionUri, "out"), ], }; webviewView.webview.html = this.htmlService.generateHtml( webviewView.webview, ); - } - private setupMessageHandler(): void { - if (!this.view) return; + this.controller = new TimelineController({ + repos: new WorkspaceRepositoryContext(this.repositories), + notifier: new VsCodeNotifier(), + channel: new VsCodeWebviewChannel(webviewView.webview), + browser: new VsCodeBrowser(), + git: new AccountGitClientFactory(this.accounts), + accounts: this.accounts, + githubApi: new AccountGitHubApi(this.accounts), + }); - this.messageHandler = new MessageHandlerService( - this.context, - this.repositories, - this.accounts, - this.commitDetailProvider, - this.gitService, - this.view, + webviewView.webview.onDidReceiveMessage((message) => + this.controller?.handle(message), ); - } - - private setupEventHandlers(webviewView: vscode.WebviewView): void { - // Handle webview becoming visible/hidden webviewView.onDidChangeVisibility(() => { if (webviewView.visible) { - this.refreshData(); + void this.controller?.refresh(); } }); - // Handle messages from webview - webviewView.webview.onDidReceiveMessage(async (message: WebviewMessage) => { - if (this.messageHandler) { - await this.messageHandler.handleMessage(message); - } - }); + void this.controller.refresh(); } - private async refreshData(): Promise { - if (this.messageHandler) { - await this.messageHandler.handleMessage({ command: "refresh" }); + /** Debounced: coalesces bursts (e.g. save-all) into a single recompute. */ + refresh(): Promise { + if (this.refreshTimer) { + clearTimeout(this.refreshTimer); } + return new Promise((resolve) => { + this.refreshTimer = setTimeout(() => { + this.refreshTimer = undefined; + void this.controller?.refresh().finally(resolve); + }, REFRESH_DEBOUNCE_MS); + }); } } diff --git a/src/webviews/tsconfig.json b/src/webviews/tsconfig.json index 0c3a01d..80e88b2 100644 --- a/src/webviews/tsconfig.json +++ b/src/webviews/tsconfig.json @@ -3,26 +3,14 @@ "target": "ES2020", "lib": ["ES2020", "DOM", "DOM.Iterable"], "allowJs": false, - "skipLibCheck": true, - "esModuleInterop": false, - "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true, "module": "Node16", "moduleResolution": "Node16", "resolveJsonModule": true, "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx", - "baseUrl": ".", - "paths": { - "@webview/*": ["./*"] - } + "noEmit": true }, - "include": [ - "." - ], - "exclude": [ - "node_modules" - ] -} \ No newline at end of file + "include": ["."], + "exclude": ["node_modules"] +} diff --git a/vite.webview.config.ts b/vite.webview.config.ts deleted file mode 100644 index 9262944..0000000 --- a/vite.webview.config.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; -import path from "path"; - -export default defineConfig({ - root: __dirname, - plugins: [react()], - resolve: { - alias: { - "@webview": path.resolve(__dirname, "src/webviews"), - }, - }, - build: { - sourcemap: true, - target: "es2020", - outDir: path.resolve(__dirname, "out/webview"), - emptyOutDir: true, - rollupOptions: { - input: { - main: path.resolve(__dirname, "src/webviews/bootstrap.tsx"), - 'commit-detail': path.resolve(__dirname, "src/webviews/commitDetail/index.tsx"), - 'history-explorer': path.resolve(__dirname, "src/webviews/historyExplorerApp/index.tsx"), - }, - output: { - entryFileNames: '[name].js', - inlineDynamicImports: false, - }, - }, - }, - define: { - "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV ?? "production"), - }, -}); \ No newline at end of file